Skip to content

feat(lease): the landing lease's CAS, over the client the engine already vendors - #812

Merged
wenzowski merged 25 commits into
mainfrom
claude/landing-loop-bundle-4puk4i
Sep 2, 2026
Merged

feat(lease): the landing lease's CAS, over the client the engine already vendors#812
wenzowski merged 25 commits into
mainfrom
claude/landing-loop-bundle-4puk4i

Conversation

@wenzowski

@wenzowski wenzowski commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Closes CLOUD-1274.
Closes CLOUD-1300.
Closes CLOUD-1298.
Closes CLOUD-1269.

DO-NOT-CLOSE CLOUD-1050
DO-NOT-CLOSE CLOUD-1158
DO-NOT-CLOSE CLOUD-516
DO-NOT-CLOSE CLOUD-969

Those four are declined because three of them are my own mis-ordered trailers, not because the rows are unfinished. .claude/rules/commits.md is explicit that the FIRST key of a Refs: trailer is the row the commit SERVED and the rest are citations. Three commits here spelled a citation first — fix(semver) named CLOUD-1050 when it served CLOUD-1274's campaign, chore(prune) named CLOUD-1158 when it explicitly declined to re-derive that row's floors, and fix(surface) named CLOUD-969 when it merely complied with a field that row had already landed. The correct repair is reordering those trailers, which needs rewriting commits already pushed; the honest one available is to decline the keys and say why. CLOUD-516 is a second Refs: line on an earlier commit and is a citation too.


CLOUD-1274 is decided here: admit a scoped network capability for the lease

That row enumerates three routes to a fleet-wide compare-and-swap and closes all three. It is right about all three, and none of them is reversed. git.rs's spawn gate stands — the lease spawns no git. The PATCH-is-not-a-CAS measurement stands.

There is a fourth route the table did not enumerate. The CAS is the git protocol's own: git-receive-pack takes a command list of old-sha new-sha ref and applies it only while the ref still reads old-sha, decided under the server's own lock — strictly stronger than --force-with-lease, which compares against what the CLIENT last observed and races anything that moved in between. Spoken as smart-HTTP that is two POSTs and a GET, over crates/batten/src/fetch.rs, the hyper + rustls client CLOUD-745 already vendored and bounded.

The row's premise that the engine must be offline was already stale when it was written. What must stay offline is check and hook — a property of the use graph, not of the crate — and policy/module-layering.rego forbids both the hooklease edge and the checklease edge. check is listed explicitly rather than left to follow from hook: a tree-scoped gate is declared read and the read-only allowlist is DERIVED from that declaration, so a check path reaching a network write would put a writing prefix on the allowlist itself.

The closure, measured rather than argued

gix-packetline/blocking-io — the same closure gix-transport's blocking-client resolves to, taken directly. No FRAMEWORK_CRATES name, no AMBIENT_CRATES name, no new links crate. The three alternatives are all refused by this tree's own gates:

candidate measured
gix as-is cannot push. gix-protocol/src is command.rs, fetch/, handshake/, ls_refs.rs, util.rs; the only push symbols in gix are push_url() config setters.
gix + its own transports resolves reqwest (barred by AMBIENT_CRATES) plus security-framework and core-foundation (both FRAMEWORK_CRATES) and ring/aws-lc-sys links crates. 265 packages.
git2 / libgit2 cannot reach HTTPS without https, and every configuration carrying it resolves openssl-sys — a FRAMEWORK_CRATES name macos-link-check refuses BY NAME with no vendored exemption. Adopting it would have meant retiring macos-link-check, which is governed_at_head and cannot be edited at all.

Measured live against this repository, not simulated

  • lease status read a tombstone the bash lease had left (vm-5433-…) — the two implementations agree on one object, which is what makes this a port rather than a parallel mechanism.
  • §7's discriminating case, live: two clones, one ref, launched together. One got took the lease; the other got lost the race for it and exit 2. A PATCH-shaped implementation would have had both believe they held it.

What only a live run found

The first real run refused with the pack carries object type 2, and a lease is a commit. A want returns the commit's closure, so a lease's answer carries the commit AND the empty tree it points at — and every fixture pack in the suite carried exactly one object, which is the shape a fixture naturally takes and a real answer never has.


The git-write capability, D0–D3

The lease proved the wire. batten land needs four git writes the engine did not have, and this branch acquires three of them plus the audit that gates the fourth.

D0 — the audit. gix/merge costs 11 activated packages, all pure Rust, no FRAMEWORK_CRATES name, no AMBIENT_CRATES name, no new links crate; macos-link-check and deny both pass. gix-command was ALREADY activated through gix-transportgix-protocol, so the spawn-capable crate is not new and no_second_git_invoker_exists still claims exactly what it proves.

worktree-mutation and parallel were tried and withdrawn, recorded in Cargo.toml so the next author does not re-run it: gix-worktree-state::checkout's Find bound is Send + Clone unconditionally while this crate's OwnShared resolves to Rc, and enabling parallel did not move the bound in this build.

D1 — push. Objects enumerated from the odb, packed, applied through receive-pack's own CAS. Proven live: a real branch pushed to this remote with no git binary.

D2 — fetch. have negotiation, then a real delta-compressed pack. objects_in refused types 6 and 7, so the first genuine fetch would have hit it; both ofs- and ref-deltas are resolved now, in the reader that already owned pack parsing, with no new dependency. Proven live: main at a commit this clone genuinely lacked, negotiated for, received, resolved, written to the odb, ref moved — confirmed outside the test with git cat-file.

D3 — rebase, and the case the design exists for. mem:workflow/landing-loop gives the loop exactly one human stop: "the only stop is a rebase that conflicts." gix-merge will take a resolution strategy and report success, which would delete it. So the conflict test asks TreatAsUnresolved::forced_resolution, the strictest reading available. Rebase::Conflicted carries the original commit and the paths, and nothing moves on a refusal — the ref is written and the worktree touched only after every commit replays, so a refusal leaves the clone exactly as it was.

Current is a third answer rather than a degenerate success: every receipt in the loop is keyed to the commit it validated, so a replay that rewrote an already-current branch would throw away a verify that is still good.

crates/batten/tests/it/rebase.rs builds its fixtures with gix and gitwrite's own writes — no git binary anywhere in the file, because a suite that shells out to build fixtures asserts nothing about a git-free engine. Its header also states what the conflict case does NOT discriminate: it separates refusing from resolving, and does not separate forced_resolution from the laxer git().

Where the writes live, and why not in git.rs

gitwrite.rs is new and the split is by EFFECT. Folding it into git.rs would make hookgitwrite, checkgitwrite, hooklease and checklease unspellable in module-layering, and hand every read-effect gate transitive reach to a remote push.

That required widening gix_is_confined_to_this_module, and the widening is the one judgement call in this PR worth a reviewer's time. Its stated premise — "two git backends coexist here ON PURPOSE and in ONE module" — expired with CLOUD-740; git.rs's own header asserts both halves of that contradiction four lines apart, and this PR repairs it. The gate becomes a closed list of three, so a fourth module still fails, plus an anti-vacuity case in both directions. Separately, no_second_repo_root_resolver_exists is narrowed to what its own comment already says: its subject is root resolution, and it was forbidding gix:: wholesale, which is git access.


What the full suite found, having never been run on this branch

Four commits had landed here without test:cargo ever running. It found real defects, not bookkeeping:

  • lease authorises returned exit 3 in a clone with no remote, short-circuiting before the arm whose entire contract is to fail open. The terms resolve ahead of every arm, so it was answered by a guard it was written to sit behind.
  • lease status -J emitted nothing on the same path — the exit code carried the verdict and the data channel went silent, which is a decode error where the caller asked a question.
  • Both had one cause. TermsMissing now splits "no remote is configured" from "something about reading it failed": the could-not-look guard exists so an UNREADABLE lease is never reported as free, and a clone with no remote has no lease ref to misread. The split is by effect — the five read arms answer, the five that reach swap still refuse.
  • The worktree write assumed Unix, so x86_64-pc-windows-gnu failed to type-check — three errors in the LIB. Found by cross-check, which is why that gate denies warnings; the class is invisible on Linux by construction. Both effects are gated now with git's own fallbacks.
  • spawn-adapters refused lease_stop's kill spawn in lib.rs and was right to. Placing lib was the available wrong answer — it would admit every future spawn in the crate's largest file. lease joins the adapter table instead.
  • semver was red on recorder::record_path's arity, a break introduced earlier on this branch and never declared. Declared now, with a BREAKING CHANGE: footer. The reporter that made it expensive to find is fixed in the same commit: it printed the lint CLASS and not the instance, so the remedy was "go run the delegated tool yourself" over a report the process already held.

Four declared ledgers gained their rows with the reasoning rather than the name: the read-only allowlist and the surface row set (the lease NOUN is unclassified in both, because half the subtree writes), the network-caller list, and the pointer-only census.


Weakens

Groomed on CLOUD-1300's Ready block, carried as commit trailers, and admitted groomed by config lint --config-from origin/main:

  • recorder-added at recorder[landing-lease]
  • program-changed at program[land-lock-peek]
  • program-changed at program[land-lock-status]

The honesty cost is recorded rather than absorbed. The claim receipt carrying the groomed half is dated after the commits it admits, because the weakenings were not groomed before the work started. The re-claim is also a takeover — the row was already In Progress with a PR attached — and the receipt says so.


What this does NOT do

No bash is deleted, and that is a scope boundary rather than an omission. land-lock.sh's acquire wait loop belongs to the LAP, not to the lease, so its cases can only be carried honestly once batten land exists. Writing those conserves arms now would claim conservation for behaviour deliberately moved, which is the laundering shell-retirement refuses.

D4 — batten land itself — is untouched. land.sh is 2,250 lines carrying a lease, speculative linearization, run cancellation and rate-limit handling. D0–D3 acquire the capability it needs; the lap is its own change.

Verification

  • mise run test:cargo3934 passed, 0 failed.
  • mise run verifyfast-forward-green.
  • mise run lint:clippy, macos-link-check, deny, cross-check — clean with the new dependency.

Several Admits: blocks ride the commits — batten.toml, policy/spawn-adapters.rego and .serena/memories/core.md are protected paths, and each write was separately admitted and spent.

🤖 Generated with Claude Code

https://claude.ai/code/session_014rncbrKRtMp7DmBu4PTLVw

@linear-code

linear-code Bot commented Sep 1, 2026

Copy link
Copy Markdown
CLOUD-1298 `lease-authorises-the-branch` reads a record nothing in this repository writes, so consumer #1 does not eat the one food CLOUD-1269 says is the only real exercise of the surface

Why

CLOUD-1280 built lease-authorises-the-branch and PR #810 landed it. The predicate decides — nine cases over the compiled binary with policy::Vocabulary::EMPTY prove it — but this repository declares no [[recorder]] that writes a lease line, so input.tree.records carries none and the predicate allows unconditionally, every time.

**That is fail-open rather than a dead gate, and the distinction is worth keeping. **CLOUD-845's class is a predicate that reads a key the engine cannot emit; this one reads a key the engine emits correctly and that happens to be empty. The refusal it cannot reach is the refusal it is designed not to reach. So nothing here is silently broken.

What is missing is the clause CLOUD-1269 states in as many words — consumer #1 eats the same food, "the only way the surface gets exercised by something other than its own tests." Until the recorder exists, every reading of this predicate is a test fixture, and a landing lap keeps spending CI against a lease nothing in the engine reads.

The shape, established rather than proposed

Two [program] rows and one [[recorder]], all in batten.toml, plus the pattern the recorder narrows on. The non-obvious parts were each probed rather than assumed:

  • status, not authorises**. **authorises answers the module's exact question but takes the branch on ARGV, and Program::args is a fixed Vec<String> frozen at config load. status takes none.
  • peek next, not peek**. **peek with no field argument is exit 2 with 51 bytes of usage text on stdout, and render_column folds whitespace — so the record's shape survives and the successor column silently carries prose that can never equal a branch. That is a fail-CLOSED deviation on the one row Value::Branch exists to keep open.
  • 2 is deliberately unmapped in the status **table. **status fails closed where it cannot observe the lease; authorises fails open. The omission is what restores the asymmetry at the boundary — adding a "2" row inverts the one behaviour the port exists to conserve.

The ordering bound, which is the real finding

A recorder writes on the post-tool event, and mise run land runs its whole lap — fetch, rebase, verify, push, ci-wait, fast-forward — inside one Bash call. So nothing is written while a lap runs, and the line batten check reads inside that lap is whatever the call *before *land left behind. linear-check is the step the workflow contract puts immediately before land, which is what makes the reading seconds old rather than a lap old.

On lap 2 and after, nothing refreshes it, so a lease a rival acquired during lap 1 is not seen. That under-denies, which is the sanctioned direction: this predicate's whole asymmetry is that a reading it could not take must allow, and a stale reading is a reading it could not retake.

The alternative was priced and rejected: selecting every Bash call keeps the record always-fresh and puts a remote lease observation on the mediated path of every call, which perf-assert budgets against. satisfied is evaluated before any column, so a narrow selector means no spawn at all on an ordinary call.


Refinement — Ready (wire the lease record so the predicate reads something real)

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

  • **Authority boundary (§1). **batten.toml (two [program] rows, one [[pattern]], one [[recorder]]) and crates/batten/tests/it/lease_record.rs. No mise-tasks/*.sh and no tests/**/*.batsland-lock.sh is read through [program] rows and never edited, so V-SHELL-RULE-EDITED is untouched and bash-surface-not-growing is unmoved. No crates/batten/src/** change: Value::Branch landed in feat(policy): the landing lease and the reland judgement, and the primitive the lease needed #810.
  • Computable predicate (§2). Driving batten hook with a selected call writes a four-column line whose branch column carries the branch; an unmapped producer status records - and never a verdict token; a call the selector does not name writes nothing at all.
  • Deliberately not in scope (§2). Retiring any landing program — CLOUD-1148's. Widening the selector to every call — priced above and rejected. Giving the record a per-lap partition, which is what a stronger freshness guarantee would need.
  • **Effect (§3). **read. The recorder spawns two read-only producer arms and writes only under $GIT_DIR.
  • Generated artifacts (§4). None — no Rust type changes, so no schema regenerates.
  • **Weakens: **recorder-added at recorder[landing-lease]. A recorder WRITES the record a gate reads, so presence lowers the bar by construction and config-lint reports every addition; this one supplies the value the predicate decides on, which is exactly the case trust.rs names as the sharper one. Declared rather than dodged: the record it writes is the record feat(policy): the landing lease and the reland judgement, and the primitive the lease needed #810's predicate was built to read, and without it that predicate allows unconditionally.
  • Output and exit (§5). Pointer-only: a verdict token, a ref name, a branch. Never a lease body, never a holder id, never an expiry.
  • **Commit / bump (§6). **feat(policy)patch. Config and tests only.
  • Test obligation (§7). Over the compiled binary in crates/batten/tests/it/lease_record.rs, driving batten hook with stubbed producers — the tier the preset suite structurally cannot be, since a with input as case fabricates the very line the engine may be unable to write. Shown able to fail per CLOUD-418: the round trip and its selector mirror (a call the selector does not name records nothing), both producer verdicts, and the could-not-look case asserted on the recorded TOKEN rather than on the file's existence — a case reading only the file would pass over a table that mapped 2.
  • Blockers (§8). None. relatedTo CLOUD-1269 (the clause this answers), CLOUD-1280 (the predicate), CLOUD-369 (the admitted successor), CLOUD-499 (liveness-not-progress, conserved), CLOUD-1170 (the token decision).

Acceptance

  • batten check over this repository reads a real lease line rather than an empty channel, and the compiled tier proves the engine writes it.
  • An unmapped producer status records could-not-look and the predicate allows — asserted on the token, not on the file.
  • A call the selector does not name spawns nothing and records nothing.
  • The ordering bound is stated where the row lives, not only here.

CLOUD-1269 Ship the `landing-loop` preset — CLOUD-1170 names PRESET as the lease predicate's home and no row owns building one, so the generic half of 3,538 lines of landing bash has a disposition and no destination

Why

CLOUD-1170 took the answer and named the home in as many words:

*"A lease predicate over *(record, instant) is generic — every consumer with a landing lease has that shape — so it is a PRESET, with the TTL and the lease's identity as consumer config."

Nothing owns building it. CLOUD-1170 owns the facts (the supplied instant, the liveness record) and its §2 excludes retiring any member. CLOUD-1148 owns the migration and, until an owner correction on 2026-08-31, excluded "shipping a landing preset" outright. So the disposition was recorded twice and the destination zero times — the punt shape AGENTS.md names: an unbuilt mechanism awaited instead of the instance in hand.

This row is the destination.

What is generic, and why that is the test that matters

Landing judgements split three ways, and only one third belongs in a preset:

part example home
effect and loop fetch, rebase, push, the CAS on the lease ref, the poll outside the engine, unchanged — CLOUD-1170's own split
consumer fact which branch is trunk, the required-check roster, the TTL, the lease's identity batten.toml
generic predicate given this lease record and this instant, may this branch spend a matrix? this preset

.claude/rules/policy-modules.md's "module or preset" test is the authority: a preset only when the predicate stays generic once the consumer's facts are pulled into batten.toml. Each predicate below passes it — none names a branch, a check roster, a task or a tracker key, which is also non-negotiable rule 1 reaching the vendored tier via presets_are_inside_the_rule_one_glob.

The four predicates

THIS TABLE'S FACT COLUMN WAS WRONG FOR THREE OF THE FOUR ROWS, AND THE DISPOSITION COLUMN IS THE CORRECTION (CLOUD-1280). Each of the three was found while BUILDING the row rather than while reading it, which is the finding: a refined row's fact column reads as checked and nothing checks it.

predicate the practice facts disposition
lease-authorises-the-branch exactly one branch at a time spends a matrix; a lapsed lease authorises nobody the lease record + a resolved freshness token blocked on a mechanism, CLOUD-1280. The record half is reachable; the FRESHNESS half has no producer the engine can read. All three routes are closed at head — a tree-surface clock is CLOUD-1170's refusal, a [[recorder]] over authorises cannot reach $2 (Program::args is a fixed Vec<String> and no Value yields the current branch) and the record is an append-only history with no per-lap partition, and recorder::Ask has one variant
target-is-fast-forwardable a landing attempt on a head that is not a descendant is refused locally, before the matrix ~~input.tree.landing~~~~, ~~~~input.tree["git-refs"]~~ RENAMED and shipped as already-landed-work-is-not-relanded. Neither named fact decides descendant-ness: git-refs deliberately dropped reachability (CLOUD-36 decides merged-ness by patch identity, now gated by policy/ancestry-decides-nothing.rego), and input.tree.landing answers has this work landed, which is a different question. The predicate is named for the fact that answers it
graded-head-is-not-regraded a SHA the forge already graded is not re-run, and a red one is not readied input.tree.forge shipped, PR #800
spend-needs-a-verified-head the event that starts CI is refused unless this exact head carries a live verification receipt receipt validity, per Rule::max_age DO NOT BUILD, and the shape is decided rather than deferred. A policy row may not declare checks — but it never needed to, since required_checks_for widens across the whole rule table. The real blockers are non-negotiable rule 1 (a preset over input.facts.receipts.verify bakes a consumer's check name into every consumer's binary) and redundancy (the generic spelling is what ready-needs-receipts already denies)

lease-authorises-the-branch is mise-tasks/land-lock.sh:1051's authorises arm, which is already a pure function and says so: "Read-only and side-effect free… a pure function of (lease state, branch) so the suite can drive every row without a second clone."

Its fail-open asymmetry is conserved verbatim, and that is the load-bearing half. land-lock.sh:1069"FAIL OPEN, EVERYWHERE IT CANNOT TELL. Every other refusal in this file fails closed, and this one deliberately does not: a lease it cannot read stops EVERY job in the fleet, where waving one matrix through costs one matrix." A port that quietly makes it fail closed has laundered a stop-the-world into a gate.

The instant reaches the predicate as a RESOLVED TOKEN, not as arithmetic

Owner decision, 2026-08-31, and it is a refinement of CLOUD-1170's "one value per invocation, on the input, treated as data" rather than a contradiction of it: the engine is still handed the instant and still never reads a clock. What changes is what Rego sees.

The precedent is landed. crates/batten/src/rules.rs:1351-1377 (Rule::max_age):

*"THE CLOCK IS THE BOUNDARY'S, NEVER adjudicate's. The comparison happens where the receipt is already being read… That is the waiver table's precedent: a waiver lapses on a date, and *today() *is handed in rather than taken inside, pinned by adjudicate_reads_no_clock_even_now_that_a_waiver_can_lapse. This column buys a fourth *Validity and no clock in the core."

So the boundary compares and the module reads fresh / expired / could-not-look. Three consequences, each a reason:

  1. **No 25th **input.tree key and no schema regeneration. The 24 keys stand.
  2. Byte-stable output survives by construction. An integer instant on the input makes two evaluations over one tree differ whenever the module does arithmetic on it; a resolved token does not. That is CLOUD-1170's own third acceptance clause — "the same evaluation repeated produces identical bytes" — obtained rather than asserted.
  3. No second authority over time. max_age already compares; a raw instant would let every module compare too, differently.

A raw input.tree.instant is not in this row's scope and nothing in the loop needs one. If a second predicate ever wants arithmetic over an arbitrary timestamp, that is its own row.

The two ways a preset ships DEAD, both already measured here

Not cautionary: both have happened in this repository.

  1. Every pattern literal is written INLINE. A [[pattern]] row is consumer config, so data.batten.patterns["x"] resolves to undefined for a consumer who wrote none; Rego reads undefined as does not hold; the module loads clean and decides nothing. crates/batten/src/policy.rs states it at the exemption's own site — the demand is *"unsatisfiable… a consumer cannot add a *[[pattern]] row on its behalf, and the preset cannot read one." CLOUD-934 predicted it in those words; CLOUD-1161's ci-hygiene preset is it happening, two predicates dead.
  2. The compiled tier supplies the empty vocabulary (patterns: &[]). batten policy test reported 330 passed over the dead version, because the load-time tier cannot see this class. crates/batten/tests/policy_presets.rs, running the bundle the way a consumer gets it, is the only tier that catches it.

And per CLOUD-857: anchor on input.call.segments, never split(input.call.command, " "), for anything that turns out to be mediated-call scoped.


Refinement — Ready (ship the generic landing judgements as a vendored, overridable bundle)

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

  • Authority boundary (§1). New crates/batten/src/policy/presets/landing-loop/*.rego; the PRESETS entry in crates/batten/src/policy.rs; the VendoredVerdict rows in crates/batten/src/verdict.rs's vendored-presets section; arms in crates/batten/tests/policy_presets.rs; and the enabling [[rule]] rows in this repository's own batten.toml. Purely additive — no mise-tasks/*.sh and no tests/**/*.bats is opened, so V-SHELL-RULE-EDITED and V-SHELL-RULE-ADDED are both untouched, and no program retires in this row.
  • Computable predicate (§2). A consumer enabling preset = "landing-loop" and supplying a lapsed lease record is refused, naming the lease key; the same consumer with a live lease is not. Each of the four predicates carries that pair.
  • Consumer ci: check in the main-branch protection ruleset #1 eats the same food (§2). This repository enables the bundle in the same change, the way batten.toml's trunk-based-preset row already does — "the only way the surface gets exercised by something other than its own tests." The row states a position on overlap with the enabled ci-hygiene bundle, which judges workflow wiring, so the two do not report one practice twice.
  • Overridability is per verdict (§2). Each predicate raises its own token in VENDORED, with a route; lease-authorises-the-branch and graded-head-is-not-regraded carry a route with a precondition (a wedged holder starving the fleet; a re-grade genuinely wanted after a runner fault). target-is-fast-forwardable carries none — rebasing is the route. Contrast V-SHELL-RULE-EDITED, whose single routeless verdict is the design and not an oversight.
  • Deliberately not in scope (§2). Retiring land.sh, land-lock.sh or main-watch.shCLOUD-1148 owns the migration and its ci-lease-precondition fetch-and-exec precondition is untouched here. Adding a raw instant key. Changing what landing currently decides: the port is behaviour-conserving, and CLOUD-499's liveness-not-progress verdict is carried, not revisited.
  • Effect (§3). read. A preset is include_str! at build time — no network, no registry, no trust-on-first-use — and every predicate reads facts the boundary already resolved. evaluator-io-check stays the gate.
  • Generated artifacts (§4). The published preset-name enum regenerates from preset_names(). mise run fix; never hand-edit. No schema/policy-input.schema.json change, which is the point of the token decision above.
  • Output and exit (§5). Pointer-only: the lease key, the ref, the SHA, the check name — never a lease body, never a fetched payload, never a process listing. The 0/1/2/3 table is untouched; a lease that cannot be read is could-not-look and allows, per the fail-open asymmetry above.
  • Commit / bump (§6). feat(policy)patch.
  • Test obligation (§7). Over the compiled binary, in crates/batten/tests/policy_presets.rs, **with **patterns: &[] — the load-time tier is insufficient by construction and 330 green tests over two dead predicates is the measurement. Shown able to fail per CLOUD-418: for each predicate, the deny case and its anti-vacuity mirror, without which the first is satisfied by a gate that refuses everything. Plus the third case this row's own asymmetry needs: an unreadable lease allows, distinguishable from both.
  • Blockers (§8). blockedBy CLOUD-1170 — the supplied instant and the liveness record are its deliverables, and three of the four predicates are inert without a fact layer to read. relatedTo CLOUD-1148 (the migration parent, whose §2 exclusion of this row was struck on 2026-08-31), CLOUD-1181 (a preset is three parallel hardcoded tables, which this row adds a sixth row to and does not fix), CLOUD-836 (the vendored-preset mechanism), CLOUD-934 (the inline-regex exemption), CLOUD-1161 (the two dead predicates), CLOUD-857 (the segments anchor), CLOUD-418, CLOUD-499.

