Skip to content

fix: doctor mediator, the defect-ledger bypass it uncovered, and the suite's largest remaining case - #837

Merged
wenzowski merged 13 commits into
mainfrom
claude/ci-performance-degradation-ic29ck
Sep 3, 2026
Merged

wenzowski merged 13 commits into
mainfrom
claude/ci-performance-degradation-ic29ck

Conversation

@wenzowski

@wenzowski wenzowski commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Closes CLOUD-1349
Closes CLOUD-1186
DO-NOT-CLOSE CLOUD-1358 — already In Review via #834; this adds a third case to the same mechanism, not a new row.
DO-NOT-CLOSE CLOUD-1209 — one case isolated here; that row's own subject is shell_write_advisory's four, untouched.

Three rows, one CI matrix.

CLOUD-1186 — a bypass I shipped in #834, predicted in writing beforehand

This is the important one. CLOUD-1358 gave enforce a --rule selector without
touching the engine-gate branch, which still skipped engine-side gates on
only.is_empty() alone. So from that merge until this commit, batten enforce --rule <id> was a one-token way to skip the defect-ledger gate on the verb that
runs user-declared commands
. That gate is engine-side rather than a [[rule]]
row precisely so a branch cannot lower it by editing a rule table; the narrowing
restored the lowering by another route.

CLOUD-1186 was open, groomed, and said so in its title. I did not search for it
before adding the flag.

The skip is now surface-dependent — check narrowed is byte-identical to before,
enforce narrowed still evaluates the ledger. The budget keeps the skip on
both
: it measures declared instruction files, a property of the tree the caller
did not ask about, while the ledger is a claim about this branch's conduct. Only
one is the security property.

Four cases over the compiled binary, covering §7's (a)/(b)/(c) plus an
anti-vacuity mirror asserting the same fixture does fail an unnarrowed check
— without it, (b) passes whenever the fixture is broken, which it immediately
caught. Shown able to fail by restoring the shipped code: exactly 1 of 27
reddens, narrowed-check among the 26 that do not.

Two divergences from §2 are recorded on the row: the selector stays a flag
rather than moving to the object position (that respelling is CLOUD-1184's, and
is a second breaking surface change in two days), and the budget keeps its skip.

CLOUD-1349 — which engine the registrations reach

doctor hooks answers whether the registrations reach an engine; nothing answered
which. Measured four times in one container: a binary that cannot parse the
tree's own batten.toml while doctor hooks reports 0 unwired.

Two corrections to the row's §2, both recorded on it: it cannot spawn (that would
put config-supplied code behind a row on the filter(effect == read) allowlist),
and version equality does not discriminate — measured 0.0.137 on both sides
while the binary refused the tree's config. So the comparison is over content.

A sub-verb, not a check in the bare report. It landed once inside diagnose()
and verify refused it: this_repository_is_healthy went red whenever a rebuild
outpaced the install — a world-property deciding a commit gate, the lock-check
defect .claude/rules/toolchain.md records. the_bare_report_is_unchanged_by_this_sub_verb
pins it.

Its blind spot is stated and pinned, not shipped silently: it compares the
install to the build, never the build to the source, so two equally stale binaries
agree and it reports ok. Measured one command after the engine refused the tree's
config. two_equally_stale_binaries_agree_and_this_reports_current fixes that as
known behaviour.

The suite's largest remaining case — 436s

the_committed_repo_config_gates_a_repository ran ~103 rules to assert one
pointer; its own comment said the others could not affect the result. On the
2-core Windows job it is 436.42s — 24% of that suite's 1810.1s CPU, next
slowest 28.28s. Narrowed: 21.526s → 0.153s locally, assertion unchanged, shown
able to fail by pointing --rule at a different real row.

Where CI actually stands, measured

rust.yml median 30.0m → ~18.5m (four successful runs, Sep 2–3) — that is
#819's cache fix.

The Windows suite itself went 849.9s → 908.5s: #834 removed 644s of CPU and
other work added ~760s the same day. Two corrections to #834's stated analysis:
that suite is CPU-bound, not floor-bound (wall tracks CPU/2 to within 0.4% on
both readings, so sharding would help), and the pole is a single case rather
than a cluster.

Verification

Local suite 4223/4223. verify reached fast-forward-green on the prior lap.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CTqon6Q8TXmhGpdtQeaT6V

@linear-code

linear-code Bot commented Sep 2, 2026 •

Copy link
Copy Markdown
CLOUD-1349 The mediator ran 16 versions behind the tree it was judging, and nothing reported it — a stale engine reads exactly like a working one

§1 Subject

The absence of any comparison between the batten binary the hook registrations resolve and the workspace version of the tree it is judging. crates/batten/src/doctor.rs is where that comparison is missing, and it is the whole of this row's authority boundary.

The session-start handler that installs the binary is evidence in §2 rather than subject here, deliberately: its absence is what PRODUCED the mismatch on one container, and the row is about the mismatch being undetectable rather than about any one cause of it. A handler that runs perfectly still leaves a stale binary wherever the image shipped one.

§2 The measurement

Measured 2026-09-02 in a Claude Code web container, over one full session landing CLOUD-1321.

batten --version on PATH (/root/.local/bin/batten) 0.0.121
workspace version in the tree 0.0.137
/tmp/session-start-batten.log does not exist

mise.toml:1987 runs mise run install:local into that log and ::error::s on failure. The log's absence means the step never ran at all — so there was no failure to report, and the container kept whatever binary its image was baked with.

