Changelog
All notable changes to this project are documented here. The format follows Keep a Changelog and the project uses Semantic Versioning.
Unreleased
[0.15.0] — 2026-10-12
Added
-
cost_bench.py summarisenow prints, under SM-2’s report, whether the delegation stance fired on each task: fired, declined below the break-even, missed above it, spawn tool not offered, or unknown. Replay rows recordspawn_offered,gather_calls,absorbed_callsandworkflow_launches, and a run whose output shows no assistant message now readsspawnsas unknown rather than 0. The break-even is FR-34’s 7.6 absorbed calls, hypothetical and overridable with--break-even; history rows carry the same block underdelegation. (#429) -
Every replay row now records counts and canonical content hashes for the loaded surface the CLI’s
initevent reports, plus the first call’s whole input asfirst_call_context;backfillderives them without erasing existing evidence when raw output is missing. A set stops when a run’s surface differs from its arm’s first run unless--allow-surface-drift. Before launch, replay recomputes known-schema arm records, reports all admission defects, and refuses pair differences outside exact harness-generated files and links. The new--effortoption pins reasoning effort on every launch; baked overrides, pre-flight mismatches and scored-run mismatches are refused. (#482) -
The cost benchmark’s new
detectcommand runs every rule detector over the replay’s saved-pstreams without calling a model, and writes one row per run per detector with the count and the turn of each firing. A replay run with--rawnow does this after each tag’s set, and its history row says which rules fired in the harness arm per task and how often across trials.detect --backfillwrites the same rows beside every results file already on disk and never rewrites the results. A run is read only from its own stream: a timed-out run, or a stream directory two sets share, gets error rows, and an existing detections file is replaced only with--overwrite. Backfilling the evidence sets already on disk is left to the owner and has not been run. Mechanism summaries retain total and measured-run denominators and report detector errors rather than silently dropping unknown runs. (#510) -
The test suite now runs every hook against a synthetic corpus of recorded tool calls under every stance variant and checks each decision against a committed matrix in
tests/fixtures/hook-calls/. A change that flips a hook’s answer for one variant fails naming the hook, the call and the variant, andpython3 tests/fixtures/hook-calls/hook_matrix.py --writerewrites the matrix after an intended change. It calls no model and runs in seconds. (#511) -
cost_bench.py replay --tier microruns three small oracle-scored tasks on a pinned small model, five reps in both arms under a 7.80 USD stop, and reports beside each run’s pass or fail and cost whether delegation, the stop gate or the output style fired. Its rows carrytier: micro, form their own series and go only tobenchmarks/micro/micro-history.jsonl; a history file refuses rows of the other tier. The contamination control still refuses its same-repository tasks, so it runs once they have independent provenance. (#512) -
Add one stance-owned delegation nudge after a tiered session reads three distinct files. (#513)
-
citizen usage --surfacelists every instruction source a session loads, grouped by owner (harness modules, your own files, the project, plugins, MCP servers, hooks and the CLI itself), each with a soft token estimate orunmeasured; MCP servers, hooks and plugins are never counted as 0.citizen scorecardgives one row per module with its instruments, tokens and measured effect, orunmeasured.cost_bench.py replay --ablations benchmarks/ablations.jsonruns bare, control and one arm per removed or set entry, each built as an image with its selection declared and admitted only when the installed file matches; it states the minimum detectable effect before any spend, orders runs from a recorded seed, and refuses an unknown id or an arm that differs from control beyond its entry.summarisereports each arm against control on cost, output tokens, the first-call prefix, turns, tool calls and pass rate separately, with n, spread and task-clustered intervals,inconclusivewhen an interval spans no effect. A schema-1 pair manifest runs as before, andfirst_call_contextis now None, not a smaller sum, when the first call leaves a field out. (#514) -
citizen usage --rulesnow lists every rule in your loaded instruction surface, rules imported from a registered primitive root included, as measured, dark or unmeasured with its reason, and states the share measured. It also loads declarative detectors from your repository’s.ruleprobe/detectors.yaml, in the format standaloneruleprobereads, runs them in the session hook, and prints a malformed entry with its file and line. (#647) -
citizen usage --by rebuildattributes Claude Code prompt-cache rebuilds to observed causes and reports unpriced or malformed evidence explicitly. (#748) -
cost_bench.py replay --pair <manifest>judges one policy change on what the whole task costs. An ablation manifest inbenchmarks/ablations/names a tag and one session-scoped factor (HARNESS_STANCE_<DIMENSION>orHARNESS_MODE); the tag’s one image runs as a reference and a treatment arm beside the bare arm, and a pair that differs in anything else is refused before any spend. Each harness arm’s decision-provider calls are copied out of its container and priced into its arm, an unpriced call named rather than counted as zero.summarisereports per-arm passes, Cost-of-Pass with and without decision calls, wall time and re-spawns on a stronger model class, with paired intervals and a post-run loaded-surface parity check. Replay rows now carrysession_ids,respawns_upandspawns_unranked. (#754) -
The live replay now reports SM-2’s result from its saved rows alone: each arm’s Cost-of-Pass and pass rate, with a descriptive Wilson interval on the pass rate, and the harness-over-bare Cost-of-Pass ratio and pass-rate difference with paired, task-clustered 95% intervals from a seeded bootstrap. The ratio is undefined, never zero or infinity, when an arm passes nothing. The verdict reads supported, not supported or inconclusive with its reason, claims “at least 15% cheaper” only when the ratio’s upper bound is at or below 0.85, and weighs a long-task subset when tasks are marked
long.cost_bench.py summariseprints it with a Pareto table of cost against pass rate, every row now records itsoutcomeandtask_long, and the replay defaults to five trials per task and arm with a 140.50 USD spend cap that fits one full set. (#795) -
cost_bench.py replay --packruns tasks from an evaluator pack kept in its own git repository, read from a pinned ref and digest, so the harness arm’s installed checkout never holds a task’s check or answer. The contamination control checks each pack task at the exact harness commit, and--dry-runnow prints its result per task and exits 2 when one is refused.scripts/replay_power.pystates the tasks, long tasks and trials a set needs for SM-2’s joint power from pilot rows. (#796) -
cost_bench.py replay --design unit-economy --unit <kind>.<id>runs the unit-by-economy two-by-two: bare and four declared-selection cells (nothing added, the unit alone, the economy concern alone, both) from one base profile derived from the tag’s catalog. Before any spend it refuses a grid whose cells differ in anything but the two factors, once the tag’s resolver has read them.summarisereports the unit’s effect, the economy concern’s and their interaction on Cost-of-Pass, pass rate and rule adherence, which is read by the unit’s own detectors from each saved stream. Every figure comes from one task-clustered paired bootstrap, and the report is result schema 1. Thecoststance gains anoffvariant, the economy concern’s off level. (#797) -
citizen usage --by adherencereports how often each recommendation is followed, as a measured rate with a 95% Wilson interval, and what following it might have saved, as a soft estimate from the named carried-context reprice estimator over the session’s own transcript. Every figure carries its label, and the two are never added together. (#798) -
citizen evidence verifynow re-derives a local proof bundle’s figures and evidence cards offline. Effort or a loaded surface the rows did not observe is reported as unknown, never as verified, and a card claiming it fails. Landing copy refuses recognized measured claims unless their exact text binds to a freshly verified card; cheaper claims additionally require support from the registered SM-2 result. (#799) -
Register observation hooks explicitly with
observation.enabled; ordinary installs keep observation off, while cost replays collect both arms into run-owned ledgers. (#867) -
Add
citizen usage --jsonfor the existing token, role, rule, decision, provider, prefix and conflict reports, using the same aggregation and unknown-data semantics as their text output. (#909) -
A
Workflowlaunch now reads themodelandefforta script’sagent()calls name. Underdelegation: tiereda literal model of thefrontierclass is refused, with the model to use instead. A literal effort above the cost variant’s highest, such asxhighormax, or a class above a custom variant’s strongest is let through and logged: the launch’sworkflow-launchrow readsover-ceiling. A value the guard cannot judge, such as a computed option, a model the adapter’s table does not hold or an unknown effort, is let through with the answerunresolved. (#915) -
A
.codex-plugin/plugin.jsonmanifest packages the skills as a Codex plugin, with no hooks, alongside the Claude Code plugin. Its shared metadata matches.claude-plugin/plugin.json, and a test fails when the two drift apart, when either version leavesVERSION, or when the Codex manifest declares hooks. (#1111) -
An oracle check can now return named metrics beside pass or fail. A task declares each metric and whether higher or lower is better; its check returns
{"pass": ..., "metrics": {...}}or the original error list. Rows record each metric, a value that is not a finite number is kept as unknown rather than 0, andcost_bench.py summarisereports each metric per arm and per task with SM-2’s task-clustered interval. A task without metrics writes the same rows as before. (#1143) -
cost_bench.py replay --ablationsnow runs on an evaluator pack. Each pack task’s contamination check runs at the exact commit--tagnames before any arm image is built or launched, a registered run must pin--pack-digest, and every ablation row records the pack’s name, version, commit and digest. The ablation plan states its worst-case cost at the run’s own--run-capbeside the minimum detectable effect, so the #514 prefix sweep has a contamination-safe task source. (#1152) -
A benchmark task’s check can now read the session it scores. A check written
check(root, stream=None)is given the run’s whole saved stream-json, subagent messages included, mounted read-only in its container, so metrics such as how much of a log was read or what a subagent cost can be scored; a one-argumentcheck(root)is called as before. Rows of a task declaring metrics recordmetric_stream, and a stream metric left null always carries its reason. (#1177) -
A new
scripts/replay_reliability.pycomputes pass^k per task and arm (whether every trial passed, and the unbiased estimate from n trials) and the all-rules-at-once rate, the share of runs in which no rule-violation detector fired, beside each detector’s own compliance rate, from a set’s saved rows anddetections.jsonlalone; an unread run is counted as unknown, never clean. (#1180) -
scripts/replay_power.pysizes a design from a pilot that passes everything or nothing when the pre-registration states--assumed-pass-rate, printed as an assumption, and takes--mde; the newscripts/equivalence.pyreads each metric’s task-clustered interval against the plan’s pre-registered margins and reports equivalent, not equivalent or inconclusive. (#1181) -
cost_bench.py replay --modeltakes several models, each run as its own stratum with its own schedule, caps and preflight; the dry run prices each,summarisereports every section per stratum and pools only when the pre-registration’s new Pooled analysis field names it, and an evidence bundle records the run’s strata. (#1182) -
cost_bench.py replay --arm-config NAME=PATHadds a harness arm built from a named stance selection beside bare and harness, such as the shippedbenchmarks/arms/maintainer.json(concise voice) andbenchmarks/arms/frugal.json(frugal cost tiering); an unknown dimension or variant is refused before any spend, the dry run lists each config arm, and every row records itsarm_configdigest. (#1183) -
The ablation sweep now holds a removal arm for every layer that costs tokens or turns, each run on its own evaluator-pack tasks and a ten-task outcome subset, and
scripts/ablations.py planlists and prices it whilescripts/ablations.py justifyreads a sweep’s rows into one keep, trim or no-evidence verdict per layer under its pre-registered equivalence margins. (#1184) -
A new
scripts/replay_judge.pyjudges two runs of one task blind and pairwise on scope discipline, design quality and reply quality, asking a pinned model with a pinned rubric twice with the order swapped and counting a changed answer as a tie, and admits a dimension only when its Cohen’s kappa against the maintainer’s hand labels, exported to a local labelling form, reaches 0.6, beside its position and length bias audits. (#1185) -
scripts/layer_scorecard.pybuilds one deterministic per-layer scorecard from registered production, rule-task, long-session, sweep and judge rows: prefix tokens and USD per run by model, behaviour against bare and against the layer’s own removal, the outcome and cost effects and the keep, trim or no-evidence verdict, under a harness-against-bare headline per stratum with equivalence verdicts, refusing exploratory rows unless--allow-exploratorymarks the whole report exploratory, and an evidence bundle may carry it. (#1186) -
cost_bench.py replay --tier long-sessiondrives an evaluator pack’s scripted multi-turn scenarios as one resumed session per scenario, arm and rep, writing a row per checkpoint and per session thatsummarisereports with scenario-clustered intervals. (#1193) -
The settings template carries the Claude Code OS sandbox, which
syncwrites for a user who setssandbox.enabledwith write access derived from the workspace, and every commandgrade-bashrates 2 or more in a git working tree is preceded by a snapshot underrefs/harness/snapshots/thatcitizen snapshot list|restore|prunemanages. (#1215) -
cost_bench.py summarisereports every arm of a run with config arms, per stratum: each arm’s pass rate and Cost-of-Pass, and each arm against bare and against harness with task-clustered intervals, labelled secondary unless the pre-registration’s new Primary arm comparisons field names them; under--rawthe runner also saves each run’s final diff as<task>-<arm>-<rep>.difffor the judge. (#1228) -
The layer sweep gains removal arms for the user-level CLAUDE.md, withheld from the arm’s image and admitted by a manifest parity check against control, and for the autonomy and plan-ceremony stances through new
offvariants, the autonomy one keeping the default’s command-gate threshold. (#1250)
Changed
- The hook replay matrix’s Stop call is now a native Stop input recorded from a Claude Code 2.1.280 run and sanitized, in place of an envelope derived from a result record. (#1100)
Fixed
-
A replay or ablation run no longer refuses an arm whose selection switches off every skill or every role: the arm-pair check now counts every directory the harness sync creates under the Claude home, such as an empty
~/.claude/skills, as the harness component, and still refuses anything else the harness arm holds. (#1287) -
Two rule detectors no longer count false positives.
secrets/git-add-secret-filenow reads only a basename that is exactlyid_rsaorid_ed25519as a private key, so staging a runbook such asdocs/id_rsa-rotation.mdor a public.pubhalf is not a hit.autonomy/denied-by-gradenow reads a Bash result as a denial only when it is the grade hook’s whole deny, reason first and nothing after it but the text the hook itself appends, so a grep that prints the hook’s signature out of a file is not a hit. Both now clear the 0.9 precision floor on the labelled corpus, and neither is listed as known below it. (#602) -
An unnamed spawn whose brief names a framework’s declared layer prompt file is refused by default, so a brief the orchestrator rewords in its own words (“use the instructions in <file>”, “do what it says”) no longer runs unconfined; a brief that edits the file, or only summarises, counts, copies, explains or compares it, still runs, and a qualified “the <words> instructions” no longer points back at the file when its words name something else. (#739)
-
Unknown autonomy variants now use the strictest command threshold, so a typo in a mode, config, session file or environment override cannot silently skip write prompts. (#868)
-
Governance now records provider errors against every affected Bash segment and continues judging later segments, while preserving fail-closed outcomes and any stronger denial. (#897)
-
The usage feed no longer prices a Workflow-tool agent against the soft budget of the role it is named for: its finished line is unbudgeted, matching the
unconfinedledger row. (#908) -
Timing-sensitive integration and handoff tests use controlled deadlines so parallel test runs do not change the behavior under test. (#910)
-
Compaction detection honors custom cost variants that inherit or explicitly select the compaction switch. (#911)
-
Qualification provisioning now removes inherited repository-local Git variables before launching children, so clone validation cannot act on an operator’s unrelated repository. (#917)
-
A named native spawn now refuses a harness role switched off by the effective user, project or session selection, even when a definition from an earlier sync remains installed. The refusal names the layer that supplied the effective switch. (#918)
-
citizen config setcan now apply a switch that repairs an already-invalid module selection while still rejecting a result with any remaining fault. (#919) -
Switching
grade-bashoff now takes it out of the Bash path. The command grader moved into a library thatgrade-bashre-exports, and the read-only allow and the delegation read count load that library alone, so a fault in the switched-off hook no longer denies every Bash command. (#920) -
Installation qualification asks Codex for its synced agent types and rejects missing harness roles. (#923)
-
A builder subagent may edit the paths its own session claimed in its own worktree: the write-intent check now takes a subagent’s worktree from the edited path, since its hook
cwdis its parent’s. (#993) -
Allow literal documentation text to mention the user configuration path without treating it as a configuration edit; actual write destinations remain protected. (#1007)
-
Resolve
git -Cpaths from the current user’s home or a safe earlier shell assignment, and name the unresolved operand when governance still needs a literal repository path. (#1008) -
Replay now excludes every current task whose reference answer remains readable from the installed harness checkout, refuses an empty eligible set before probes or model calls, and fails closed on snapshot Git errors. Observed transcript paths are normalized before installed-checkout reads fail the attempt and are recorded. (#1019)
-
Release validation now includes staged files when it checks the pending commit against
HEAD, while immutable commit and tag comparisons continue to exclude later staged work. (#1050) -
Correct plugin delivery, review and migration guidance; derive workflow agent status from journal results and require explicit run-completion evidence. (#1058)
-
Read ANSI-C
$'…'strings, here-document markers and quoted tilde-prefixes as bash does, so a command after an escaped quote, a quoted or commented<<, or a mismatched delimiter no longer passes the read-only check or grades below the push it runs, and a quotedcd "~/x"no longer resolves to the home directory. (#1088) -
harness usagecounts a deduplication ratio recorded as NaN or infinity as unknown, so a malformed ledger line no longer adds a measured run or turns the footer and--jsonratio intonan. (#1091) -
Grade a here-document body as the commands it holds wherever the shell runs them: the
$(…)and backtick substitutions of an unquoted body, and the whole of a body fed to a shell (bash <<EOF,cat <<EOF | sh,eval "$(cat <<EOF …)",source /dev/stdin), so a push or a forced push inside one no longer grades as inert text. A body passed only tocatwith a quoted delimiter is still data, and a substitution in a body that cannot be read grades unknown or higher. (#1109) -
cost_bench.py detect --rawreads a one-policy pair’s reference and treatment streams too; it matched only the bare and harness arm names, so a pair’s rule firings were skipped. (#1112) -
A
cdbehindbuiltin,time,command,nogloboreval, a brace-expanded or variablecd, and acdunder CDPATH, aHOME=xprefix or a physical-path option, and acdafter&&,||or|, on the same line or the next, or inside a conditional or loop no longer place a later push or policy write in a directory bash is not in: the walk follows it or leaves the directory unknown. A command that runs only where such acdsucceeded, as inif cd d; then git push; fior[ -d d ] && cd d && git push, is placed there. (#1113) -
The pinned plugin scanner reports no high findings: test stand-in credentials no longer read as secrets, and the sandbox skill’s settings example moved to its own file, its deny list unchanged. (#1120)
-
The grade-bash hook-speed tests measure CPU time over batches, and the neutralize speed check measures one run’s CPU time, instead of wall-clock time, so parallel test suites no longer fail them while a quadratic grader still does. (#1122)
-
Grade text a shell or another interpreter reads from its standard input as what it holds: a here-string (
sh <<<'…'), text anechoorprintfpipes to a shell or toxargs sh -c, a program fed to Python, Perl, Ruby, Node, awk, make or osascript, and a script written to a file and then run. A push in any of them now grades 2 and a forced push 3, where each graded 1. A program the hook cannot parse grades unknown, or as the commands its lines and strings name. Plain data passed to a command that runs nothing keeps its grade. (#1129) -
The grade-bash hook-speed tests take the median of paired large-and-small CPU-time batches instead of the best of each, so a move between fast and slow cores under load no longer fails the growth check while a quadratic grader still does. (#1135)
-
A sourced script, a trap other than on EXIT,
shopt -s cdable_vars,enable -n cd, an alias, or a function defined on the line,cditself included, no longer let a later push or policy write be placed in a directory bash is not in: the directory is left unknown, so the push or write asks. A command behind a long chain of prefixes such astimeornice, which crashed the grader and let it through, now grades 3, and a command either Bash hook cannot read is graded or left to the prompt, never passed. (#1136) -
The Bash grader no longer loses a redirect or a command behind a wrapper’s options: a write into a governance policy file or the approvals store through
nice -n 5,env -i,timeout -s KILL 5,stdbuf -o0,command -poruv runis decided as the unwrapped write is,envreads every word holding a=as an assignment and splitsenv -Sinto the command line it holds, and an option a wrapper does not take, or anenv -Sstring env would interpret itself, grades the wrapped command 3 rather than passing it with no decision. (#1139) -
Replay arm pairs pass the pair check again: the files a harness install writes beyond its links (the global git ignore file, the sync’s ownership record, its empty lock file and the empty plans directory) count as the harness component, and the bare arm holding one is refused. (#1155)
-
A freeze record still naming a tagged release’s branch now fails the test suite, and the stale v0.14.2 record is reopened. (#1161)
-
Keep git’s automatic maintenance out of the repositories the evaluator pack materializes and every repository the test suite creates, so a background maintenance run no longer breaks a tree scan or a temporary directory’s cleanup. (#1164)
-
The micro tier’s caps now fit the harness arm’s first turn: 0.25 USD per run and 0.15 USD per preflight, with a 7.80 USD stop. A preflight that runs out of budget is now reported as a budget stop, with its cost and cap, not as “no reply”. (#1165)
-
The power-pilot pre-registration no longer writes a literal the runner reads as an unfilled placeholder, and the test suite now runs the runner’s plan check over every pre-registration so one it would refuse cannot merge. (#1166)
-
The stop gate now honours the exit its block message offers: after a block, a final message with a line that starts
Gate cannot pass:and gives a reason ends the turn as unverified, logged as adeclineddecision. An ordinary finish is still blocked while the gate is red. (#1169) -
The micro tier’s
micro-stop-gatetask no longer fails the edit its stop gate exists to cause: the prompt allows updating the tests that call the renamed function, and the check requires them updated rather than unchanged. The evaluator pack carries the same fix from version 1.2.0. (#1170) -
Replay trials start with a cold prompt cache: each mounts its own nonce as the managed memory file, ahead of the session segment, so a later rep of a task no longer reads an earlier rep’s segment from cache and understates the arm with the larger one. Rows record
cache_nonceandcache_basis, andsummarisestates the basis. (#1174) -
The delegation verdict can now count read-only Bash commands (
git log,gh … view,rg,sed -n,cat,findand pipelines orcd … &&sequences of them) as gather calls, using theallow-readonly-bashhook’s classifier; re-scoring the 2026-10-02 pilot with it changes no verdict. (#1192) -
Price Claude Opus 5.5 and Claude Sonnet 5.5 from Anthropic’s pricing page, so
harness usageno longer leaves the current Opus unpriced; a session whose per-model breakdown names an unpriced model is now reported as unpriced rather than charged at another model’s rate, and a test fails when a model in a recorded fixture stream has no price. (#1195) -
Run the
designeranddesign-judgeroles on thestrongclass at high effort instead of the frontier class the tiered delegation stance forbids, and fail generation for any role that resolves to the frontier class without a documentedfrontier_exception. (#1196) -
The fresh-session hand-off now holds on 1M-token windows: each cost variant names a soft threshold (160,000), a hard one (400,000 under
balanced, 200,000 underfrugal) and a per-call cost curve, the feed’s line names the cost multiple, and past the hard threshold the turn’s end is blocked once with the hand-off instruction, released on the next stop, logged as an adherence event and skipped in headless runs. (#1197) -
A new
steer-pollingBash hook notes a foregroundsleepover 30 seconds, a loop that sleeps, or an untimedgh pr checks --watch,gh run watchordocker waitwith the background-notification and Monitor alternatives, denies a foreground sleep past five minutes, and logs each decision; the cache-hygiene rule gains the matching line. (#1198) -
A new
session-capshook counts each session’s live subagents and web searches, its subagents’ searches included, warns past 80% of the cost variant’s fan-out cap and the research rule’s 200-search cap, denies a spawn, aWorkflowlaunch or a search at the cap (loggingwould-denyinstead in a headlessclaude -prun), and logs each decision. (#1199) -
The stop gate now verifies instead of timing out or skipping: it runs a repository’s
## Stop gateblock when one is declared (this repository’s covers the lint, the BMad checks and the hook suites in about 75 seconds on an idle machine and about two minutes under heavy load, inside the hook’s 240-second limit), trusts a worktree of a trusted checkout, kills the whole gate on a timeout, and logs every timeout and forced release with its reason and elapsed time. (#1200) -
A session’s own write-intent claim in another worktree now only warns, never refuses, and a claim whose branch has landed is swept with the claims whose worktree is gone. (#1201)
-
The test suite no longer writes rows into the real hook decision log or any other harness state under the user’s home: it runs on a disposable home, a guard test fails if a hook row reaches the real one, and
docs/usage.mdshows how to find and drop the rows earlier runs left. (#1202) -
grade-bash grades every route that discards uncommitted work alike (
git restoreof the working tree,git reset --hard/--merge,git read-tree,git checkout <rev> <path>, andrm,truncateor an empty write of a modified tracked file, among others), no longer grades a verb that appears only in quoted text or in a program that cannot run it, allows reading the approvals store, refuses a destructive command whose grading runs past its deadline, and writes a decision-log row for every Bash decision. (#1203) -
The usage feed no longer reports Claude Code’s own end-of-turn agent, a
SubagentStopwith no start, no type and no transcript, asunknown finished, spend unknown; such stops were most of the feed’s unknown lines, and a spawned agent whose transcript is missing still saysspend unknown. (#1204) -
Make every hook’s decisions measurable from the decision log: the output filter logs bytes in and out, every spawn reroute and each advisory hook writes a row, read-only allows are one count summary per session, fresh-session follow-through no longer needs the observation entry point, the log rotates at a configurable size, the exporter backs off an absent collector, and the tool-output scanner no longer flags the harness’s own files. (#1205)
-
The daily
bmad traceabilityworkflow no longer fails on accepted issues still waiting for a BMad ID: its live audit now prints them aswarning:lines under the new--warn-unreservedflag, while missing issues, title drift, parent gaps and projection drift still fail it. (#1206) -
A reply claiming the tests pass is blocked once at Stop, and a
git pushasks, when no green gate run is recorded for the current tree or the pushed commit;citizen gateruns and records the## Gateblock. (#1207) -
The bash grader no longer asks for the user’s yes when an inline program only reads the harness configuration or a governance policy file, or when gh issue or pull request text in quotes names one; every write to them still asks. (#1229)
-
A stance-gated rule detector now scores a replay run only when the stance selection its arm ran with enables it, so the concise voice’s
voice/scaffold-leakno longer counts the default scannable arm or the bare arm as violations of the all-rules-at-once rate. (#1237) -
Summarise a run stopped at its spend cap by balancing each task to the smaller arm’s trial count and marking the verdict partial, instead of refusing the whole set. (#1238)
-
The long-session tier reads a resumed turn’s cost, tokens and cost by tier as the change in the session’s running totals, so session cost, the cap stop, per-turn curves and segment figures are no longer inflated, and each checkpoint’s check gets the session’s totals from before its segment at
/session-baseline.json. A turn whose resume lost the session, its result naming another session or its totals falling, errors the session asresume-lostand counts at the rest of the cap. (#1243) -
A long-session turn’s budget is now never below 0.01 USD, a session stops before a turn whose budget could cross the whole run’s spend cap, and the dry-run ceiling counts each session at its cap plus that minimum. (#1252)
-
The fan-out cap no longer counts a subagent as finished when another hook blocked its stop: every
SubagentStopis journalled, the one after a block included, and a stopped subagent whose transcript gains a turn after the stop counts as live again until its next stop. (#1254)
[0.14.2] — 2026-09-28
Added
- Added a privacy policy for the Claude Directory plugin and linked it from the manifest and README. (#1052)
Changed
-
Qualified the v0.14.2 release from frozen source
040462636205d8edc766775ebe2cc9571ac6b217on Claude Code 2.1.283 for macOS and Linux. -
Documented the external services that Claude Directory workflows may contact when users request them. (#1054)
Fixed
- Package the Claude Directory distribution from a minimal regular-file bundle so repository history, planning artifacts, installer code and symlinks no longer trigger unrelated directory review holds. (#1056)
[0.14.1] — 2026-09-28
Changed
-
Documentation-only patch releases can now carry unchanged native evidence from an immutable prior release. The validator pins immutable tag commits, preserves evidence provenance across chained patches and fails closed on unknown or changed claims, evidence, limitations, ancestry or any ineligible release path, with a one-release v0.14.1 exception for the reviewed validator and release-reporting files. (#1036)
-
Living documentation now uses Model Citizen, canonical repository links and
citizencommands while preserving compatibility identifiers and dated history. (#1040)
Fixed
-
Recognize model-written framework briefs that refer back to a declared prompt file with a qualified instruction phrase, so the spawn stays confined to its routed review role. (#739)
-
Made Model Citizen discoverable by registries that require standard YAML and readable plugin component files. (#1037)
0.14.0 — 2026-09-27
Added
-
The static context figure now prices every always-loaded rule, selected stance variant and listed description on its own, in a
filesmap inbenchmarks/static.jsonthat sums to the totals within rounding.scripts/cost_bench.py static --checkprints the token and dollar delta of every file that moved since the committed figure, so a pull request that grows one rule names that rule. The 5% growth gate andbenchmarks/allow.jsonstill apply to the total. (#509) -
Added a write-intent ledger for parallel writers.
harness intent claimrecords the paths and globs a session will write, planned new files included, andharness intent releasedrops them; a claim whose process has died is ignored and swept. On Claude Code, an Edit, Write, MultiEdit or NotebookEdit to a path a live sibling session in the same repository has claimed is warned the first time and denied the second;coordination.repeat_overlap: "warn"in the config never denies. Codex raises no edit event, so its builders runharness intent checkbefore committing. Every warning and denial is anintent-overlaprow in the decision log,harness intent mergerecords whether a landing branch merged cleanly, andharness usage --conflictsreports both per week. The check is theintent-overlaphook id, socitizen config set hooks.intent-overlap offswitches it off. (#542) -
Added one selection document and one resolver for it. Stances, rules, hooks, skills, workflows and roles are all kinds in one JSON shape, read from a mode file, the user configuration, the project file
HARNESS_PROJECT_CONFIGnames, and a new session file named byHARNESS_SESSION_CONFIG, in that precedence belowHARNESS_MODEandHARNESS_STANCE_*.harness selectionlists every unit of every kind with its value and the layer that set it, and its JSON reads back unchanged as a session file. A project or session file may now select any kind, and one carrying identity, permissions or another non-selection key is refused with the key named. Switch kinds are resolved and reported here; sync does not yet act on anoffrule, skill, workflow, role or hook. (#554) -
Added hook ids and the
hooksswitch kind. Every policy module that answers a lifecycle event has an id, its basename underpolicy/hooks/, andharness config set hooks.<id> offstops the dispatcher loading it from the next event on both Claude Code and Codex, with no sync. Inline logic follows its owner: Bash grading and its ask tograde-bash, read-only and plan-mode allows toallow-readonly-bash, band routing and the integration notice totier-agent-spawns. Role confinement, by name, marker, framework mapping or evasion, and the Workflow launch guard have no id and stay on with every hook off. The four core ids,grade-bash,stop-gate,brief-guardandneutralize-tool-output, may beoffonly while the user configuration setscore_switches_acknowledgedtrue;config set,syncandharness selectionrefuse otherwise before writing anything, and a hook meeting an unacknowledgedoffkeeps running.harness cataloglists the ids with kindhooks, and each declares its manifest inpolicy/hooks/manifests.json. (#555) -
Added switches for rules, skills, workflows and roles that sync acts on. A unit set
off, for example withharness config set rules.decisions-and-plans off, is absent from both runtimes: no rule link or CodexAGENTS.mdsection, no skill link in either skills home, no command or generated Codex skill, no agent definition in either home, and the spawn hook does not route to anoffband worker. A projection an earlier sync made is retired, andharness diffreports a switch changed since the last sync. The repository’s rules are now linked one file at a time into a real~/.claude/rules/harness/directory; the directory link earlier releases made there is migrated on the next sync and journaled, and uninstall removes the directory.harness selectionprints the always-loaded line count of the selection in force; the lint cap is unchanged. (#556) -
Added modes: named selection bundles in
primitives/modes/<name>.json, selected withharness config set mode <name>orHARNESS_MODEfor one session. Two ship:full, the defaults, andminimal, which keeps every hook and the cost, delegation and autonomy stances, turns the other stances off or light and switches every workflow off. A mode sits below every key you typed and above the stancesharness initwrote as defaults, whichconfig.jsonnow records underinit_defaults;harness selectionnames each mode key a higher layer shadowed. An unknown mode, a mode file naming an uninstalled unit or a non-selection key, a mode name two roots both define, and a mode switching a core hook off withoutcore_switches_acknowledgedare refused before sync writes anything. Seedocs/modes.md. (#557) -
Qualification evidence is now invalidated per acceptance case inside each target. The catalog’s versioned
case_pathsmap names the source paths each required case depends on, so a change under a mapped path makes only the cases that name it stale, and a rerun of those cases alone completes the claim. A change under a path no case maps still invalidates the whole record. Each new evidence record states the map version and digest it assumed, and a record without them keeps the whole-target rule. The first map assigns both runtimes’worker.pyto the four cases that run a role worker. (#582) -
Added
--codex-session-logintoscripts/native_acceptance.py. With it, each case’s disposableCODEX_HOMEgets anauth.jsonsymlink to the operator’s Codex session login, read fromCODEX_HOMEor~/.codex, the way the Codex worker adapter links it, so a Codex target can be driven by the runner instead of by hand. The flag is off by default, refused for a Claude Code target and when no login file exists, and every string in the login file is redacted from each observation, the progress log and the record. Codex verdicts stayunverifieduntil an operator compares a runner-driven case against a hand run and passes--home-confirmed. (#612) -
Every row the usage ledger and the decision log write now names its
schema_version, and every reader carries fields and versions it does not know instead of refusing them, so a ledger written by a newer release reads without error in an older one. A renamed field ships a fold map in the writing module and is read under its new name, and no existing row is rewritten to get there. The rule is documented under “Ledger schema” in docs/usage.md. (#644) -
Added a manifest for every switchable module. Each shipped rule, skill, role and workflow declares its claims, surface, instruments, slot, dependencies and conflicts in
primitives/manifests.json, and hooks without switch ids remain outsidepolicy/hooks/manifests.json; hooks with switch ids already declare theirs there. The selection resolver refuses a shipped module with no manifest or a missing field, a switched-on module whose dependency is off, two switched-on modules that conflict, and two that claim one slot unless one cedes it, naming each module involved.harness selectionshows each switch unit’s instruments, orunmeasured. A module from your own primitive root may omit its manifest and keeps resolving. (#788) -
Every new row in the usage ledger and the decision log now names the
profile_fingerprintof the profile that wrote it: a digest of each switched-on module’s content, the stance variants, the configuration that reaches the model or a hook, and the harness version. A row whose profile could not be resolved carriesnull. Role workers and replay rows carry it too, withbarefor the replay arm that loads no harness. The field ships under schema version 1, which this release is the first to carry. A row written before this reads as unattributed and is never given a guessed fingerprint, andharness usage --by profiletells two profiles apart by their rows alone. (#789) -
A session row in the usage ledger now carries
context_attribution: the tokens each switched-on rule, skill, role and workflow and each stance put into context before the first prompt, labelled a soft estimate and naming its method, four characters a token over the module’s resident text. A skill, role or workflow counts its listing entry, never the body it loads on demand. Replay rows carry it too, with no module for the bare arm, and a rescan never attributes a past session to today’s selection. Every decision-log row now names themodulewhose hook made the decision, ashooks/<id>, ornullfor a point no hook owns. (#790) -
An observation-only hook entry point,
adapters/<runtime>/observe.py, sits besidehook.pyand outside its dispatcher. It appends one identifier-only row per hook event to~/.local/state/agent-harness/observation.jsonl, never a message body, and fails open and silent: exit 0, no output, errors to a local error log. Its registration reads the same event table ashook.py, andharness_core.observation.bare_installwrites a bare arm’s whole footprint, the entry point and its registration. A zero-footprint test drives a scripted session against a stub model through every installed hook event and proves each model request byte-identical with observation on and off, in the bare arm and the harness arm.harness syncdoes not register the entry point yet. (#791) -
Adherence events. When the usage feed says the fresh-session line, it now appends an
emittedrow to~/.local/state/agent-harness/adherence.jsonlnaming the recommendation, the emitting module, the session and the turn.policy/hooks/adherence.pyanswers each emission from the observation ledger asfollowed,not_followedorunknown, and computes a rate per recommendation from identifiers alone. Recording never changes what the feed says. The observation entry point is not registered in live sessions yet, so each session start answers a live emissionunknownonce it is a day old. (#792) -
Added an evidence standard for published results,
docs/evidence-standard.md, which lists the twelve things every proof set must carry and what satisfies each, quoting SM-2 where it defines the terms. Addeddocs/pre-registration-template.md, the plan a proof run fills in and commits before its first trial, so the order of plan and trial can be checked from the history. (#794) -
Added a
concisevoice variant. It picks each reply’s shape by what the reply is for (a one-line done message, a short answer, a report, a decision, a brief or a deep dive) and holds every reply to seven rules. On Claude Code it also selects the built-in Concise output style, verified present in Claude Code 2.1.280, with earlier versions unverified; Codex receives the same stance text through AGENTS.md. The default voice is unchanged. The What-changed reply template no longer appears in the always-loaded rules, and two new detectors count template labels and heading-first replies under this voice. The always-loaded caps rise to 225 lines and by the stance’s 620 tokens to pay for it. (#811) -
Branches that each reserve a BMad ID now merge without an issue-map conflict. A git merge driver keeps main’s map, adds the branch’s entries and the larger
next_ids, and lets main’s entry win when both sides mapped one issue; a second driver makes the merge commit carry a freshly rendered sprint status.citizen worktree createregisters both where the repository ships them, and a genuine duplicate still conflicts and fails the audit. (#819) -
The
localdecision provider now reads a user-levelgovernance.jsonbesideconfig.jsonunder the repository’s.agent-harness/governance.json: the repository file wins for defaults and pair entries, and caps combine by the lower value. Arepo:<name>pair applies to every branch of that repository, after an exactrepo:<name>/<branch>pair and before the class default, andcoding.pr_mergeis a new action class. Every answer names the file that supplied its level, andharness decideandharness doctorlist the policy files read. (#874) -
Added the
citizencommand;harnessremains a supported alias. (#877) -
Bound the decision provider into Bash command grading. When
governance.provideris notnone,grade-bashclassifies each simple command ascoding.git_push,coding.git_commit,coding.pr_merge,coding.deployorcoding.shell_exec, derives itsrepo:<name>/<branch>counterparty from the directory it runs in (git -C <dir>and an earliercd <dir>included, and the repository’s name rather than a worktree directory’s), and asks the provider about every command the autonomy stance lets through. The provider only tightens: itsaskis an ask in a prompting mode and a deny with an approval code in auto mode, and a configured provider that raises asks, naming the error, instead of allowing. An agent write to a governance policy file or to the userconfig.json, by Bash or a file tool, and anyharness config set governance..., is always asked about, and each governed decision is onegovernancerow in the decision log. Under providernonethe hook’s output is unchanged. (#884) -
Added the workspace map: set
workspaces_dirto the folder holding your.code-workspacefiles and they become the only definition of which folders belong together, worked out on every call with an optionaloverrides.jsonfor shared folders.citizen workspace listshows each workspace’s members and instruction size and how every shared folder resolves, andcitizen workspace open NAMElaunches Claude Code, or Codex with--codex, in the first folder with the others as--add-dir. Seedocs/workspaces.md. (#935) -
A session opened in any member folder of a workspace now learns, at start-up, the workspace’s other folders and their instructions: a new
workspace-sessionhook, registered as its own SessionStart entry on Claude Code and Codex so it has its own output cap, supplies each member’sCLAUDE.mdorAGENTS.md, local file and unscoped rules, skipping on Claude Code the members the running session already loads through--add-dir. Past 9,000 characters the instructions go to a file under~/.local/state/agent-harness/workspaces/that the session is told to read first, and in the desktop app Claude is told to request a member folder on first use. Whileworkspaces_diris set,syncalso ownsCLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1, by the native telemetry rules. Seedocs/workspaces.md. (#936)
Changed
-
A project or session stance selection that differs from the synced one now reaches the model as that variant’s text at session start, counted against the always-loaded token budget
citizen lintenforces, less what the sync already loads; a variant that does not fit is named with a pointer to its file. A session whose selection matches the synced one gets nothing injected, and thecustom-stancequalification case now observes a session selection natively beside the project one. (#276) -
scripts/cost_bench.py replayruns its two arms as fresh Docker containers instead of profile directories, so nothing from your machine reaches either one: the bare arm is the Linux qualification image’s pinned base plus Claude Code, with the base’s own Codex client removed, and the harness arm adds this repository at the full commit a--tagnames. The installed harness is no longer an arm, and--bare-config,--harness-config,--harness-repoand--claudeare gone. Each build writes a declaration of its inputs and a hashed manifest of everything the image holds;cost_bench.py arms checkbuilds each arm twice with no cache and fails if the manifests differ. A run mounts only the task snapshot, passesCLAUDE_CODE_OAUTH_TOKENby name, reaches only the model API through an allowlist proxy (arms probe-egressproves it), and scores the agent’s work in a fresh container.--verify-tasksruns each task’s gate and checks in that container too, where they no longer read your live configuration through HOME and go red for it. Every row records the image, its manifest and declaration digests, and the harness ref and commit. This closes the personal-layer leak in #428. (#559) -
CI now holds the Python 3.9 floor on every pull request and merge queue run. The required
testcheck parses every tracked Python source under a real Python 3.9, so newer syntax fails it, and a newtest-py39job runs the whole suite under Python 3.9 to catch newer standard-library use. That job is not yet a required check. (#640) -
The “on the way” list in
product.jsonand the README now names only planned work the roadmap backs, each entry tied to an open issue on a milestone: proof set 1 against bare Claude Code (#799) and the superpowers mode (#558). The README carries no em dash, including the compatibility note generated into it, and a test keeps its “on the way” list equal toproduct.json. (#648) -
A red smoke tier now stops the qualification round:
scripts/qualification_round.pylaunches no target when the tier fails or times out, records the tier’s result, why the round stopped and the targets it did not run inround.json, and exits non-zero. This enforces FR-52, which says a round passes the model-free tier before any native case runs;--skip-smokestill records the tier asskippedand runs every target. (#688) -
Five native case drivers now observe what their qualification procedure step asks for.
cost-posturechecks that every role the cost variant leaves unchanged is still the same link rather than an identical copy, and that the band worker’s brief ends with its row’s budget sentence.spawn-confinementjudges its routed review run a second time with the run’s worker state moved aside and requires that judgement to fail.hook-compositionrequires the harness’s ownPostToolUsenotice in the write turn’s transcript beside the user’s hook.migration-uninstallreads the hand edit as drift fromharness diffbefore uninstalling. The procedure now states the narrower claims no run shows: on Claude Code a multi-file patch is one turn’s file-tool writes, and the auto posture is claimed as a mode, not as a restriction. (#742) -
/buildgains a step that answers a review bot’s threads before a pull request goes up for approval: it fixes or replies to each one and resolves it, asks the bot for one more review after a fix, and leaves a human’s thread alone./landstops on an unresolved review thread and names it, instead of learning about it when a branch rule refuses the merge. This repository now runs CodeRabbit on its own pull requests, from a.coderabbit.yamltuned to it. (#785) -
The repository’s
## Gateblock now runsscripts/bmad_issue_sync.py sprint-status --checkandscripts/bmad_issue_sync.py auditbefore the unit tests, as CI’stestjob does, so a story whose filled sections change its derived status fails the local gate and thestop-gatehook instead of only the pull request’stestcheck. (#820) -
The
plan-authoringskill now has you run the repository’s named lint on a plan before posting it and after each revision, since/buildcommits the approved plan into its pull request and a plan reworded at build time is no longer the one that was approved. For the harness lint, copy the plan alone into an empty directory and run the checkout’sbin/citizen linton that directory. (#821) -
/buildstep 6 now says what a finished bot review looks like: the bot’s commit status on the head commit completing, such as CodeRabbit’ssuccess: Review completed. The empty review a bot’s thread reply creates no longer reads as a pass, aReview skippedstatus or a review that never starts now prompts a request with the bot’s review command, and the wait for a pass rises from ten minutes to fifteen, since a requested re-review can take close to nine. (#822) -
bmad-code-reviewin this repository now runs straight through when an approved plan invokes it, or when it is invoked withunattended: it proceeds past the context summary, applies every patch, records every finding in the story and ends with only the decision-needed findings as numbered questions. Invoked any other way it still halts at each checkpoint, and blocking halts stop it in both modes. (#823) -
/landnow gates only on required checks, and on an issue-ownership or closing-link check where the repository runs one. It names every other check or commit status with its state but never waits on it, so a review bot’s status that sits pending for minutes no longer reads as a reason to stop. (#824) -
The plugin is now
model-citizen@model-citizen. An existingagent-harness@agent-harnessinstall keeps its ID and fails to load once Claude Code updates its copy of the marketplace, so move it: with a checkout installed, runcitizen upgrade(orcitizen upgrade --dry-runto see the commands first). It uninstallsagent-harness@agent-harness, removes theagent-harnessmarketplace, adds the marketplace again from its recorded source and installsmodel-citizen@model-citizen, in that order, through theclaude plugincommands. A plugin-only install runs the same four steps with/pluginin a session; adding the marketplace again before removing the old one does nothing. Skills move from/agent-harness:<name>to/model-citizen:<name>.citizen doctorrecognizes either ID, and it andcitizen syncnamecitizen upgradewhile the old ID is enabled. (#878) -
Renamed the product to Model Citizen. (#879)
-
Links now point at the renamed repository,
JakeSelby/model-citizen, and the new site, https://model-citizen.dev: the installer, the release notes, the issue sync tool’sbootstrapdefault, the issue template, README, CONTRIBUTING, the install and release docs, and the plugin manifest’shomepageandrepository. The landing copy names the site as the repository homepage and trades thecoding-agenttopic formodel-citizen. A newretired-namerule incitizen lintfails a line in this repository that names the old brand, repository slug or site host, outside the dated areas that keep them on purpose and the “Formerly” lines. Existing links keep working, since GitHub and the old host redirect. (#880) -
The always-loaded set is ten lines and about 160 tokens lighter, down to 213 of its 225-line cap and about 4,204 of 4,822 tokens, so the next rule or stance line fits without raising the cap. The cuts remove only restatement and detail a pointed-to skill or document already carries: the concise voice’s closing summary of every shape’s opener and one rationale clause, the decision block’s shape (in
plan-authoring), the credentials bullet folded into one line that keeps its sources and bans, thebrief-guardhook’s behaviour, and the global instructions’ note on why the cap exists (inharness-authoring). (#933) -
Every test of cost, efficacy or a change to the system now follows an eleven-point experiment protocol in
docs/evidence-standard.md: pre-registered, run in fresh containers holding exactly the named components, and never from a host profile, a live checkout or a disposable home. Section 3 no longer allows a run without a container.scripts/cost_bench.py replayrefuses to start without--pre-registration <path>, a committed, dated plan with its question, hypothesis, primary metric, decision rule and sample size filled;--exploratoryruns without one, labels every row exploratory and writes no history row. Before any launch the replay also refuses an arm whose manifest differs from its declaration, which carries settings, hooks or other configuration no declared component supplies, or which names your home directory, your profile or this checkout in its recorded inputs, a mount or a variable. An errored, crashed or timed-out run now counts as a failed attempt with its cost in the summary and verdict, and stays countable apart by itserrorfield. (#1010)
Fixed
-
The
cost=maxstance allowed compaction while the always-loadedcache-hygiene.mdrule still said never to compact and thecache-hygiene/compactdetector counted every compaction as a miss. The rule now names the exception, the detector no longer counts a compaction under acostvariant whosecompactionswitch iscompact-allowed, anddocs/preferences.mddocuments that the switch decides and that a session override lifts the rule for that session only. (#117) -
The preferences guide no longer says a sync links a
HARNESS_STANCE_*value. Such a variable is a session selection:citizen stancesand the hooks resolve it and the session-start hook injects its text, while a sync keeps linking your user-level variant.citizen diffnow says, when a project or session selection changes a resolved stance, that it compares against your user-level selection and does not count that selection as drift. (#294) -
A Claude Code
Workflowscript could run a constrained role such asreviewerin session, unconfined, because a script’sagent()calls never reach the spawn hooks. The pre-tool hook now reads the launch: it refuses a script that names a constrained role inagentTypeor carries aharness-role:marker for one, with the sameharness role runinstruction a native spawn gets, refuses a script file too long to read in full and every launch underdelegation: off, and writes aworkflow-launchrow to the decision log. A script’s other agents are still not routed to a band. (#576) -
harness usage --by rolenow reports Workflow-tool agents in their own(workflow)bucket instead of under the role their agent definition is named for, so they no longer move that role’s percentiles. The usage ledger marks such a rowunconfined: trueand records its soft budget asnull, because the tool launched it without a spawn hook or a budgeted brief. (#577) -
Qualification provisioning now excludes its
.harness-round-clonemarker in the frozen clone’s own.git/info/excludeand refuses to finish if the clone is still dirty, so a freshly provisioned round runs without hand edits instead of failing every case on the runner’s clean-checkout check. (#677) -
Under the
delegation: offstance, thetier-agent-spawnshook asked for confirmation of every spawn while the stance said a spawn is denied outright. The lifecycle already denies the spawn before this hook runs, which is why the engine was seen denying it; the hook now denies too, with the lifecycle’s reason, and its header comment says so, so a direct run of the hook agrees with the stance. (#687) -
A resumed qualification round ran every case again.
scripts/native_acceptance.pynow skips each case its durable log already holds apassedorfailedverdict for at the same source commit, client version and routing, and says so on stderr. A failure is kept rather than rerun so its evidence survives the resume; anunverifiedcase runs again and its new result supersedes the old one. (#689) -
A framework review layer’s spawn whose brief the model wrote in its own words is now refused when it tells the subagent to follow or apply one of the layer’s declared prompt files, in any wording. A brief that edits, updates or rewrites that file, or reads it for another reason, still runs. Step 9 of the qualification procedure again requires that refusal, and the
spawn-confinementcase fails when the model’s own brief runs, so the catalog’s lexical-recognition limitation is dropped. (#739) -
harness role runlaunched from inside a Claude Code session logged in withCLAUDE_CODE_OAUTH_TOKENalone started a Claude worker that failed with “Not logged in”, because the client strips the token from its tool subprocesses. Inside a session the Claude adapter now runsclaude auth statusunder the worker’s environment first and refuses before launch, naming the missing token and the two fixes, when no login is confirmed. The token is never written anywhere. (#759) -
A native spawn refused because it names a constrained role, by
subagent_typeor by aharness-role:line in its brief, now writes arole-confinementdeny row to the decision log naming the role and what named it, as framework-spawn and evasion refusals already did. The usage ledger no longer counts a refusedAgentcall, one whose result is an error and which left no subagent transcript, in a session’ssubagentsfigure; a session file in the older format, with subagent turns written in as sidechain lines, still counts every call. (#760) -
The refusal a natively spawned constrained role gets now names the CLI by the absolute path of the checkout’s
bin/harnessinstead of a bareharness, which no documented install puts onPATH. A test runs the command the refusal names with noharnessonPATH. (#761) -
Qualification provisioning with
--bmadnow installs BMad with the exact commanddocs/bmad.mdgives the optional integration suite,--shimsincluded, soharness integration apply bmadon a freshly provisioned framework root exits 0 instead of reporting legacy review-name drift. (#762) -
harness role runaccepted a--read-diras broad as/tmp, so a review worker could read every other run’s files, including an earlier report naming a blind review’s planted defects. It now refuses/, the home directory, each system temporary root and any directory above one of them, naming the directory and a dedicatedmktemp -dsubdirectory to grant instead. (#772) -
A release pull request that assembles the changelog fragments into a new version section now passes the lint’s fragment rule without adding a waiver, so no waiver outlives the release. (#773)
-
The live replay’s two arms are comparable again. Each scored run passes its task’s
max_turnsto the CLI, deniesWebFetchandWebSearchto both arms whatever their profile allows, trusts its snapshot for the stop-gate hook for that run only, and is captured asstream-jsonwith hook events, so every row records how often the Stop hook ran and blocked.usage-pricesleaves the task set for the manifest’s newretiredlist, since no arm could pass it honestly, and a test now fails when the benchmark docs state a task count the manifest does not hold. (#793) -
The Scannable output style, the default voice, now declares
keep-coding-instructions: true, so selecting it keeps Claude Code’s default coding instructions in the system prompt instead of replacing them. A test now requires the key on every output style the harness ships. (#810) -
/close-outnow asks once, before it lands anything, for a go-ahead that names its follow-ups, the merges and any pull request the filing needs. When filing writes tracked files, such as an issue map or story files, it runs from a new worktree off the updated default branch and puts what it wrote in its own pull request, which the close-out lands itself, and it no longer archives until that pull request has merged. Before, nothing landed the filing’s changes, so they were left unmerged when the session archived. (#816) -
Files an agent sends with
SendUserFilein a Remote Control session now open in the app. Claude Code serves a sent file only from under the session’s working directory, so a newstage-user-filespolicy copies a file from anywhere else — a temp or scratch directory, a task worktree, the main checkout seen from a worktree — into.agent-harness/outbox/there, which git ignores, and sends the copy. (#817) -
Changing a stance variant no longer leaves the old variant in place when its link was made through
claude/stances, the alias ofprimitives/stances. Sync used to report the harness’s own link as a user redirect, exit 2, and refuse to relink even with--adopt; a link that resolves to the recorded file is now relinked, and a link to a file outside the stance sources is still preserved. (#828) -
Files an agent sends from a session a Remote Control host started now open in the app. Claude Code gives such a session no upload route, so every file arrived marked “not delivered” and the iOS app greyed its card out. Every host
harness remote-control installwrites now setsCLAUDE_CODE_BRIEF_UPLOAD=1, an undocumented Claude Code variable its sessions inherit, so the file is uploaded with the signed-in account; a folder’senvcan set it to an empty string to turn it off. Re-runharness remote-control installto apply it to running hosts. (#858) -
In
automode the classifier refused theHARNESS_CONFIRMED=1marker as a bypass of a safety hook, so a commandgrade-bashgated could never run, whatever the user said. The auto-mode deny now names an approval code; the user repliesapprove <code>as the whole message, a newapprovalsUserPromptSubmit hook records it (a prompt with any other text records nothing, so a notification turn carrying agent-controlled text cannot approve anything), and the same command, with no marker, passes once in that session within thirty minutes. A Bash write to the approvals store grades 3 and a file-tool write to it is denied. The marker still confirms inbypassPermissions, and prompting modes still ask. (#859) -
The read-only Bash grammar read an operator written directly after a closing parenthesis as part of the preceding token, so
(true);lswas split as one command. Separators directly after a closing parenthesis or brace, a separator directly before an opening parenthesis, and a redirect directly after a closing parenthesis are now split as they are with spaces around them, in bothallow-readonly-bashandgrade-bash. (#896) -
With a decision provider configured,
grade-bashno longer treats issue and pull request text that mentions a policy path as a policy-file write: a quoted--bodyor--titlevalue, or a quoted here-document fed to--body-file -, on a singlegh issueorgh prcreate, comment, edit or review command with nothing else on the line. Every other line that names a policy path is still a level-1 write, because almost any command may run code that writes a path it only names, and redirects,tee,sed -i,cpandmvtargets are still judged as before. (#898) -
The
grade-bashhook graded a segment made only of a redirect, as after a subshell in(ls) > out.txtor(ls)>out.txt, at 0, so theaskandconfirm-writesstances let the file write through without a prompt. A redirect that writes a file now grades at least 1 whatever the segment around it, and a/dev/nulltarget still grades 0. (#899) -
grade-bashtold the user thatgh repo renamelocks the repository read-only for everyone, because the rename shared the archive branch and so printed the archive’s reason. The rename still grades 3, but its reason now says it moves the repository to a new name and that the old URLs redirect only while no repository takes the old name. (#900) -
Governance attributed a piped command after a literal
cdtorepo:unknown/local, socd <repo> && git push origin HEAD 2>&1 | tail -1was refused where the same push without the pipe was allowed. The directory walk ingrade-bashnow follows Bash precedence: every element of a pipeline starts in the directory in effect when the pipeline begins, and only acdinside a pipeline element, a subshell or a background job is confined to it. (#903) -
scripts/bmad_issue_sync.pysent every GitHub API call to the issue map’s stored slug, which still names the repository’s old name. GitHub answers a write to an old slug with a 307 thatgh apidoes not follow, so after the renamenewfailed before filing andapplycould not update issues. API calls now go to the repository’s current slug, and the planning-block links keep the slug the map stores. (#904) -
Two edits at once from one session and worktree could both read an intent-overlap counter of zero and both be warned, so the repeat overlap was never denied. The counter’s read-modify-write now holds an exclusive lock on the hit-counter directory, so the second concurrent edit is denied. (#930)
-
The governance check on
harness config set governanceno longer gates issue and pull request text that only mentions the command. It takes the same one exemption as the policy-file guard, judged by the same lexer: quoted text passed to gh’s built-in issue and pull request create, comment, edit and review subcommands. A real invocation stays a level-1 action wherever it sits, including after a separator or a pipe, in a subshell, or undereval, a shell or an interpreter. (#931) -
The worker context-budget test that proves a renamed skill directory refuses the spawn no longer rewrites
primitives/roles/design-judge.mdin the checkout while it runs. It breaks a copy of the role tree in a temporary root instead, so a sync, a live spawn or a second test run reading the checkout at the same moment never sees a role that names an unknown skill. (#932) -
Adherence emissions are now answered in a live session. Nothing outside the tests called
adherence.settle, so the ledger heldemittedrows that never got aresponse. Each session start now settles every emission that is due, writing one response row per emission, holding no message text and adding nothing to what the session is told. (#1013) -
citizen config unset workspaces_dirswitches workspace support off from the CLI. Before it, the key could be removed only by editing the user configuration by hand, sinceconfig setrefuses an empty value. The newconfig unsetremoves only keys whose absence is a documented state, so it cannot bypass the checksconfig setruns on switches, acknowledgements and governance keys. (#1014) -
Recognize a framework layer prompt through an explanatory em-dash clause, so a later directive to follow those instructions cannot run the layer as an unconfined native subagent. (#1031)
[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.