Acceptance

  • preset_names() includes landing-loop, and enabling it by name in a [[rule]] row loads.
  • Each of the four predicates has a deny case, an anti-vacuity mirror, and — for the lease — an unreadable-lease case that allows, all in crates/batten/tests/policy_presets.rs over the compiled binary with patterns: &[].
  • No data.batten.patterns[...] reference appears anywhere under presets/landing-loop/.
  • Every verdict raised is declared in VENDORED, and every VENDORED row added is raised by a predicate — both directions, which the loader already refuses.
  • This repository enables the bundle, and the enabling row states its position on ci-hygiene overlap.
  • No mise-tasks/ program and no .bats suite is added, edited or deleted, and bash-surface-not-growing is unmoved.
  • CLOUD-1148's child rows can cite this bundle as the successor for the generic half of authorises, rather than re-deriving it.

Filed 2026-08-31 after an owner decision that the landing loop ships as an overridable preset. The disposition existed on CLOUD-1170 and the exclusion on CLOUD-1148; neither was a destination.

CLOUD-1274 DECIDED — admit a scoped network capability for the lease: its CAS is receive-pack's OWN, spoken over the HTTP client the engine already vendors, so all three closed routes stay closed and none is reversed

DECISION (2026-09-01)

The first of the three answers is taken: a scoped network capability is admitted for the lease. It is admitted through a fourth route this row's table did not enumerate, which is why it costs none of the three closures the table is right to defend.

**None of the three routes is reversed. **git.rs's spawn gate stands — the lease spawns no git. Cargo.toml's gix feature list is untouched — no gix network feature is enabled. The PATCH-is-not-a-CAS measurement stands — nothing here uses the REST refs endpoint.

The fourth route

**The CAS is the git protocol's own. **git-receive-pack takes a command list of <old-sha> <new-sha> <ref> and applies it only while the ref still reads <old-sha>, decided under the server's own lock. That is **strictly stronger than **--force-with-lease, which compares against what the CLIENT last observed and races anything that moved in between. It is spoken as smart-HTTP: two POSTs and a GET.

So the operation needs no git binary, no gix transport, and no REST endpoint. It needs an HTTPS client — and the engine has had one since CLOUD-745. crates/batten/src/fetch.rs is hyper + rustls, bounded (15s connect / 60s total, one scoped current-thread runtime), with provision.rs and mcp.rs as live consumers.

This row's premise that the engine must be offline was already stale when the row was written. What must stay offline is check and hook, not the engine — and that is a property of the use graph, not of the crate.

Closure, measured rather than argued

The framing is gix-packetline/blocking-io — the same closure gix-transport's blocking-client resolves to, taken directly. **No **FRAMEWORK_CRATES **name, no **AMBIENT_CRATES **name, no new **links **crate. **macos-link-check and deny both pass.

The three alternatives were measured against this tree's own gates and all are refused:

candidate measured
gix as-is **cannot push. **gix-protocol/src is command.rs, fetch/, handshake/, ls_refs.rs, util.rs. The only push symbols in gix are push_url() config setters.
gix + its own transports resolves reqwest (barred by AMBIENT_CRATES) plus security-framework and core-foundation (both FRAMEWORK_CRATES) and ring/aws-lc-sys links crates. 265 packages.
git2 / libgit2 cannot reach HTTPS without https, and every configuration carrying it resolves openssl-sys — a FRAMEWORK_CRATES name macos-link-check rule 2 refuses BY NAME, with no vendored exemption. vendored-openssl does not change it. Adopting it would have meant retiring macos-link-check itself, which is governed_at_head and so cannot be edited at all.

§2's invariant holds, and is asserted rather than reviewed

policy/module-layering.rego:271-272 forbids **both **hook -> lease **and **check -> lease over the resolved use graph. check is listed explicitly rather than left to follow from hook: a tree-scoped gate is declared read and the read-only allowlist is DERIVED from that declaration, so a check path reaching a network write would put a writing prefix on the allowlist itself. evaluator-io-check is untouched — Rego still cannot reach the network.

What landed

Eight commits on claude/landing-loop-bundle-4puk4i, PR #812 (draft): crates/batten/src/lease.rs (wire, CAS, pack writer/reader, lease-commit minting through gix::objs, the body model, authorises, the four mint bodies, the turn decision, local corroboration state, liveness/progress/bail, health) and a batten lease verb with ten arms — acquire renew hold held release status authorises peek reserve check. Three write and seven read; the noun is unclassified per §5/CLOUD-90.

**The predecessor's exit vocabulary is deliberately NOT carried. **land-lock authorises answered 0 run / 3 stop / 2 could not look because inside that one file 1 already meant "held by someone else". The engine has one table and no per-verb exception (non-negotiable rule 5), so a stop is 2 and an unreadable lease is 3. The workflow moves in the same change.

Measured live against button-inc/batten, not simulated

  • lease status read a tombstone the bash lease had left (vm-5433-…) — the two implementations agree on one object, which is what makes this a port rather than a parallel mechanism.
  • acquirestatusauthorises (0 for its own branch, 2 for another) → peekheldrelease, all green.
  • §7's discriminating case, live: two clones, one ref, launched together. One got took the lease; the other got lost the race for it and exit 2. A PATCH-shaped implementation would have had both believe they held it.

What this costs, stated rather than absorbed

  • One spawn is added, and it is not git**. **lease_stop spawns kill -TERM to stop a stalled land, under an #[expect(clippy::disallowed_types)] inventory row. There is no in-process way to signal another process, and the alternative is leaving a wedged land running — the fleet-wide stall that path exists to end.
  • The two-writer case is measured live and is NOT an in-tree automated case. It needs a real remote and a credential, so it cannot live in crates/batten/tests/. The unit tier pins the report-status PARSE (a_lost_race_is_rejected_rather_than_an_error), not the race. That is a real gap in §7's terms and is stated rather than papered over.

The seconds correction runs the OTHER way now

This row's fourth acceptance clause asks that every row quoting a recoverable total including land-lock be corrected downward. That is now inverted: the 195.1s IS reachable. Any row recording it as unrecoverable on this row's authority needs correcting, and this row is where they point.

What this does NOT do

**No bash is deleted yet, and that is a scope boundary rather than an omission. **land-lock.sh's acquire wait loop — jittered backoff and aging — belongs to the LAP, not to the lease, so its cases can only be carried honestly once batten land exists. Writing those conserves arms now would claim conservation for behaviour deliberately moved, which is the laundering shell-retirement refuses. land-lock.sh, land-lock-check.sh and ci-lease-precondition.sh therefore retire together with land.sh, under CLOUD-1148.


Why

CLOUD-1170 decided the lease/liveness class splits into "a predicate the engine decides and a loop that stays outside", and its largest member is land-lock195.1s, 17.8% of the 1097.1s corpus, and the floor of the test:bats lane because that lane runs --no-parallelize-within-files. Every plan for recovering those seconds assumes land-lock can retire behind a batten lease verb the way ci-wait retired behind batten pr watch.

It cannot, and the reason is a capability gap rather than a missing fact. Measured 2026-09-01 while building CLOUD-1170.

The lease is a compare-and-swap over a REMOTE ref, and that is not incidental

mem:workflow/landing-loop records four properties the design was pressure-tested into, each costing an incident. Two of them decide this row:

  • It is a BRANCH. *"The agent proxy 403s a push outside *refs/heads, and GitHub does not enforce the fast-forward rule off refs/heads *either — a parentless orphan *PATCH *with *force:false was ACCEPTED on a custom namespace. The atomicity the design rests on exists only on refs/heads."
  • **Renewal is **--force-with-lease=<ref>:<observed>, a true CAS. "PATCH *with *force:false does not give one."

So the operation is specifically git push --force-with-lease to refs/heads/batten-land-lock, plus git ls-remote and git fetch to observe it. Fleet-wide shared state; there is no local substitute.

Three routes to that operation, all closed

route closed by
spawn git from shipped source crates/batten/src/git.rs's own test: "Nothing in this crate does any more (CLOUD-740) — ask gix through open". It scans all of src/ up to #[cfg(test)] and is shown able to fail. Note it forbids the LITERAL program git; gh is fine (pr_watch.rs:457 spawns it under an #[expect] inventory row), so this is not a general spawn ban and cannot be satisfied by adding one.
gix in-process Cargo.toml:216-220: "What is left off is the whole network half — gix's default pulls transports and credential helpers, and a library that can fetch has no business inside a gate that must be offline and byte-stable." The enabled features are revision, sha1, sha256.
gh api on the git-refs endpoint Not a CAS — the landing-loop memory's second bullet above, measured. A PATCH cannot express "update only if the ref still points at the sha I observed", and the orphan-accepted incident is what proved the weaker form unsafe.

Each closure is a landed decision with a stated reason, not an oversight. None should be reversed casually: routes 1 and 2 exist to keep the engine offline and byte-stable, which is the property evaluator-io-check and house style §5 are both about, and route 3 is refused by a measurement.

What this changes

  • CLOUD-1170's facts are not the blocker, and deciding it did not unblock land-lock. The supplied instant lands cleanly (PR feat(facts)!: a supplied instant, four lease/liveness retirements, and two engine defects they exposed #793) and the expiry predicate is expressible; the CAS is what has no home.
  • The 195.1s is not recoverable by any current plan. Consumer rows that quote a recoverable total including land-lock are quoting seconds nothing can reach — the same class CLOUD-1166 records for counts, one type over.
  • ci-lease-precondition (1.3s) dies with land-lock and is blocked by the same wall. land-lock-check (0.5s) reads a lease BODY handed to it and may be separable.
  • The other members are LOCAL — kill -0, mkdir, file reads — so this row does not block them.

What must not happen

Deleting mise-tasks/land-lock.sh and pasting its body into an inline [tasks."land-lock"] block in mise.toml. That satisfies shell-retirementmise.toml is outside its perimeter — and retires nothing, while making the census read as progress. It is the exact laundering bash-surface-not-growing exists to catch, one layer over, and it is the obvious move for anyone who reaches this wall without reading it.


Refinement — Ready (decide where a fleet-wide CAS may live)

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

  • Authority boundary (§1). The decision, and whichever of Cargo.toml's gix feature list or crates/batten/src/git.rs's spawn gate it changes. **No **mise-tasks/ **program and no **tests/**/*.bats is edited or addedV-SHELL-RULE-EDITED refuses the first and V-SHELL-RULE-ADDED the second, so this row cannot fix its consumer by touching it.
  • Computable predicate (§2). Whichever answer is taken, one invariant holds and is testable: **the engine performs no network operation on any **check **or **hook path. A capability admitted for the lease must be unreachable from both, asserted over the resolved use graph the way policy/module-layering.rego already forbids the hook -> fetch edge — never by a comment saying the verb is only called from land.
  • Deliberately not in scope (§2). Retiring the local members of CLOUD-1170's table, which this does not block. Changing what the lease MEANS (CLOUD-499's liveness-not-progress verdict is conserved). The four pressure-tested lease properties, which any successor conserves rather than re-derives.
  • Effect (§3). Whichever answer is taken is at least write and reaches the network; it may not be read, and it may not be reachable from a verb the derived allowlist admits. §5's rule that a read noun with a write-bearing subtree leaks a prefix onto that allowlist (CLOUD-90) decides the shape.
  • Generated artifacts (§4). Cargo.lock if a dependency lands; schema/* only if a row key changes. mise run fix; never hand-edit.
  • Output and exit (§5). Pointer-only: the lease key, the observed sha, the holder's own id — never a credential and never a URL carrying one. Exit follows the 0/1/2/3 table, and a lease that cannot be reached is 3 rather than a false 2, because a gate that could not observe the lease has not judged it. Every lease gate fails OPEN, per the landing-loop memory: "the cost of failing open is one matrix and the cost of failing closed is the fleet."
  • Commit / bump (§6). feat(lease) if a capability lands, docs if the answer is a recorded refusal — patch either way below 0.1.0.
  • Test obligation (§7). Over the compiled binary in crates/batten/tests/. Shown able to fail per CLOUD-418, and the discriminating case is the CAS itself: two writers observing one sha, one push winning and one refused — a test that only exercises the winner passes over a PATCH-shaped implementation, which is precisely the unsafe form. Plus the anti-vacuity mirror, and an assertion that no network capability is reachable from check or hook.
  • Blockers (§8). relatedTo CLOUD-1170 (the facts row this unblocks nothing for), CLOUD-1148 (landing policy), CLOUD-843 (the campaign), CLOUD-499 (the conserved verdict), CLOUD-418.

Acceptance

  • One answer is taken in writing: admit a scoped network capability for the lease, keep land-lock in bash and exempt it by name, or replace the CAS with a mechanism that has one.
  • If land-lock stays bash, it is exempted by name with its reason, and a gate reports a member of CLOUD-1170's table that carries neither a successor nor an exemption — so the next arrival is a finding rather than a rediscovery of this wall.
  • The engine performs no network operation reachable from check or hook, asserted over the use graph rather than reviewed.
  • Every row quoting a recoverable-seconds total that includes land-lock is corrected, or states that the figure is unreachable under the current answer.

Found by building CLOUD-1170: the facts landed, and the member they were built for still had nowhere to go.

CLOUD-1300 A recorder's record is branch-keyed with no PR identity, so a branch reset onto merged trunk inherits the previous PR's record — and `filed-over-own-diff`'s exemption then reads the wrong PR's closing keys

Why

recorder::record_path keys a record by (record name, branch) and nothing else. A branch name is reused all the time — the trunk-based lifecycle says so outright, and this repository's own agent instructions say that when a designated branch's PR has merged you "restart your designated branch from the latest default branch (keep the same branch name)".

So the record survives the PR that produced it, and the next PR on that branch name reads the previous one's lines as if they were its own.

Measured, this session, not reasoned about. After PR #810 merged and the branch was reset onto the new trunk, .git/batten-receipts/pr-closes.<branch> still held:

closes 2:CLOUD-1269,CLOUD-1279

Those are #810-era keys. The PR actually open on that branch was #812, closing CLOUD-1298 — which appeared nowhere in the record.

Why that is worse than a stale number

filed-over-own-diff (policy/filed-here.rego) exempts a row the PR closes, and it reads that from this record. With the record stale, the exemption is evaluated against a different PR's keys, so:

  • an honest file-then-fix is refused, because the row it closes is not in the record; and
  • the reverse is reachable too — a row named by the PREVIOUS PR's closes-record would be exempted on a PR that does not close it at all. That direction is the dangerous one, because it exempts silently and nothing downstream re-checks.

batten.toml:599-604 already records the coupling class for agent-sourced; this is the same class reached through a reused branch name rather than a deleted row.

Two further facts, both measured while establishing the first:

  1. Deleting the stale record does not exempt. The predicate needs a POSITIVE record naming the key, so an absent channel refuses where an absent finding would pass. Could-not-look is not modelled on this arm.
  2. The record cannot be minted at all in some environments. It is written by a [[recorder]] over a gh pr view Bash call, and a session without the gh CLI has no route to it. The exemption is then unreachable by construction and the only remaining route is the declared override — which is what this session had to spend, on a PR whose body did carry Closes CLOUD-1298.

AND THE OVERRIDE IT FORCES YOU ONTO CANNOT SURVIVE THE LANDING LOOP

This is the half that turns an annoyance into a wedge, and it was measured four times in one session rather than reasoned about.

An admission binds (rule, verdict, subject, HEAD, epoch)lib.rs:8298-8311 resolves the CURRENT head and admission::admitted matches on it. mise run land rebases every lap by design, and a rebase mints a new SHA. So:

  1. spend the admission, run land;
  2. land fetches, rebases onto the advanced trunk, mints a new head;
  3. the admission no longer matches, batten-check refuses, verify fails, the lap stops;
  4. re-spend at the new head, and go to 1.

land has no way to re-issue an admission — it stops for a rebase conflict, a failed verify or red CI, and this arrives as the second. **So a branch that needs a **filed-over-own-diff **override can only land during a lull in **main, and the busier the trunk the less likely that is. Measured on PR #812: four re-issues across a587a807, bae026e2, 3321cb76 and one before them, each invalidated by the next lap's rebase.

Two further sharp edges found the same way:

  • The admission binds the PREDICATE id, not the enabling row's. Two admissions were refused for naming filed-here (the [[rule]] id) where the finding carries filed-over-own-diff. The refusals were correct and the error message does not say which name it wants.
  • It is per SUBJECT, so one row naming two files needs two. CLOUD-1298's §1 names batten.toml and its §7 names crates/batten/tests/it/lease_record.rs; the second refusal only appears after the first is admitted, so the cost is discovered one lap at a time.

This is a mechanism question, not a plea for a looser gate. The candidate answers all live in the same place: bind an admission to the CHANGE rather than to the head (a patch id, or the PR), let land carry a spent admission across its own rebase, or let a row's §1 paths be declared as the subject set in one issuance. Which one is this row's to decide; that it is currently unlandable-under-contention is the finding.

Adjacent, and worth naming so it is not re-derived

The same shape one level down is already recorded on CLOUD-1298: recorder_records returns every line in write order with no per-lap partition, so the lease predicate reads the last line and states that bound explicitly. A per-PR partition and a per-lap partition are the same missing axis at two scales — the record knows which branch and never which attempt.


Refinement — Ready (give a record an identity beyond its branch)

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

  • **Authority boundary (§1). **crates/batten/src/recorder.rs (record_path, and whatever mints the identity), crates/batten/src/lib.rs at the write_records call site, and crates/batten/tests/it/board_record.rs. Not policy/filed-here.rego — the predicate is correct and reads what it is given.
  • Computable predicate (§2). A record written on branch b under one PR is not read as evidence on branch b under a different PR; the same PR's own lines still are. Both directions, since the dangerous one is the false exemption rather than the false refusal.
  • Deliberately not in scope (§2). The per-LAP partition the lease record wants — same axis, different granularity, and CLOUD-1298 states that bound rather than closing it. Making the pr-closes record mintable without gh, which is its own question.
  • **Effect (§3). **read for the predicate; the recorder's own write is unchanged in kind, only in path.
  • Generated artifacts (§4). None expected unless the identity reaches a config type.
  • Output and exit (§5). Pointer-only: a record name and a key. Never a PR body.
  • **Commit / bump (§6). **fix(recorder)patch, unless the identity changes a public type.
  • Test obligation (§7). Over the compiled binary, driving batten hook twice under two identities on one branch name. Shown able to fail per CLOUD-418: the false-exemption direction needs its own case, because a suite that only proves the honest PR is admitted passes over the bug.
  • Blockers (§8). None. relatedTo CLOUD-1298 (where this was measured), CLOUD-1051 (the recorder), CLOUD-1057 (the same staleness shape on branch receipts' base line).

Acceptance

  • A branch reset onto merged trunk does not inherit the previous PR's record, demonstrated over the compiled binary.
  • The false-exemption direction is exercised, not only the false-refusal one.
  • Whether could-not-look should be modelled on this arm is answered in writing — today an absent record refuses, and that asymmetry is undeclared.

CLOUD-1050 CLOUD-122's refusal contract has no mechanism, and the Rego migration is where it is lost: `msg` is a free string, so every remedy defect is expressible and none is checkable

Dependency graph. It blocks CLOUD-910 as well as 1051 and 1053: the wave must not port refusal prose into an ABI this issue immediately replaces.

Shell-retirement specialization

Generated Batten completions are derived output and are outside this issue's shell-retirement boundary. The boundary is authored mise-tasks/** guard rules and their Bats suites.

This migration must not edit in place or preserve the shell rules/tests it touches. It ports their predicates into Rego modules consumed by the policy engine, adds compiled-binary coverage, and deletes the retired authored shell surfaces in the same change. The initial retirement ledger covers tests/run-shape.bats, tests/privileged-lane.bats, and tests/replay.bats; each deletion names a carried, subsumed, or changed Rego/Rust successor and a replacement binary test. No replacement shell task or Bats fixture is permitted.

CLOUD-1059 is the explicit prerequisite that makes this a runnable policy rather than a promise. This issue is blocked by 1059 and begins only when its migration gate is available.

Correction — scope and prerequisite decisions

The REGISTRY covers both emitters; the ENFORCEMENT is Rego-only. An earlier revision scoped the registry itself to Rego policy output, and review found that breaks the two consumer rows: CLOUD-1051's migration targets (prose-only-check, filed-here-check) are mise-tasks/*.sh, and CLOUD-1053's worked example is one of them. A policy-only registry plus a separate native-gate authority is precisely the two-authorities-that-drift defect this row exists to kill, recreated one layer up.

So: [[policy.verdict]] is batten.toml data and holds refusal classes for both Rego policy output and native Refusal { rule, reason, fix } sites. Only the emitter differs. What stays Rego-only is the custom Regal rule banning msg and sprintf, because that is where a Rego AST exists to lint; the native side is held to the same registry by the engine's own decoder and by the exhaustiveness check, not by Regal.

subjects, plural, is canonical — an ordered list, never a bare subject. Each element is a tagged variant: {path}, {path, line}, {count}, or {artifact} naming a task/env/document. Routes are likewise closed variants — command, document, issue, override — never free text. The decoder and finding identity are both ambiguous until these are fixed, which is why they are named here rather than left to implementation.

The implementation migrates the whole public ABI: policy/**/*.rego, embedded policy presets, policy fixtures, and the native Refusal sites. The current public shape is Violation { rule, msg }; every emitter moves together, because a half-migrated ABI is two shapes and a decoder that must accept both.

Finding identity changes from message text to (rule, verdict, subjects). Today it hashes the message, so a reworded remedy churns every baseline. The migration needs that identity plan landed with it, or baselines silently collide or reset.

Registry equality and route resolution run during policy::load — that is the answer, stated once. The earlier revision said this AND said a dedicated batten.toml command row with hk.pkl inputs and CI execution AND said "no parallel task is introduced", which is three positions. Running at load makes the check unavoidable for every consumer and needs no new row; batten policy test exercises the same code path rather than re-implementing it. Review is right that the existing Regal/OPA rows do not invoke batten policy test — which is exactly why the check must not depend on them.

Schemas that actually move: schema/batten.schema.json and schema/batten.local.schema.json, because [[policy.verdict]] is new configuration. schema/policy-input.schema.json does not — this row changes policy OUTPUT, not input. Whether a new output schema is introduced is a decision this row must make explicitly rather than leave to mise run schema to reveal; if one is, it is named here before implementation. derived-check gates whichever move.

Dependency graph. This issue defines and ships the registry and migration surface. It blocks CLOUD-1051 (which consumes override.precondition), CLOUD-1053 (which consumes the same stable token registry), and CLOUD-910 — the wave must not port refusal prose into an ABI this issue immediately replaces.
Why

CLOUD-122 established the contract — "every deny points to the fix" — and it is Done. CLOUD-871 established that remedy prose steers the agent, and it is Done. Neither shipped a mechanism, which is why the class recurred five times in one session, measured below.

The reason it cannot be enforced today is one line of shape. A policy emits:

violation contains {
	"rule": "command-task-defined",
	"msg": sprintf("batten.toml's `%s` row runs task `%s`, which…", [row.id, row.task]),
} if { … }

policy/command-task-defined.rego:120. Every defect below is expressible in that shape and none is checkable, because msg is a free string. msg is where all of it hides.

The Rego migration is the deadline. CLOUD-910 authors twenty modules in this format and CLOUD-843's campaign migrates the whole gate corpus onto it. Every gate ported under today's shape is a gate that gets ported twice. That is what makes this urgent rather than merely correct.

Measured, 2026-08-25, one session

# defect evidence
A the remedy is on a channel the reader filters out prose-only-check.sh:182 is the only line of its refusal without an ::error:: prefix. In one land log: line 777 diagnosis (prefixed), line 780 the correct remedy (unprefixed), line 782 a paraphrase (prefixed). land echoes a step's last words through that filter. The remedy was never received.
B a second authority restates it and drifts mise.toml:1469 drops the fold-it-in route entirely and turns the override's precondition ("if the prose is the deliverable and cannot wait") into "to record the exception" — a claim you must defend becomes bookkeeping
C the stated cause outruns the verdict claim-check printed CLOUD-819 not-todo (in Backlog) then "not pullable — someone is already on it". Nobody was. One msg serves not-todo, assigned and has-pr; the sentence is true for two of the three
D the route set is a binary and the missing third is the cheap one A and B compose: with the fold-it-in route gone, the only arm carrying an exact env var is the override
E the remedy names a route the reader cannot reach no-tool-substitution on ls -l <path> offered "a range of one file's contents, a pattern across the tree, paths by glob, or what a name resolves to" — none answers whether a path is a symlink. CLOUD-990 and CLOUD-729 are prior instances

D is the one that costs most, and it is a generator rather than a defect. When the listed routes do not fit, the override is the only concrete arm, so an incomplete remedy does not merely fail to redirect — it manufactures the CLOUD-680 shape. Measured: an override was requested for a change that needed none, and the correct route was the clause dropped at B.

The mechanism already exists and is pointed the other way

CLOUD-876 landed exactly the enforcement technique, wired as batten.toml rows: a custom Regal rule in .regal/rules/ (policy-modules-bind-input), plus regal lint policy/, opa check -s schema/ policy/, and regal test .regal/rules. CLOUD-930 landed the fmt half beside it.

That machinery types the policy INPUT — # METADATA schemas: — and nothing types the OUTPUT. This row is the same technique aimed at the refusal value. No new tool, no new pin, no new gate wiring.

The change

Delete msg from every emitter. A refusal is {rule, verdict, subjects}: verdict drawn from a closed set the emitter declares, subjects an ordered list of tagged pointers. Remedy text becomes data in ONE place, keyed by verdict, rendered by the engine.

Four of the five defects become unrepresentable rather than merely refused: the policy has no channel to pick (A), no string for a caller to copy (B), no way for one message to serve three verdicts (C), and typed routes make reachability checkable (E).

A verdict from a closed declared set is a token, so the registry that holds the set also holds each class's definition and routes — which is what makes remedy text a countable artifact for the first time. That matters beyond this row: CLOUD-908's conserves ratchet makes a port account for deleted test cases, and CLOUD-990 recorded that nothing makes a port account for a refusal's remedy text. A registry closes that at exactly the moment the migration would otherwise lose it.


Refinement — Ready

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

  • Source of truth (§1). The refusal value at every emitter — policy/**/*.rego, the embedded presets, and the native Refusal sites — plus the [[policy.verdict]] registry and the engine's renderer. ONE authority for remedy text across both emitters, which is the whole point and what B violates today. Scoping the registry to one emitter would reintroduce B at the layer above.
  • Computable predicate (§2). Four commands, four exit codes, no judgement:
    1. no violation/deny value under policy/** carries a msg key, and none contains sprintf — a custom Regal rule, in the shape policy-modules-bind-input already uses;
    2. the verdicts set a module declares equals the registry's keys for it, both directions — an emitted verdict with no entry fails, an entry nothing emits fails;
    3. every route names a resolvable artifact — the technique command-task-defined already applies to command rows, generalised;
    4. override is optional, but where present carries precondition and at least one non-override route, so a gate cannot hand a reader a binary whose only concrete arm is the bypass.
  • Effect (§3). read. Lint over the policy corpus plus a registry lookup; no new spawn, no network, no new config key.
  • Generated artifacts (§4). schema/batten.schema.json and schema/batten.local.schema.json move, because [[policy.verdict]] is new configuration. schema/policy-input.schema.json does not — this changes OUTPUT. Whether a new output schema is introduced is decided in this row before implementation, not discovered from mise run schema. derived-check gates whichever move.
  • Output & exit (§5). This changes the SHAPE of every refusal, which is the point. Pointer-only becomes syntactic rather than a discipline: with sprintf banned from the value, no policy can interpolate content into a message. Exit codes unchanged.
  • Commit / bump (§6). feat(policy)! — the refusal shape is a consumer-visible contract, so the break is declared rather than hidden.
  • Test obligation (§7). Shown able to fail (CLOUD-418), each reddening exactly one predicate: reintroduce msg in a fixture module ⇒ (1) reds; drop a registry entry for an emitted verdict ⇒ (2) reds; name a nonexistent task in a route ⇒ (3) reds; declare an override with no precondition ⇒ (4) reds. Plus the anti-vacuity arm — a conforming module passes all four, so a check that refused everything would fail.
  • Blockers (§8). None. relatedTo CLOUD-122 (the contract this enforces), CLOUD-871 (the class, Done without a mechanism), CLOUD-876 (the technique, aimed at the input), CLOUD-930 (the Rego gate wiring), CLOUD-1020 (does a rule fire and do rules conflict — a different question about the same corpus), CLOUD-680 (the override-as-menu shape D generates), CLOUD-990 (a prior instance of E, and the conserves gap).

Acceptance

  • No refusal value at any emitter — policy module, preset, or native Refusal site — carries free text.
  • A verdict without a registry entry, and an entry no verdict emits, both fail.
  • A route naming an unresolvable artifact fails.
  • An override without a precondition, or as the only route, fails.
  • Each of the four is shown red before green, and a conforming module passes all four.

What this row must NOT claim

  • Not that prose shrinks. land's disk-exhaustion message is the best refusal in this tree because of its prose — observation, what it is about ("the environment, NOT this branch"), the cheapest route, and the thing that route cannot do. Under this shape that prose moves off the hot path into the class definition, read once and able to be long. It is relocated, not cut.
  • Not that first contact gets cheaper. It costs slightly more there — token plus a dereference — and pays on repeats. Mitigate with a short inline gloss; state the trade rather than hiding it.
  • Not that well-formedness is discrimination. Every predicate here is static. Whether a migrated gate still discriminates is CLOUD-1020's question and CLOUD-931's gap (batten policy test is wired to no task, so mutant cannot reach a policy module). If gates migrate faster than that closes, mutant-census reports rising coverage over falling evidence.

Not in scope, filed separately rather than absorbed: the override as an issued capability rather than a knowable env var (CLOUD-1051), the token-shaped hot path and its dereference verb (CLOUD-1053), and the instruction-surface replay that is the same defect at larger scale (CLOUD-1052).

Grooming decisions — Ready

Registry and owner. Add [[policy.verdict]] entries to batten.toml as the sole registry. Each entry owns a stable verdict token, short gloss, class definition, routes, and optional override.precondition. Every emitter declares the tokens it can emit and emits only {rule, verdict, subjects} — plural, an ordered list of tagged pointers. crates/batten/src/policy.rs decodes that shape, and crates/batten/src/hook.rs and crates/batten/src/rules.rs render it for both emitters. No policy module, native site, shell guard, or caller carries remedy prose.

Mechanism and wiring. Add a custom Regal rule under .regal/rules/custom/ and enable it in .regal/config.yaml: it rejects a free-text msg or sprintf inside a refusal value, while permitting unrelated policy strings. That rule is Rego-only by construction. Registry equality and route resolution run inside policy::load, so every consumer pays them and no batten.toml row is needed to make them fire; batten policy test drives the same code path for the suite. The existing policy-modules-bind-input, policy-modules-type-check and policy-lint-rule-tests rows keep the Regal half honest and are not asked to carry the registry half.

Artifacts, contract, and release. Regenerate schema/batten.schema.json and schema/batten.local.schema.json with mise run schema; mise run schema-check and mise run derived-check gate them. The checks remain read-only. Clean is 0, malformed configuration is 1, a policy violation is 2, and internal failure is 3; rendered output stays pointer-only. Land as feat(policy)! → patch until 0.1.0.

History replay before severity. Before any newly introduced denial is enabled, replay each new predicate over git rev-list origin/main, recording commits examined, firings, and false positives in the implementation evidence. Fixtures cover free-text refusal output, an emitted-but-unregistered token, an unused registry entry, an unresolved route, and an override lacking either a precondition or a non-override route; a conforming module passes.

Scope boundary. Stated once in the dependency-graph paragraph at the top of this row rather than repeated here.

CLOUD-1158 `[prune]`'s floors are measured against a test-target count that grows one per retired gate, and nothing reds when that basis moves

Why

Floor requires a measured date and validates it as a calendar date — deliberately, because "a stale measurement reads exactly like a fresh one". That is CLOUD-266's rule, discharged. But a date is not the basis; it is a pointer to a basis nobody wrote down, so nothing can tell whether the world under the number has moved.

It has moved, and it moves on a schedule this repository sets for itself.

Corrected 2026-08-29, same day, before dispatch. Three claims in the first draft were loose. §2 put the comparison in Floor::validate, which is on the path every mediated tool call pays for — see §2 for the correction and the precedent. The "131 MB" figure below was the rlib, not the linked binary. And the 41 → 110 series compares two counts taken different ways; both are now labelled.

The arithmetic nobody watches

Retained bytes after a perfectly successful prune are keep × stems × size. keep = 2 and size are stable. stems is not:

distinct integration-test stems, CLOUD-766, 2026-08-20 ~41
crates/batten/tests/*.rs, 2026-08-29 110
tests/*.bats suites still standing 147

Every one of those .rs files is an independent cargo test target — crates/batten/Cargo.toml sets no autotests = false and declares no [[test]], so cargo discovers all 110 — and each links against the workspace library. Sizes, corrected: 131 MB is the libbatten rlib, not the linked binary. CLOUD-766 measured integration test binaries at 36–100 MB (cli-<hash> at 63 MB and 36 MB); the lib test target on this container is 144–172 MB.

The two counts in the table were taken differently, and the row should not imply one series. ~41 is *stems resident in *deps on a tree where the suite had been built (CLOUD-766, 2026-08-20); 110 is tracked test files (git ls-files, 2026-08-29). Both are test targets in substance, but one counts what was built and the other counts what exists. And the count is not drifting by accident: CLOUD-843's bash retirement adds one per retired gate.claude/rules/policy-modules.md makes the compiled-binary tier the second, non-optional tier of every retirement, so each of the 147 standing suites owes a crates/batten/tests/*.rs when it goes.

So the floor's basis is being multiplied by the campaign the repository is mid-way through, and the mechanism that would notice is the one thing Floor does not record.

Limit of this reading, stated rather than smoothed over: the container's clone is shallow (50 commits, back to 2026-08-26), so the intermediate points between 41 and 110 were not measurable from it. 41 is CLOUD-766's recorded figure and 110 is git ls-files; the trend between them is inferred from CLOUD-843's mechanism, not sampled.

What that costs today

[prune.warm].measured = "2026-08-22". [prune.cold].measured = "2026-08-29". Both carry multiplier = 1, so each floor is exactly the worst lap observed on the day it was taken — CLOUD-861's follow-up already recorded that the x2 became x1 when the number moved, and that whether that was deliberate is written down nowhere.

A floor taken against a smaller stem count is not merely conservative, it is wrong in the direction that fails silently: the check passes, the build then writes more than the basis anticipated, and the exhaustion arrives as a rustc IO error inside a test run. That is the presentation prune.rs's own header says the refusal exists to prevent.

Why it is worth a row

CLOUD-861 makes the floor ratchet from observation, which handles the number. It does not handle the basis: a ratcheting floor over a growing base climbs until it exceeds the container's allowance, and a floor nothing can satisfy is the failure batten.toml's [prune] comment already names as the one that gets a gate switched off. This is the sensor that makes that visible before it happens, and it is a different predicate from either CLOUD-861's or CLOUD-1157's.


Refinement — Ready (a floor records the basis it was measured against, not only the date)

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

  • **Source of truth (§1). **Floor and Floor::validate in crates/batten/src/prune.rs, and the [prune.warm] / [prune.cold] tables in batten.toml. The basis is the consumer's — which glob and which count — and the comparison is the engine's, which is what keeps non-negotiable rule 1. mise-tasks/target-prune.sh no longer exists (retired whole under CLOUD-1059); mise-tasks/** and tests/**/*.bats are refused by V-SHELL-RULE-EDITED with no override and no bypass_env.

  • **Computable predicate (§2). **Floor gains the basis beside the date: a declared glob and the file count it held when the lap was measured, plus a tolerance. The comparison reds when the live count for that glob has moved past the tolerance since measured — a count against a count, an exit code over the tree, no judgement and no model verdict.

    The comparison must NOT live in Floor::validate — corrected 2026-08-29 before dispatch, because the first draft put it exactly there. crates/batten/src/config.rs:1038 calls prune.validate() from the shared config-load path, which **every **batten invocation runs, batten hook on every mediated tool call included. A glob walk there is taxed onto the PreToolUse path against perf-assert's ceiling, and batten check over 654 tracked files already costs ~3.6 ms above the process floor. The repository has ruled on this exact shape once: claim-race-check was moved off the mediated call because a round trip on every tool call is disqualifying, and RuleKind::scopes pairs every spawning kind with RuleScope::Tree alone.

    So: Floor carries the declared basis (it is config, and validating its shape at load is free), and the comparison against the live tree runs in batten target prune — already Cost::Effect on Surface::VerifyOnly, which perf-assert deliberately budgets no ceiling for — or as a tree-scoped rule row under verify. Either is acceptable; Floor::validate is not.

  • Effect (§3). No new verb and no new surface. Shape validation of the declared basis stays at config load; the tree comparison is read-only and runs on Surface::VerifyOnly, never on the mediated call.

  • Output & exit (§5). Pointer-only per non-negotiable rule 4: the glob, the declared count, the live count, the tolerance and the measured date. Never a file listing — the count is the finding, and the paths are unbounded.

  • **Commit / bump (§6). **fix(prune)patch until 0.1.0. The workspace is 0.0.130, so release-plz bumps the patch whatever the type says.

  • **Test obligation (§7). **crates/batten/tests/target_prune.rs, the compiled-binary tier, since the live count has to come from a real tree rather than from a fabricated one. Shown able to fail (CLOUD-418):

    • a floor whose declared basis count no longer matches the fixture tree is refused by the verify-surface comparison, naming both numbers;
    • the placement case: a mediated call (batten hook) over a tree whose basis count has moved is not refused and does not walk the tree — red if the comparison is put back on the config-load path, which is what the first draft specified;
    • anti-vacuity twin: a count inside the tolerance loads clean, and a tree at exactly the declared count loads clean — without these the check is "always red", which is a gate nobody keeps;
    • a floor declaring no basis at all is refused, on the same ground measured already is: an absent basis reads exactly like a satisfied one.
  • Blockers (§8). None. It composes with CLOUD-861 and CLOUD-1157 and depends on neither: the number, the reclaim and the basis are three separate mechanisms, and this one is landable alone. relatedTo CLOUD-861 (the ratcheting floor this keeps satisfiable), CLOUD-1157 (the reclaim that sets what size × stems actually retains), CLOUD-266 (the budget-carries-its-measurement rule this extends), CLOUD-843 (the campaign moving the term).

Acceptance

  • Each floor in [prune] declares the basis it was measured against — a glob and a count — beside its date.
  • A tree whose live count for that glob has moved past the declared tolerance is refused on the verify surface, naming the declared count, the live count and the date.
  • batten hook does not walk the tree for this, and perf-assert is unmoved — the comparison is off the mediated call.
  • A tree inside the tolerance loads clean, so the check discriminates rather than always refusing.
  • The declared basis is the consumer's, in batten.toml; no count and no glob appears in crates/batten.

Provenance. Found while diagnosing why mise run land still exhausts the disk when it laps, with batten target prune in place and released in v0.0.129. The reading that produced it: keep = 2 bounds copies per stem and nothing bounds stems, and the repository is actively multiplying stems as a matter of policy.

CLOUD-516 The claim receipt is keyed by branch name, so a branch restarted after its PR merged inherits a stale claim and `claim-guard` passes on it silently

Why

Measured 2026-08-13. .git/batten-receipts/claim.claude-groom-cloud-491-qv83e7 contains CLOUD-230. The work done on that branch this session was CLOUD-507, CLOUD-505, CLOUD-456 and CLOUD-513 — none of which CLOUD-230 has anything to do with. claim-guard was satisfied on every edit behind all four, by a claim for an unrelated issue, and reported nothing.

The cause is that the receipt is keyed by branch name, and a name is not a branch. After its PR merged, the branch was restarted twice with git checkout -B claude/groom-cloud-491-qv83e7 origin/main — the documented remedy for a merged PR, since new work must not stack on already-merged history. That repoints the name at a new base and discards the old commits. The receipt, living in .git/batten-receipts/ under the name, survived both.

Branch-keying was the right call and remains right for the case it was chosen for. claim-check's own header argues it: a claim attests to a decision about an issue that every commit on the branch continues to serve, so a SHA-keyed receipt would demand a re-claim per commit. That reasoning is sound. What it does not cover is the name outliving the branch it described.

This is the failure class this repo treats as worse than no gate. claim-guard did not error, did not warn, and did not skip — it passed, on evidence that had expired. A gate reporting green on state it never checked is the silent false green that linear-check's unguarded fetch, lock-check's regenerate-and-diff, and verify's unguarded body were each fixed for. The fix in every one of those cases was the same shape: record what the verdict was made against, and treat it as absent when that moves. linear-check's receipt already records the origin/main it was linear against; ready-guard's key to a SHA. The claim receipt records neither.

Refinement — Ready

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

  • Source of truth (§1). The claim receipt under $(git rev-parse --git-dir)/batten-receipts/, and git. No tracker read: the question is entirely about local state, which is what makes it answerable in a hook at all.

  • The naive predicate is wrong, and the reason is the landing loop (§2). The obvious rule — void the receipt when the branch's merge-base with origin/main moves — fires on every lap, because land rebases onto the current origin/main each time round and that is the loop working, not a fault. A rule that demanded a re-claim per lap would be refused within a day, and it would be refused correctly.

  • Mechanism as a computable predicate (§2). The receipt records the origin/main SHA it was minted against. It is void when both: the current merge-base with origin/main differs from the recorded one, and the branch carries no commits of its own (git rev-list --count origin/main..HEAD is 0).

    That conjunction is exactly a restart and nothing else:

    situation                                      base moved   own commits   verdict
    ---------------------------------------------------------------------------------
    claim, then work                               no           0 -> n        valid
    a lap rebases onto newer main                  yes          >=1           VALID  (the loop working)
    main moves, branch untouched                   yes          >=1           valid
    checkout -B <name> origin/main after a merge   yes          0             VOID
    fresh branch, fresh claim                      n/a          0             re-minted anyway
    

    A restarted branch is the one state that has both a moved base and nothing of its own, because the restart discarded the commits that were the branch. No timestamps, no reflog, no heuristics.

  • Effect (§3). read for the predicate; the receipt write already exists on claim-check's pullable path and does not change class. No new verb and no SURFACE change.

  • Output & exit contract (§5). claim-check keeps 0 pullable / 1 not pullable / 2 unreadable stdin. A void receipt reads as absent, so claim-guard's existing refusal and message are reused unchanged — the remedy is already "run claim-check for the issue you mean to pull", which is exactly right here. Pointer-only: the branch and the recorded base, never a body.

  • Commit / bump (§6). fix(claim-check) — patch until 0.1.0 regardless of type.

  • Test obligation (§7). tests/claim-check.bats and tests/claim-guard.bats, over a fixture clone, one row per line of the table above. Mutation-checked per CLOUD-418: with the own-commits half of the conjunction removed, the rebase row must go red — that is the direction a careless fix breaks, and the row that proves the conjunction is doing work rather than decorating.

    The regression case is this branch: a receipt naming an issue, then checkout -B <name> origin/main, then an edit — refused, where today it passes.

  • Blockers (§8). None. relatedTo CLOUD-431 — it widens the same receipt for a different question (was the block refined, and by whom), so the two touch one file and should be sequenced rather than merged; CLOUD-444 — it proposes retiring claim-guard into the engine while keeping branch-keying, and would carry this defect across unchanged; CLOUD-377 — the other way this receipt is not minted when it should be; CLOUD-514 — which proposes a third branch-keyed receipt and inherits this until it lands.

Acceptance

  • A branch restarted onto a new base carries no usable claim, and the first edit on it is refused.
  • A branch landing normally is never asked to re-claim, on any lap, however many times it rebases.
  • The rebase row is shown able to fail when the conjunction is weakened.
  • A receipt with no recorded base reads as void rather than as valid, so receipts predating this change do not grandfather themselves in.

Not in this issue

Whether the receipt should carry more than a base — the ready-lint verdict and updatedAt that CLOUD-431 adds for a different question. And retiring claim-guard into the engine, which is CLOUD-444's.

CLOUD-969 The emitted spec carries no version and no stable ids, so a third party reading it has nothing to pin against — add `spec_version` and a stable id per command path

Why

batten spec is this repository's answer to jdx's usageCLOUD-114 was literally titled "emit the usage spec at runtime (batten usage)" before being folded into CLOUD-27/CLOUD-20, and CLOUD-244 dropped the KDL encoding while keeping the concept. It is the one machine contract a third party reads.

**Measured against **main on 2026-08-30, it carries no version and no identity. spec.rs:131-141:

pub struct SpecDocument {
    #[serde(flatten)]
    pub command: CommandSpec,
    pub read_only_allowlist: Vec<String>,
}

Two keys. CommandSpec (spec.rs:36-48) carries path, about, effect, flags, subcommands. There is **no **spec_version, and the only identity a row has is its path — the human-facing spelling, which is exactly the thing the surface-repair work (CLOUD-1179, CLOUD-1180) is about to change.

So a consumer that reads the spec today has:

  • nothing to pin against. Every change is indistinguishable from every other change; there is no way to say "I understand spec shape N" and no way for the binary to say "I emit shape N".
  • no stable handle. read_only_allowlist is a list of paths, and the whole point of §5's derivation is that a consumer honours it. Rename payload field and every consumer's pinned allowlist silently stops matching — in the direction where a command it thought was read-only is now unrecognised, or worse, a path it still trusts no longer means what it did.

This is the ordinary versioning obligation that CLOUD-1179's window makes cheap: §2 says "no back-compatibility surface and no deprecation aliases" pre-0.1.0. **After **0.1.0 an unversioned contract has to be versioned with consumers already on it.

Why this is now its own row

This row was previously scoped as "Preset package and capability discovery" — one issue covering both the spec contract and the canonical preset-package manifest. Those are different sizes and different blockers: the spec half is a struct with two new fields and its pinning test, unblocked and landable today; the manifest half is a design against policy.rs's three parallel hardcoded tables and blocks CLOUD-970. Split so the small half is not held behind the large one. The manifest half is CLOUD-1181.

What "stable id" has to mean here, or it buys nothing

**2026-08-30 — two more fields belong in this row, found while pressure-testing the surface redesign. **CommandSpec is emitted with five fields and the surface declares more than it publishes. effect already ships — that is correct as the Why states, and is not the gap. The gaps are:

  • data_channel **never leaves the binary. **CommandDecl declares data_channel: bool (surface.rs:408), and CommandSpec has no such field; walk() and describe() never read it. It is a build-time-only column feeding every_data_emitting_verb_declares_the_json_flag (surface.rs:3045) and is_noun() (surface.rs:2723). A consumer of the emitted spec can only infer the data channel by scanning each row's flags for one named json.
  • positional **is not expressible at all. **FlagSpec (spec.rs:18-30) has no positional field, so a positional argument emits as long: null, takes_value: trueindistinguishable from a flag that lost its long form. A consumer reconstructing an invocation from the spec emits a broken command line and gets no signal. This becomes acute under CLOUD-1184's grammar, where check's rule selector moves from --rule into the object position: the change would be invisible to every spec consumer.

Both are the same defect as this row's own: the published contract omits what the surface declares, so a versioned spec would be versioning an incomplete document. They belong here rather than in a separate row precisely because spec_version should not ship first and then immediately need to move.

This also sharpens the third acceptance bullet. Under CLOUD-1184 read_only_allowlist becomes derived from the verb rather than filtered from the effect table — which is a stronger answer than either option that bullet offers, and removes the "keyed on a spelling that is about to change" hazard by construction.

An id that is derived from the path is the path with extra steps and re-breaks on the same rename. The id has to be **declared on the **CommandDecl **row in **surface.rs — the one command authority (CLOUD-19's directive; surface.rs:387-411) — so a rename changes path and leaves id alone, and SURFACE remains the only place a command is described.

That is also what makes the id checkable: spec::tests::the_emitted_surface_is_exactly_the_committed_row_set already pins the row set literally, so an id added to a row and an id changed on a row are both visible in that same diff.


Refinement — Ready (version and identify the emitted spec)

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

  • Authority boundary (§1). crates/batten/src/spec.rs and crates/batten/src/surface.rs, plus the derived artifacts mise run fix regenerates. One authority: the id is a field on CommandDecl, never a second table keyed by path. No mise-tasks/ program and no tests/**/*.bats is added or edited.
  • Computable predicate (§2). batten spec --format json emits a spec_version at the document root, and every command row carries a stable id declared on its SURFACE row. Two invariants hold in cargo test: every declared path has an id, and no id is declared twice — the same shape as every_path_is_declared_once, over the new field.
  • Deliberately not in scope (§2). Capability ids for anything other than a command path — no preset ids, no policy-module ids, no provider capability vocabulary; that is CLOUD-1181's manifest and CLOUD-970's resolver. Any back-compatibility machinery: pre-0.1.0 there is nothing to be compatible with, and adding a shim now is the deprecation surface §2 refuses. Deciding the version's bump policy for 0.1.0 and after — state the rule, do not build enforcement for it.
  • Effect (§3). read. spec is already a read leaf and stays one; this adds fields to what it emits and changes no effect annotation.
  • Output and exit (§5–§6). Byte-stable JSON across two runs of the same binary; spec_version is a literal in the source, never derived from a clock, an environment variable or the crate version at runtime — a version that moves with CARGO_PKG_VERSION says "the binary changed", which is what the tag already says, and tells a consumer nothing about the shape. Exit follows the 0/1/2/3 table unchanged.
  • Commit / bump (§6). feat(spec)patch until 0.1.0 on the consumer surface. The library half is mise run semver's: SpecDocument and CommandSpec are pub, so adding public fields is a semver event that task decides, and this row records its verdict rather than asserting one.
  • Test obligation (§7). Over the compiled binary in crates/batten/tests/, plus the two spec.rs unit invariants. Shown able to fail per CLOUD-418, three observed: (a) a SURFACE row with no id fails the completeness assertion; (b) two rows sharing an id fails the uniqueness assertion; (c) spec --format json run twice is byte-identical, and the emitted document contains spec_version. Add each id to the_emitted_surface_is_exactly_the_committed_row_set's literal list with a comment, per that test's own convention.
  • Blockers (§8). None — this is the unblocked half of the split and needs no manifest. relatedTo CLOUD-1181 (the preset manifest, formerly this row's second half), CLOUD-970 (which consumes both), CLOUD-1180 (the agent subtree, whose discovery leaf reads this document), CLOUD-1179 (the §2/binary drift, which is why path is not a durable id), CLOUD-973 (extending the emitted surface), CLOUD-244 (which dropped the KDL encoding and set the "SURFACE is authoritative" rule), CLOUD-418.

Acceptance

  • batten spec --format json emits spec_version, and its value is a source literal with a stated rule for when it moves.
  • Every SURFACE row declares an id; the completeness and uniqueness assertions both exist and were each shown to fail.
  • read_only_allowlist is reconciled with the id: either it emits ids alongside paths, or the row records why paths alone are sufficient for the allowlist specifically. Leaving §5's safety-critical derivation keyed on a spelling that is about to change is the failure this row exists to prevent.
  • mise run fix then mise run verify regenerates every derived artifact to a zero byte diff.

Split out of the former "Preset package and capability discovery" while recovering the batten agent subtree (CLOUD-1180): the manifest half blocks CLOUD-970 and needs a design, and the spec half is a struct field that was holding behind it.

Review in Linear

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 48 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: 380613b2-3fe6-4849-b5f7-8402b7ec8412

📥 Commits

Reviewing files that changed from the base of the PR and between 103b971 and 6363a2e.

⛔ 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 (20)
  • .serena/memories/core.md
  • batten.toml
  • completions/batten.bash
  • completions/batten.fish
  • completions/batten.zsh
  • crates/batten/src/claim.rs
  • crates/batten/src/cli.rs
  • crates/batten/src/lease.rs
  • crates/batten/src/lib.rs
  • crates/batten/src/record.rs
  • crates/batten/src/rules.rs
  • crates/batten/src/spec.rs
  • crates/batten/src/surface.rs
  • crates/batten/tests/it/main.rs
  • crates/batten/tests/it/obligations_bound.rs
  • crates/batten/tests/it/plan_complete.rs
  • crates/batten/tests/it/pointer_only.rs
  • crates/batten/tests/it/record_closes.rs
  • policy/module-layering.rego
  • policy/spawn-adapters.rego
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: cfb8bf72-a578-48bf-a9e4-9ce88cd8d584

📥 Commits

Reviewing files that changed from the base of the PR and between dd4d921 and 103b971.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • crates/batten/tests/it/snapshots/it__snapshots__golden_json_schema.snap is excluded by !**/*.snap
📒 Files selected for processing (23)
  • .claude/rules/rust.md
  • .serena/memories/core.md
  • Cargo.toml
  • batten.toml
  • completions/batten.bash
  • completions/batten.fish
  • completions/batten.zsh
  • crates/batten/Cargo.toml
  • crates/batten/src/cli.rs
  • crates/batten/src/git.rs
  • crates/batten/src/gitwrite.rs
  • crates/batten/src/lease.rs
  • crates/batten/src/lib.rs
  • crates/batten/src/rules.rs
  • crates/batten/src/semver.rs
  • crates/batten/src/spec.rs
  • crates/batten/src/surface.rs
  • crates/batten/tests/it/main.rs
  • crates/batten/tests/it/narrow_adoption.rs
  • crates/batten/tests/it/pointer_only.rs
  • crates/batten/tests/it/rebase.rs
  • policy/module-layering.rego
  • policy/spawn-adapters.rego

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds Git smart-HTTP landing leases with compare-and-swap updates, packfile handling, lifecycle commands, health checks, and stalled-holder termination. It adds local Git object writes and in-process rebasing with worktree synchronization. Recorder data now uses claim-scoped paths. Command surfaces, completions, manual pages, policy configuration, output budgets, stable command IDs, and validation tests are expanded.

Merge Risk: ⚪ Minimal · up to 103b9

The PR adds the lease workflow and related record, policy, CLI, and documentation changes with the stated checks passing; no actionable merge-blocking risk remains beyond normal checks and review.


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 force-pushed the claude/landing-loop-bundle-4puk4i branch from 717afda to a8788b3 Compare September 1, 2026 19:33
wenzowski added a commit that referenced this pull request Sep 1, 2026
`filed-over-own-diff` refuses `batten.toml` on this branch, and the route its
own class declares — R-CLOSE-IT-IN-THE-BODY — is the one taken: PR #812's body
opens with `Closes CLOUD-1298`. The gate cannot see it, and the reason is a
mechanism gap rather than an unmet condition.

The exemption reads the `pr-closes` RECORD, minted by a `[[recorder]]` over a
`gh pr view` Bash call. This environment has no `gh`, so the evidence channel is
unavailable — and fabricating a tool result to mint the record would forge the
very evidence the exemption exists to demand.

TWO FURTHER FACTS, FOUND WHILE ESTABLISHING THAT, AND THE FIRST IS A DEFECT:

  * The record was STALE, still naming CLOUD-1269 and CLOUD-1279 from the PR
    this branch carried before it was reset onto the merged trunk.
    `record_path` is branch-keyed with no PR identity, so a reused branch name
    inherits the previous PR's closes-record and the exemption then reads the
    wrong PR's keys. Same family as the lease record's history bound one level
    up: no per-PR partition.
  * Removing that stale line does not exempt either. The predicate needs a
    POSITIVE record naming the key, so an absent channel refuses where an
    absent finding would have passed.

Admits: e8a1d7bfdb26149ed897edccf84b90248b37b2f15ac077991be687da6d75f0f8
Admits-rule: filed-here
Admits-verdict: V-FILED-OVER-OWN-DIFF
Admits-subject: batten.toml
Admits-head: 717afda
Admits-epoch: c4d86ca484c6cc808f9252e1394b9ad10993d6646fdcf5864522e2af3d23a020
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: Nothing is deferred, so the toll prices a punt that did not happen. Paying it any other way means either not filing the row — leaving the wiring's ordering bound, its rejected wide-selector alternative and its `peek next` defect recorded nowhere — or splitting the file from the fix across two branches, which is the batching this repository's whole trunk discipline exists to prevent.
Admits-answer-precondition: CLOUD-1298 DOCUMENTS this change rather than deferring it. Its §1 names `batten.toml` because `batten.toml` is what the change edits — the two `[program]` rows, the `[[pattern]]` and the `[[recorder]]` that make #810's landed predicate read something real. The row was filed and fixed in the same branch, and the fix is in this diff.
Admits-answer-rejected-route: R-CLOSE-IT-IN-THE-BODY IS THE ROUTE I TOOK, and the override is only because the gate cannot see it. PR #812's body opens with `Closes CLOUD-1298`. The exemption reads the `pr-closes` RECORD, which is minted by a `[[recorder]]` over a `gh pr view` Bash call, and this environment has no `gh` CLI — so the evidence channel is unavailable rather than unsatisfied, and I will not fabricate a tool result to mint it. Two further facts found while establishing that: the record was also STALE, still naming CLOUD-1269/CLOUD-1279 from the PR this branch carried before it was reset, because `record_path` is branch-keyed with no PR identity; and removing that stale line does not exempt either, since the predicate needs a positive record rather than an absent one. R-FIX-IT-HERE does not apply because there is nothing to fix — the row is not a punt. R-FILE-IT-AFTER-LANDING would land the mechanism with its own reasoning unrecorded, which is the failure the row exists to prevent.
Weakens: program-changed program[land-lock-status]
Weakens: program-changed program[land-lock-peek]
Weakens: recorder-added recorder[landing-lease]
wenzowski added a commit that referenced this pull request Sep 1, 2026
…e alone

A branch NAME outlives the branch it described. `git checkout -B <name>
origin/main` discards the commits that were the branch while every name-keyed
file survives — CLOUD-516's finding, which the claim receipt already answers
for itself by recording the base it was made against. A `[[recorder]]`'s
record had no such discriminator at all, so the next attempt on a reused
branch name read the previous one's lines as its own.

MEASURED HERE, NOT REASONED ABOUT. After PR #810 merged and this branch was
reset onto the new trunk, `pr-closes.<branch>` still held `closes
2:CLOUD-1269,CLOUD-1279` — that PR's keys. The PR actually open was #812,
closing CLOUD-1298, which appeared nowhere in the record.

THE DANGEROUS DIRECTION IS THE SILENT ONE. `filed-over-own-diff` exempts a row
the PR CLOSES and reads that from this record, so a stale record does not
merely refuse an honest file-then-fix: a row named by the PREVIOUS PR's
closes-record would be EXEMPTED on a PR that does not close it, with nothing
downstream to re-check.

THE CLAIM RATHER THAN THE BASE, and the difference is what makes it usable. A
base moves on every rebase and `land` rebases every lap, so keying a record on
one would discard it mid-landing — the failure this prevents, arriving by
another route. A claim is re-minted per PULL and is stable across every rebase
in between, so it partitions exactly the attempts that must not see each other.

`claimed_token` is order-insensitive over a multi-key claim, or a re-claim of
the same work would partition itself away from its own record. `None` is
could-not-look and keeps the OLD path, which is what makes this a partition
rather than a migration: nothing that could not be attributed is moved, and an
unclaimed branch behaves exactly as before.

THREE READERS, AND THE THIRD IS THE ONE A NARROW FIX MISSES. `append_all`
writes, `recorder_records` projects the tree fact — and `filed-here`'s own
end-of-turn checklist reads the board record directly in `lib.rs`. Without the
same partition there it would list a previous attempt's rows as this one's.

Test obligation: four cases over the compiled binary, driving `batten hook`
twice under two claims on one branch name. The false-exemption case is the one
a naive suite misses, so it is written first and asserts the ABSENCE of the
previous attempt's key; its anti-vacuity mirror proves one claim still
accumulates across calls, without which the case is satisfied by a partition
so eager that no record survives a lap at all.

Refs: CLOUD-1300
Refs: CLOUD-516
@wenzowski wenzowski changed the title feat(policy): wire the lease record, so consumer #1 eats this food too feat(lease): the landing lease's CAS, over the client the engine already vendors Sep 1, 2026
@wenzowski
wenzowski force-pushed the claude/landing-loop-bundle-4puk4i branch from b55f6c1 to dd4d921 Compare September 1, 2026 20:34
wenzowski added a commit that referenced this pull request Sep 1, 2026
`filed-over-own-diff` refuses `batten.toml` on this branch, and the route its
own class declares — R-CLOSE-IT-IN-THE-BODY — is the one taken: PR #812's body
opens with `Closes CLOUD-1298`. The gate cannot see it, and the reason is a
mechanism gap rather than an unmet condition.

The exemption reads the `pr-closes` RECORD, minted by a `[[recorder]]` over a
`gh pr view` Bash call. This environment has no `gh`, so the evidence channel is
unavailable — and fabricating a tool result to mint the record would forge the
very evidence the exemption exists to demand.

TWO FURTHER FACTS, FOUND WHILE ESTABLISHING THAT, AND THE FIRST IS A DEFECT:

  * The record was STALE, still naming CLOUD-1269 and CLOUD-1279 from the PR
    this branch carried before it was reset onto the merged trunk.
    `record_path` is branch-keyed with no PR identity, so a reused branch name
    inherits the previous PR's closes-record and the exemption then reads the
    wrong PR's keys. Same family as the lease record's history bound one level
    up: no per-PR partition.
  * Removing that stale line does not exempt either. The predicate needs a
    POSITIVE record naming the key, so an absent channel refuses where an
    absent finding would have passed.

Admits: e8a1d7bfdb26149ed897edccf84b90248b37b2f15ac077991be687da6d75f0f8
Admits-rule: filed-here
Admits-verdict: V-FILED-OVER-OWN-DIFF
Admits-subject: batten.toml
Admits-head: 717afda
Admits-epoch: c4d86ca484c6cc808f9252e1394b9ad10993d6646fdcf5864522e2af3d23a020
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: Nothing is deferred, so the toll prices a punt that did not happen. Paying it any other way means either not filing the row — leaving the wiring's ordering bound, its rejected wide-selector alternative and its `peek next` defect recorded nowhere — or splitting the file from the fix across two branches, which is the batching this repository's whole trunk discipline exists to prevent.
Admits-answer-precondition: CLOUD-1298 DOCUMENTS this change rather than deferring it. Its §1 names `batten.toml` because `batten.toml` is what the change edits — the two `[program]` rows, the `[[pattern]]` and the `[[recorder]]` that make #810's landed predicate read something real. The row was filed and fixed in the same branch, and the fix is in this diff.
Admits-answer-rejected-route: R-CLOSE-IT-IN-THE-BODY IS THE ROUTE I TOOK, and the override is only because the gate cannot see it. PR #812's body opens with `Closes CLOUD-1298`. The exemption reads the `pr-closes` RECORD, which is minted by a `[[recorder]]` over a `gh pr view` Bash call, and this environment has no `gh` CLI — so the evidence channel is unavailable rather than unsatisfied, and I will not fabricate a tool result to mint it. Two further facts found while establishing that: the record was also STALE, still naming CLOUD-1269/CLOUD-1279 from the PR this branch carried before it was reset, because `record_path` is branch-keyed with no PR identity; and removing that stale line does not exempt either, since the predicate needs a positive record rather than an absent one. R-FIX-IT-HERE does not apply because there is nothing to fix — the row is not a punt. R-FILE-IT-AFTER-LANDING would land the mechanism with its own reasoning unrecorded, which is the failure the row exists to prevent.
Weakens: program-changed program[land-lock-status]
Weakens: program-changed program[land-lock-peek]
Weakens: recorder-added recorder[landing-lease]
wenzowski added a commit that referenced this pull request Sep 1, 2026
…e alone

A branch NAME outlives the branch it described. `git checkout -B <name>
origin/main` discards the commits that were the branch while every name-keyed
file survives — CLOUD-516's finding, which the claim receipt already answers
for itself by recording the base it was made against. A `[[recorder]]`'s
record had no such discriminator at all, so the next attempt on a reused
branch name read the previous one's lines as its own.

MEASURED HERE, NOT REASONED ABOUT. After PR #810 merged and this branch was
reset onto the new trunk, `pr-closes.<branch>` still held `closes
2:CLOUD-1269,CLOUD-1279` — that PR's keys. The PR actually open was #812,
closing CLOUD-1298, which appeared nowhere in the record.

THE DANGEROUS DIRECTION IS THE SILENT ONE. `filed-over-own-diff` exempts a row
the PR CLOSES and reads that from this record, so a stale record does not
merely refuse an honest file-then-fix: a row named by the PREVIOUS PR's
closes-record would be EXEMPTED on a PR that does not close it, with nothing
downstream to re-check.

THE CLAIM RATHER THAN THE BASE, and the difference is what makes it usable. A
base moves on every rebase and `land` rebases every lap, so keying a record on
one would discard it mid-landing — the failure this prevents, arriving by
another route. A claim is re-minted per PULL and is stable across every rebase
in between, so it partitions exactly the attempts that must not see each other.

`claimed_token` is order-insensitive over a multi-key claim, or a re-claim of
the same work would partition itself away from its own record. `None` is
could-not-look and keeps the OLD path, which is what makes this a partition
rather than a migration: nothing that could not be attributed is moved, and an
unclaimed branch behaves exactly as before.

THREE READERS, AND THE THIRD IS THE ONE A NARROW FIX MISSES. `append_all`
writes, `recorder_records` projects the tree fact — and `filed-here`'s own
end-of-turn checklist reads the board record directly in `lib.rs`. Without the
same partition there it would list a previous attempt's rows as this one's.

Test obligation: four cases over the compiled binary, driving `batten hook`
twice under two claims on one branch name. The false-exemption case is the one
a naive suite misses, so it is written first and asserts the ABSENCE of the
previous attempt's key; its anti-vacuity mirror proves one claim still
accumulates across calls, without which the case is satisfied by a partition
so eager that no record survives a lap at all.

Refs: CLOUD-1300
Refs: CLOUD-516
@wenzowski
wenzowski force-pushed the claude/landing-loop-bundle-4puk4i branch from f482b8a to cf0e751 Compare September 2, 2026 01:23
wenzowski added a commit that referenced this pull request Sep 2, 2026
`filed-over-own-diff` refuses `batten.toml` on this branch, and the route its
own class declares — R-CLOSE-IT-IN-THE-BODY — is the one taken: PR #812's body
opens with `Closes CLOUD-1298`. The gate cannot see it, and the reason is a
mechanism gap rather than an unmet condition.

The exemption reads the `pr-closes` RECORD, minted by a `[[recorder]]` over a
`gh pr view` Bash call. This environment has no `gh`, so the evidence channel is
unavailable — and fabricating a tool result to mint the record would forge the
very evidence the exemption exists to demand.

TWO FURTHER FACTS, FOUND WHILE ESTABLISHING THAT, AND THE FIRST IS A DEFECT:

  * The record was STALE, still naming CLOUD-1269 and CLOUD-1279 from the PR
    this branch carried before it was reset onto the merged trunk.
    `record_path` is branch-keyed with no PR identity, so a reused branch name
    inherits the previous PR's closes-record and the exemption then reads the
    wrong PR's keys. Same family as the lease record's history bound one level
    up: no per-PR partition.
  * Removing that stale line does not exempt either. The predicate needs a
    POSITIVE record naming the key, so an absent channel refuses where an
    absent finding would have passed.

Admits: e8a1d7bfdb26149ed897edccf84b90248b37b2f15ac077991be687da6d75f0f8
Admits-rule: filed-here
Admits-verdict: V-FILED-OVER-OWN-DIFF
Admits-subject: batten.toml
Admits-head: 717afda
Admits-epoch: c4d86ca484c6cc808f9252e1394b9ad10993d6646fdcf5864522e2af3d23a020
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: Nothing is deferred, so the toll prices a punt that did not happen. Paying it any other way means either not filing the row — leaving the wiring's ordering bound, its rejected wide-selector alternative and its `peek next` defect recorded nowhere — or splitting the file from the fix across two branches, which is the batching this repository's whole trunk discipline exists to prevent.
Admits-answer-precondition: CLOUD-1298 DOCUMENTS this change rather than deferring it. Its §1 names `batten.toml` because `batten.toml` is what the change edits — the two `[program]` rows, the `[[pattern]]` and the `[[recorder]]` that make #810's landed predicate read something real. The row was filed and fixed in the same branch, and the fix is in this diff.
Admits-answer-rejected-route: R-CLOSE-IT-IN-THE-BODY IS THE ROUTE I TOOK, and the override is only because the gate cannot see it. PR #812's body opens with `Closes CLOUD-1298`. The exemption reads the `pr-closes` RECORD, which is minted by a `[[recorder]]` over a `gh pr view` Bash call, and this environment has no `gh` CLI — so the evidence channel is unavailable rather than unsatisfied, and I will not fabricate a tool result to mint it. Two further facts found while establishing that: the record was also STALE, still naming CLOUD-1269/CLOUD-1279 from the PR this branch carried before it was reset, because `record_path` is branch-keyed with no PR identity; and removing that stale line does not exempt either, since the predicate needs a positive record rather than an absent one. R-FIX-IT-HERE does not apply because there is nothing to fix — the row is not a punt. R-FILE-IT-AFTER-LANDING would land the mechanism with its own reasoning unrecorded, which is the failure the row exists to prevent.
Weakens: program-changed program[land-lock-status]
Weakens: program-changed program[land-lock-peek]
Weakens: recorder-added recorder[landing-lease]
wenzowski added a commit that referenced this pull request Sep 2, 2026
…e alone

A branch NAME outlives the branch it described. `git checkout -B <name>
origin/main` discards the commits that were the branch while every name-keyed
file survives — CLOUD-516's finding, which the claim receipt already answers
for itself by recording the base it was made against. A `[[recorder]]`'s
record had no such discriminator at all, so the next attempt on a reused
branch name read the previous one's lines as its own.

MEASURED HERE, NOT REASONED ABOUT. After PR #810 merged and this branch was
reset onto the new trunk, `pr-closes.<branch>` still held `closes
2:CLOUD-1269,CLOUD-1279` — that PR's keys. The PR actually open was #812,
closing CLOUD-1298, which appeared nowhere in the record.

THE DANGEROUS DIRECTION IS THE SILENT ONE. `filed-over-own-diff` exempts a row
the PR CLOSES and reads that from this record, so a stale record does not
merely refuse an honest file-then-fix: a row named by the PREVIOUS PR's
closes-record would be EXEMPTED on a PR that does not close it, with nothing
downstream to re-check.

THE CLAIM RATHER THAN THE BASE, and the difference is what makes it usable. A
base moves on every rebase and `land` rebases every lap, so keying a record on
one would discard it mid-landing — the failure this prevents, arriving by
another route. A claim is re-minted per PULL and is stable across every rebase
in between, so it partitions exactly the attempts that must not see each other.

`claimed_token` is order-insensitive over a multi-key claim, or a re-claim of
the same work would partition itself away from its own record. `None` is
could-not-look and keeps the OLD path, which is what makes this a partition
rather than a migration: nothing that could not be attributed is moved, and an
unclaimed branch behaves exactly as before.

THREE READERS, AND THE THIRD IS THE ONE A NARROW FIX MISSES. `append_all`
writes, `recorder_records` projects the tree fact — and `filed-here`'s own
end-of-turn checklist reads the board record directly in `lib.rs`. Without the
same partition there it would list a previous attempt's rows as this one's.

Test obligation: four cases over the compiled binary, driving `batten hook`
twice under two claims on one branch name. The false-exemption case is the one
a naive suite misses, so it is written first and asserts the ABSENCE of the
previous attempt's key; its anti-vacuity mirror proves one claim still
accumulates across calls, without which the case is satisfied by a partition
so eager that no record survives a lap at all.

Refs: CLOUD-1300
Refs: CLOUD-516
wenzowski added a commit that referenced this pull request Sep 2, 2026
…k instructs

`target-prune` refused with `declared 152, live 164, tolerance 10`, and `verify`
reported that as "not enough disk" with 8.1 GB free — the misattribution
CLOUD-1153 is open for, and which the guard's own comment explains cannot be
fixed at that site.

The remedy is the one the block states: move `count` and `measured` TOGETHER, and
leave the floors alone, since no independent measurement was taken. Same shape
and same half as the 2026-09-01 move one paragraph up.

What is worth a line is what the drift now MEANS. The block above predicted this
move; since the target consolidation a tracked test file is no longer a proxy for
a linked stem — 164 files still link 2 targets — so this basis has become a trend
counter over a quantity that no longer drives the bytes the floors budget. Still
worth watching, and it no longer implies a floor is wrong.

Refs: CLOUD-1158, CLOUD-1210, CLOUD-1153

Admits: a75ceb0754f824775bf2e0a84d27b4c68448f20180687009ae00c9701111b6f7
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-head: cf0e751
Admits-epoch: bf22c7740a43e8f5a6246262dffe5982be8443105cd39137c5c7e9becf5c919a
Admits-author: alec@wenzowski.com
Admits-prev: d7b0aaf76565dfa1a3ca87a2a2454c0e210bd854260e69f2bcebcd506ad4b28b
Admits-answer-lost: Without it `target-prune` refuses every lap — `declared 152, live 164, tolerance 10` — and `verify` reports that refusal as "not enough disk" with 8.1 GB free, which is CLOUD-1153's misattribution. So the branch cannot verify at all, and the operator is sent to free space that was never short. The block's own instruction is to move `count` and `measured` together when it reds; declining leaves a gate red for a reason its own config names the fix for.
Admits-answer-precondition: `batten.toml` IS the owning surface for a `[prune.*.basis]` row — no other file the engine reads declares one, and `target-prune` resolves the count from this table alone. The class's own remedy for this path is "change it in a pull request", which is exactly what this is: branch claude/landing-loop-bundle-4puk4i with draft PR #812 open, so the two-line change lands in a diff a reviewer sees.
Admits-answer-rejected-route: config read first is the one rejected, and it is circular for this subject: it says read the committed config rather than write it, and the committed config IS what declares a stale count. patch run first does not apply either — there is no patch to run: the basis is a declared integer this tree measures directly (`git ls-files 'crates/batten/tests/**/*.rs'` = 164), not a derived artifact any task regenerates.
@wenzowski
wenzowski force-pushed the claude/landing-loop-bundle-4puk4i branch 2 times, most recently from 939189f to d86eec2 Compare September 2, 2026 03:10
wenzowski added a commit that referenced this pull request Sep 2, 2026
`filed-over-own-diff` refuses `batten.toml` on this branch, and the route its
own class declares — R-CLOSE-IT-IN-THE-BODY — is the one taken: PR #812's body
opens with `Closes CLOUD-1298`. The gate cannot see it, and the reason is a
mechanism gap rather than an unmet condition.

The exemption reads the `pr-closes` RECORD, minted by a `[[recorder]]` over a
`gh pr view` Bash call. This environment has no `gh`, so the evidence channel is
unavailable — and fabricating a tool result to mint the record would forge the
very evidence the exemption exists to demand.

TWO FURTHER FACTS, FOUND WHILE ESTABLISHING THAT, AND THE FIRST IS A DEFECT:

  * The record was STALE, still naming CLOUD-1269 and CLOUD-1279 from the PR
    this branch carried before it was reset onto the merged trunk.
    `record_path` is branch-keyed with no PR identity, so a reused branch name
    inherits the previous PR's closes-record and the exemption then reads the
    wrong PR's keys. Same family as the lease record's history bound one level
    up: no per-PR partition.
  * Removing that stale line does not exempt either. The predicate needs a
    POSITIVE record naming the key, so an absent channel refuses where an
    absent finding would have passed.

Admits: e8a1d7bfdb26149ed897edccf84b90248b37b2f15ac077991be687da6d75f0f8
Admits-rule: filed-here
Admits-verdict: V-FILED-OVER-OWN-DIFF
Admits-subject: batten.toml
Admits-head: 717afda
Admits-epoch: c4d86ca484c6cc808f9252e1394b9ad10993d6646fdcf5864522e2af3d23a020
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: Nothing is deferred, so the toll prices a punt that did not happen. Paying it any other way means either not filing the row — leaving the wiring's ordering bound, its rejected wide-selector alternative and its `peek next` defect recorded nowhere — or splitting the file from the fix across two branches, which is the batching this repository's whole trunk discipline exists to prevent.
Admits-answer-precondition: CLOUD-1298 DOCUMENTS this change rather than deferring it. Its §1 names `batten.toml` because `batten.toml` is what the change edits — the two `[program]` rows, the `[[pattern]]` and the `[[recorder]]` that make #810's landed predicate read something real. The row was filed and fixed in the same branch, and the fix is in this diff.
Admits-answer-rejected-route: R-CLOSE-IT-IN-THE-BODY IS THE ROUTE I TOOK, and the override is only because the gate cannot see it. PR #812's body opens with `Closes CLOUD-1298`. The exemption reads the `pr-closes` RECORD, which is minted by a `[[recorder]]` over a `gh pr view` Bash call, and this environment has no `gh` CLI — so the evidence channel is unavailable rather than unsatisfied, and I will not fabricate a tool result to mint it. Two further facts found while establishing that: the record was also STALE, still naming CLOUD-1269/CLOUD-1279 from the PR this branch carried before it was reset, because `record_path` is branch-keyed with no PR identity; and removing that stale line does not exempt either, since the predicate needs a positive record rather than an absent one. R-FIX-IT-HERE does not apply because there is nothing to fix — the row is not a punt. R-FILE-IT-AFTER-LANDING would land the mechanism with its own reasoning unrecorded, which is the failure the row exists to prevent.
Weakens: program-changed program[land-lock-status]
Weakens: program-changed program[land-lock-peek]
Weakens: recorder-added recorder[landing-lease]
wenzowski added a commit that referenced this pull request Sep 2, 2026
…e alone

A branch NAME outlives the branch it described. `git checkout -B <name>
origin/main` discards the commits that were the branch while every name-keyed
file survives — CLOUD-516's finding, which the claim receipt already answers
for itself by recording the base it was made against. A `[[recorder]]`'s
record had no such discriminator at all, so the next attempt on a reused
branch name read the previous one's lines as its own.

MEASURED HERE, NOT REASONED ABOUT. After PR #810 merged and this branch was
reset onto the new trunk, `pr-closes.<branch>` still held `closes
2:CLOUD-1269,CLOUD-1279` — that PR's keys. The PR actually open was #812,
closing CLOUD-1298, which appeared nowhere in the record.

THE DANGEROUS DIRECTION IS THE SILENT ONE. `filed-over-own-diff` exempts a row
the PR CLOSES and reads that from this record, so a stale record does not
merely refuse an honest file-then-fix: a row named by the PREVIOUS PR's
closes-record would be EXEMPTED on a PR that does not close it, with nothing
downstream to re-check.

THE CLAIM RATHER THAN THE BASE, and the difference is what makes it usable. A
base moves on every rebase and `land` rebases every lap, so keying a record on
one would discard it mid-landing — the failure this prevents, arriving by
another route. A claim is re-minted per PULL and is stable across every rebase
in between, so it partitions exactly the attempts that must not see each other.

`claimed_token` is order-insensitive over a multi-key claim, or a re-claim of
the same work would partition itself away from its own record. `None` is
could-not-look and keeps the OLD path, which is what makes this a partition
rather than a migration: nothing that could not be attributed is moved, and an
unclaimed branch behaves exactly as before.

THREE READERS, AND THE THIRD IS THE ONE A NARROW FIX MISSES. `append_all`
writes, `recorder_records` projects the tree fact — and `filed-here`'s own
end-of-turn checklist reads the board record directly in `lib.rs`. Without the
same partition there it would list a previous attempt's rows as this one's.

Test obligation: four cases over the compiled binary, driving `batten hook`
twice under two claims on one branch name. The false-exemption case is the one
a naive suite misses, so it is written first and asserts the ABSENCE of the
previous attempt's key; its anti-vacuity mirror proves one claim still
accumulates across calls, without which the case is satisfied by a partition
so eager that no record survives a lap at all.

Refs: CLOUD-1300
Refs: CLOUD-516
wenzowski added a commit that referenced this pull request Sep 2, 2026
…k instructs

`target-prune` refused with `declared 152, live 164, tolerance 10`, and `verify`
reported that as "not enough disk" with 8.1 GB free — the misattribution
CLOUD-1153 is open for, and which the guard's own comment explains cannot be
fixed at that site.

The remedy is the one the block states: move `count` and `measured` TOGETHER, and
leave the floors alone, since no independent measurement was taken. Same shape
and same half as the 2026-09-01 move one paragraph up.

What is worth a line is what the drift now MEANS. The block above predicted this
move; since the target consolidation a tracked test file is no longer a proxy for
a linked stem — 164 files still link 2 targets — so this basis has become a trend
counter over a quantity that no longer drives the bytes the floors budget. Still
worth watching, and it no longer implies a floor is wrong.

Refs: CLOUD-1158, CLOUD-1210, CLOUD-1153

Admits: a75ceb0754f824775bf2e0a84d27b4c68448f20180687009ae00c9701111b6f7
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-head: cf0e751
Admits-epoch: bf22c7740a43e8f5a6246262dffe5982be8443105cd39137c5c7e9becf5c919a
Admits-author: alec@wenzowski.com
Admits-prev: d7b0aaf76565dfa1a3ca87a2a2454c0e210bd854260e69f2bcebcd506ad4b28b
Admits-answer-lost: Without it `target-prune` refuses every lap — `declared 152, live 164, tolerance 10` — and `verify` reports that refusal as "not enough disk" with 8.1 GB free, which is CLOUD-1153's misattribution. So the branch cannot verify at all, and the operator is sent to free space that was never short. The block's own instruction is to move `count` and `measured` together when it reds; declining leaves a gate red for a reason its own config names the fix for.
Admits-answer-precondition: `batten.toml` IS the owning surface for a `[prune.*.basis]` row — no other file the engine reads declares one, and `target-prune` resolves the count from this table alone. The class's own remedy for this path is "change it in a pull request", which is exactly what this is: branch claude/landing-loop-bundle-4puk4i with draft PR #812 open, so the two-line change lands in a diff a reviewer sees.
Admits-answer-rejected-route: config read first is the one rejected, and it is circular for this subject: it says read the committed config rather than write it, and the committed config IS what declares a stale count. patch run first does not apply either — there is no patch to run: the basis is a declared integer this tree measures directly (`git ls-files 'crates/batten/tests/**/*.rs'` = 164), not a derived artifact any task regenerates.
wenzowski added a commit that referenced this pull request Sep 2, 2026
… the floor

`target-prune` refused a lap at 7895MB free against the 7938MB warm floor — 43 MB
short — while `target/tmp` held 796MB across 5156 fixture directories. No
declared root reached it, so the escalation ran out of things to drop and refused
with most of a gigabyte of pure residue on the volume, and `verify` reported it
as a floor nothing could recover.

The omission compounds rather than merely costing once: `CARGO_TARGET_TMPDIR`
regrows every suite run and `land` runs `verify` per lap, so a loop that cannot
drop this wedges on a floor it could satisfy — measured here, where clearing the
directory by hand was the only way to get a lap open.

`cold = false`, and for this root that is not a judgement call the way it is for
`incremental` or `build`: nothing under it is a build artifact at all.
`crates/batten/tests/it/common`'s `scratch` wipes each fixture directory before
writing it, so no run has ever depended on what a previous one left. The next
build reads none of it, and a suite that finds it gone materialises exactly what
it always did.

Refs: CLOUD-1274, CLOUD-1157, CLOUD-1030

Admits: 4dbbadf82afb221c8f858667af6b24230f209292fcfb2635e4f23cab7c62d640
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-head: 939189f
Admits-epoch: 2a9622027a67597e054de0a84c63b1eede1c82ef4c2a2ea4abdfebefc70008d0
Admits-author: alec@wenzowski.com
Admits-prev: e984bf1c5db245235cb04fa64f1b842ac10e7ead56e897eac5a1b5810f3491ff
Admits-answer-lost: Measured minutes ago: `target-prune` refused this lap at 7895MB free against a 7938MB floor — 43MB short — while `target/tmp` held 796MB across 5156 test-fixture scratch directories that no declared root reaches. So the escalation ran out of things to drop and refused, with 800MB of pure residue on disk. Without the row every future lap re-hits it, because a full suite run regrows that directory: `land` runs `verify` per lap, so the loop wedges on a floor it could satisfy. Declining means clearing it by hand before every lap, which is the reclaim this table exists to automate.
Admits-answer-precondition: `batten.toml` IS the owning surface for `[[prune.regrowable]]` — the roots prune may drop are a table in that file and nowhere else, so a missing root cannot be declared anywhere but here. The class's remedy for this path is "change it in a pull request", which is what this is: branch claude/landing-loop-bundle-4puk4i, draft PR #812 open, one added table and its comment in a diff a reviewer sees.
Admits-answer-rejected-route: config read first is circular: the regrowable table IS the committed policy, so reading it rather than writing it cannot add a root prune has never known. patch run first does not apply: no task regenerates this table — each row is hand-declared with a `cold` verdict and a comment saying whether losing it makes the next cargo build a full one, which is a judgement, not a derivation.
@wenzowski
wenzowski force-pushed the claude/landing-loop-bundle-4puk4i branch from b7a7906 to 103b971 Compare September 2, 2026 03:59
wenzowski added a commit that referenced this pull request Sep 2, 2026
`filed-over-own-diff` refuses `batten.toml` on this branch, and the route its
own class declares — R-CLOSE-IT-IN-THE-BODY — is the one taken: PR #812's body
opens with `Closes CLOUD-1298`. The gate cannot see it, and the reason is a
mechanism gap rather than an unmet condition.

The exemption reads the `pr-closes` RECORD, minted by a `[[recorder]]` over a
`gh pr view` Bash call. This environment has no `gh`, so the evidence channel is
unavailable — and fabricating a tool result to mint the record would forge the
very evidence the exemption exists to demand.

TWO FURTHER FACTS, FOUND WHILE ESTABLISHING THAT, AND THE FIRST IS A DEFECT:

  * The record was STALE, still naming CLOUD-1269 and CLOUD-1279 from the PR
    this branch carried before it was reset onto the merged trunk.
    `record_path` is branch-keyed with no PR identity, so a reused branch name
    inherits the previous PR's closes-record and the exemption then reads the
    wrong PR's keys. Same family as the lease record's history bound one level
    up: no per-PR partition.
  * Removing that stale line does not exempt either. The predicate needs a
    POSITIVE record naming the key, so an absent channel refuses where an
    absent finding would have passed.

Admits: e8a1d7bfdb26149ed897edccf84b90248b37b2f15ac077991be687da6d75f0f8
Admits-rule: filed-here
Admits-verdict: V-FILED-OVER-OWN-DIFF
Admits-subject: batten.toml
Admits-head: 717afda
Admits-epoch: c4d86ca484c6cc808f9252e1394b9ad10993d6646fdcf5864522e2af3d23a020
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: Nothing is deferred, so the toll prices a punt that did not happen. Paying it any other way means either not filing the row — leaving the wiring's ordering bound, its rejected wide-selector alternative and its `peek next` defect recorded nowhere — or splitting the file from the fix across two branches, which is the batching this repository's whole trunk discipline exists to prevent.
Admits-answer-precondition: CLOUD-1298 DOCUMENTS this change rather than deferring it. Its §1 names `batten.toml` because `batten.toml` is what the change edits — the two `[program]` rows, the `[[pattern]]` and the `[[recorder]]` that make #810's landed predicate read something real. The row was filed and fixed in the same branch, and the fix is in this diff.
Admits-answer-rejected-route: R-CLOSE-IT-IN-THE-BODY IS THE ROUTE I TOOK, and the override is only because the gate cannot see it. PR #812's body opens with `Closes CLOUD-1298`. The exemption reads the `pr-closes` RECORD, which is minted by a `[[recorder]]` over a `gh pr view` Bash call, and this environment has no `gh` CLI — so the evidence channel is unavailable rather than unsatisfied, and I will not fabricate a tool result to mint it. Two further facts found while establishing that: the record was also STALE, still naming CLOUD-1269/CLOUD-1279 from the PR this branch carried before it was reset, because `record_path` is branch-keyed with no PR identity; and removing that stale line does not exempt either, since the predicate needs a positive record rather than an absent one. R-FIX-IT-HERE does not apply because there is nothing to fix — the row is not a punt. R-FILE-IT-AFTER-LANDING would land the mechanism with its own reasoning unrecorded, which is the failure the row exists to prevent.
Weakens: program-changed program[land-lock-status]
Weakens: program-changed program[land-lock-peek]
Weakens: recorder-added recorder[landing-lease]
wenzowski added a commit that referenced this pull request Sep 2, 2026
…e alone

A branch NAME outlives the branch it described. `git checkout -B <name>
origin/main` discards the commits that were the branch while every name-keyed
file survives — CLOUD-516's finding, which the claim receipt already answers
for itself by recording the base it was made against. A `[[recorder]]`'s
record had no such discriminator at all, so the next attempt on a reused
branch name read the previous one's lines as its own.

MEASURED HERE, NOT REASONED ABOUT. After PR #810 merged and this branch was
reset onto the new trunk, `pr-closes.<branch>` still held `closes
2:CLOUD-1269,CLOUD-1279` — that PR's keys. The PR actually open was #812,
closing CLOUD-1298, which appeared nowhere in the record.

THE DANGEROUS DIRECTION IS THE SILENT ONE. `filed-over-own-diff` exempts a row
the PR CLOSES and reads that from this record, so a stale record does not
merely refuse an honest file-then-fix: a row named by the PREVIOUS PR's
closes-record would be EXEMPTED on a PR that does not close it, with nothing
downstream to re-check.

THE CLAIM RATHER THAN THE BASE, and the difference is what makes it usable. A
base moves on every rebase and `land` rebases every lap, so keying a record on
one would discard it mid-landing — the failure this prevents, arriving by
another route. A claim is re-minted per PULL and is stable across every rebase
in between, so it partitions exactly the attempts that must not see each other.

`claimed_token` is order-insensitive over a multi-key claim, or a re-claim of
the same work would partition itself away from its own record. `None` is
could-not-look and keeps the OLD path, which is what makes this a partition
rather than a migration: nothing that could not be attributed is moved, and an
unclaimed branch behaves exactly as before.

THREE READERS, AND THE THIRD IS THE ONE A NARROW FIX MISSES. `append_all`
writes, `recorder_records` projects the tree fact — and `filed-here`'s own
end-of-turn checklist reads the board record directly in `lib.rs`. Without the
same partition there it would list a previous attempt's rows as this one's.

Test obligation: four cases over the compiled binary, driving `batten hook`
twice under two claims on one branch name. The false-exemption case is the one
a naive suite misses, so it is written first and asserts the ABSENCE of the
previous attempt's key; its anti-vacuity mirror proves one claim still
accumulates across calls, without which the case is satisfied by a partition
so eager that no record survives a lap at all.

Refs: CLOUD-1300
Refs: CLOUD-516
wenzowski added a commit that referenced this pull request Sep 2, 2026
…k instructs

`target-prune` refused with `declared 152, live 164, tolerance 10`, and `verify`
reported that as "not enough disk" with 8.1 GB free — the misattribution
CLOUD-1153 is open for, and which the guard's own comment explains cannot be
fixed at that site.

The remedy is the one the block states: move `count` and `measured` TOGETHER, and
leave the floors alone, since no independent measurement was taken. Same shape
and same half as the 2026-09-01 move one paragraph up.

What is worth a line is what the drift now MEANS. The block above predicted this
move; since the target consolidation a tracked test file is no longer a proxy for
a linked stem — 164 files still link 2 targets — so this basis has become a trend
counter over a quantity that no longer drives the bytes the floors budget. Still
worth watching, and it no longer implies a floor is wrong.

Refs: CLOUD-1158, CLOUD-1210, CLOUD-1153

Admits: a75ceb0754f824775bf2e0a84d27b4c68448f20180687009ae00c9701111b6f7
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-head: cf0e751
Admits-epoch: bf22c7740a43e8f5a6246262dffe5982be8443105cd39137c5c7e9becf5c919a
Admits-author: alec@wenzowski.com
Admits-prev: d7b0aaf76565dfa1a3ca87a2a2454c0e210bd854260e69f2bcebcd506ad4b28b
Admits-answer-lost: Without it `target-prune` refuses every lap — `declared 152, live 164, tolerance 10` — and `verify` reports that refusal as "not enough disk" with 8.1 GB free, which is CLOUD-1153's misattribution. So the branch cannot verify at all, and the operator is sent to free space that was never short. The block's own instruction is to move `count` and `measured` together when it reds; declining leaves a gate red for a reason its own config names the fix for.
Admits-answer-precondition: `batten.toml` IS the owning surface for a `[prune.*.basis]` row — no other file the engine reads declares one, and `target-prune` resolves the count from this table alone. The class's own remedy for this path is "change it in a pull request", which is exactly what this is: branch claude/landing-loop-bundle-4puk4i with draft PR #812 open, so the two-line change lands in a diff a reviewer sees.
Admits-answer-rejected-route: config read first is the one rejected, and it is circular for this subject: it says read the committed config rather than write it, and the committed config IS what declares a stale count. patch run first does not apply either — there is no patch to run: the basis is a declared integer this tree measures directly (`git ls-files 'crates/batten/tests/**/*.rs'` = 164), not a derived artifact any task regenerates.
wenzowski added a commit that referenced this pull request Sep 2, 2026
… the floor

`target-prune` refused a lap at 7895MB free against the 7938MB warm floor — 43 MB
short — while `target/tmp` held 796MB across 5156 fixture directories. No
declared root reached it, so the escalation ran out of things to drop and refused
with most of a gigabyte of pure residue on the volume, and `verify` reported it
as a floor nothing could recover.

The omission compounds rather than merely costing once: `CARGO_TARGET_TMPDIR`
regrows every suite run and `land` runs `verify` per lap, so a loop that cannot
drop this wedges on a floor it could satisfy — measured here, where clearing the
directory by hand was the only way to get a lap open.

`cold = false`, and for this root that is not a judgement call the way it is for
`incremental` or `build`: nothing under it is a build artifact at all.
`crates/batten/tests/it/common`'s `scratch` wipes each fixture directory before
writing it, so no run has ever depended on what a previous one left. The next
build reads none of it, and a suite that finds it gone materialises exactly what
it always did.

Refs: CLOUD-1274, CLOUD-1157, CLOUD-1030

Admits: 4dbbadf82afb221c8f858667af6b24230f209292fcfb2635e4f23cab7c62d640
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-head: 939189f
Admits-epoch: 2a9622027a67597e054de0a84c63b1eede1c82ef4c2a2ea4abdfebefc70008d0
Admits-author: alec@wenzowski.com
Admits-prev: e984bf1c5db245235cb04fa64f1b842ac10e7ead56e897eac5a1b5810f3491ff
Admits-answer-lost: Measured minutes ago: `target-prune` refused this lap at 7895MB free against a 7938MB floor — 43MB short — while `target/tmp` held 796MB across 5156 test-fixture scratch directories that no declared root reaches. So the escalation ran out of things to drop and refused, with 800MB of pure residue on disk. Without the row every future lap re-hits it, because a full suite run regrows that directory: `land` runs `verify` per lap, so the loop wedges on a floor it could satisfy. Declining means clearing it by hand before every lap, which is the reclaim this table exists to automate.
Admits-answer-precondition: `batten.toml` IS the owning surface for `[[prune.regrowable]]` — the roots prune may drop are a table in that file and nowhere else, so a missing root cannot be declared anywhere but here. The class's remedy for this path is "change it in a pull request", which is what this is: branch claude/landing-loop-bundle-4puk4i, draft PR #812 open, one added table and its comment in a diff a reviewer sees.
Admits-answer-rejected-route: config read first is circular: the regrowable table IS the committed policy, so reading it rather than writing it cannot add a root prune has never known. patch run first does not apply: no task regenerates this table — each row is hand-declared with a `cold` verdict and a comment saying whether losing it makes the next cargo build a full one, which is a judgement, not a derivation.
@wenzowski
wenzowski marked this pull request as ready for review September 2, 2026 04:42
@wenzowski
wenzowski marked this pull request as draft September 2, 2026 04:48
CLOUD-1280 landed `lease-authorises-the-branch` and this repository declared no
`[[recorder]]` writing a lease line, so `input.tree.records` carried none and
the predicate allowed unconditionally. Fail-open rather than a dead gate — the
refusal it could not reach is the refusal it is designed not to reach — but
every reading of it was a test fixture, which is the one thing CLOUD-1269 calls
"the only way the surface gets exercised by something other than its own tests."

Three non-obvious choices, each PROBED rather than assumed:

  * `status`, not `authorises`. The `authorises` arm answers the module's exact
    question and takes the branch on ARGV; `Program::args` is a fixed
    `Vec<String>` frozen at config load, so a row could only hard-code one
    branch forever. `status` takes none.
  * `peek next`, not `peek`. `peek` with no field argument is exit 2 with 51
    bytes of USAGE TEXT on stdout, and `render_column` folds whitespace — so the
    record's shape survives and the successor column silently carries prose that
    can never equal a branch. That is a fail-CLOSED deviation on the one row
    `Value::Branch` exists to keep open, and it was live in the first draft.
  * `2` is deliberately unmapped in the `status` table. `status` fails closed
    where it cannot observe the lease; `authorises` fails open. An unmapped
    status records could-not-look, which equals neither token, so the refusal
    cannot hold. The OMISSION is what restores the asymmetry at the boundary —
    adding a `"2"` row inverts the one behaviour the port exists to conserve.

THE ORDERING BOUND, STATED WHERE THE ROW LIVES. A recorder writes on the
post-tool event, and `mise run land` runs its whole lap inside ONE Bash call, so
nothing is written while a lap runs and the line `batten check` reads is
whatever the call before `land` left. `linear-check` is the step the contract
puts immediately before `land`, which makes the reading seconds old rather than
a lap old. On lap 2 and after nothing refreshes it, so a lease a rival acquired
during lap 1 is not seen — under-denying, which is the sanctioned direction for
a predicate whose whole asymmetry is that a reading it could not take allows.

The wide selector was priced and rejected: every Bash call would keep the record
always-fresh and put a remote lease observation on the mediated path of every
call, which `perf-assert` budgets against. `satisfied` is evaluated before any
column, so a narrow selector spawns nothing on an ordinary call — asserted, not
assumed, by the selector's own anti-vacuity case.

Test obligation: seven cases over the compiled binary in
`crates/batten/tests/it/lease_record.rs`, driving `batten hook` and reading the
file back. The preset suite structurally cannot be this tier — a `with input as`
case fabricates the very line the engine may be unable to write. The
could-not-look case asserts the recorded TOKEN rather than the file's existence,
because a case reading only the file would pass over a table that mapped `2`.

Weakens: recorder-added recorder[landing-lease]

Refs: CLOUD-1298
Refs: CLOUD-1269

Admits: 8b8119b1795d43416aa547233174231a5a75e42828d43440f3df535ee5befaf3
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: cb33679
Admits-epoch: 8ae3d03b504fed658464f45a75ce2f4963f458a806777852c084456400109939
Admits-author: alec@wenzowski.com
Admits-prev: 8dcfee86e3267762a0716a9cfbc1fba755289534cd5c298c11265497f3f2e93d
Admits-answer-lost: Without the recorder the preset predicate merged in #810 has no record to read, so `input.tree.records` carries no lease line and the predicate allows unconditionally. That is the fail-open direction and so not a dead gate in CLOUD-845's sense, but it means consumer #1 does not eat its own food — the clause CLOUD-1269 names as "the only way the surface gets exercised by something other than its own tests" — and a landing lap keeps spending CI against a lease nothing in the engine reads.
Admits-answer-precondition: `batten.toml` IS the owning surface for a `[[recorder]]`, a `[program]` and a `[[pattern]]` row: it is the one authority the engine reads for all three, so a recorder that writes the landing-lease record cannot be declared anywhere else. This adds the row that makes `lease-authorises-the-branch` non-decorative in this repository, and it lands in a PR diff a reviewer sees.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is circular for this subject, because `batten.toml` IS the owning surface for a recorder, a program and a pattern row — no other file the engine consults declares one. R-RESTORE-IT would revert the rows and leave the predicate reading a record nothing writes.

Admits: c90a5c16aadfea21155819bed70c8150b24134cc025a3b2728a7b836ef5e9e83
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: cb33679
Admits-epoch: d900ba911dec5d109316dc19515aded5283414e1bf481ca30fa363aec876a210
Admits-author: alec@wenzowski.com
Admits-prev: 8b8119b1795d43416aa547233174231a5a75e42828d43440f3df535ee5befaf3
Admits-answer-lost: Without the recorder the preset predicate merged in #810 has no record to read, so `input.tree.records` carries no lease line and the predicate allows unconditionally. Fail-open, so not a dead gate in CLOUD-845's sense, but consumer #1 then does not eat its own food — the clause CLOUD-1269 calls "the only way the surface gets exercised by something other than its own tests" — and a landing lap keeps spending CI against a lease nothing in the engine reads.
Admits-answer-precondition: `batten.toml` IS the owning surface for a `[[recorder]]` and a `[[pattern]]` row: it is the one authority the engine reads for both, so the recorder that writes the landing-lease record and the selector it narrows on cannot be declared anywhere else. They are one block in one edit because an admission is single-use and the two rows are one mechanism. It lands in a PR diff a reviewer sees.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is circular for this subject, because `batten.toml` IS the owning surface for a recorder and a pattern row — no other file the engine consults declares either. R-RESTORE-IT would revert the block and leave the predicate reading a record nothing writes.

Admits: d7b0aaf76565dfa1a3ca87a2a2454c0e210bd854260e69f2bcebcd506ad4b28b
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: cb33679
Admits-epoch: 8a8bfc7d345f36b6d4d97458d63cf6aecb8216a100325631b4243c7a240efca3
Admits-author: alec@wenzowski.com
Admits-prev: c90a5c16aadfea21155819bed70c8150b24134cc025a3b2728a7b836ef5e9e83
Admits-answer-lost: Without the fix the successor column records a usage-error string on every write. `render_column` folds whitespace so the record SHAPE survives, which is what makes this silent: the module compares that string against the branch, finds them unequal, and the refusal stands. That is a fail-CLOSED deviation on the admitted-successor row — precisely the laundering CLOUD-1269 forbids and the exact case `Value::Branch` was added to prevent.
Admits-answer-precondition: `batten.toml` IS the owning surface for a `[program]` row, so the argv a recorder column runs cannot be corrected anywhere else. This fixes a defect in the block admitted moments ago: `land-lock peek` REQUIRES a field argument and the row omitted it, so the program exited 2 and wrote a usage line to stdout that the column would have recorded as the successor. Probed directly — `peek` alone is exit 2 with 51 bytes of usage text, `peek next` is exit 0 and empty. It lands in a PR diff a reviewer sees.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is circular for this subject, because `batten.toml` IS the owning surface for a `[program]` row's argv — no other file the engine consults declares one. R-RESTORE-IT would restore the argv-less invocation and reinstate the fail-closed successor row.
`filed-over-own-diff` refuses `batten.toml` on this branch, and the route its
own class declares — R-CLOSE-IT-IN-THE-BODY — is the one taken: PR #812's body
opens with `Closes CLOUD-1298`. The gate cannot see it, and the reason is a
mechanism gap rather than an unmet condition.

The exemption reads the `pr-closes` RECORD, minted by a `[[recorder]]` over a
`gh pr view` Bash call. This environment has no `gh`, so the evidence channel is
unavailable — and fabricating a tool result to mint the record would forge the
very evidence the exemption exists to demand.

TWO FURTHER FACTS, FOUND WHILE ESTABLISHING THAT, AND THE FIRST IS A DEFECT:

  * The record was STALE, still naming CLOUD-1269 and CLOUD-1279 from the PR
    this branch carried before it was reset onto the merged trunk.
    `record_path` is branch-keyed with no PR identity, so a reused branch name
    inherits the previous PR's closes-record and the exemption then reads the
    wrong PR's keys. Same family as the lease record's history bound one level
    up: no per-PR partition.
  * Removing that stale line does not exempt either. The predicate needs a
    POSITIVE record naming the key, so an absent channel refuses where an
    absent finding would have passed.

Admits: e8a1d7bfdb26149ed897edccf84b90248b37b2f15ac077991be687da6d75f0f8
Admits-rule: filed-here
Admits-verdict: V-FILED-OVER-OWN-DIFF
Admits-subject: batten.toml
Admits-head: 717afda
Admits-epoch: c4d86ca484c6cc808f9252e1394b9ad10993d6646fdcf5864522e2af3d23a020
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: Nothing is deferred, so the toll prices a punt that did not happen. Paying it any other way means either not filing the row — leaving the wiring's ordering bound, its rejected wide-selector alternative and its `peek next` defect recorded nowhere — or splitting the file from the fix across two branches, which is the batching this repository's whole trunk discipline exists to prevent.
Admits-answer-precondition: CLOUD-1298 DOCUMENTS this change rather than deferring it. Its §1 names `batten.toml` because `batten.toml` is what the change edits — the two `[program]` rows, the `[[pattern]]` and the `[[recorder]]` that make #810's landed predicate read something real. The row was filed and fixed in the same branch, and the fix is in this diff.
Admits-answer-rejected-route: R-CLOSE-IT-IN-THE-BODY IS THE ROUTE I TOOK, and the override is only because the gate cannot see it. PR #812's body opens with `Closes CLOUD-1298`. The exemption reads the `pr-closes` RECORD, which is minted by a `[[recorder]]` over a `gh pr view` Bash call, and this environment has no `gh` CLI — so the evidence channel is unavailable rather than unsatisfied, and I will not fabricate a tool result to mint it. Two further facts found while establishing that: the record was also STALE, still naming CLOUD-1269/CLOUD-1279 from the PR this branch carried before it was reset, because `record_path` is branch-keyed with no PR identity; and removing that stale line does not exempt either, since the predicate needs a positive record rather than an absent one. R-FIX-IT-HERE does not apply because there is nothing to fix — the row is not a punt. R-FILE-IT-AFTER-LANDING would land the mechanism with its own reasoning unrecorded, which is the failure the row exists to prevent.
Weakens: program-changed program[land-lock-status]
Weakens: program-changed program[land-lock-peek]
Weakens: recorder-added recorder[landing-lease]
…e alone

A branch NAME outlives the branch it described. `git checkout -B <name>
origin/main` discards the commits that were the branch while every name-keyed
file survives — CLOUD-516's finding, which the claim receipt already answers
for itself by recording the base it was made against. A `[[recorder]]`'s
record had no such discriminator at all, so the next attempt on a reused
branch name read the previous one's lines as its own.

MEASURED HERE, NOT REASONED ABOUT. After PR #810 merged and this branch was
reset onto the new trunk, `pr-closes.<branch>` still held `closes
2:CLOUD-1269,CLOUD-1279` — that PR's keys. The PR actually open was #812,
closing CLOUD-1298, which appeared nowhere in the record.

THE DANGEROUS DIRECTION IS THE SILENT ONE. `filed-over-own-diff` exempts a row
the PR CLOSES and reads that from this record, so a stale record does not
merely refuse an honest file-then-fix: a row named by the PREVIOUS PR's
closes-record would be EXEMPTED on a PR that does not close it, with nothing
downstream to re-check.

THE CLAIM RATHER THAN THE BASE, and the difference is what makes it usable. A
base moves on every rebase and `land` rebases every lap, so keying a record on
one would discard it mid-landing — the failure this prevents, arriving by
another route. A claim is re-minted per PULL and is stable across every rebase
in between, so it partitions exactly the attempts that must not see each other.

`claimed_token` is order-insensitive over a multi-key claim, or a re-claim of
the same work would partition itself away from its own record. `None` is
could-not-look and keeps the OLD path, which is what makes this a partition
rather than a migration: nothing that could not be attributed is moved, and an
unclaimed branch behaves exactly as before.

THREE READERS, AND THE THIRD IS THE ONE A NARROW FIX MISSES. `append_all`
writes, `recorder_records` projects the tree fact — and `filed-here`'s own
end-of-turn checklist reads the board record directly in `lib.rs`. Without the
same partition there it would list a previous attempt's rows as this one's.

Test obligation: four cases over the compiled binary, driving `batten hook`
twice under two claims on one branch name. The false-exemption case is the one
a naive suite misses, so it is written first and asserts the ABSENCE of the
previous attempt's key; its anti-vacuity mirror proves one claim still
accumulates across calls, without which the case is satisfied by a partition
so eager that no record survives a lap at all.

Refs: CLOUD-1300
Refs: CLOUD-516
CLOUD-1274 recorded that `land-lock` cannot retire because its lease is a
compare-and-swap over a remote ref and all three routes to one are closed. The
routes are closed; the conclusion was not the only one available. Measured
rather than argued, against this tree's own gates:

  * gix CANNOT PUSH. `gix-protocol` ships `fetch`, `handshake` and `ls_refs`,
    and the only `push` symbols in gix are `push_url()` config setters.
  * gix's OWN TRANSPORTS resolve `reqwest` — barred by `AMBIENT_CRATES` — plus
    `security-framework` and `core-foundation`, both `FRAMEWORK_CRATES` names,
    and `ring`/`aws-lc-sys` `links` crates. 265 packages.
  * git2/libgit2 cannot reach HTTPS without its `https` feature, and EVERY
    configuration carrying it resolves `openssl-sys` — a `FRAMEWORK_CRATES`
    name `macos-link-check` refuses BY NAME, with no vendored exemption.
    `vendored-openssl` does not change it. Adopting it would have meant
    retiring `macos-link-check` itself, which is `governed_at_head` and so
    cannot be edited at all — the gate protecting the SDK-free macOS build.

THE FOURTH ROUTE NEEDS NO NEW DEPENDENCY. `gix-transport`'s `blocking-client`
resolves to `gix-packetline/blocking-io` and nothing else: the pkt-line framing
and the `Transport` trait, with no HTTP client, no TLS backend and no credential
helper. The protocol is spoken over `crate::fetch`, the hyper + rustls client
CLOUD-745 already vendored and bounded. Measured closure: no `FRAMEWORK_CRATES`
name, no `AMBIENT_CRATES` name, no new `links` crate — `defmt` is the only
`links` entry and is already in this lockfile, unactivated. `macos-link-check`
and `deny` both pass with the dependencies in the tree.

AND THE CAS IS THE PROTOCOL'S OWN. receive-pack takes `<old> <new> <ref>` and
applies it only while the ref still reads `<old>`, decided under the server's
lock — strictly stronger than `--force-with-lease`, which compares against what
the CLIENT last observed and races anything that moved in between.

The failure directions are what the suite is about, because each one loses the
fleet rather than a test: a proxy error page must not parse as an empty ref set
(that reads as "the lease is unheld" and hands the matrix to everyone at once);
a truncated pkt-line is could-not-look rather than a short read; a lost race is
`Rejected` and NOT an error, since reporting it as one makes a caller fail OPEN
on a rival's win; and a report carrying no unpack status is could-not-look,
because a push whose result cannot be read has not been shown to have applied.

`lease` is placed in `module-layering.rego` beside `fetch`, with BOTH
`hook -> lease` and `check -> lease` forbidden. That is CLOUD-1274's own
acceptance condition — "unreachable from `check` and `hook`, asserted over the
resolved `use` graph" — as a gate rather than a review note. `check` is listed
explicitly rather than left to follow from `hook`: a tree-scoped gate is
declared `read` and the read-only allowlist is DERIVED from that declaration, so
a `check` path reaching a network write would put a writing prefix on the
allowlist itself.

Not yet done, and stated so it is not mistaken for finished: the read half still
needs upload-pack and a packfile reader for the lease body, and `swap` needs a
packfile writer to create a lease commit. The wire framing is in place for both.

Refs: CLOUD-1274, CLOUD-1170, CLOUD-1148, CLOUD-745, CLOUD-499

Admits: 7843b699d22f5d4e27e0a4258c37e60bae064a888bb6ed4238377f2e329014bb
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: ca03a08
Admits-epoch: a3bc9784338c60320facae84ba664ef95ef7db9013356e2019f963d2fbeab9be
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: `module-map-check` fails and this branch cannot land at all: the gate refuses a new `crates/batten/src/*.rs` carrying no `mem:core` row, and `lease.rs` is new. Dropping the row to avoid this clause trades a reported write for a gate that refuses outright, and leaves the module map missing the one module this commit adds — the map that `.claude/rules/rust.md` defers to outright, so an absent row makes that deferral untrue.
Admits-answer-precondition: The memory EDIT was made through the owning surface — Serena's `edit_memory` — and this articulation is not a claim otherwise. What that surface cannot express is the COMMIT: `edit_memory` writes a memory and produces no commit, so the change reaches `main` only as a commit whose diff writes the protected path, which is the object this clause judges. The row is one `module-map-check` requires, and it is additive: one new `lease.rs` bullet beside `fetch.rs`, no existing row touched, visible in the diff it lands in.
Admits-answer-rejected-route: R-RESTORE-IT is the one rejected: restoring the committed bytes deletes the `lease.rs` map row and `module-map-check` then refuses the branch, so the route reaches a strictly worse state than the write it undoes. R-USE-THE-OWNING-SURFACE was NOT rejected — it was taken, for the edit — but it cannot be taken for the commit, because no Serena tool commits.
…ther than a second one

The wire half landed with `gix-transport` and `gix-protocol` in the manifest and
NOTHING IN THE TREE USING EITHER — the pkt-line framing was hand-rolled beside
them. Two defects in one: a manifest comment describing a route the code did not
take, and a second parser over a wire the vendored crate already reads.

Both are closed by taking the framing itself. `gix-packetline/blocking-io` is
what `gix-transport`'s `blocking-client` resolved to, so the closure is
byte-identical to the one already measured — no HTTP client, no TLS backend, no
credential helper, and `macos-link-check` and `deny` both still pass. The reason
to take it rather than keep the hand-rolled version is
`.claude/rules/policy-modules.md`'s, one domain over: a second parser is a second
AUTHORITY, and the two can disagree about a truncation or length case neither
author had in mind.

WHAT THE MODULE STILL OWNS IS THE BOUNDARY, and it is not framing. upload-pack
answers `NAK` and then the packfile with nothing framed between them, so
`pkt_split` returns the lines AND the tail — a reader that stopped at the first
unparseable header without handing back the remainder could never reach the lease
body at all. `PACK` is not four hex digits, which is exactly how that boundary is
recognised.

THE READ HALF: `fetch_object` asks upload-pack for one id with plain
`want`/`done` — no `deepen`, no sideband. A lease is a parentless commit over the
empty tree, so its whole closure is itself: there is no history to shorten and a
shallow negotiation would add a section to parse for no object saved. It refuses
an answer that does not carry the id asked for, which is the load-bearing
direction — a server answering with some OTHER object is not an answer about this
lease, and accepting it would let an unrelated body decide who holds the matrix.

THE WRITE HALF: `lease_object` mints through `gix::objs` rather than formatting a
commit by hand, for the same one-authority reason. Parentless and over the empty
tree, so it shares history with nothing and can never fast-forward over a live
lease. The identity is supplied rather than read from configuration, which is a
portability fix and not a style choice: `commit-tree` refuses with "Author
identity unknown" wherever no `user.email` is configured, and the bash
predecessor measured every acquiring test passing locally and failing in CI for
exactly that reason.

THE CALLER SUPPLIES THE INSTANT AND THE MESSAGE MUST CARRY A NONCE, and the
function deliberately does not salt itself: a mint that invented its own nonce
could not be tested for the property the nonce exists to have. Git addresses by
content, so two mints agreeing on every field produce the SAME id, and pushing an
id the ref already points at is an "up to date" no-op that REPORTS SUCCESS — a
rejected claim read as a win. Measured on the predecessor: without it a second
acquire reported "acquired" rather than recognising its own lease.

`pack_of` and `objects_in` are undeltified in both directions. A lease is one
small commit, so a delta would save nothing and would put a second encoding on a
path whose whole job is to be unambiguous; the reader refuses one rather than
growing a resolver no test could reach.

The truncation case is the one a length check alone does not catch, and it was
measured here rather than reasoned about: a zlib stream's last bytes are its
adler-32, so a member truncated by exactly that much still inflates to its full
declared size. The test passed until the reader also required the stream to have
ENDED. That is the direction that matters — a lease body read from a short answer
is a confident wrong verdict about who holds the matrix.

Refs: CLOUD-1274, CLOUD-1170, CLOUD-1148, CLOUD-745

Admits: 0b8fb803ba1699ff3d34591e46d32b94eb54cd554356424eacf3f1248138d7c6
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a8788b3
Admits-epoch: a3bc9784338c60320facae84ba664ef95ef7db9013356e2019f963d2fbeab9be
Admits-author: alec@wenzowski.com
Admits-prev: 7843b699d22f5d4e27e0a4258c37e60bae064a888bb6ed4238377f2e329014bb
Admits-answer-lost: The `mem:core` row for `lease.rs` states that the framing comes from `gix-transport`'s `blocking-client`, which this commit makes false: the dependency is `gix-packetline` directly and the two crates it names are gone from the manifest. Leaving it is a map entry describing a route the tree no longer takes, in the one file `.claude/rules/rust.md` defers to outright for what each module owns — and `module-map-check` cannot see a row that is present and wrong.
Admits-answer-precondition: The memory EDIT was made through the owning surface — Serena's `edit_memory` — and this articulation is not a claim otherwise. What that surface cannot express is the COMMIT: `edit_memory` writes a memory and produces no commit, so the change reaches `main` only as a commit whose diff writes the protected path, which is the object this clause judges. The edit is confined to the `lease.rs` bullet this same branch added, corrects it to what the manifest now says, and is visible in the diff it lands in beside the manifest change that makes it necessary.
Admits-answer-rejected-route: R-RESTORE-IT is the one rejected: restoring the committed bytes puts back a description of a dependency this commit removes, so the route reaches a state that is worse than the write it undoes rather than safer. R-USE-THE-OWNING-SURFACE was NOT rejected — it was taken, for the edit — but it cannot be taken for the commit, because no Serena tool commits.
`land-lock.sh`'s `authorises` is the arm CI depends on and the arm nothing in the
engine could express. It is also the one whose failure direction is inverted
against every other refusal in that file, so porting it as "another predicate"
would land the asymmetry as an accident rather than as the design.

FAIL OPEN, AND ONLY HERE. A lease the reader cannot see stops EVERY job in the
fleet; waving one matrix through costs one matrix. So `authorises(None, …)` is
`Run`, and so is a lease naming no branch — which during the field's own rollout
was not an edge case, it was every lease. Both are asserted rather than left to
follow from the code reading naturally, because a later reader tightening this
"for consistency" is exactly the change the assertions exist to redden.

THE BOUND IS TWO, NOT ONE. A branch that reserved the slot behind the holder is
buying the matrix that OVERLAPS the holder's merge, so stopping it would cancel
the very run the reservation exists to start and leave the queue cold with the
mechanism intact and useless. Exactly one successor by construction — the slot is
filled by a CAS — so nothing counts, compares ages or breaks ties.

THREE FIELDS DECIDE NOTHING AND THAT IS STRUCTURAL. `holder` decides ownership,
`expires` decides liveness, and `branch`/`head`/`next`/`progress` are read by
waiters and by CI and by no ownership predicate at all. An identity another clone
could DERIVE — from a branch, a head, an issue key — is an identity another clone
could accidentally claim, which is the two-holders bug the design exists to
prevent. The doc comment says so at the type rather than at each use.

`expires: 0` is a SENTINEL, not an instant. A release is a declaration and an
expiry is an inference, and only the second needs a clock; three separate bash
renderers printed a wall-clock epoch as an age before that distinction was drawn,
each found live. `released()` is checked before `expired()` everywhere, and the
test that pins it passes `now = 0` so a reader cannot mistake which one answered.

`parse_body` refuses a commit that does not open with the banner, and refuses one
carrying no `expires:`. Both are the same refusal `parse_advertisement` makes
about a body that never announced a service: parsed loosely, a foreign object
yields empty fields, and empty fields read as a lease nobody holds. The TTL
default belongs to the caller that knows its own, and a parser inventing one
would report a lease it could not read as one it could.

Refs: CLOUD-1274, CLOUD-1148, CLOUD-420, CLOUD-369, CLOUD-499, CLOUD-433
…d as

Every write the bash lease makes goes through one operation, and that is the
property worth conserving before any verb is: acquire, renew, the heartbeat's
steal, release and reserve are all one compare-and-swap. `cas` is that operation,
and the four body constructors above it are the only things that differ between
those five paths.

WHAT SEPARATES THEM IS WHAT THEY CARRY FORWARD, and each direction cost an
incident:

  * `claim` deliberately does NOT carry `next`. A fresh acquire is a new turn and
    the previous holder's successor has already had its admission; carrying it
    would authorise a third branch, then a fourth, and the bound the whole design
    rests on would drift upward one handover at a time.
  * `renewal` DOES carry it. A renewal re-mints the whole body, so a `next`
    written by a waiter between two beats is erased within one beat — silently, by
    the holder — and the admitted successor is then cancelled by CI mid-run.
    `progress` carries for the mirrored hazard: a caller that cannot compute one
    would erase it by the act of renewing, leaving the lease unstealable-forever.
  * `reservation` moves ONE field. The holder id and the expiry are re-minted as
    they were: one that moved the holder id would be a steal wearing a different
    name, and one that recomputed the expiry would hand the holder a fresh TTL
    every time a waiter arrived.
  * `tombstone` sets the expiry to the sentinel rather than deleting the ref,
    which leaves the lease instantly claimable and keeps every write one CAS.

Each has a test naming its own hazard, and each asserts the NONCE MOVED — because
a re-mint that agreed with the object the ref already carries is a push git reports
as "up to date", which is a rejected claim reading as a win.

`observe` takes the sha and the body FROM ONE READ, and that is a correctness
property rather than an economy. The predecessor took the sha from `ls-remote` and
the body from `FETCH_HEAD` — one file per clone, in a task that runs concurrently
inside a single clone by design — and measured 16 OF 40 concurrent reads returning
the wrong body. What that costs is not a bad message: it pairs THIS lease's sha
with ANOTHER lease's holder, so a release CASes against that sha while judging
ownership from that holder, which is precisely the theft the CAS exists to prevent.

A lease that will not parse is given a full TTL from now rather than being read as
free — a lease we do not understand is respected until it ages out, never ignored,
which is the same misread as an unreachable remote one step in.

`nonce`'s fallback is not a constant, and that is the one line in it worth reading
twice: a fixed fallback would make two clones that both failed to read entropy
mint the SAME object, which is exactly the collision the nonce exists to prevent.
The fallback is therefore the one value guaranteed to differ between two processes
on one machine.

`Terms` records why the lease lives on `refs/heads` rather than leaving it to look
like a default nobody questioned: a custom namespace is the better home and this
sandbox's git proxy refuses any push outside `refs/heads`, and GitHub does not
enforce the fast-forward rule off `refs/heads` either — a parentless orphan was
ACCEPTED on a custom namespace, which is the whole safety property gone.

Refs: CLOUD-1274, CLOUD-1148, CLOUD-369, CLOUD-499, CLOUD-433
…wer to

`batten lease` is the command surface `land-lock.sh` had: acquire, renew, hold,
held, release, status, authorises, peek, reserve. Three of the nine write and six
read, and the surface's effect column is where that split lives — a write-bearing
subtree under a `read` noun leaks onto the derived allowlist for any consumer that
treats an entry as a prefix, so the noun is `unclassified` and each row carries its
own answer.

THE PREDECESSOR'S EXIT VOCABULARY IS NOT CARRIED, and dropping it is the
correction rather than a loss. `land-lock authorises` answered `0` run / `3` stop /
`2` could not look, because inside that one file `1` already meant "held by someone
else". The engine has ONE table and no per-verb exception (non-negotiable rule 5):
`2` is the policy verdict everywhere and `1`/`3` are the only codes a Batten
failure produces. So a stop is `2` and a lease that cannot be read is `3`. A CI
caller keyed on the old numbers would read a refusal as an error, which is why the
workflow moves in the same change rather than after it.

THE WAIT LOOP IS DELIBERATELY NOT PORTED. The bash `acquire` blocked with a
jittered exponential backoff and an aging term; that belongs to the LAP, not to
the lease. A caller that already laps — fetch, rebase, verify, wait — re-observes
on its own schedule, and a second loop nested inside it is a wall clock where an
exit condition already exists. What is conserved is the DECISION, which is the part
a rival can get wrong; what is dropped is a sleep, which is the part the caller
owns. The one delay that remains is the heartbeat's, and it carries the inventory
annotation the ban demands, naming the beat it comes from and the two conditions
it exits on.

THE CREDENTIAL IS RESOLVED INSIDE THE TRANSPORT AND RETURNED TO NOBODY. It is not
a field of `Terms` or of any other value, because a token in a struct is a token
in that struct's `Debug`, and every report here is a pointer (non-negotiable rule
4). An absent credential is not an error: a public remote needs none and a private
one answers `401`, which every arm already reports as could-not-look.

`status` renders prose for a human and `-J` for everything else, which is what
stops a caller parsing the sentence and turning a message into an interface.
`peek`'s field set is CLOSED for the mirrored reason: the value of that verb over
the prose is that a caller can act on the answer, so an unknown field name is a
usage error rather than an empty line that reads as an unset field.

---

`mise run fix` HAS NOT COMPLETED SINCE THE SUITE MOVED UNDER `tests/it/`, and this
is the change that needed it.

`[tasks.snapshots]` named `crates/batten/tests/snapshots` and `--test snapshots`,
neither of which has existed since the move. `find` exits 1 on a directory that is
not there, and the body is a deliberate `&&` chain, so the accept never ran — the
failure was not a stale snapshot but the regeneration step itself. Every surface
change since has had to regenerate by hand or not at all.

Repaired here rather than filed, because a wrongly refusing gate is a defect and
not an answer, and this one refused the exact task this commit's own derived
artifacts depend on. `mise.toml` is an inline body outside `shell-retirement`'s
perimeter, so the repair is an edit and not a retirement.

Refs: CLOUD-1274, CLOUD-1148, CLOUD-1177
… said so

The first run against the real remote refused with `the pack carries object type
2, and a lease is a commit`. Everything before that point had worked — the
advertisement parsed, the upload-pack POST went out, a real packfile came back and
its first header decoded — and the reader was wrong about what a real answer
contains.

A `want` RETURNS THE COMMIT'S CLOSURE, not the commit. A lease is a parentless
commit over the empty tree, so its closure is two objects: the commit and that
tree. The reader assumed every member was a commit, and it passed every synthetic
case in the suite because each fixture pack carried exactly one object — which is
the shape a fixture naturally takes and the shape a real answer never has.

So `Object` carries its kind, `pack_of` writes it, and `objects_in` refuses only a
delta or an unassigned number. The hash moves with it: a member is hashed AS ITS
OWN KIND, because the loose header the id is taken over names it — hashing a tree
as a commit yields an id nothing on the remote carries, and `fetch_object`'s "does
this answer carry the object that was asked for" check would then refuse every
real answer while looking like a strict reader.

The new case builds a two-object pack in the shape a real one arrives in, so the
class cannot come back through a fixture that happens to carry one object.

MEASURED END TO END AGAINST THE REAL REMOTE, all of it through the vendored
client with no git binary and no library push:

  * `lease status` read a tombstone the BASH lease left — `released holder=vm-5433-…`.
    The two implementations agree on one object, which is what makes this a port
    rather than a parallel mechanism.
  * `lease acquire` took it: ref advertisement, a receive-pack command list, a pack
    carrying a minted lease commit, and a report-status saying it applied.
  * `authorises` answered `0` for the branch the lease names and `2` for one it
    does not; `peek branch` printed the field; `held` passed with margin.
  * THE RACE, which is the acceptance condition and the thing a fixture cannot
    show: two clones, one ref, both deciding to take a released lease, launched
    together. One got `took the lease`, the other got `lost the race for it` and
    exit 2. A blind write would have had both believe they held it — which is the
    two-holders bug this whole design exists to prevent.

Refs: CLOUD-1274, CLOUD-1148
The verb landed with a heartbeat that renewed until the lease stopped being this
clone's, and that is only one of the three ways a hold ends. The other two are
what `land-lock`'s own `#MUTANT` rows are about, so a retirement claiming to carry
its cases could not have been written honestly without them.

LIVENESS, FIRST EACH BEAT. A heartbeat whose land is gone must not renew a lease
for nobody. A kill, an OOM, and an un-reaped task stop all skip the land's own
trap, and an orphan that keeps renewing blocks every rival while the lease reads as
a healthy hold. Release FIRST, then exit, so the fleet frees now rather than after
a TTL nobody is refreshing.

EXISTENCE IS NOT ENOUGH, and that is measured rather than cautious: pids recycle,
and this container was observed wrapping its pid space inside twenty minutes — well
under the stall bound. So the pid must still BE a process whose command line
carries the marker. Anything that cannot be evaluated reads as GONE, because a
wrongly released lease costs one lap and the holder's own fence catches it before
it acts, while a wrongly renewed one wedges the fleet for as long as nobody
notices.

PROGRESS, WHICH LIVENESS CANNOT SEE. A land can be alive, with a trap that would
fire perfectly well, and have stopped landing — and it keeps beating, so every
rival waits on it forever. The registry's stamps are READ rather than
re-derived: this answers "what did the writer record", never "is this task
healthy", which is `bail`'s question and lives beside the other predicates.

THE TWO STAMPS ARE NOT FOLDED INTO ONE MAXIMUM, and folding them is the mistake
worth stating rather than merely avoiding. The hang bound may only be applied
WHILE A LOOP IS ACTUALLY TICKING — exactly `tick_at > advance`. Folded, a
three-beat bound would be applied to a verify step that legitimately runs for
minutes, and the mechanism's first act would be to kill healthy landings. The
case pins both directions.

NO ENTRY, NO VERDICT. A land whose bookkeeping never registered is not evidence
of a stall, and killing one on that reading would be inventing the finding. That
is the fail-OPEN half of a deliberately asymmetric pair: the rival's own steal
fails CLOSED on the same field — no token, no steal — because releasing a lease
wrongly costs one lap and stealing one wrongly puts two holders on the same trunk.

The heartbeat is also the one caller that may REPLACE the progress token rather
than carry it, and it is the only one: every other path carries what it found,
since erasing a token it cannot compute would make the lease look
unstealable-forever to every rival.

RELEASE FIRST, SIGNAL SECOND, and re-corroborate before the signal. The release
always lands and is what frees the fleet; the signal's promptness depends on what
the land is blocked in, so ordering them the other way would make a fleet-wide
unwedge wait on a signal that might be pending. The liveness probe is re-run
immediately before the kill rather than inferred from the one at the top of the
beat, for the pid-recycling reason above. `SIGTERM`, so the land's own trap runs —
a kill would leave exactly the orphaned state the liveness probe exists to clean
up after, which is the spawn annotation's own argument for staying.

The bail reason is left where the agent will look, because a landing that stops
without saying why reaches its agent as "verify and CI disagree" and the remedy it
then reaches for is wrong. Writing it swallows every failure: a reason that cannot
be written must not become a second failure on top of the first.

Refs: CLOUD-1274, CLOUD-1148, CLOUD-499, CLOUD-432, CLOUD-470
…he gate that reads it

`land-lock-check` cannot retire while `land-lock` does, or the reverse: they
re-implement held/free/lapsed/wedged/garbage independently, so porting one alone
leaves two authorities over a single lease vocabulary. This is the other half, and
it needed a change to the reader first.

`observe` SUBSTITUTED A FULL-TERM BODY for a ref it could not parse. That keeps the
safe behaviour — a lease we do not understand is respected until it ages out,
never treated as free — and it also made the wrong ref indistinguishable from a
healthy hold, so a stray push blocked landing for a term with nothing anywhere
saying why. Silent and safe is exactly the pair non-negotiable rule 2 is about:
the failure direction was right and nothing could report it.

So garbage is its own state. **Every DECISION still treats it as held**, and the
two arms fail in opposite directions on purpose:

  * `turn` answers `Wait`. Taking a ref nobody can read means overwriting whatever
    a stray push put there, and a well-meant fix that races a real holder is worse
    than waiting out a term. Its two corroboration clocks read zero, so no amount
    of watching makes it stealable.
  * `authorises` answers `Run`, like everything else it cannot read. Stopping the
    fleet over a ref somebody mis-pushed is the cost that arm exists never to pay.

`health` is the reporting half, and `lease check` is the gate over it. A NEW ROW
rather than a flag on the status row: the read-only allowlist is
`filter(effect == read)` with no second list, so a refusing flag on a reporting row
would drop the reporting invocation every consumer already uses.

IT BELONGS ON A CLOCK, NEVER ON THE LANDING PATH, and the row says so. Neither
refusal is a correctness hazard for the trunk — the lease decides who goes first,
never what may land — so on the landing path it would fail whichever PR happened to
be in flight over a condition that PR did not cause and cannot fix. That is the
same split the tree already draws between a property of the commit and a property
of the world.

TWO WEDGES, AND ONE OF THEM PASSES EVERY CLOCK CHECK. A horizon beyond one term
cannot come from this protocol, which only ever mints one term from now — so it
means somebody wrote the ref by hand or under a different term, and it would block
the fleet for however long it says. The other is a lease with NO HOLDER: release
requires recognising your own id, so nobody can ever leave it, however healthy its
expiry looks. Both are reported and neither is repaired, because overwriting a
lease this cannot understand is how a well-meant fix races a real holder.

A release is rendered as a DECLARATION and a lapse as an inference, which is what
stops a tombstone printing as an expiry half a century in the past — the class that
was found live three times in the predecessor's own renderers.

Refs: CLOUD-1274, CLOUD-1148, CLOUD-393, CLOUD-433
CLOUD-1274 decided the lease could speak the git protocol's own CAS. This is the
same route widened from one object to a branch, and it is the first of the four
writes `batten land` needs.

THE AUDIT CAME FIRST AND IT IS WHAT LICENSED THE REST. Three closures resolved
and gated separately, because they fail for different reasons:

  * `gix/merge` — 7 new packages (`gix-merge`, `gix-imara-diff`, `gix-worktree`,
    `gix-ignore`, `gix-pathspec`, `gix-submodule`, `dashmap`). Clean.
  * pack/traverse/odb — already resolved; zero new audit.
  * `gix/worktree-mutation` — 3 reachable (`gix-worktree-state`, `io-close`, and
    winapi, which is Windows-only). Clean.

No `FRAMEWORK_CRATES` name, no `AMBIENT_CRATES` name, no new `links` crate;
`macos-link-check` and `deny` pass on every arm. **Neither feature is enabled
here** — an unused feature is dead weight, and they land with the stage that uses
them. This commit needs no manifest change at all.

TWO THINGS THE AUDIT CORRECTED, both of which had been written down as facts:

  * `worktree-mutation` does NOT drag in `gix-command`, the external-program
    spawner the plan predicted would be its cost.
  * `gix-command` is ALREADY reachable in the shipped tree —
    `gix-transport` <- `gix-protocol` <- `gix` — and has been throughout. So the
    worry that acquiring it would make "nothing in this crate spawns git" false
    in substance was aimed at a state the tree was already in, and
    `no_second_git_invoker_exists` still claims exactly what it proves: that THIS
    crate's source names no `git` to spawn.

WHAT IS SENT, AND WHY IT IS NOT THE WHOLE CLOSURE. The full closure of a head is
the entire history, unaffordable on every lap. `objects_to_send` takes the commits
in `base..head`, their trees and blobs, MINUS everything reachable from `base`'s
own tree. The subtraction is sound because the remote HAS `base` — it is the tip
being pushed onto — so nothing an out-of-range ancestor contributes can be
missing. Sending too many is harmless and sending too few is a broken push, so
every uncertainty resolves toward including the object: an id the odb cannot
produce on the exclusion side is simply not excluded.

`base` is the ref's CURRENT value read from the ADVERTISEMENT, never a local
guess. A branch this clone rebased carries commits the remote never had, and any
other base either sends too little or re-sends history the remote has had for
months. An absent ref advertises zero, which is both "must not exist" to the CAS
and "subtract nothing" here — so a first push sends the whole closure, correctly,
and is the one large case.

THE EXCLUSION/SEND SPLIT IS AN EXPLICIT FLAG, not an inferred one. A first draft
told the two walks apart by whether the output vector was already non-empty; that
reads fine and is wrong the first time a caller passes an empty send list. It is a
parameter now.

MEASURED LIVE, and this is the shape no lease fixture has: `refs/heads/batten-d1-push-probe`
was created on the real remote pointing at this branch's head — a full-history
first push, commits and trees and blobs enumerated from the odb, packed, and
applied through receive-pack. 43s. Every pack the lease suite builds carries one
or two objects, which is exactly the shape that hid the object-kind defect until a
live run, so the two new suite cases drive the enumeration over this repository's
own history instead.

`delete_ref` lands beside it because the lap deletes its branch after the merge,
and it carries a measured note rather than a promise: THIS SANDBOX'S PROXY 403s A
REF DELETION, by this code and by the `git` binary alike. That costs the loop
nothing — `mem:workflow/landing-loop` already records the closing "could not
delete" as expected output, since auto-delete-on-merge normally wins the race —
but it does mean the scratch ref above could not be cleaned up from here.

Refs: CLOUD-1274, CLOUD-1148, CLOUD-740
…h never needed

D2's wire half. The reader that served the lease could not have read a real
fetch, and the reason is worth stating because it is the same shape as the
object-kind defect one commit back: a lease pack carries two undeltified objects,
so `objects_in` REFUSED type 6 and 7 outright and no fixture could tell.

A REAL SERVER DELTA-COMPRESSES ANY MULTI-OBJECT PACK, so the first genuine fetch
would have hit that refusal. Both forms are read now:

  * an OFFSET delta names its base by distance back through the pack, so the
    reader tracks where each member started — the pack's own coordinate system,
    and the only thing an ofs-delta can be resolved against, since it deliberately
    carries no id.
  * a REFERENCE delta names its base by id. A base OUTSIDE the pack is refused
    rather than resolved from the odb: nothing here asks for a thin pack, so
    accepting one would be a path no test reaches.

THE OFFSET ENCODING IS NOT THE SIZE ENCODING, and reusing the header decoder for
it is the trap this carries a comment about: the base distance adds `1 << 7` at
each continuation, so a decoder written from the size varint reads a base subtly
too close and resolves against the wrong object — which then hashes to an id
nothing asked for, surfacing far from the cause.

The delta application checks BOTH declared sizes, for the same reason: a stream
that produces the wrong length produces the wrong OBJECT, and an object hashed
from wrong bytes is a confident wrong answer rather than a visible corruption. A
zero-length insert is refused rather than treated as a no-op — git never emits
one, and tolerating it lets a corrupt delta loop.

`have` NEGOTIATION IS THE WHOLE ECONOMY, not an optimisation. `fetch_object`
sends `want`/`done` with no `have`, which is right for a parentless commit whose
closure is two objects and catastrophic for a branch tip: the server would send
the entire history every lap. The window is a COUNT (256) over every local tip
rather than just HEAD — a lap's branch and the trunk it lands onto usually share
an ancestor neither one's own short window reaches. Too few costs a bigger pack
and never a wrong answer, which is the direction to err in.

An object the odb already carries short-circuits before the wire, and reports a
fetch that moved nothing rather than a no-op, because the caller's next question
is what the ref reads now. A ref the remote does not advertise is an ERROR: "there
is nothing to fetch" and "the thing you named is not there" are different answers
and only one is safe to continue from.

MEASURED LIVE against this repository's `main` at `9ece058c`, which this clone
genuinely did not have — so negotiation, GitHub's own delta-compressed pack, the
offset decoder and the delta application all ran over bytes a fixture cannot
produce, and every member hashed back to the id the reader derived for it.

THE CASE IS GUARDED AGAINST PASSING VACUOUSLY, which the first draft was not: it
reads whether the odb already had the head BEFORE fetching, and demands a
non-empty answer when it did not. Reading that afterwards would be reading the
state the fetch just created, and allowing empty unconditionally would pass over a
fetch that never ran — the silent-empty-answer shape this tree keeps finding.

Refs: CLOUD-1274, CLOUD-1148
…module that admits it writes

D2's other half. The wire half proved the engine could READ a fetch; this is what
makes the fetch usable, and it needed a decision about where the writes go before
it needed any code.

NOT `git.rs`, AND THAT IS THE POINT. That module is documented as read-only over
gix — in its own header and in `mem:core` — and the only write to a remote
anywhere in the crate is `lease::swap`. `repo.edit_reference` and the odb handle's
write are both ungated and were sitting right there, so adding them to `git.rs`
would have been the easy move and would have made a stated property false rather
than changing it on purpose. The split is by EFFECT, not by subject: deciding
which objects a push must carry stays `git::objects_to_send` because it is a READ.

`module-layering` forbids `hook -> gitwrite` and `check -> gitwrite`, for the
reason it forbids the same edges into `lease`: a gate declared `read` must not
reach a write, and the read-only allowlist is DERIVED from that declaration rather
than reviewed. Absence from `declared_modules` is `V-LAYER-UNPLACED` rather than
an allow, so the module is named there too.

LOOSE OBJECTS RATHER THAN A PACK, and the trade is stated rather than assumed.
`gix-pack` could index the received pack, but `write_to_directory` sits behind
`streaming-input`, which is off here and pulls `parking_lot` and `gix-tempfile` to
turn on. A lap's fetch is a handful of commits, so loose costs a few files and no
dependency. Loose is not a lesser form — it is what git itself writes for new
objects. A fetch of thousands would want the pack path; bring a number and it is
the answer.

WRITTEN THROUGH THE ODB HANDLE, not `Repository::write_object`, which takes a
typed value and RE-SERIALISES it — round-tripping bytes the pack reader already
produced and hashed through a second encoder. The handle takes the payload and the
kind it was hashed as, so what lands is exactly what was read. And the id the odb
returns is checked against the one the reader derived: they are computed the same
way, so a disagreement means the bytes changed in between, which is precisely what
a delta applied wrongly produces. That must not become an object under a
plausible-looking name.

The ref write is deliberately unconditional. A fetch writes a remote-TRACKING ref
— a record of what the remote said, not something two processes race for. The
compare-and-swap that matters is the remote one, and that is `lease::swap`'s.

MEASURED END TO END, and independently confirmed outside the test: `main` at
`9ece058c`, a commit this clone did not have, was negotiated for, received in a
delta-compressed pack, resolved, written into the odb as a real commit object, and
a ref moved onto it — then read back with `git cat-file`, which says `commit`.

The round trip is its own case because a wire test cannot reach it: reading
objects off the network proves the READER, and only writing them and finding them
again proves the engine can USE what it fetched, which is the whole capability.

Refs: CLOUD-1274, CLOUD-1148, CLOUD-740

Admits: 282aea756a5c539bfa504ce9bc245cecea95473c0ac8b1215207a8fbb375f12e
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: 2270edf
Admits-epoch: b0870e454b67df619d775075d032fb2dbd36df83e451a647bab95c0cfdc96bcc
Admits-author: alec@wenzowski.com
Admits-prev: 0b8fb803ba1699ff3d34591e46d32b94eb54cd554356424eacf3f1248138d7c6
Admits-answer-lost: Nothing. The row is an ADDITION: `gitwrite.rs` is a module this same commit creates, and `module-map-check` refuses the commit precisely because the map does not yet name it. No existing row is edited, reworded or removed, so there is no prior content to lose.
Admits-answer-precondition: The memory edit was made through the owning surface — Serena's `edit_memory` — and this articulation is not a claim otherwise. What that surface cannot express is the COMMIT: `edit_memory` writes a memory and produces no commit, so the change reaches `main` only as a commit whose diff writes the protected path, which is the object this clause judges. The write is one added bullet describing a module created in the same diff, and `module-map-check` is red until it lands.
Admits-answer-rejected-route: R-RESTORE-IT is the one rejected: restoring the committed bytes removes the row for a module the tree now contains, which is the exact state `module-map-check` refuses — the route reaches a red gate rather than a safe one. R-USE-THE-OWNING-SURFACE was NOT rejected; it was taken for the edit, and cannot be taken for the commit because no Serena tool commits.
…flict

The landing loop's lap is fetch, rebase, verify, push. Two of the three writes
exist now; this is the third, and it is the one that decides whether the loop can
run unattended at all.

A CONFLICTING REPLAY IS A RETURNED VALUE, NOT AN ERROR AND NOT A RESOLUTION.
`mem:workflow/landing-loop` gives the loop exactly one human stop — "the only
stop is a rebase that conflicts" — and `gix-merge` will take a resolution
strategy and report success, which would delete it. So the conflict test asks
`TreatAsUnresolved::forced_resolution`, the strictest reading available: an entry
a strategy resolved still counts as unresolved. `Rebase::Conflicted` carries the
ORIGINAL commit and the paths, because its caller has to report a pointer rather
than swallow an internal failure.

NOTHING MOVES ON A REFUSAL. The ref is written and the worktree touched only
after every commit in the range has replayed, so a refusal leaves the clone
exactly as it was — no detached HEAD, no `--abort` to remember, and no
half-replayed state for the next lap to start from. A refusal that left one
behind would be worse than one that resolved.

`Current` is a third answer rather than a degenerate success, and it is load
bearing: every receipt in the loop is keyed to the commit it validated, so a
replay that rewrote an already-current branch would throw away a `verify` that is
still good and buy a CI run to re-prove it.

THE WORKTREE HALF IS WRITTEN BY HAND, AND THE FEATURE THAT WOULD HAVE DONE IT IS
WITHDRAWN RATHER THAN CARRIED. `gix-worktree-state::checkout` does all of this,
and its `Find` bound is `Send + Clone` unconditionally while this crate's
`OwnShared` resolves to `Rc`; `gix/parallel` is the feature that would make it
`Arc`, and enabling it did not move the bound in this build. Both features are
recorded in `Cargo.toml` as tried and dropped, so the next author does not re-run
the experiment. What a lap needs is a handful of changed paths, and the filter
pipeline `merge` already brings is what writes them — so a repository with a
clean/smudge driver still gets the bytes git would write.

Two traps in that half, both of which fail silently:

  * An index built from a tree carries ZERO stat data, and git compares size
    before content — so writing it straight out makes every tracked file read as
    modified. Each entry keeps the stat the existing index held, or gets a fresh
    one for a path this replay wrote.
  * Writing adds and overwrites; nothing in it removes. A path the new tree does
    not carry has to be unlinked, or the next `verify` compiles a file that is not
    in the commit.

`merge` ACTIVATES `gix-diff/blob`, which the `gix-diff` line had spent four
paragraphs declining, so that decision is amended at its own site rather than
left standing as a claim this manifest falsifies. Its dependency-cost half is
dead and is recorded as dead; its correctness half — what a gate reads must not
be reconfigurable by the repository it reads — stands, and the rebase is the
opposite case that takes the opposite answer. 11 activated packages, no
`FRAMEWORK_CRATES` name, no `AMBIENT_CRATES` name, no new `links` crate;
`macos-link-check` and `deny` both pass. `gix-command` was already activated
through `gix-transport`, so `no_second_git_invoker_exists` still claims exactly
what it proves.

A replayed commit loses its signature, as `git rebase` without `-S` does: a
carried-over `gpgsig` would be a signature over bytes that no longer exist, which
looks like provenance and is not. A merge commit in the range is refused rather
than flattened.

`.claude/rules/rust.md` said "`tokio` appears nowhere in `Cargo.lock`, and there
is no `async fn` and no `.await`". That went false when CLOUD-745 vendored hyper
and has been false since; corrected in place rather than dropped.

Refs: CLOUD-1274, CLOUD-1148, CLOUD-745, CLOUD-739
…g refused

`ToWorktreeOutcome::as_read` answers the NARROW question — did an external driver
hand back a stream — and returns `None` for `Unchanged` and `Buffer`, which is
every path in a repository that configures no clean/smudge driver. Reading that
`None` as "cannot be read" turned the common case into a refusal, so a clean
replay failed with `is behind a delayed filter` over a plain text file.

The outcome IS the reader; the delayed case has to be asked separately because
its `Read` impl PANICS rather than erroring, which is the reason the two
questions looked like one.

Found by the tier, not by reading: three of the four cases were green over it,
because the conflict refusal and the already-current answer both return before
anything reaches the worktree. Only the clean replay writes a file.

The suite's own header now says what its conflict case does NOT discriminate: it
separates refusing from resolving, and it does not separate
`TreatAsUnresolved::forced_resolution` from the laxer `git()`, since a two-sided
edit of one line is unresolved under either. Building that case needs a
resolution STRATEGY to be configured, and none is.

Refs: CLOUD-1274
…k instructs

`target-prune` refused with `declared 152, live 164, tolerance 10`, and `verify`
reported that as "not enough disk" with 8.1 GB free — the misattribution
CLOUD-1153 is open for, and which the guard's own comment explains cannot be
fixed at that site.

The remedy is the one the block states: move `count` and `measured` TOGETHER, and
leave the floors alone, since no independent measurement was taken. Same shape
and same half as the 2026-09-01 move one paragraph up.

What is worth a line is what the drift now MEANS. The block above predicted this
move; since the target consolidation a tracked test file is no longer a proxy for
a linked stem — 164 files still link 2 targets — so this basis has become a trend
counter over a quantity that no longer drives the bytes the floors budget. Still
worth watching, and it no longer implies a floor is wrong.

Refs: CLOUD-1158, CLOUD-1210, CLOUD-1153

Admits: a75ceb0754f824775bf2e0a84d27b4c68448f20180687009ae00c9701111b6f7
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-head: cf0e751
Admits-epoch: bf22c7740a43e8f5a6246262dffe5982be8443105cd39137c5c7e9becf5c919a
Admits-author: alec@wenzowski.com
Admits-prev: d7b0aaf76565dfa1a3ca87a2a2454c0e210bd854260e69f2bcebcd506ad4b28b
Admits-answer-lost: Without it `target-prune` refuses every lap — `declared 152, live 164, tolerance 10` — and `verify` reports that refusal as "not enough disk" with 8.1 GB free, which is CLOUD-1153's misattribution. So the branch cannot verify at all, and the operator is sent to free space that was never short. The block's own instruction is to move `count` and `measured` together when it reds; declining leaves a gate red for a reason its own config names the fix for.
Admits-answer-precondition: `batten.toml` IS the owning surface for a `[prune.*.basis]` row — no other file the engine reads declares one, and `target-prune` resolves the count from this table alone. The class's own remedy for this path is "change it in a pull request", which is exactly what this is: branch claude/landing-loop-bundle-4puk4i with draft PR #812 open, so the two-line change lands in a diff a reviewer sees.
Admits-answer-rejected-route: config read first is the one rejected, and it is circular for this subject: it says read the committed config rather than write it, and the committed config IS what declares a stale count. patch run first does not apply either — there is no patch to run: the basis is a declared integer this tree measures directly (`git ls-files 'crates/batten/tests/**/*.rs'` = 164), not a derived artifact any task regenerates.
… instance

`semver check` printed `Failing lint(s): function_parameter_count_changed` and
nothing else, so the remedy was "go run the delegated tool yourself and read its
report" — over a report this process already held in `Compared::report`,
unparsed. Measured on this branch: four tool calls to learn the subject was
`recorder::record_path`, all of them re-running a comparison that had already
run.

`subjects()` reads the tool's own `Failed in:` items and the refusal prints them
under the class line, one per item. Still a pointer and never a payload
(non-negotiable rule 4): the path, the line, and the tool's one-line summary of
the delta — no rustdoc, no signature, no source.

THE PATH IS RELATIVISED AGAINST THE ROOT, and that is house-style §6 rather than
tidiness: the tool prints an absolute path, so passing it through would make this
gate's bytes differ between a developer's clone and a runner's. The strip is
conditional, and a case pins the other direction — a path the prefix does not
match keeps what the tool gave, because a mangled pointer is worse than a long
one. A third case pins the empty answer, since a lint can fail with no per-item
block and a fabricated pointer would be worse than none.

Keying on the `, in <path>:<line>` tail rather than on the `Failed in:` header is
deliberate: if that format moves, subjects go missing rather than getting paired
with the wrong lint.

BREAKING CHANGE: `recorder::record_path` takes four parameters where it took
three — the claim is the fourth, and it is what the record partition is keyed by.
It was introduced by ea10dc8 ("fix(recorder): partition a record by the claim,
not by the branch name alone") and the declaration belongs on that commit; it is
here because amending a commit already pushed to this branch was refused, and a
declaration in the range is what `semver::declared_break` reads. Below `0.1.0`
release-plz still cuts a patch whatever the footer says (`.claude/rules/commits.md`
records that measured case), so this buys the changelog marker and an honest
history rather than a bump.

Refs: CLOUD-1050, CLOUD-1300
… stopped compiling for Windows

`materialise` reached for `std::os::unix::fs::symlink` and `PermissionsExt` with
no `cfg`, so `x86_64-pc-windows-gnu` failed to type-check — three errors, in the
LIB rather than in a test, which means the portability claim this repository
makes was broken outright rather than narrowed.

Found by `cross-check`, which is the point of that gate and the reason it denies
warnings (CLOUD-397): the class is invisible on Linux by construction, so no
amount of local running would have shown it and no reader was going to catch it.

Both effects are gated now, and each non-Unix arm is git's OWN fallback rather
than a silent no-op — a symlink becomes a regular file holding its target's path,
which is what `core.symlinks=false` does, and the executable bit is not modelled
by the filesystem at all, so there is nothing to set. Deliberately not
`std::os::windows::fs::symlink_file`: it needs a privilege an ordinary account
does not hold, so it would fail where the fallback degrades.

Stated rather than assumed: neither arm is exercised here, because this
repository ships a Linux and a macOS binary. They are written to be obviously
right, not to be measured.

Refs: CLOUD-1274, CLOUD-397
…solver gate stops standing in for that

Two gates were red, and had been since the first `lease.rs` commit on this
branch — which is the finding under the finding: four commits landed without
`test:cargo` ever running.

`gix_is_confined_to_this_module` REFUSED ON A PREMISE THAT HAS EXPIRED. It read
"two git backends coexist here ON PURPOSE and in ONE module, so the in-process
half cannot spread" — and CLOUD-740 ended that; this module's own header says
NOTHING HERE SPAWNS `git` ANY MORE, four lines under a heading that still
announced two backends. Both halves of that contradiction were in one file.

So it is WIDENED to a closed list of three — `git.rs`, `gitwrite.rs`, `lease.rs`
— rather than deleted, because two properties outlive the premise. Git access
stays enumerable: a fourth module still fails, which is what the assertion
actually bought. And the EFFECT split is `policy/module-layering.rego`'s
forbidden edges over the resolved use graph, which is strictly stronger than
confinement ever was.

THE ARGUMENT THAT DECIDED IT is that merging the two modules into `git.rs` —
the other way to make this gate green — is the WORSE design and the gate pointed
at the safer one only by accident of its wording. `check` and `hook` legitimately
reach `git.rs`; folding the remote-write half in here makes `hook -> gitwrite`,
`check -> gitwrite`, `hook -> lease` and `check -> lease` unspellable and hands
every read-effect gate transitive reach to a push.

`every_declared_git_module_exists_and_reaches_gix` is the anti-vacuity half, both
directions: a name no file carries would exempt nothing while reading as
coverage, and a name whose file reaches gix nowhere is an entry that should be
dropped rather than left to grow the list into an allowlist.

`no_second_repo_root_resolver_exists` IS NARROWED TO WHAT IT ALREADY SAID. Its
own comment states the subject outright — "what is forbidden is *root
resolution*, not git access" — and its token list then forbade `gix::`, which is
git access. The two were one predicate only while one module held all of it.
`gix::discover` is the resolver's own spelling and still matches both forms this
crate could write, so the teeth stay on an actual second resolver while the
who-may-reach-the-backend question goes back to the gate that owns it.

Refs: CLOUD-1274, CLOUD-718, CLOUD-34, CLOUD-740
The full suite had never run on this branch. It found five defects in the lease
verb and three ledgers the new rows were missing from, and two of the five are
behaviour rather than bookkeeping.

`authorises` WAS RETURNING 3 IN A CLONE WITH NO REMOTE, short-circuiting before
the arm whose entire contract is to fail open. That arm exists so a lease nobody
can read never stops the fleet; the terms resolve ahead of every arm, so it was
being answered by a guard it was written to sit behind. It answers `Run` now.

`lease status -J` EMITTED NOTHING on the same path — the exit code carried the
verdict and the data channel stayed silent, which is a decode error where the
caller asked a question. A data channel emits its document unconditionally; the
code carries the verdict and the document carries the answer.

Both had one cause, and fixing it is the change: `TermsMissing` splits "no remote
is configured" from "something about reading it failed". The could-not-look guard
exists so an UNREADABLE lease is never reported as a free one, and a clone with
no remote has no lease ref to misread — folding the two together made the verb an
error in every clone that has not been pushed anywhere. The split is by EFFECT,
the same line the read-only allowlist draws: the five read arms answer, and the
five that reach `swap` still refuse, because acquiring a lease that has nowhere
to live is not something a missing remote makes safe.

`lease::stop` MOVED OUT OF `lib.rs`, where `spawn-adapters` refused it and was
right to: a spawn belongs in a placed adapter, and the CLI dispatch is not one.
Placing `lib` was the available wrong answer — it would admit every future spawn
in the crate's largest file. `lease` joins the table instead, on the argument
`symbols`, `pinned` and `prune` already carry there: the pid comes off the lease
record and whether that process still exists is a property of the machine.

Three ledgers gain their rows, each with the reasoning rather than the name:

  * the read-only allowlist and the surface row set — five arms and eleven paths.
    The NOUN is unclassified in both, because half the subtree writes and a
    consumer treating an entry as a prefix must not be handed the five that do.
  * the network-caller list — `lease.rs` is the third module allowed to call the
    adapter, and the one that had to be argued: every other route to a git remote
    from this crate resolves a crate `macos-link-check` refuses by name.
  * the pointer-only census — ten arms, all `PointerOnly`, five of them declared
    could-not-look over a corpus with no remote. That census matters more here
    than elsewhere: these arms read `GH_TOKEN`, and `credential` returns it to
    nobody precisely so it cannot reach a `Debug`.

One doc comment said "clobbered", which is a `[[verb]]` name in the primitives
fixture, so non-negotiable rule 1's scan refused the file. Reworded.

Refs: CLOUD-1274, CLOUD-393, CLOUD-92, CLOUD-121

THE BRANCH'S THREE WEAKENINGS ARE DECLARED HERE, and the placement is the honest
one available rather than the ideal one. `config lint --config-from origin/main`
reports them against the bundle as a whole, and the commits that performed each
edit are already pushed and cannot be amended from this session — so the trailers
ride the branch's current head, which is where `lint::declared` reads them
(`origin/main..HEAD`, so nothing inherited from trunk can admit them).

The trailer is only ever HALF the admission. The other half is CLOUD-1300's
groomed body, which now names the same three pairs, and the claim receipt carries
them — so neither source can admit a weakening the other has not seen. That
receipt also records what it overrode: the row was already In Progress with a PR
attached, so the re-claim is a `takeover`, written into the receipt rather than
left implicit.

Weakens: recorder-added recorder[landing-lease]
Weakens: program-changed program[land-lock-peek]
Weakens: program-changed program[land-lock-status]
… the floor

`target-prune` refused a lap at 7895MB free against the 7938MB warm floor — 43 MB
short — while `target/tmp` held 796MB across 5156 fixture directories. No
declared root reached it, so the escalation ran out of things to drop and refused
with most of a gigabyte of pure residue on the volume, and `verify` reported it
as a floor nothing could recover.

The omission compounds rather than merely costing once: `CARGO_TARGET_TMPDIR`
regrows every suite run and `land` runs `verify` per lap, so a loop that cannot
drop this wedges on a floor it could satisfy — measured here, where clearing the
directory by hand was the only way to get a lap open.

`cold = false`, and for this root that is not a judgement call the way it is for
`incremental` or `build`: nothing under it is a build artifact at all.
`crates/batten/tests/it/common`'s `scratch` wipes each fixture directory before
writing it, so no run has ever depended on what a previous one left. The next
build reads none of it, and a suite that finds it gone materialises exactly what
it always did.

Refs: CLOUD-1274, CLOUD-1157, CLOUD-1030

Admits: 4dbbadf82afb221c8f858667af6b24230f209292fcfb2635e4f23cab7c62d640
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-head: 939189f
Admits-epoch: 2a9622027a67597e054de0a84c63b1eede1c82ef4c2a2ea4abdfebefc70008d0
Admits-author: alec@wenzowski.com
Admits-prev: e984bf1c5db245235cb04fa64f1b842ac10e7ead56e897eac5a1b5810f3491ff
Admits-answer-lost: Measured minutes ago: `target-prune` refused this lap at 7895MB free against a 7938MB floor — 43MB short — while `target/tmp` held 796MB across 5156 test-fixture scratch directories that no declared root reaches. So the escalation ran out of things to drop and refused, with 800MB of pure residue on disk. Without the row every future lap re-hits it, because a full suite run regrows that directory: `land` runs `verify` per lap, so the loop wedges on a floor it could satisfy. Declining means clearing it by hand before every lap, which is the reclaim this table exists to automate.
Admits-answer-precondition: `batten.toml` IS the owning surface for `[[prune.regrowable]]` — the roots prune may drop are a table in that file and nowhere else, so a missing root cannot be declared anywhere but here. The class's remedy for this path is "change it in a pull request", which is what this is: branch claude/landing-loop-bundle-4puk4i, draft PR #812 open, one added table and its comment in a diff a reviewer sees.
Admits-answer-rejected-route: config read first is circular: the regrowable table IS the committed policy, so reading it rather than writing it cannot add a root prune has never known. patch run first does not apply: no task regenerates this table — each row is hand-declared with a `cold` verdict and a comment saying whether losing it makes the next cargo build a full one, which is a judgement, not a derivation.
…snapshot is regenerated

`CommandDecl` gained a required `id` while this branch was out (CLOUD-969's
stable identity, the thing a third party pins against instead of `path`). The
rebase merged `surface.rs` without a conflict — the eleven `lease` rows and the
new field are in different places — so the file came through clean and did not
compile.

Each row takes the dotted form of its own path, which is what every neighbouring
row uses and what the golden schema already renders.

THE SNAPSHOT WAS RESOLVED BY HAND MID-REBASE AND THAT RESOLUTION WAS WRONG, which
is worth saying rather than quietly regenerating over. It is a generated file, its
one conflict hunk was the command list, and taking the applied commit's side kept
a shape that predates main's schema change — so the tree carried a golden file
describing a surface the binary no longer emits. `mise run fix` is the authority
and has regenerated it; the lesson is that a generated artifact should be
regenerated after the rebase rather than merged during it, whichever side looks
right at the time.

Refs: CLOUD-969, CLOUD-1274
…d so

`holder_alive` reads `/proc/<pid>/cmdline`, which exists on Linux and nowhere else
this crate builds for. That was left implicit, so on Windows the read failed, the
unevaluable-reads-as-gone arm answered `false` for every pid, and the ANTI-VACUITY
case beside it went red — the case whose entire job is to prove the predicate does
not answer `false` for everything.

IT WAS RIGHT TO GO RED. On that target the predicate really is vacuous, so this is
the assertion working rather than a fixture needing relaxation. What was missing is
that the vacuity was undeclared: it followed silently from a missing file instead
of from a decision.

`cross-check` STRUCTURALLY CANNOT SEE THIS, and that is worth recording beside the
fix. It type-checks against `x86_64-pc-windows-gnu` and does not run — a `/proc`
path compiles perfectly on a platform that has no `/proc`. This is the second
portability defect on this branch and the first one `cross-check` did catch
(`materialise`'s unconditional `std::os::unix`), so the pair marks exactly where
that gate's reach ends: it finds what will not COMPILE, never what will not WORK.

So the split is declared. The Linux arm keeps the `/proc` read; the other arm is
inert by construction and says so — every pid reads as gone, which is the stated
asymmetry taken to its limit rather than a wrong answer, and a steal still needs
the corroboration clocks `turn` requires. Each arm carries its own case, so the
inertness is pinned rather than inferred from a `cfg` on the function.

Refs: CLOUD-1274, CLOUD-397, CLOUD-418
@wenzowski
wenzowski force-pushed the claude/landing-loop-bundle-4puk4i branch from 517fbfc to 35e8232 Compare September 2, 2026 05:46
…claim itself

The rebase onto `d3b01510` brought `record.rs` and `plan-complete`, and the
collision was semantic rather than textual: git merged the files cleanly because
they do not overlap, and the tree did not compile. Two `record_path` call sites
arrived on the three-argument signature this branch had already widened.

`None` WOULD HAVE COMPILED AND BEEN WRONG IN A NEW WAY. One of those sites is
`pr-closes` — the record CLOUD-1300 was measured on. The reader partitions now, so
an unpartitioned writer does not restore the old behaviour: the reader looks under
the partitioned name, finds nothing, and REFUSES where it used to wrongly exempt.
Both sites resolve the claim through one `claim_of`, so the writer and the reader
cannot spell the partition differently.

THE CLAIM RECEIPT IS NEVER PARTITIONED BY ITS OWN TOKEN, on both sides, and this
is the half a suite caught rather than a reader. `VERB_WRITTEN` is
`["claim", "plan"]`, and `claimed_token` DERIVES the partition from
`claim.<branch>` — so keying that file by the answer it produces is circular. The
reader looked for `claim.<branch>.<token>`, found nothing, and the record vanished.

`plan-complete`'s anti-vacuity arm is what went red, and the failure is worse than
a lost key: that module asks whether the branch pulled a row BEFORE it asks
whether the row was planned, so an unreadable claim record stops the predicate
deciding at all. A gate switched off, not a finding moved — and it reported as
`[]` against an expected `plan-unrecorded`, which is exactly the silent direction
CLOUD-418 exists for.

`plan_complete.rs`'s own fixture helper carries the same exclusion, and its
comment is what pointed here: "so a change to the naming breaks this tier rather
than silently pointing the reader and the writer at different files." It did.

The other three fixtures from `main` take `None` honestly — none of them mints a
claim receipt, so the unpartitioned path is the one their writer would use.

Refs: CLOUD-1300, CLOUD-418
@sonarqubecloud

sonarqubecloud Bot commented Sep 2, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@wenzowski
wenzowski marked this pull request as ready for review September 2, 2026 06:32
@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit 6363a2e into main Sep 2, 2026
19 of 20 checks passed
@wenzowski
wenzowski deleted the claude/landing-loop-bundle-4puk4i branch September 2, 2026 06:55
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