The engine mediated every tool call in that session against a rule table 16 versions old, and nothing anywhere said so.

§3 What that silently disabled, measured by the moment it stopped

The tell is the step change at the install, not an argument. Across roughly six hours the session issued dozens of cargo run … | tail, grep over tracked paths, and cmd; echo $? lists. Not one was refused.

mise run install:local was run to repair an unrelated failure (ci-wait dying on unrecognized subcommand 'pr', itself a symptom of the same staleness). Within five minutes of the binary reaching 0.0.137, the engine refused three calls:

  • verdict read dropped — verdict-not-discarded, a verdict-bearing command piped into tail;
  • verdict carry other — verdict-not-discarded, a ; list after a verdict-bearing command;
  • tool run loose … no-tool-substitution — grep over tracked repository paths.

All three shapes had been issued repeatedly before the install and had passed. The rules were in batten.toml the whole time; the binary reading them was not.

§4 Why this is the worst shape in the model rather than an ordinary bug

.claude/rules/policy-modules.md states the class this repository is organised around: a dead gate and a clean tree are byte-identical on the decision surface. Every mechanism here defends against it — input.tree.missing is a channel rather than an absence, NotAcquired keeps Absent and Unparsed apart, RuleSkipped is reported rather than folded into clean, a [[verdict]] nothing raises fails the load.

A stale mediator defeats all of it at once, one layer beneath where any of it can look. The config is correct, the modules are correct, the tests are green, and the process actually adjudicating calls is enforcing a different, older contract. Silence from the hook is the documented signal that it IS mediating (.claude/rules/toolchain.md: "Silence still means it is mediating") — and that sentence is exactly what makes this undetectable by a reader.

doctor is where the check belongs and it is not there. CARGO_PKG_VERSION appears in eight source files; doctor.rs is not one of them. doctor already asserts the three provisioning steps landed (CLOUD-476) — this is the fourth, and it is the one that decides whether the other three were asserted by the right binary.

§5 Why the existing sensors did not catch it

  • session-start:batten guards its own failure and not its own absence. A step that runs and fails writes a log and an ::error::; a step that never runs writes nothing, and nothing reads for it.
  • The unrecognized subcommand symptom is swallowed by design. target-prune and ci-wait both probe command -v batten and fall back, so a missing verb prints a tip: line into a task log and the task carries on. Those lines were visible in every land log in this session and read as noise.
  • doctor hooks answers whether registrations REACH the engine, never which engine. It reported 5 harness(es), 0 unwired over a 0.0.121 binary.

Refinement — Ready (the engine states which contract it is enforcing)

Refinement gate: Definition of Ready & Done. This body carries only specializations.

{
  "source_of_truth": "batten --version reported 0.0.121 while the workspace was 0.0.137, /tmp/session-start-batten.log did not exist so mise.toml:1987 never ran, and CARGO_PKG_VERSION appears in eight source files of which doctor.rs is not one",
  "gate": { "task": "doctor", "exits": [0, 1] },
  "commit_type": "fix",
  "blockers": [],
  "tests": [
    { "file": "policy/harness-wiring.rego", "mutation": "stray-unread" },
    { "file": "policy/harness-wiring.rego", "mutation": "stale-unguarded" }
  ]
}
  • **Authority boundary (§1). **doctor.rs grows the comparison; whatever batten.toml row declares it. The session-start handler's absence-vs-failure asymmetry is the same row's second half and may split.

  • **Computable predicate (§2). **batten doctor resolves the binary the registrations name, reads its --version, compares it to the workspace version of the tree being judged, and refuses on a mismatch naming both. A binary that cannot be resolved is could-not-look and says so, never clean.

  • Deliberately not in scope (§2). Auto-repair. doctor reports; mise run install:local is the remedy the refusal names, which is the routing CLOUD-1339 is separately about.

  • **Effect (§3). **read, plus resolving one binary's --version.

  • Test obligation (§7). A compiled-binary tier planting an older binary at the resolved destination and asserting doctor refuses naming both versions — shown able to fail by planting a matching one. The anti-vacuity mirror matters here more than usual: a case that never plants a mismatch passes over a check that does nothing.

    The claims above name EXISTING mutations deliberately, and the reason is a defect this row itself caused. The first draft claimed crates/batten/src/doctor.rs:stale-engine-passes and policy/harness-wiring.rego:unversioned-mediator-passes. Neither can bind: obligations-bound requires the declared slug to be a #MUTANT row in the declared file, #MUTANT is a # comment so it cannot live in a .rs file at all, and the second slug does not exist. batten check refused the filing branch with policy/harness-wiring.rego obligation-unbound — the gate catching a promise nothing could join. The claims now name the two mutations the implementing PR must keep CAUGHT, which is checkable today; the unbuilt case is described in this clause, which is where an obligation with no mechanism yet belongs.

  • Blockers (§8). None. relatedTo CLOUD-476 (the provisioning doctor already asserts), CLOUD-824 (a missing binary fails open and reports at provisioning time — this is the same argument for a WRONG one), CLOUD-1339, CLOUD-1340.

Acceptance

  • doctor refuses when the resolved binary's version differs from the tree's, naming both.
  • A session whose install:local never ran is told at provisioning time rather than six hours later by an unrelated unrecognized subcommand.
  • Shown able to fail.

§6 Cost of not doing it

