feat(lease): the landing lease's CAS, over the client the engine already vendors - #812
Conversation
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 **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 proposedTwo
The ordering bound, which is the real findingA recorder writes on the post-tool event, and 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 Refinement — Ready (wire the lease record so the predicate reads something real) Refinement gate: Definition of Ready & Done. This body carries only specializations.
Acceptance
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:
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 mattersLanding judgements split three ways, and only one third belongs in a preset:
The four predicatesTHIS 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.
Its fail-open asymmetry is conserved verbatim, and that is the load-bearing half. The instant reaches the predicate as a RESOLVED TOKEN, not as arithmeticOwner 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.
So the boundary compares and the module reads
A raw The two ways a preset ships DEAD, both already measured hereNot cautionary: both have happened in this repository.
And per CLOUD-857: anchor on Refinement — Ready (ship the generic landing judgements as a vendored, overridable bundle) Refinement gate: Definition of Ready & Done. This body carries only specializations.
Acceptance
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. ** The fourth route**The CAS is the git protocol's own. ** 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. This row's premise that the engine must be offline was already stale when the row was written. What must stay offline is Closure, measured rather than arguedThe framing is The three alternatives were measured against this tree's own gates and all are refused:
§2's invariant holds, and is asserted rather than reviewed
What landedEight commits on **The predecessor's exit vocabulary is deliberately NOT carried. ** Measured live against
|
| 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-lockare quoting seconds nothing can reach — the same class CLOUD-1166 records for counts, one type over. ci-lease-precondition(1.3s) dies withland-lockand 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-retirement — mise.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'sgixfeature list orcrates/batten/src/git.rs's spawn gate it changes. **No **mise-tasks/**program and no **tests/**/*.batsis edited or added —V-SHELL-RULE-EDITEDrefuses the first andV-SHELL-RULE-ADDEDthe 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 **hookpath. A capability admitted for the lease must be unreachable from both, asserted over the resolvedusegraph the waypolicy/module-layering.regoalready forbids thehook -> fetchedge — never by a comment saying the verb is only called fromland. - 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
writeand reaches the network; it may not beread, and it may not be reachable from a verb the derived allowlist admits. §5's rule that areadnoun with a write-bearing subtree leaks a prefix onto that allowlist (CLOUD-90) decides the shape. - Generated artifacts (§4).
Cargo.lockif 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/3table, and a lease that cannot be reached is3rather than a false2, 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,docsif the answer is a recorded refusal — patch either way below0.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 aPATCH-shaped implementation, which is precisely the unsafe form. Plus the anti-vacuity mirror, and an assertion that no network capability is reachable fromcheckorhook. - Blockers (§8).
relatedToCLOUD-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-lockin bash and exempt it by name, or replace the CAS with a mechanism that has one. - If
land-lockstays 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
checkorhook, asserted over the use graph rather than reviewed. - Every row quoting a recoverable-seconds total that includes
land-lockis 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:
- 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.
- The record cannot be minted at all in some environments. It is written by a
[[recorder]]over agh pr viewBash call, and a session without theghCLI 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 carryCloses 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:
- spend the admission, run
land; landfetches, rebases onto the advanced trunk, mints a new head;- the admission no longer matches,
batten-checkrefuses,verifyfails, the lap stops; - 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 carriesfiled-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.tomland its §7 namescrates/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.rsat thewrite_recordscall site, andcrates/batten/tests/it/board_record.rs. Notpolicy/filed-here.rego— the predicate is correct and reads what it is given. - Computable predicate (§2). A record written on branch
bunder one PR is not read as evidence on branchbunder 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-closesrecord mintable withoutgh, which is its own question. - **Effect (§3). **
readfor 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 hooktwice 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.
relatedToCLOUD-1298 (where this was measured), CLOUD-1051 (the recorder), CLOUD-1057 (the same staleness shape on branch receipts'baseline).
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 nativeRefusalsites — 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:
- no
violation/denyvalue underpolicy/**carries amsgkey, and none containssprintf— a custom Regal rule, in the shapepolicy-modules-bind-inputalready uses; - the
verdictsset a module declares equals the registry's keys for it, both directions — an emitted verdict with no entry fails, an entry nothing emits fails; - every route names a resolvable artifact — the technique
command-task-definedalready applies tocommandrows, generalised; overrideis optional, but where present carriespreconditionand at least one non-override route, so a gate cannot hand a reader a binary whose only concrete arm is the bypass.
- no
- 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.jsonandschema/batten.local.schema.jsonmove, because[[policy.verdict]]is new configuration.schema/policy-input.schema.jsondoes not — this changes OUTPUT. Whether a new output schema is introduced is decided in this row before implementation, not discovered frommise run schema.derived-checkgates 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
sprintfbanned 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
msgin 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 anoverridewith noprecondition⇒ (4) reds. Plus the anti-vacuity arm — a conforming module passes all four, so a check that refused everything would fail. - Blockers (§8). None.
relatedToCLOUD-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 theconservesgap).
Acceptance
- No refusal value at any emitter — policy module, preset, or native
Refusalsite — 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
overridewithout aprecondition, 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 testis wired to no task, somutantcannot reach a policy module). If gates migrate faster than that closes,mutant-censusreports 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). **
FloorandFloor::validateincrates/batten/src/prune.rs, and the[prune.warm]/[prune.cold]tables inbatten.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.shno longer exists (retired whole under CLOUD-1059);mise-tasks/**andtests/**/*.batsare refused byV-SHELL-RULE-EDITEDwith no override and nobypass_env. -
**Computable predicate (§2). **
Floorgains 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 sincemeasured— 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:1038callsprune.validate()from the shared config-load path, which **every **batteninvocation runs,batten hookon every mediated tool call included. A glob walk there is taxed onto thePreToolUsepath againstperf-assert's ceiling, andbatten checkover 654 tracked files already costs ~3.6 ms above the process floor. The repository has ruled on this exact shape once:claim-race-checkwas moved off the mediated call because a round trip on every tool call is disqualifying, andRuleKind::scopespairs every spawning kind withRuleScope::Treealone.So:
Floorcarries the declared basis (it is config, and validating its shape at load is free), and the comparison against the live tree runs inbatten target prune— alreadyCost::EffectonSurface::VerifyOnly, whichperf-assertdeliberately budgets no ceiling for — or as atree-scoped rule row underverify. Either is acceptable;Floor::validateis 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
measureddate. 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
measuredalready 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.
relatedToCLOUD-861 (the ratcheting floor this keeps satisfiable), CLOUD-1157 (the reclaim that sets whatsize × stemsactually 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 hookdoes not walk the tree for this, andperf-assertis 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 incrates/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/mainmoves — fires on every lap, becauselandrebases onto the currentorigin/maineach 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/mainSHA it was minted against. It is void when both: the current merge-base withorigin/maindiffers from the recorded one, and the branch carries no commits of its own (git rev-list --count origin/main..HEADis0).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 anywayA 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).
readfor the predicate; the receipt write already exists onclaim-check's pullable path and does not change class. No new verb and noSURFACEchange. -
Output & exit contract (§5).
claim-checkkeeps0pullable /1not pullable /2unreadable stdin. A void receipt reads as absent, soclaim-guard's existing refusal and message are reused unchanged — the remedy is already "runclaim-checkfor 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 until0.1.0regardless of type. -
Test obligation (§7).
tests/claim-check.batsandtests/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.
relatedToCLOUD-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 retiringclaim-guardinto 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 usage — CLOUD-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_allowlistis a list of paths, and the whole point of §5's derivation is that a consumer honours it. Renamepayload fieldand 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. **
CommandSpecis emitted with five fields and the surface declares more than it publishes.effectalready ships — that is correct as the Why states, and is not the gap. The gaps are:
data_channel**never leaves the binary. **CommandDecldeclaresdata_channel: bool(surface.rs:408), andCommandSpechas no such field;walk()anddescribe()never read it. It is a build-time-only column feedingevery_data_emitting_verb_declares_the_json_flag(surface.rs:3045) andis_noun()(surface.rs:2723). A consumer of the emitted spec can only infer the data channel by scanning each row'sflagsfor one namedjson.positional**is not expressible at all. **FlagSpec(spec.rs:18-30) has nopositionalfield, so a positional argument emits aslong: null, takes_value: true— indistinguishable 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, wherecheck's rule selector moves from--ruleinto 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_versionshould not ship first and then immediately need to move.This also sharpens the third acceptance bullet. Under CLOUD-1184
read_only_allowlistbecomes 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.rsandcrates/batten/src/surface.rs, plus the derived artifactsmise run fixregenerates. One authority: the id is a field onCommandDecl, never a second table keyed by path. Nomise-tasks/program and notests/**/*.batsis added or edited. - Computable predicate (§2).
batten spec --format jsonemits aspec_versionat the document root, and every command row carries a stableiddeclared on itsSURFACErow. Two invariants hold incargo test: every declared path has an id, and no id is declared twice — the same shape asevery_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.0there is nothing to be compatible with, and adding a shim now is the deprecation surface §2 refuses. Deciding the version's bump policy for0.1.0and after — state the rule, do not build enforcement for it. - Effect (§3).
read.specis already areadleaf 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_versionis a literal in the source, never derived from a clock, an environment variable or the crate version at runtime — a version that moves withCARGO_PKG_VERSIONsays "the binary changed", which is what the tag already says, and tells a consumer nothing about the shape. Exit follows the0/1/2/3table unchanged. - Commit / bump (§6).
feat(spec)— patch until0.1.0on the consumer surface. The library half ismise run semver's:SpecDocumentandCommandSpecarepub, 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 twospec.rsunit invariants. Shown able to fail per CLOUD-418, three observed: (a) aSURFACErow with no id fails the completeness assertion; (b) two rows sharing an id fails the uniqueness assertion; (c)spec --format jsonrun twice is byte-identical, and the emitted document containsspec_version. Add each id tothe_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.
relatedToCLOUD-1181 (the preset manifest, formerly this row's second half), CLOUD-970 (which consumes both), CLOUD-1180 (theagentsubtree, 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 jsonemitsspec_version, and its value is a source literal with a stated rule for when it moves.- Every
SURFACErow declares an id; the completeness and uniqueness assertions both exist and were each shown to fail. read_only_allowlistis 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 fixthenmise run verifyregenerates 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.
|
Warning Review limit reachedNext included review available in 48 minutes. View limit detailsLimit 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. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Free Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (20)
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Free Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (23)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe 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 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 FreeYour 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 |
717afda to
a8788b3
Compare
`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
b55f6c1 to
dd4d921
Compare
`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
f482b8a to
cf0e751
Compare
`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
…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.
939189f to
d86eec2
Compare
`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
…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.
… 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.
b7a7906 to
103b971
Compare
`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
…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.
… 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.
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
517fbfc to
35e8232
Compare
…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
|
❌ The last analysis has failed. |
|
/fast-forward |
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.mdis explicit that the FIRST key of aRefs: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, andfix(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 secondRefs: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 nogit. ThePATCH-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-packtakes a command list ofold-sha new-sha refand applies it only while the ref still readsold-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, overcrates/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
checkandhook— a property of the use graph, not of the crate — andpolicy/module-layering.regoforbids both thehook→leaseedge and thecheck→leaseedge.checkis listed explicitly rather than left to follow fromhook: a tree-scoped gate is declaredreadand the read-only allowlist is DERIVED from that declaration, so acheckpath 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 closuregix-transport'sblocking-clientresolves to, taken directly. NoFRAMEWORK_CRATESname, noAMBIENT_CRATESname, no newlinkscrate. The three alternatives are all refused by this tree's own gates:gix-protocol/srciscommand.rs,fetch/,handshake/,ls_refs.rs,util.rs; the onlypushsymbols in gix arepush_url()config setters.AMBIENT_CRATES) plussecurity-frameworkandcore-foundation(bothFRAMEWORK_CRATES) andring/aws-lc-syslinkscrates. 265 packages.https, and every configuration carrying it resolvesopenssl-sys— aFRAMEWORK_CRATESnamemacos-link-checkrefuses BY NAME with no vendored exemption. Adopting it would have meant retiringmacos-link-check, which isgoverned_at_headand cannot be edited at all.Measured live against this repository, not simulated
lease statusread 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.took the lease; the other gotlost the race for itand exit 2. APATCH-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. Awantreturns 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 landneeds 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/mergecosts 11 activated packages, all pure Rust, noFRAMEWORK_CRATESname, noAMBIENT_CRATESname, no newlinkscrate;macos-link-checkanddenyboth pass.gix-commandwas ALREADY activated throughgix-transport←gix-protocol, so the spawn-capable crate is not new andno_second_git_invoker_existsstill claims exactly what it proves.worktree-mutationandparallelwere tried and withdrawn, recorded inCargo.tomlso the next author does not re-run it:gix-worktree-state::checkout'sFindbound isSend + Cloneunconditionally while this crate'sOwnSharedresolves toRc, and enablingparalleldid 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
gitbinary.D2 — fetch.
havenegotiation, then a real delta-compressed pack.objects_inrefused 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:mainat a commit this clone genuinely lacked, negotiated for, received, resolved, written to the odb, ref moved — confirmed outside the test withgit cat-file.D3 — rebase, and the case the design exists for.
mem:workflow/landing-loopgives the loop exactly one human stop: "the only stop is a rebase that conflicts."gix-mergewill take a resolution strategy and report success, which would delete it. So the conflict test asksTreatAsUnresolved::forced_resolution, the strictest reading available.Rebase::Conflictedcarries 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.Currentis 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 averifythat is still good.crates/batten/tests/it/rebase.rsbuilds its fixtures withgixandgitwrite's own writes — nogitbinary anywhere in the file, because a suite that shells out to build fixtures asserts nothing about agit-free engine. Its header also states what the conflict case does NOT discriminate: it separates refusing from resolving, and does not separateforced_resolutionfrom the laxergit().Where the writes live, and why not in
git.rsgitwrite.rsis new and the split is by EFFECT. Folding it intogit.rswould makehook→gitwrite,check→gitwrite,hook→leaseandcheck→leaseunspellable inmodule-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_existsis narrowed to what its own comment already says: its subject is root resolution, and it was forbiddinggix::wholesale, which is git access.What the full suite found, having never been run on this branch
Four commits had landed here without
test:cargoever running. It found real defects, not bookkeeping:lease authorisesreturned 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 -Jemitted 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.TermsMissingnow 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 reachswapstill refuse.x86_64-pc-windows-gnufailed to type-check — three errors in the LIB. Found bycross-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-adaptersrefusedlease_stop'skillspawn inlib.rsand was right to. Placinglibwas the available wrong answer — it would admit every future spawn in the crate's largest file.leasejoins the adapter table instead.semverwas red onrecorder::record_path's arity, a break introduced earlier on this branch and never declared. Declared now, with aBREAKING 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
leaseNOUN 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
groomedbyconfig lint --config-from origin/main:recorder-addedatrecorder[landing-lease]program-changedatprogram[land-lock-peek]program-changedatprogram[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'sacquirewait loop belongs to the LAP, not to the lease, so its cases can only be carried honestly oncebatten landexists. Writing thoseconservesarms now would claim conservation for behaviour deliberately moved, which is the launderingshell-retirementrefuses.D4 —
batten landitself — is untouched.land.shis 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:cargo— 3934 passed, 0 failed.mise run verify—fast-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.regoand.serena/memories/core.mdare protected paths, and each write was separately admitted and spent.🤖 Generated with Claude Code
https://claude.ai/code/session_014rncbrKRtMp7DmBu4PTLVw