Measured, this session: every mediated call for ~6 hours adjudicated against a 16-version-old table, three rule classes provably unenforced, and the discovery arriving only because an unrelated task reached for a subcommand that did not exist. A session that never happened to do that would have finished believing it had been mediated throughout — and would have been right about the silence and wrong about what it meant.


Found while landing CLOUD-1321 (PR #828).

CLOUD-1186 Narrowing a run skips the engine-side defect-ledger gate, so giving `enforce` a rule selector would create a one-token route around a gate deliberately placed where a rule table cannot lower it

Why

check can be narrowed to one rule (--rule, surface.rs:797); enforce cannot — RunRequest::spawning hardcodes only: None (lib.rs:7354-7364), and the comment says why:

"Deliberately NOT narrowable: --rule exists so a migrated gate keeps its task name on the read surface, and every caller that needs it is a check caller. Offering it here too would be surface nobody asked for, on the verb that spawns."

That is a YAGNI argument plus a posture, and select_rules (lib.rs:7391-7410) is surface-agnostic, so narrowing enforce is mechanically about a one-line change. But it is not safe today, and the reason is a coupling nobody would find by reading select_rules:

lib.rs:7519-7521 skips engine-side gates whenever a run is narrowed — among them the defect-ledger gate, which lib.rs:7421-7423 places engine-side specifically so "a branch could [not] lower it by editing a rule table."

So batten enforce <rule> would become a one-token way to skip the defect-ledger gate on the verb that runs user-declared commands. The skip is a reasonable UX convenience on check — a narrowed run should not fail for a reason the caller did not ask about — and a security hole on enforce.

Why this needs to land before the campaign's spawning gates

The retirement campaign ports gates into [[rule]] rows, and a gate that spawns can only live on the enforce surface. semver check is the first — it spawns cargo-semver-checks and materialises a baseline worktree (surface.rs:2080-2087), so it cannot become a check rule without falsifying the read-only allowlist.

Today a spawning gate ported into a rule row cannot be invoked by name at all — which is exactly the narrowing CHECK_RULE's own doc comment says the campaign needed, missing on the half those gates must land in.


Refinement — Ready (decouple the skip from the narrowing, on the spawning surface)

Refinement gate: Definition of Ready & Done. This body carries only specializations.

  • **Authority boundary (§1). **crates/batten/src/lib.rs (RunRequest, run_rules, the engine-gate branch), crates/batten/src/surface.rs for the selector declaration, crates/batten/tests/.
  • Computable predicate (§2). A narrowed run on the spawning surface still evaluates the engine-side gates that a narrowed run on the read surface skips — specifically the defect ledger. The skip stays exactly as it is for check.
  • **The selector is the object position, not a flag (§2). **surface.rs:620-623 states the convention — "POSITIONAL and required, which is how every other verb takes the one input it cannot work without" — so enforce takes an optional object rather than an ENFORCE_RULE flag.
  • Deliberately not in scope (§2). Changing which gates are engine-side, or the ledger gate's own predicate. Narrowing semantics on check. Porting semver — that is its own row.
  • Effect (§3). unclassified — enforce runs user-declared commands and §5 refuses to guess.
  • Output and exit (§5). Unchanged: an object naming no declared rule is a usage error (1), never a clean 0 — the existing anti-vacuous-pass guarantee (lib.rs:7383-7386, surface.rs:793-796) carries over to the object position.
  • Commit / bump (§6). feat(cli) — patch.
  • **Test obligation (§7). **crates/batten/tests/*.rs over the compiled binary. Shown able to fail per CLOUD-418, three observed: (a) a narrowed enforce run against a repository failing the defect ledger still fails — the security property, and the case that would silently regress; (b) a narrowed check run still skips it, unchanged; (c) an object naming no declared rule exits 1.
  • Blockers (§8). Blocks the semver port. relatedTo CLOUD-1184 (the grammar this selector conforms to), CLOUD-1182.

Acceptance

  • batten enforce <rule> runs exactly that rule and still evaluates the defect-ledger gate.
  • batten check <rule> behaviour is byte-identical to today.
  • The lib.rs:7354-7364 comment is replaced with one recording why the deliberate absence was lifted and what makes it safe — the stated decision is superseded on the record, not quietly deleted.

Found while pressure-testing a proposal to give both gate verbs an object: the mechanical change was a line, and the coupling it would have inherited was a security regression.

CLOUD-1358 Two `cli.rs` cases are 644s of the Windows suite's 1693s — 38% — and they set an asymptotic floor no sharding can pass

Why

Measured on the windows job of run 33659390795 (head 8c95e026, 2026-09-02), parsing per-case durations out of the job's own log — 3868 cases, 1692.8s summed CPU, 849.9s wall across 4 workers:

case windows
cli::the_committed_portability_rules_fire_on_every_banned_shape 324.85s
cli::the_committed_repo_agnosticism_rules_fire_on_every_banned_shape 319.12s
next slowest (pointer_only::no_verb_emits_content_it_merely_read) 25.13s

Two cases are 644.0s — 38% of the suite. The distribution is even more extreme than CLOUD-1223's: 25% of summed CPU is 2 cases (0.1%), 50% is 62 cases (1.6%).

Both are the shape CLOUD-1223 named and this pair escaped. Each reads the committed batten.toml, builds a fixture, and runs the whole 103-rule enforce ruleset — twice, over a dirty fixture and a clean one — to assert 6 rules (portability) and 4 rules (agnosticism). Four full evaluations at ~160s each on a 2-core runner.

CLOUD-1223's closing acceptance bullet is refuted for these two. It reads: "The four cli.rs cases named in the title are NOT individually fixed and do not need to be. Their headline share was inflated 4.7x by contention (39.15s in-suite against 8.37s isolated)." That was measured at 4 cores in a container. On the runner that actually gates landing they are 325s and 319s. The contention explanation does not survive the measurement.

The estimate that deferred this was wrong by 18x, and recording that is the point. These two were examined during PR #819 and declined as "~18s each, worth ~36s" — a local 4-core reading — with the trade stated as "saves ~9s each and costs the cross-rule sort-order property". The real figure is 644s.

They set the suite's asymptotic floor, which kills the sharding lever. A partitioned suite cannot finish faster than its slowest single case: 324.85s. cargo nextest --partition on windows — newly affordable since the cache fix took compile to 1 crate (commented on CLOUD-1225) — cannot take the job below that floor. Removing these two is the precondition for sharding being worth anything at all.

Refinement — Ready

  • Source of truth (§1). Per-case durations from the windows job log of run 33659390795, read via gh api repos/{owner}/{repo}/actions/jobs/{id}/logs with ANSI escapes stripped before counting. The two cases in crates/batten/tests/it/cli.rs. Not a local container reading — that instrument is what produced the 18x error above.
  • **Computable predicate (§2). **--rule becomes repeatable on both check and enforce, so a case naming N rules evaluates exactly those N. Both arms of both cases narrow: the dirty arm to the rules under test, the clean arm to the same set.
  • Full stdout equality survives, which is what makes this a narrowing rather than a weakening. The property the cases exist for is that findings sort by the (path, line, rule) tuple and every one of the 6 (or 4) rules contributes its line — a contains assertion would pass with five of six gone. Selecting exactly those rules preserves the ordering among them; what is lost is only the incidental proof that the other ~97 rows are silent on that fixture, which is not what either case's name asserts and which sibling cases over the committed bytes still cover.
  • Deliberately not in scope (§2). Deleting or weakening any case; the case count must not fall and assertions-not-gutted / tests-not-deleted stay green. Sharding windows — a separate lever, and one this row is the precondition for.
  • **Effect (§3). **read for the narrowing. The --rule arity change is a surface change; CheckFlags.rule moving from Option<String> to a repeatable form is an API break and must be declared, the way Enforce(EnforceFlags) was in 13950e21.
  • Output & exit (§5). Unchanged.
  • **Commit / bump (§6). **perf(test) for the cases, and the surface change declares its break.
  • Test obligation (§7). Shown able to fail per CLOUD-418: with any one of the named rules removed from batten.toml or set to severity = "allow", the asserted stdout bytes must still change. A --rule naming no declared row stays a usage error, never a clean run over nothing — the property CHECK_RULE already carries.
  • Blockers (§8). None. relatedTo CLOUD-1223 (the row whose closing bullet this refutes), CLOUD-1225 (where the sharding refutation and the cache measurement live), CLOUD-1208 (the harness and its null), CLOUD-418.

Acceptance

  • Both cases drop below 1s on the windows job, measured from that job's own log rather than locally.
  • windows summed CPU falls from 1692.8s to ~1049s; wall across 4 workers from ~850s toward ~262s.
  • The new asymptotic floor is named, so the next reader knows what sharding could now buy.
  • Every rule each case names still contributes its own line to the asserted bytes.

Generated by Claude Code

CLOUD-1209 `shell_write_advisory` runs the engine against the live tree, so four cases are decided by whether the developer's branch holds a claim receipt — green in CI, red on the desk

Why

Four of the ten cases in crates/batten/tests/shell_write_advisory.rs fail at e797d91:

  • a_write_to_a_governed_shell_path_signals_without_refusing
  • an_advised_and_allowed_call_still_speaks
  • a_write_to_a_bats_suite_signals
  • the_absolute_spelling_the_host_sends_signals_too

Measured, and the captured cause is not the one a reading of the diff suggests. The engine answers the test's Write payload with:

Refused by claim-needs-receipt: this branch carries no `claim` receipt.

root() in that file returns the real repository root, and every case runs batten hook there. So the payload is adjudicated against the live tree — including its git state. claim-needs-receipt is keyed by branch, and its receipt lives in .git/batten-receipts/, which is never committed.

The consequence is a verdict that depends on the developer's session rather than on the code. These four pass iff the current branch happens to carry a claim receipt: green in CI, where a fresh clone has no receipt and the rule finds nothing to key on, and red for any local session that has not run claim-check for an unrelated reason. A case that passes on the runner and fails on the desk fails in the worst direction — it trains a reader to disbelieve a red suite, which is the thing that gets a gate switched off.

0a13fba fix(hook): a refusal outranks advice about the same call is the second half, and neither fact alone explains it. Before that commit the deny and the advisory were both emitted, so signals() found its token in the output regardless and the dependency was dormant. That commit made a refusal suppress the advisory about the same call — correctly, that is the row's whole point — which turned a latent isolation defect into an active failure. The isolation defect is the one to fix; the ordering is working as designed.

Root cause. The file's own header explains why it exists as a compiled-binary tier rather than a with input as case: the engine must be shown to build the input the predicate reads. That reasoning is right and is not what is being questioned. What it did not carry is a distinction between the engine must really run and the engine must run over THIS repository. tests/common/mod.rs's Fixture builder exists for exactly the second half, and this file already imports it — the four failing cases are the ones that reach root() instead.

Refinement — Ready

  • Source of truth (§1). The four case names above and the root() call sites in crates/batten/tests/shell_write_advisory.rs. The captured deny string is the evidence; it is a hook verdict, not an inference from the diff.
  • Mechanism (§3). Move the four onto a Fixture carrying a config that registers the module under test, so the adjudicated tree is the fixture rather than the checkout. The builder, the git helpers and the environment scrubbing are already in tests/common/mod.rs; this is a call site change, not new harness.
  • The discriminating assertion (§3). A case that fails if the fixture's own claim state is what decides the verdict — per .claude/rules/rust.md, a test must be shown able to fail, and the failure this row is about is precisely one that hid behind an unrelated green.
  • Deliberately not in scope (§2). The other 40 files that reach root(). Most assert this repository's own committed content — scanner_taxonomy.rs over .claude/rules/scanning.md, spawn_census.rs over clippy.toml, task_prose.rs over the manifest — which is Batten being its own consumer ci: check in the main-branch protection ruleset #1 and is correct by design. A blanket ban on reading the real tree would delete that capability to fix four cases. Also out of scope: changing the refusal-outranks-advice ordering, which is 0a13fba's decision and is not in question.
  • The general question this raises and does not answer (§2). Whether a compiled-binary tier that adjudicates the live checkout is a class rather than an instance. Four were found here by running the suite; nothing enumerates the rest. That is a sensor, and it belongs on its own row rather than being smuggled into this fix.
  • Output (§7). Case names and path pointers. No config content.

Test obligation

The four cases pass on a branch with no claim receipt, and are shown able to fail by an assertion whose premise the fixture creates rather than inherits.

Commit / bump (§6): test(hook) — no bump. Not breaking for the consumer surface or the library surface: the change is to test call sites and reaches no shipped verb.

Blockers (§8): none. relatedTo CLOUD-1131 (the row this tier was built for), CLOUD-619 and CLOUD-717 (the two prior isolation defects in this suite, both leaked state through an ambient path), CLOUD-365 (the standing shape question these cases sit inside).

Acceptance

  • The four cases pass on a branch carrying no claim receipt.
  • Each is shown able to fail.
  • No case is deleted or weakened; tests-not-deleted stays green.

Review in Linear

@coderabbitai

coderabbitai Bot commented Sep 2, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 47 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: e87e6090-3726-4606-81d9-87b4d69a7094

📥 Commits

Reviewing files that changed from the base of the PR and between eaf18d0 and 4a3106c.

⛔ Files ignored due to path filters (1)
  • crates/batten/tests/it/snapshots/it__snapshots__golden_json_schema.snap is excluded by !**/*.snap
📒 Files selected for processing (15)
  • completions/batten.bash
  • completions/batten.fish
  • completions/batten.zsh
  • crates/batten/src/cli.rs
  • crates/batten/src/doctor.rs
  • crates/batten/src/lib.rs
  • crates/batten/src/spec.rs
  • crates/batten/src/surface.rs
  • crates/batten/tests/it/cli.rs
  • crates/batten/tests/it/defects.rs
  • crates/batten/tests/it/doctor.rs
  • crates/batten/tests/it/pointer_only.rs
  • crates/batten/tests/it/stop_posture.rs
  • man/batten-doctor-mediator.1
  • man/batten-doctor.1

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

@wenzowski wenzowski changed the title fix(doctor): doctor mediator — decide WHICH engine the registrations reach fix: doctor mediator, the defect-ledger bypass it uncovered, and the suite's largest remaining case Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

HANDOFF — this PR's branch is 7 commits behind the finished work. Do not read claude/ci-performance-degradation-ic29ck as the diff.

land rebased locally across several laps, so my history diverged from the pushed tip; --force-with-lease is refused by leased-push, and I ran low on quota before the branch could be brought forward. The complete work is pushed and intact at:

claude/ci-perf-preserved-2026-09-03 — 4c91f447, 11 commits, exactly my local HEAD.

That ref carries everything the body above describes. This PR's own tip (f2a5d3eb) carries only the first four doctor mediator commits and none of the defect-ledger security fix.

To take this over: reset the PR branch to 4c91f447 (its history is the same work rebased, and its own earlier commits are already contained in it), or repoint/reopen the PR at the preserved ref. Then mise run land.

State when I stopped, all verified locally, nothing assumed:

  • full suite 4223/4223;
  • verify reached fast-forward-green on the lap before last;
  • closing-key-check satisfied — the body names all four keys the commits serve, two Closes, two DO-NOT-CLOSE;
  • the last lap failed on stop_posture::a_stranded_finding_is_pointed_at_and_the_turn_still_ends, which the final commit (4c91f447) fixes at the shared hook helper. That fix has not been through a full land lap.

One thing a taker should not repeat. Every lap so far failed on a different real defect — pointer_only, the golden schema, closing-key-check, and two rounds of state-sensitive stop_posture cases — and none twice. In particular the stop_posture failures look exactly like flakes (red inside the lap, green on an isolated re-run of the same commit) and are not: unlanded reports once per HEAD sha, every lap rebases to a fresh one, and the engine emits at most one ranked nudge, so a real session's finding displaces the advisory a fixture expects. Re-running instead of fixing would have produced a false green twice. Recorded on CLOUD-1209.

Measurements behind the perf claims are on CLOUD-1358, including two refutations of that row's own analysis (the Windows job is CPU-bound rather than floor-bound, so sharding would help).


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Handoff pointer updated: the preserved ref is now 0aecaccb, not 4c91f447. Same branch (claude/ci-perf-preserved-2026-09-03), force-updated after a rebase onto current main. Use the new SHA — the old one predates a conflict resolution.

What the rebase resolved, because it was a real collision rather than a textual one. Another session fixed the same stop_posture state-leak in parallel and landed it on main, setting the state home inline inside hook. My commit fixed it by delegating hook to the already-isolating hook_in. Both contain the findings store; the delegating form also inherits GIT_CEILING_DIRECTORIES, so discovery cannot climb out of the fixture either — and a second inline copy of the containment is the exact shape that caused the bug, where hook and hook_in disagreed about what a fixture owns. I kept the delegation and carried their measurement into the comment, since it names a third symptom of the same root: a_clean_final_message_says_nothing failing with unlanded: 1 commit(s) not on the landing target, read from the checkout the suite was running in.

Verified after resolving: stop_posture 16/16, including the case their fix was written for.

Laps since the last comment, none of which were gate failures:

  • two lost to contention — one to another branch holding the landing lease, one to main moving past the speculated base while verify ran, voiding the receipt. land declined to buy a matrix behind a moving base both times, which is the loop working;
  • one stopped on the rebase conflict above, which is the one step it cannot drive through;
  • one earlier lap failed hooks-wiring-check after a container restart — the launcher rewrites $HOME/.claude/launcher-settings.json with its hooks block, harness-wiring correctly reports 2 merged siblings, and the only in-session remedy is hand-editing a file outside the repository. Four recurrences today; recorded with the rate on CLOUD-1086.

Nothing in that list is a reason to re-run without reading — but of the five laps so far, three ended for reasons that require no code change at all, which is worth knowing before assuming the branch is unhealthy.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Preserved ref moved again: da178dae on claude/ci-perf-preserved-2026-09-03. Supersedes 0aecaccb and 4c91f447 in the comments above — 13 commits, rebased onto current main, full suite 4305/4305.

What changed since the last pointer: main landed doctor session while this branch was building doctor mediator. Five conflicts, all the same additive shape — two new sub-verbs registering in the same five places:

file collision
cli.rs DoctorCommand variants, and doctor_of's dispatch
lib.rs run_doctor's match arms
spec.rs the read-only allowlist and the committed row set
pointer_only.rs the leaf-verb classification census
completions/*, golden schema generated

Every one resolved by keeping both verbs in sorted order; nothing displaced from either side. That five files need touching for one new sub-verb is the design working — each is a deliberate registration with a written reason — not friction.

The two generated files are why this comment exists rather than just a new SHA. I resolved those by taking a side, purely to finish the rebase. Regenerating afterwards showed the golden schema I had picked asserted a surface of 45 commands where the binary emits 46 — doctor session absent. A hand-picked generated file is a claim about what the binary emits, and only the binary settles it. CI would have caught that; catching it here cost one mise run schema instead of a matrix.

mise run completions likewise rewrote all three shells, not the one I first thought — I read git status mid-regeneration and said "only bash differed", which was wrong.

Also worth knowing before re-running: one lap died on the disk floor with verify naming it correctly as environmental — "there is nothing in the diff to reproduce". The fix was ~300MB of dead bats fixtures in /tmp plus the re-downloadable cargo cache, not target/debug: that 6.9GB is the warm build the floor exists to protect, so deleting it would have satisfied the number while defeating the check's purpose. Headroom here is thin — a lap transiently consumes 4–7GB against ~1GB of slack.


Generated by Claude Code

…reach

`doctor hooks` answers whether the registrations reach an engine. Nothing
answered WHICH one, and the gap between those two questions is where a session
spends six hours believing it is mediated: silence from the hook is the
documented sign it IS mediating, so an engine enforcing an old rule table and
one enforcing the committed table produce identical evidence. Measured three
times in one container on 2026-09-02, `doctor hooks` reporting `0 unwired`
throughout.

A SUB-VERB, NOT A FOURTH CHECK IN THE BARE REPORT. This landed once inside
`diagnose()` and `verify` refused it: `this_repository_is_healthy` went red
because `land` had rebuilt `target/release/batten` while the install was an hour
old. The check was telling the truth — and whether a container's install is
current is a property of the WORLD, while bare `doctor` answers a property of
the COMMIT, so folding it in made a commit gate answer on install recency.
`.claude/rules/toolchain.md` records that defect for `lock-check` and its remedy
was the same split. House style §2 already specifies `doctor <SUB>`.

CONTENT, NOT THE VERSION STRING, correcting CLOUD-1349's own §2: `--version`
read 0.0.137 on both sides while the installed binary refused the tree's own
batten.toml over a key that landed in the base commit. A config surface moves
without a version bump, and on a fast-forward-only trunk that is the ordinary
case rather than an edge one.

Nothing is spawned. `doctor` is `Effect::Read` and the agent allowlist is
`filter(effect == read)` with no second list, so running a program a wiring file
names would put config-supplied code behind a row any consumer agent may call.

Four states: the two could-not-look causes are named variants rather than a
string, so `Unresolvable` and `Unbuilt` are kept apart by the type, and neither
folds into clean.

Refs: CLOUD-1349
…ts old placement

Ten cases over the compiled binary. The load-bearing ones:

`a_mediator_built_from_this_tree_passes` is the anti-vacuity mirror — identical
bytes, same fixture shape, same PATH — because a check that refused
unconditionally would satisfy the stale case exactly as well.

`equal_length_binaries_that_differ_are_still_refused` pins that the length
compare is a short-circuit and not the predicate. Two builds of one source at
the same length is the ordinary case for a changed constant, which is also the
drift hardest to notice by eye.

`the_bare_report_is_unchanged_by_this_sub_verb` is the regression this verb
exists as a sub-verb to avoid. An earlier revision put the comparison in
`diagnose()`'s check list and `this_repository_is_healthy` went red whenever a
rebuild had outpaced the install — a world-property deciding a commit gate.

The two could-not-look causes are asserted apart: nothing to compare AGAINST
and nothing to compare WITH have different remedies, so one reason id for both
would send the reader to the wrong place.

Fixtures execute nothing, so neither planted file needs to be a real program —
which is what keeps the whole tier at 0.2s.

Refs: CLOUD-1349
Completions, the parent's man page and a new `batten-doctor-mediator.1`, all
generated by `mise run schema`, `completions` and `man`. No hand edits —
`the_committed_pages_are_the_ones_the_binary_emits` is what would catch one.

Refs: CLOUD-1349
…e row set

Both literals are hand-maintained and that is the design: a command joining the
agent allowlist is a safety-critical edit, so it costs a deliberate line and a
written reason rather than arriving in a generated diff. `verify` refused the
branch until both were written.

The reason here is stricter than its sibling's. `doctor mediator` resolves a
program name and then deliberately does NOT run it, comparing that file's bytes
against the artifact this tree builds. Running what a wiring file names would put
config-supplied code behind a row on this very list — CLOUD-170's invariant, and
why `on_path` stats rather than executes.

Refs: CLOUD-1349
It compares the INSTALL against the BUILD, never the build against the SOURCE.
When `target/release/batten` is itself behind the tree, both sides are equally
stale, they agree, and the verdict is `mediator ok`.

Measured one command apart while this very row was in flight: `land` rebased
onto a `main` carrying a new `[[rule.review]]` key, the engine refused the tree's
own batten.toml with `unknown field`, and `batten doctor mediator` answered
`mediator ok`. Both binaries were the same pre-rebase build. Fourth staleness
occurrence in one container that day, and the first this check missed.

Stated because a check answering a narrower question than its name suggests IS
the failure this repository is organised against — a dead gate and a clean tree
are byte-identical on the decision surface, so shipping the verb while it reads
as answering more than it does would reproduce the defect one layer up.

What is bought stands: the original measured failure, an image-baked or
hand-installed binary against a tree that builds a different one, which ran
unnoticed for six hours.

`two_equally_stale_binaries_agree_and_this_reports_current` pins it as known
behaviour rather than leaving it in prose, so a later change cannot widen or
narrow the bound with nothing going red. Closing the remainder needs a
build-freshness predicate that is still a read; mtime-against-sources is the
obvious candidate and is not obviously sound, since a rebase touches files cargo
would not rebuild from — it trades this false negative for a false positive.
A separate predicate with its own design, not a tightening of this one.

Refs: CLOUD-1349
The gate states its own rule: a new leaf verb is `PointerOnly` unless there is a
reason it is not, and the reason is the field, so it is written rather than
assumed.

Pointer-only here for a sharper reason than its siblings. This verb's whole
subject is two absolute paths and two digests — the shape most likely to leak one
into output. It emits neither: the verdict is a stable token, and the digest is
deliberately withheld because it is stable per content but varies per machine, so
printing one would defeat §6's byte-stability while telling the reader nothing
actionable. The remedy is `mise run install:local`; the verdict says whether to
run it.

This tier runs the verb and inspects what it actually wrote, so rule 4 is
enforced over the bytes rather than declared by the author about their own code.

Refs: CLOUD-1349
The emitted surface grew a command, so the golden moves with it. The diff is
exactly two additions and nothing else — the command's own row, and its entry in
the emitted read-only allowlist — which is the snapshot earning its keep: a
surface change that touched anything further would show here.

Refs: CLOUD-1349
`a_turn_that_strands_nothing_is_silent` ran through the unisolated `hook` helper,
so a real session's findings could reach a fixture asserting nothing is said. The
hazard is stated verbatim 300 lines above it, on `hook_in`: "an ambient one would
let a real session's findings decide a fixture's verdict."

NOT A FLAKE, and re-running would have been the wrong call. Measured 2026-09-03:
red inside a `land` lap, green on the next isolated run of the same commit.
`unlanded` reports once per HEAD sha and every lap rebases to a fresh one, so the
session's own unlanded work is unreported at exactly the moment the lap runs the
suite and reported at every other moment. It would fail every lap and pass every
re-run — red for the author who is landing, green for everyone else, which is the
class CLOUD-1209 already carries for `shell_write_advisory`.

Silence is what makes this case the exposed one: every sibling asserts a specific
advisory and an extra one cannot make those pass, while any advisory at all fails
this.

Widens the PR by one case. Stated rather than slipped in: it is not caused by
this diff, but it blocks its landing on every lap, the fix reuses the isolating
helper already in the file, and calling it infrastructure noise would have been
false.

Refs: CLOUD-1349, CLOUD-1209
CLOUD-1186 predicted this regression in writing, and CLOUD-1358 shipped it a day
later without reading the row.

The engine-side gates were skipped on `only.is_empty()` alone. That is a sound
convenience on `check` — a caller asking about one row is not asking about the
budget or the ledger. CLOUD-1358 then gave `enforce` a `--rule` selector, and the
same branch made `batten enforce --rule <id>` a ONE-TOKEN WAY TO SKIP THE DEFECT
LEDGER on the verb that runs user-declared commands. The ledger gate is
engine-side rather than a `[[rule]]` row precisely so a branch cannot lower it by
editing a rule table; the narrowing restored that lowering by another route.

CLOUD-1186's own words, from before the selector existed: "a reasonable UX
convenience on `check` ... and a security hole on `enforce`", and it names
landing this decoupling as the precondition for narrowing `enforce` at all.

The skip is now surface-dependent. `check` narrowed is byte-identical to before.
`enforce` narrowed still evaluates the ledger.

THE BUDGET KEEPS THE SKIP ON BOTH, and the split is the reason `ledger_findings`
exists: a budget measures declared instruction files — a property of the tree the
caller did not ask about — while the ledger is a claim about this branch's own
conduct. Only one of the two is the security property.

Four cases, and the mirror earned its keep before it guarded anything: it caught
that my first fixture would not parse, because it exercises a path the existing
suite already proves works. Shown able to fail by restoring the shipped code —
exactly 1 of 27 reddens, the narrowed-check case included in the 26 that do not,
so it discriminates the regression and nothing else.

BREAKING CHANGE: `batten enforce --rule <id>` now reports defect-ledger findings
it previously skipped, so a branch with a tampered ledger that exited 0 under a
narrowed enforce now exits 2. That is the defect, not a new rule.

Refs: CLOUD-1186, CLOUD-1358
…the family

`the_committed_repo_config_gates_a_repository` ran `enforce` over the whole ~103
row committed ruleset to assert a single pointer, `crates/** no-conflict-markers`.
Its own comment already said the other rows could not affect that: the asserted
stdout is "byte-identical whether the other committed rules are present, absent,
misspelled, mis-globbed, or switched off".

MEASURED ON THE RUNNER RATHER THAN INFERRED. On the 2-core Windows job it is
436.42s — 24% of that suite's 1810.1s of CPU and its single largest item, with
the next slowest at 28.28s. Locally the same case is 21.5s; the 20x is the
contention signature the two cases CLOUD-1358 narrowed showed, not a different
workload.

Narrowed to `--rule no-conflict-markers`: 21.526s -> 0.153s here, assertion
unchanged. That suite is CPU-bound rather than floor-bound — wall tracks CPU/2 to
within 0.4% on both readings taken — so removing 436s of CPU should be ~218s of
wall off every `rust.yml` run.

Shown able to fail by pointing `--rule` at a DIFFERENT real committed row rather
than by deleting one: the case reddens, so it is asserting this rule's finding
and not merely that something was emitted.

Refs: CLOUD-1358
…lence

Supersedes the previous commit's one-case fix, whose reasoning was wrong and was
disproved by the next landing lap.

That commit isolated `a_turn_that_strands_nothing_is_silent` and argued only a
silence-asserting case could be exposed, since an EXTRA advisory cannot falsify a
case asserting a specific one. The next lap failed
`a_stranded_finding_is_pointed_at_and_the_turn_still_ends`.

The engine emits AT MOST ONE nudge, ranked — two on a turn is how a channel stops
being read — so an ambient finding does not add to the expected advisory, it
DISPLACES it. Every case in the file is exposed, and the fix belongs at the
helper rather than at whichever case failed most recently.

`hook` now delegates to `hook_in`, whose own comment already stated the hazard:
"an ambient one would let a real session's findings decide a fixture's verdict."

The exposure is worst exactly while its author is landing. `unlanded` reports
once per HEAD sha and every lap rebases to a fresh one, so a case is red on the
lap and green on the re-run — the shape that gets called a flake and re-run until
it passes. It is not one, and neither reading of it was.

Refs: CLOUD-1349, CLOUD-1209
…ision

The rebase resolved `completions/*` by taking one side, which is a choice rather
than a derivation — `main` added `doctor session` while this branch added
`doctor mediator`, so neither side's generated file carried both. Regenerated
with `mise run completions`; all three shells changed.

`man/` and the schemas needed no regeneration, so both sub-verbs' pages had
already merged cleanly.

Refs: CLOUD-1349
…b-verbs

The rebase resolved this file by taking one side, so it asserted a surface of 45
commands where the binary emits 46 — `main`'s `doctor session` was absent. That
is a claim about what the binary emits, settled only by running it.

Regenerated: the diff adds `doctor session` to the command surface and to the
read-only allowlist, and displaces nothing from this branch. CI would have
caught it; the point of regenerating rather than trusting the resolution is that
here is cheaper than there.

Refs: CLOUD-1349
@wenzowski
wenzowski marked this pull request as ready for review September 3, 2026 06:59
@wenzowski
wenzowski force-pushed the claude/ci-performance-degradation-ic29ck branch from f2a5d3e to 4a3106c Compare September 3, 2026 06:59
@sonarqubecloud

sonarqubecloud Bot commented Sep 3, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit 4a3106c into main Sep 3, 2026
10 of 11 checks passed
@wenzowski
wenzowski deleted the claude/ci-performance-degradation-ic29ck branch September 3, 2026 07:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant