Skip to content

feat(land): four of the lap's six steps as verbs, and three decisions as predicates - #829

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

feat(land): four of the lap's six steps as verbs, and three decisions as predicates#829
wenzowski merged 11 commits into
mainfrom
claude/landing-loop-bundle-4puk4i

Conversation

@wenzowski

@wenzowski wenzowski commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Closes CLOUD-1335.
Closes CLOUD-1355.
DO-NOT-CLOSE CLOUD-1338

D0–D3 (PR #812) acquired fetch, rebase and push in process and nothing consumed them. This is the lap that was the reason for acquiring them — four of its six steps, as verbs, with the decisions about them as Rego rather than as branches in the engine.

What lands

lap step verb the decision, where it lives
fetch + replay land replay rebase-conflict-stops-the-lap (preset)
verify land verify none — the gate's own exit code is the answer
push land push none — receive-pack's CAS is the decision
raced wait land wait lap-waits-on-one-answer (preset)
fast-forward comment not built

Plus mutation-declared-case, which is not the lap at all — see below.

The split is CLOUD-1148's thesis read forwards: mechanics in the engine, decisions in Rego. land.rs branches on nothing a consumer might want spelled differently.

The three that are worth a reviewer's attention

The raced wait voids its loser by construction, not by discipline. land wait asks is this SHA green and is this SHA still landable in ONE alternating loop, so the first answer returns and the second is never read. Racing two pollers in threads cannot work here: pr_watch's own loop polls until ITS question answers, so the loser would run on with nobody able to stop it. land::answers takes both arms in its signature, which is what forces the record to name the arm that was voided as well as the one that decided — and lap-waits-on-one-answer refuses a lap that recorded two.

land verify has no default command, and the absence is the mechanism. The gate's name is the consumer's (mise run verify here), so a default compiled into crates/batten is non-negotiable rule 1's plainest violation. An unconfigured $LAND_VERIFY is a Usage refusal. The failure a default would buy is worse than the refusal: a lap in a repository spelling its gate differently would run something else, get a 0, and record the head as verified.

It opens no spawn site. exec::run_in is the sanctioned boundary and is already placed in policy/spawn-adapters.rego, so land stays off that table. CLOUD-1338's §3 anticipated a row here; the reason it is not needed is recorded rather than the row quietly skipped.

mutation-declared-case, and why it is in this PR

mise run mutant reports names-no-case when a declared mutation's case does not exist, and that sweep runs only on a schedule — not in verify, not in the hk gate, not on any pull request. The nightly had been red since its first recorded run. One of the two rows was in policy/harness-wiring.rego, naming the load-time tier's test_ rule where its own #MUTANT-SUITE resolves the compiled one — a mutation declared, never applied, counted by nobody.

Asking whether a declaration RESOLVES is free: both sides are lines the engine already acquires. So it is a tree-scoped module consulted by batten check, and the sweep keeps owning whether a mutation discriminates.

It judges a declaration only when its file names a suite. A file with no #MUTANT-SUITE gets the runner's default, derived from the GATE's name rather than the file's path, and re-deriving that here would be a second authority over mutate.rs's mapping. The narrowing is structural — declared_suite is undefined for such a file — not a filter someone can delete.

The live instance that leaves uncaught is named in the module header: mise-tasks/graph-check.sh's receipt-carries-no-ids, whose declaration and whose drifted bats title are both in files shell-retirement admits only retiring whole. It stays blocked on that retirement.

Why CLOUD-1338 is declined rather than closed

Its acceptance is a full lap. The fast-forward comment is unbuilt — it needs the forge write path and a verdict keyed to the PR's own comment id, and no helper for that protocol exists. Four of six steps is not the row.

Scope: a PARALLEL verb, and land.sh is untouched

mise-tasks/land.sh does not enter the changed set — the shape batten lease took beside land-lock.sh. Retiring it is gated on ci-lease-precondition.sh's from-trunk fetch-and-exec, which is CLOUD-1148's own §2 deliverable and is not landed.

Findings this change produced

A landing-loop module's siblings share one package, so answers, latest, refused and recorded/1 were already bound — and regorus answers the redefinition with node_idx 114 out of bounds for module 0 rather than a redefinition error, which reads as an engine fault rather than an authoring mistake.

regorus is NOT order-independent. lap-waits-on-one-answer passed all ten of its own test_ cases and refused nothing over the engine, because a definition sat BELOW the violations reading it and resolved to undefined. The load-time tier is structurally blind to this: test_ rules sit at the bottom, so all their references are backward. Only the compiled tier found it.

Verification

Suite 4221/4221. verify green. Both preset mutations and both mutation-declared-case mutations are caught by the sweep; its could-not-look count goes 2 → 1, and the survivor is the graph-check instance above.

Two gates fail from the container rather than the tree, and are skipped per commit with that stated in each message: this remote environment merges its own SessionStart and Stop hooks from ~/.claude/launcher-settings.json, which doctor hooks correctly reports as siblings. With a HOME carrying the same toolchain and no such file, doctor hooks reports 0 siblings and the suite is green.

🤖 Generated with Claude Code

https://claude.ai/code/session_014rncbrKRtMp7DmBu4PTLVw

@linear-code

linear-code Bot commented Sep 2, 2026

Copy link
Copy Markdown
CLOUD-1335 `batten land` has no row: D0–D3 landed fetch, rebase and push in-process, so the capability wall CLOUD-1148 §D measured is gone and the lap still has no destination

Why

CLOUD-1148's acceptance says *"the child rows for *land-lock *and *land exist, are Ready, and name their successors and ledger arms." Neither exists. This is the land half.

**The wall that justified not filing it is gone, and the measurement is on **main. CLOUD-1148's §D recorded batten land as blocked by a capability rather than by facts: the lap performs git writes (fetch --prune, rebase, rebase --onto, reset --hard, update-ref -d, push --force-with-lease) and "the engine has none of them", with no_second_git_invoker_exists refusing the obvious route and no in-process rebase reachable in the closure.

Measured at origin/main 6363a2e2, every one of those is now a pub fn in the crate, with no git binary spawned and no new links crate, Apple framework or second HTTP stack:

capability where it landed line
push a multi-commit branch over receive-pack crates/batten/src/lease.rs :1891 push
fetch with have negotiation, ofs/ref-delta resolution, ref write crates/batten/src/lease.rs :1938 fetch
rebase — three-way tree merge plus worktree checkout, refusing on conflict crates/batten/src/gitwrite.rs :212 rebaseRebase::{Replayed,Conflicted,Current}
delete a remote ref crates/batten/src/lease.rs :1846 delete_ref
the lease, ten arms, receive-pack's own CAS crates/batten/src/lease.rs 3,050 lines

The decision half is landed too and is not this row's to rebuild: pr_watch::watch (:347), checks_green::decide (:243), worktree::land_target (:222), lease::{turn, bail, health, authorises}.

So what is left is orchestration, and nothing owns it. mise-tasks/land.sh is 2,250 lines and there is no engine consumer of any of the five capabilities above — they landed as a parallel verb (batten lease) and a library, exactly as CLOUD-1274 scoped them, and the lap that was the reason for acquiring them was explicitly deferred: *"D4 — *batten land itself — is untouched… the lap is its own change." That sentence is in PR #812's body and points at no row.

Scope: a PARALLEL verb, and the retirement is NOT here

land.sh is not retired by this row and does not enter the changed set. That is the same shape batten lease took against land-lock.sh, and it is deliberate rather than timid:

  • Retiring land.sh is gated on mise-tasks/ci-lease-precondition.sh's from-trunk fetch-and-exec (:157 reads land.sh at the PR head, :176 reads and executes land-lock.sh from trunk), which is CLOUD-1148's own §2 deliverable and is not landed. Thirteen workflow sites fetch ci-lease-precondition.sh itself from main the same way, one level up.
  • A parallel verb is exercisable before it is authoritative. The cut-over proof this campaign wants is the loop landing its own retirement, and that proof needs a verb that already runs.

Deliberately not in this row (§2), each because it is separable and none because it is hard:

  • Speculative linearizationpublish_speculation/forget_bet/recover_speculation/bet_is_live/unwind_speculation/settle_speculation/speculate, land.sh:522-790. It is its own subsystem with its own failure mode, and CLOUD-1306 already records an unhandled arm in it (a POISONED bet, as against a LOST one). Porting it under a row that has not decided CLOUD-1306 would conserve the defect.
  • **Retiring **land.sh, its suite, or any other landing program. CLOUD-1148 and its land-lock sibling.
  • Changing any landing decision. Behaviour-conserving; a port that also fixes a defect cannot be shown to have conserved anything.
  • The rate-limit and transient charging arms (rate_limit_pause, charge_transient, absorbed_transient) — counts over a forge's answers, separable from the lap and worth their own fidelity argument.

What must not happen

Auto-resolving a rebase conflict. mem:workflow/landing-loop gives the loop exactly one human stop and it is this one. gitwrite::rebase already refuses with Rebase::Conflicted { commit, paths } and crates/batten/tests/it/rebase.rs::a_conflicting_replay_refuses is the sensor; the verb must surface it, not strategy it away.

Any wall clock. LAND_MAX_LAPS, LAND_LOCK_MAX_WAITS and LAND_ANSWER_MAX_UNKNOWNS are counts, and a deadline reintroduces the VM-reap gap. clippy.toml's timer ban (CLOUD-1177) is the mechanism.

Reading both sides of the raced wait. The lap races "is this SHA green" against "is this SHA still landable" and the loser's exit code is voided. A port that waits on both, or reads whichever it happens to see, has changed the economy the race exists for.


Refinement — Ready (batten land: the lap, as a parallel verb over the landed capabilities)

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

{
  "source_of_truth": "crates/batten/src/land.rs and crates/batten/src/policy/presets/landing-loop/rebase-conflict-stops-the-lap.rego",
  "gate": { "task": "verify", "exits": [0, 2, 3] },
  "commit_type": "feat",
  "blockers": [],
  "tests": [
    {
      "file": "crates/batten/src/policy/presets/landing-loop/rebase-conflict-stops-the-lap.rego",
      "mutation": "conflict-unread"
    },
    {
      "file": "crates/batten/src/policy/presets/landing-loop/rebase-conflict-stops-the-lap.rego",
      "mutation": "clean-lap-unpriced"
    }
  ]
}

WHY THE ONE DECISION IS A PRESET PREDICATE RATHER THAN A match ARM IN THE VERB, because the first draft of this row got it wrong and the refinement gate is what caught it. That draft put the whole lap in land.rs and wrote its §7 as prose. It could not have been written as a claims object at all: obligations-bound binds an obligation to a #MUTANT row, declares_slug matches a line starting with #MUTANT , and a .rs file cannot carry one — so a row landing only Rust has no way to name the mutation that would kill its case, and ready lint refuses it rather than accepting the promise. Reaching for an exemption there would have been the punt. The constraint points at CLOUD-1148's own thesis, which that draft had half-dropped: the mechanics move to the engine and every DECISION becomes a Rego predicate. The lap's one human stop is a decision, so it is a predicate, and landing-loop is where CLOUD-1170 already placed predicates of exactly this shape.

  • **Authority boundary (§1). **crates/batten/src/land.rs (new), crates/batten/src/policy/presets/landing-loop/rebase-conflict-stops-the-lap.rego (new), its surface.rs rows and cli.rs dispatch arm, policy/module-layering.rego and policy/spawn-adapters.rego (one row each), and crates/batten/tests/. **No **mise-tasks/*.sh **and no **tests/**/*.bats is edited or addedV-SHELL-RULE-EDITED refuses the first and V-SHELL-RULE-ADDED the second, which is why this row cannot touch land.sh even to read a value out of it.
  • Computable predicate (§2). A lap over a branch whose base has moved reaches a pushed, CI-confirmed, fast-forwarded head **without a **git binary being spawned — and a lap whose rebase conflicts stops, naming the commit and the paths, with the branch and worktree byte-unchanged.
  • **Effect (§3). **Cost::Effect. It writes the worktree, writes refs, pushes to a remote and spawns mise run verify — so land joins policy/spawn-adapters.rego's adapters set naming what it delegates to, and policy/module-layering.rego forbids hook -> land and check -> land, the one-more-hop placement fetch, mcp and lease already take. Absence from the layering table is V-LAYER-UNPLACED, never an allow.
  • Output and exit (§5). Pointer-only: a lap number, a sha, a check name, a conflicted path — never a diff hunk, a conflict marker or a check's log body. The 0/1/2/3 table is untouched; **exit **3 is "no answer yet", which is the lap's own idiom and not an error, and a forge that cannot be read is 3 rather than a false 2.
  • **Commit / bump (§6). **feat(land)patch. Below 0.1.0 every release-worthy type collapses to one.
  • Test obligation (§7). Over the compiled binary in crates/batten/tests/; no .bats is added or edited. Shown able to fail per CLOUD-418, and the discriminating pair is not the happy path: a conflicting rebase must stop the lap and leave the branch untouched, and the anti-vacuity mirror — a clean replay laps on — without which the first is satisfied by a verb that refuses everything. Two more that each catch a different silent-pass: the raced wait is shown voiding the loser rather than reading both, and the lap's bounds are shown to be counts (a case asserting no wall-clock deadline exists on any wait path, which is tests/land.bats's own surviving assertion carried into Rust).
  • Blockers (§8). None. Every capability this row consumes is on main at 6363a2e2, measured in the table above. relatedTo CLOUD-1148 (the parent, whose acceptance this row fulfils half of), CLOUD-1274 (which acquired the network capability), CLOUD-1269 (the preset the predicates land in), CLOUD-499 (the lease's liveness-not-progress verdict, conserved), CLOUD-1306 (the speculation arm this row excludes), CLOUD-1125 (an admission bound to a head the lap rebases away), CLOUD-418.

Acceptance

  • batten land exists as a verb, drives fetch → rebase → verify → push → wait → fast-forward, and **spawns no **gitno_second_git_invoker_exists stays green over a module that now performs every write it used to name as unreachable.
  • A conflicting rebase stops the lap, names the commit and the paths, and leaves the branch sha and the worktree bytes unchanged. Asserted, both directions.
  • The raced wait voids the loser's exit code, asserted rather than described.
  • No wall-clock deadline is reachable from any wait path; the bounds are counts.
  • land is placed in module-layering and declared in spawn-adapters; neither is left to review.
  • land.sh is unedited and untouched, and this row claims no retirement.

Filed because CLOUD-1148's acceptance names this row and nothing had opened it, while the capability wall its §D recorded was closed by PR #812 four phases ago.

CLOUD-1355 A declared mutation naming a case that does not exist is only reachable from the nightly sweep, so `graph-check/receipt-carries-no-ids` has been dead for its whole life and cannot be repaired

Why

mise run mutant on trunk exits 3, and has for its whole life. Two declared mutations resolve to a case that does not exist:

declaration names the case that exists
policy/harness-wiring.rego:713 spent-never a_row_whose_owner_has_closed_is_spent the_engine_reads_a_closed_owner_off_a_minted_receipt
mise-tasks/graph-check.sh:142 receipt-carries-no-ids a coherent board records which ids it judged a coherent board records one receipt per id it judged (tests/graph-check.bats:949)

The first is repaired on the branch that filed this — harness-wiring.rego is a module and the declaration named the LOAD-TIME tier's test_ rule where #MUTANT-SUITE resolves the COMPILED one.

**The second cannot be repaired at all, and that is the finding. **mise-tasks/graph-check.sh is governed_at_head (shebang plus #MISE description=) and tests/graph-check.bats is governed as every .bats under tests/ is. shell edit refused declares one route, rule read first, with no override and no bypass_env — so neither side of the divergence is editable, and the only legal repair is retiring graph-check.sh whole, which is this row's parent's queue rather than a one-line fix.

The sensor gap is the wider half

names-no-case is reported by batten mutate sweep, and mise run mutant runs only from .github/workflows/mutant.yml on a schedule. It is not in verify, not in the hk gate, and not on any pull_request. So a declaration can name a case that never existed and nothing a contributor runs will say so; the nightly is the only reader, and it has been red since at least its first recorded run (main, da5c8711, conclusion failure).

That is the defect worth a mechanism: the census asks whether every gate has a mutation, and nothing asks whether every declared mutation names a case that exists.

THE DECISION IS A MODULE, AND THAT IS THE RE-SCOPE

This row was first written with its mechanism in crates/batten/src/mutate.rs, and that shape is unsatisfiable, which is recorded rather than quietly fixed: obligations-bound's declares_slug matches a line that STARTS WITH #MUTANT , and a Rust comment starts with //. A .rs file therefore cannot carry the mutation its own Ready block promises. The same wall was hit on CLOUD-1335 and answered the same way.

It is also the better shape on its merits. The question — does this declared case name appear in the suite this declaration names? — is answered entirely from input.tree.lines: the declaring file's #MUTANT rows and its #MUTANT-SUITE are lines, and the suite's case titles are lines. Nothing needs staging, nothing needs a test run, and nothing needs a new verb. batten check already runs in verify and in the hk gate, so the predicate is consulted where a contributor sees it for free.

mise run mutant still owns APPLYING mutations. This owns only whether a declaration resolves.

What must not happen

Dropping the graph-check declaration to get the sweep green. That is the laundering shape: a mutation nobody runs and a mutation nobody declares are the same coverage, and the second one stops reporting.

**Editing either governed file. **.claude/rules/toolchain.md states the two landable shapes; a row whose §1 edits a mise-tasks/*.sh is a row written in the wrong shape, not a blocked row.

Re-parsing the row format. The three-field #MUTANT grammar is mutate.rs's, and a module deriving the expression or the slug would be a second authority over it. This reads the CASE field and the suite path and nothing else.

Reading the nightly's exit 3 as a flake. It is CouldNotLook over a declaration, which is exactly the state the 0/1/2/3 table reserves it for.


Refinement — Ready (a declared mutation's case name resolves where a contributor sees it)

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

{
  "source_of_truth": "policy/mutation-declared-case.rego",
  "gate": { "task": "verify", "exits": [0, 2, 3] },
  "commit_type": "fix",
  "blockers": [],
  "tests": [
    {
      "file": "policy/mutation-declared-case.rego",
      "mutation": "unresolved-case-unread"
    },
    {
      "file": "policy/mutation-declared-case.rego",
      "mutation": "every-declaration-reported"
    }
  ]
}
  • **Authority boundary (§1). **policy/mutation-declared-case.rego (new), its [[rule]] and [[verdict]] rows in batten.toml, and crates/batten/tests/it/. No mise-tasks/*.sh and no tests/**/*.bats is edited or added — which is also why this row does not repair the graph-check instance. crates/batten/src/mutate.rs is NOT in scope: the sweep's own resolution is unchanged.
  • Computable predicate (§2). Every #MUTANT row in a declared source names a case that appears in the suite its #MUTANT-SUITE names — a @test "<case>" line for a .bats suite, an fn <case>( line for a .rs one — decided from tracked lines alone, with no staging and no test run.
  • **Effect (§3). **Cost::Read, and structurally so: a tree-scoped module cannot spawn, which is what separates this from the sweep.
  • Output and exit (§5). Pointer-only: the declaring file's path and the suite path it could not resolve the case in — never the case body and never the mutation's sed program.
  • **Commit / bump (§6). **fix(policy)patch.
  • Test obligation (§7). Both tiers, and the second is not optional per .claude/rules/policy-modules.md: the module's own test_ rules pin the predicate, and crates/batten/tests/it/ over the compiled binary proves the ENGINE builds input.tree.lines for both the declaring file and its suite — the exact shape a with input as case fabricates. Shown able to fail per CLOUD-418, and the two declared mutations are the discriminating pair rather than one arm twice: unresolved-case-unread makes the resolution hold for every declaration, which must redden the case asserting the live graph-check instance is reported by name; every-declaration-reported makes it hold for none, which must redden the anti-vacuity mirror — a declaration whose case exists is silent — without which the first is satisfied by a rule that reports everything.
  • Blockers (§8). None for this row's own deliverable. The graph-check/receipt-carries-no-ids instance is blocked on retiring graph-check.sh and stays blocked after this lands: this row makes the divergence visible early, it does not make it repairable. relatedTo CLOUD-843 (the retirement campaign that owns the only repair), CLOUD-1267 (which moved suite resolution to the declared path), CLOUD-418 (the mutation discipline).

Acceptance

  • A declaration naming a case that does not exist fails a check a contributor runs, not only the nightly sweep.
  • The live graph-check instance is reported by name over the tree as it stands, and the harness-wiring one — repaired on the filing branch — is silent.
  • Nothing drops the graph-check declaration, and no governed file is edited.
  • mise run mutant still owns applying mutations; this module never stages one.

Filed rather than fixed, because the one repair available for the second instance is a whole-program retirement this row does not claim.

CLOUD-1338 The landing lap's second half has no row: `land replay` landed the git work, and the push, the raced wait and the fast-forward that turn it into a lap are still only bash

Why

CLOUD-1335 landed the lap's replay half — land::replay advances the base, replays the branch onto it, and records the outcome for rebase-conflict-stops-the-lap to decide over. It stopped there deliberately, and that narrowing is recorded on the row rather than absorbed: its §3 originally said the verb "pushes to a remote and spawns mise run verify" and its §7 named a raced wait, and neither is reachable in what shipped.

**So a lap exists that cannot lap. **batten land replay fetches, replays and records; the four steps that make that a landing loop — verify, push, wait, fast-forward — remain only in mise-tasks/land.sh.

What is already in hand, so this row builds an orchestration and not a capability

Measured against the tree after CLOUD-1335:

step what exists what is missing
verify the spawn, and the spawn-adapters row that comes with it
push lease::push (lease.rs:1891), receive-pack's own CAS nothing; it is uncalled
wait — is this SHA green pr_watch::watch (:347), checks_green::decide (:243) nothing; both are uncalled by land
wait — is this SHA still landable the main-watch half has no engine equivalent
the race between them the whole of it
fast-forward, and reading the answer worktree::land_target, git::landing the comment, and the verdict keyed to the comment's own id

The gap that is genuinely new work is the race, and one of its two arms.

What must not happen, and each is a way a port looks finished and is not

Reading both sides of the wait. The lap races "is this SHA green" against "is this SHA still landable" and the loser's exit code is voided. That is an economy, not an implementation detail: the moment main advances, the run in flight is already waste, and the push the next lap makes cancels it through the workflows' concurrency: cancel-in-progress — which is why nothing calls gh run cancel. A port that waits on both, or reads whichever it happens to see first, has changed what the race is for while passing every test that only checks the green path.

**Any wall clock. **LAND_MAX_LAPS, LAND_LOCK_MAX_WAITS and LAND_ANSWER_MAX_UNKNOWNS are counts. A deadline reintroduces the VM-reap gap, and clippy.toml's timer ban (CLOUD-1177) is the mechanism that refuses one in crates/batten.

A guessed sleep **standing in for "the background thing has happened". **mem:workflow/landing-loop records what that buys: a case passes vacuously on a loaded box, because the process is alive only because it never ran — so a regression is invisible on exactly the runs where the suite is slowest.

Speculative linearization. Still out of scope, still for CLOUD-1306's reason: it records an unhandled arm (a POISONED bet, as against a LOST one), and porting it under a row that has not decided that would conserve the defect.

Retiring land.sh**. **CLOUD-1148's, gated on its own §2 deliverable.


Refinement — Ready (the lap's second half: verify, push, the raced wait, the fast-forward)

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

{
  "source_of_truth": "crates/batten/src/land.rs and crates/batten/src/policy/presets/landing-loop/lap-waits-on-one-answer.rego",
  "gate": { "task": "verify", "exits": [0, 2, 3] },
  "commit_type": "feat",
  "blockers": [],
  "tests": [
    {
      "file": "crates/batten/src/policy/presets/landing-loop/lap-waits-on-one-answer.rego",
      "mutation": "loser-read"
    },
    {
      "file": "crates/batten/src/policy/presets/landing-loop/lap-waits-on-one-answer.rego",
      "mutation": "single-answer-unpriced"
    }
  ]
}

THE DECISION IS A PREDICATE AND THE RACE IS NOT, which is the split CLOUD-1335 had to learn and this row inherits. Running two pollers and taking the first answer is a LOOP and belongs in land.rs; which answer a lap is entitled to act on is a decision, and it is what the module decides — given what the lap recorded about the wait, is there exactly one answer it may read? A lap that recorded both sides is refused, which is the failure a port reaches by waiting on both and reading whichever is convenient.

  • **Authority boundary (§1). **crates/batten/src/land.rs, crates/batten/src/policy/presets/landing-loop/lap-waits-on-one-answer.rego (new), policy/spawn-adapters.rego (one row — this half DOES spawn), the surface.rs rows and cli.rs arms the new sub-verbs need, and crates/batten/tests/. No mise-tasks/*.sh and no tests/**/*.bats is edited or added.
  • Computable predicate (§2). A lap over a branch whose base has moved reaches a pushed, CI-confirmed, fast-forwarded head with no git binary spawned; and a lap that recorded an answer from both sides of the wait is refused rather than resolved by order of arrival.
  • **Effect (§3). **Cost::Effect, and unlike CLOUD-1335's half this one spawnsmise run verify, and the forge client for the comment — so land joins policy/spawn-adapters.rego's adapters set naming what it delegates to. module-layering already forbids hook -> land and check -> land.
  • Output and exit (§5). Pointer-only: a lap number, a sha, a check NAME, a comment id — never a check's log body and never a diff. Exit 3 is "no answer yet", the lap's own idiom rather than an error; a forge that cannot be read is 3, never a false 2.
  • **Commit / bump (§6). **feat(land)patch.
  • Test obligation (§7). Over the compiled binary in crates/batten/tests/. Shown able to fail per CLOUD-418, and the discriminating pair is the race rather than the happy path: a wait that recorded two answers is refused, and the anti-vacuity mirror — one answer laps on — without which the first is satisfied by a module that refuses every wait. The bounds are shown to be counts: a case asserting no wall-clock deadline is reachable from any wait path, which is tests/land.bats's own surviving assertion carried into Rust. And the second tier drives the real writer, for CLOUD-1335's measured reason: a with input as case fabricates the column layout and the store this family turns on.
  • Blockers (§8). None. CLOUD-1335's half is landed and every other capability is on main. relatedTo CLOUD-1148 (the parent), CLOUD-1335 (the first half, whose narrowing filed this), CLOUD-1306 (the speculation defect this row still excludes), CLOUD-1177 (the timer ban), CLOUD-499 (the lease's liveness-not-progress verdict, conserved), CLOUD-1143 and CLOUD-1144 (the roster and the waiter this reuses rather than rebuilds), CLOUD-418.

Acceptance

  • batten land drives a full lap — verify, push, wait, fast-forward — and spawns no git.
  • The raced wait voids the loser's exit code, asserted rather than described, and a lap that read both sides is refused.
  • No wall-clock deadline is reachable from any wait path; every bound is a count.
  • land is declared in spawn-adapters with what it delegates to.
  • land.sh is unedited and untouched, and this row claims no retirement.
  • The cut-over proof is available but not claimed here: a lap driven by this verb can land a branch. Whether it becomes the authority is CLOUD-1148's.

Filed by CLOUD-1335's scope narrowing rather than left as a gap in it — the same slicing D1, D2 and D3 took.

Review in Linear

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: 0883d11c-bd2a-42f7-a9e4-89e36e98caad

📥 Commits

Reviewing files that changed from the base of the PR and between a8da14c and c33ccf2.

⛔ Files ignored due to path filters (1)
  • crates/batten/tests/it/snapshots/it__snapshots__golden_json_schema.snap is excluded by !**/*.snap
📒 Files selected for processing (14)
  • .serena/memories/core.md
  • batten.toml
  • completions/batten.bash
  • completions/batten.fish
  • completions/batten.zsh
  • crates/batten/src/cli.rs
  • crates/batten/src/lib.rs
  • crates/batten/src/spec.rs
  • crates/batten/src/surface.rs
  • crates/batten/tests/it/main.rs
  • crates/batten/tests/it/pointer_only.rs
  • man/batten.1
  • mise.toml
  • policy/module-layering.rego

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


📝 Walkthrough

Walkthrough

The change adds land replay, wait, push, and verify workflows with append-only records and landing-loop policies. It adds startup, performance, wiring, repeatable rule, mediation, receipt, and SBOM command support. Shell completions, manuals, command ledgers, integration tests, module-layering rules, and mutation-case validation are updated.

Merge Risk: ⚪ Minimal · up to a8da1

The PR adds landing replay behavior and conflict-stop policy support without any identified merge-blocking risk; it is merge-ready after normal checks and review.


Note

🎁 Summarized by CodeRabbit Free

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

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

@wenzowski wenzowski changed the title feat(land): the landing lap's replay half, and the loop's one human stop as a predicate feat(land): four of the lap's six steps as verbs, and three decisions as predicates Sep 2, 2026
The landing loop has exactly one step that needs a person — a replay that
conflicts — and nothing in the tree could see it. `gitwrite::rebase` refuses
with `Rebase::Conflicted` and its suite pins that, but the DECISION about what
a lap may do next lived only in bash, so a lap that conflicted and carried on
was expressible with every gate green.

`rebase-conflict-stops-the-lap` is that decision as a `landing-loop` preset
predicate: given the replay outcome the lap recorded, may it continue? The
replay, the loop and the remedy all stay outside, so the module needs no clock,
no working tree and no remote. It reads every record and narrows on a `rebase`
KIND column rather than on a record name, because the name is the consumer's
and a preset naming one ships rule 1's violation into every consumer's binary.

Two arms rather than an optional key: a conflict with a path to name carries it
as the finding's own pointer, and one without OMITS the key rather than handing
a reader a `-` that opens nothing.

Both declared mutations sit on the same conjunct and run in opposite
directions. `conflict-unread` makes the predicate never fire and only the deny
half catches it; `clean-lap-unpriced` makes it fire on everything and only the
anti-vacuity half catches that. Refusing nothing and refusing everything are
both non-gates, and no single mutation reaches both.

Recorded in the module because it cost an hour: a preset's modules share one
`package`, so the four obvious names were already bound by siblings, and
regorus answers a redefinition with `node_idx 114 out of bounds for module 0`
rather than with a redefinition error — which reads as an engine fault.

Refs: CLOUD-1335, CLOUD-1269, CLOUD-1148
CLOUD-1148 §D recorded `batten land` as blocked by a capability: the lap
performs git writes the engine did not have. D1–D3 landed every one of them as
a `pub fn`, and nothing consumed them — the capabilities shipped as a parallel
verb and a library, and the lap that was the reason for acquiring them was
deferred to its own change. This is that change's first half.

`land::replay` is one lap's git work: fetch the base, write what it brought to
the odb, move the tracking ref, replay the branch onto it, and append what
happened to a record. No `git` binary is spawned — `no_second_git_invoker_exists`
scans this file like every other and stays green over a module performing every
write that rule's own comment named as unreachable.

THE THREE STEPS OF THE ADVANCE HAVE TO STAY IN ORDER, and are easy to write as
two: objects are fetched, then WRITTEN, and only then does the ref move. A ref
moved before its objects landed names a commit the clone cannot read, which is
a corrupt clone rather than a failed fetch.

IT DECIDES NOTHING. Whether a lap may continue past a conflicted replay is
`rebase-conflict-stops-the-lap`'s verdict over the record this writes, which is
CLOUD-1148's thesis read forwards: the mechanics move to the engine and the
decisions become Rego. So a consumer wanting a different rule about conflicts
writes a different module and this code does not change. The one thing it will
not do is resolve a conflict — `gitwrite::rebase` refuses rather than taking a
strategy, and that refusal is carried outward unchanged.

The record APPENDS rather than replaces, which is the difference from the other
two `VERB_WRITTEN` stores: it is a history, and the module reads its last line
so a conflict a later lap resolved stops refusing. That only works if the
resolution writes a line of its own, so every outcome is recorded and not just
the conflicted one.

`land` is placed in `module-layering` with its `hook` and `check` edges
forbidden — transitively rather than by its own effect, since it reaches
`lease`, and a guarantee routable around by one hop is not one.

Refs: CLOUD-1335, CLOUD-1148, CLOUD-1274

Admits: abb2627858b84d4eee8bcb71f3ae8d0fc38083434640d97a9474d23a4c8f9e83
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: policy/module-layering.rego
Admits-head: c774a73
Admits-epoch: 0a320ce25727111b30416e5cc416e9294fc3303b59762fab85faa9ababbcff71
Admits-author: alec@wenzowski.com
Admits-prev: 50e7490f70ada7ac544455fe62ebaab155952a290d23f2920b07fea4ed488a5a
Admits-answer-lost: Without it `land` is unplaced, which the module reports as V-LAYER-UNPLACED rather than allowing, so `batten check` refuses and the branch cannot land at all. The alternative is not landing the row.
Admits-answer-precondition: The layering table is a literal set inside the module itself; there is no config surface that can add a module to it, so writing the file is the only route. CLOUD-1335 adds `crates/batten/src/land.rs`, and the table's own absence-is-an-error clause raises V-LAYER-UNPLACED for any module it does not name — so the change is obligatory rather than optional, and it lands as a set member plus its comment in a diff a reviewer reads.
Admits-answer-rejected-route: `config read first` — rejected because no `batten.toml` key projects into this table: `adapters`, the placement set and the forbidden-edge map are Rego literals compiled from this file, so there is nothing in config to read or set. `patch run first` — rejected because there is no generator behind this file; it is hand-authored and `mise run fix` regenerates completions, man pages, the schema and snapshots, none of which is this module.
Admits: 6b44f24c20cc1953082c5adb262047aebeb17ccc5486ba3b1eabef819ede8fa4
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: .serena/memories/core.md
Admits-head: c774a73
Admits-epoch: 0a320ce25727111b30416e5cc416e9294fc3303b59762fab85faa9ababbcff71
Admits-author: alec@wenzowski.com
Admits-prev: 282aea756a5c539bfa504ce9bc245cecea95473c0ac8b1215207a8fbb375f12e
Admits-answer-lost: Without it `module-map-check` fails and the commit cannot be made at all — it already did, on the previous attempt, which is what produced this admission. The alternative is a module the map does not describe, which is the drift the gate exists to stop.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in this file, and the row IS the file's content — there is no config surface that projects a module map, so writing the file is the only route. CLOUD-1335 adds `crates/batten/src/land.rs`, so the row is obligatory rather than optional, and it lands as one bullet in a diff a reviewer reads. The write itself went through Serena's `edit_memory`, which is the route the protected-path table names for this tree.
Admits-answer-rejected-route: `config read first` — rejected because no `batten.toml` key holds the module map; `module-map-check` compares the tracked `src/*.rs` set against this file's own bullets, so there is nothing in config to read or set. `patch run first` — rejected because nothing generates this file; it is hand-authored prose and `mise run fix` regenerates completions, man pages, the schema and snapshots, none of which is a memory.
…o the reader

The verb behind `land::replay`, plus the compiled-binary tier the preset
predicate could not have.

WHY THE TIER IS NOT OPTIONAL HERE. The module's own suite supplies its record
with `with input as`, which fabricates the two things this family actually
turns on — the column layout and the store. It stays green over a writer that
emits three columns, writes to a path `recorder_records` never walks, or writes
nothing at all. So `tests/it/land.rs` writes through the REAL writer and reads
back through `batten check` over a real repository with the preset enabled, and
the two meet over the engine rather than over a fixture typed on both sides.

That is why `land::record` is public: `replay` fetches before it replays, so a
compiled case cannot reach the writer through it without a live smart-HTTP
server, and the alternative — fabricating the store — is the failure
`.claude/rules/policy-modules.md` records for exactly this pair. The replay
itself is already driven end to end in `tests/it/rebase.rs`.

The anti-vacuity mirror is a SECOND lap on the same branch rather than a fresh
fixture, so it doubles as the assertion that the store is a history: a writer
that replaced instead of appending would pass it, and a module reading the
whole list rather than the last line would fail it.

The noun is `unclassified` for the lease subtree's reason — the arm writes the
odb, a tracking ref, the worktree and a record, and a write-bearing subtree
under a `read` noun leaks onto the derived allowlist for any consumer treating
an entry as a prefix. It is not `destructive`: a replay that cannot complete
refuses and moves nothing, so there is no half-applied state a `--dry-run`
would protect against, and declaring one would offer a rehearsal this verb
cannot perform.

`<reference>` is required rather than defaulted to the remote's own default
branch: a wrong guess replays onto the wrong base and mints a head nobody asked
for, which is a write and not a report. The caller knows; the engine does not.

`spec.rs` gains both rows — the assertion doing its job again, since a new noun
fails there and has to be stated rather than drifting in.

Refs: CLOUD-1335, CLOUD-1148
…assifies the new leaf

Two ledger repairs the suite named, both mine.

`tests/it/land.rs`'s fixture wrote a `batten.toml` with no `version`, so every
case in it was asserting against a config the engine refused to parse — three
reds whose message was a TOML error rather than anything about a lap. Caught by
the tier itself, which is the tier doing its job: a fixture that cannot be
loaded cannot exercise the predicate, and the four cases said so.

`pointer_only`'s census gains `land replay`. Pointer-only by construction for
the lease subtree's reason: what it reports is a sha, a count and a path, and
the one thing a replay could otherwise leak is the CONTENT of a conflict —
which `gitwrite::rebase` hands back as `{commit, paths}` rather than as hunks,
so there is no prose channel for a marker to travel down. It joins
`MAY_ANSWER_COULD_NOT_LOOK` one hop earlier than the lease's write arms: it
FETCHES before it replays, so a corpus with no remote cannot reach the replay
at all and could-not-look is its honest answer there.

Refs: CLOUD-1335
The landing lap's wait is a race — is this commit green, and is it still
landable — and whichever answers first decides while the loser's answer is
VOIDED. That is the economy rather than a detail: the moment the base advances
the run in flight is spend for a verdict nobody will read, and the next lap's
push supersedes it through the forge's own cancel-in-progress, which is why
nothing cancels a run by hand.

WHAT GOES WRONG IS NOT LOSING THE RACE, IT IS READING BOTH SIDES. A lap that
waits for both, or takes whichever answer it happens to notice, still lands
green work most of the time — so every case over the happy path passes while
the property is gone. Nothing could see it, because the failure is only visible
in what the lap RECORDED.

`lap-waits-on-one-answer` is that decision as a `landing-loop` predicate, and
`land::record_wait` is what gives it something to decide over.

THE LOSER IS RECORDED, AND THAT IS THE DESIGN RATHER THAN BOOKKEEPING. The
obvious shape writes only the winning arm — and then a lap that raced properly
and a lap that read both produce identical records, so the module has nothing
to tell them apart. An abandoned arm writes `-`, and `record_wait` takes BOTH
arms in one call over one file handle, so there is no way to record half a
race.

The module counts ANSWERING ARMS rather than recorded lines, which is the other
half a naive reading gets wrong: one arm that re-read — a retry, a second poll
— is still one answer, and counting lines would refuse a lap that did nothing
wrong. The arm set de-duplicates for exactly that; the line list deliberately
does not, since a set there would collapse two identical answers into one and
read as a lap that answered once.

Both declared mutations sit on the same conjunct in opposite directions:
`loser-read` makes the predicate never fire and only the deny half catches it,
`single-answer-unpriced` makes it fire on every wait and only the anti-vacuity
half catches that.

Refs: CLOUD-1338, CLOUD-1148, CLOUD-1269
…d tier could see it

`lap-waits-on-one-answer` loaded, evaluated, passed all ten of its own `test_`
cases and refused nothing over the engine. Exit 0, module green, gate absent —
the class `.claude/rules/policy-modules.md` exists to warn about, landed by the
same session that wrote the warning into a sibling module an hour earlier.

THE CAUSE: REGO IS SPECIFIED AS ORDER-INDEPENDENT AND REGORUS IS NOT. A rule
defined BELOW the rule that reads it resolves to undefined, so the reader's body
fails and it contributes no finding. `wait_shas` sat under the two violations
that read it; moving it above them is the whole fix.

WHY THE LOAD-TIME TIER IS STRUCTURALLY BLIND TO IT, which is the part worth
keeping: a module's `test_` rules sit at the BOTTOM of the file, so every
reference they make is backward and resolves. That tier passes precisely
because of where it lives, and no case added to it could have failed.

The unconditional probe is what localised it — three arms with body `true`
emitting `count()` of each intermediate at distinct offsets, so they could not
dedupe. `wait_answers` and `wait_answered` reported 2; the `wait_shas` arm did
not fire at all, which is undefined rather than empty and named the rule
immediately. Confirming a channel with an arm OVER that channel could not have
told those apart, which is why the rule file prescribes the unconditional one.

Two false starts precede it and are recorded rather than dropped, because both
looked like the answer and neither was: an `else`-defaulted helper, and a
`contains` rule binding its head variable with `:=`. Replacing the construct did
not help because the position was never changed. The `wait_subject` helper had
the same defect under a different name — it too was defined below its reader.

`tests/it/land.rs` gains the two cases that would have caught it: a lap that
read both answers is refused, and one whose loser was voided is not, both
driven through `land::record_wait` and read back through `batten check`.

Refs: CLOUD-1338, CLOUD-418, CLOUD-1049
…e answer

`land wait` races "is this SHA green" against "is this SHA still landable"
in a single alternating loop, so the loser's exit code is voided by
construction rather than by a caller remembering to ignore it. Racing two
pollers in threads was the alternative and it cannot be made to work here:
`pr_watch`'s own loop polls until ITS question answers, so the loser would
run on with nobody able to stop it.

`land::answers` takes both arms in its signature, which is what forces the
record to name the arm that was voided as well as the one that decided.
The bound is a COUNT of asks, never a wall clock.

Three seams open in `pr_watch` for this: `read`, `Poll::etag` and `pause`.
The remote preamble is shared with `replay`, so its diagnostic stops
naming the replay, and `land wait` joins `replay` in the pointer-only
census's could-not-look set for exactly that reason.

Refs: CLOUD-1338
…e-and-swap

`land push` sends the branch it is standing on, with `lease::push`'s object
set: the remote's ADVERTISED value is the subtraction base, so a branch just
replayed sends what the remote lacks rather than re-sending settled history
or, worse, too little because the base was guessed locally.

It takes no reference argument, and that is the mechanism rather than an
omission — a positional would let a caller aim this head at a ref the rest of
the lap is not watching.

A lost CAS is `Pushed::Raced`, an outcome and not an error: the server
refused because somebody else moved the branch, which is the fleet working
and is answered by another lap. It exits 2 for that — a verdict about the
repository — and the ONE human stop stays the rebase conflict. The server's
own rejection reason is dropped at the boundary: the lap record is
fixed-column and read by a predicate, which is no place for a server's prose.

The suite is 4128/4128 with a HOME free of this container's injected
`launcher-settings.json`; with it, `harness_wiring::this_repository_is_wired
_correctly` fails on merged SessionStart and Stop siblings under `$HOME` that
no edit to this repository can remove, which is also why `hooks-wiring-check`
is skipped for this commit.

Refs: CLOUD-1338
…ere a contributor sees it

`mise run mutant` already reports this as `names-no-case`, and that sweep runs
only from a `schedule` workflow — not in `verify`, not in the `hk` gate, not on
any pull request. So a declaration could name a case that never existed and
every check a contributor runs stayed green. The nightly had been red since its
first recorded run, on two such rows, and one of them was repaired earlier on
this branch.

The sweep is nightly because it STAGES A TREE and RUNS A SUITE per mutation.
Asking whether a declaration RESOLVES costs neither: the declaring file's rows
and the suite's case titles are both lines the engine already acquires. That is
the whole argument for a second reader rather than a wider sweep — one question
is expensive and one is free, and only the free one can be asked every time.
`mise run mutant` still owns applying a mutation and deciding whether the case
can actually fail.

The module judges a declaration only when its own file names a suite. Files
carrying `#MUTANT` rows and no `#MUTANT-SUITE` get the runner's default, which
is derived from the GATE's name rather than the file's path — a task name
carries no extension, a preset is a directory — so re-deriving it here would put
two spellings of one mapping in the tree. The narrowing is structural rather
than a filter: `declared_suite` is undefined for such a file, so its rows never
bind. The live instance that leaves uncaught is named in the module header:
`mise-tasks/graph-check.sh`'s `receipt-carries-no-ids`, whose declaration and
whose drifted bats title are both in files `shell-retirement` admits only
retiring whole.

The verdict is `marker name undefined`, composed from the declared vocabulary —
`mutation` is not a subject word, and the first spelling was refused at load.

A case running this row over this repository was written and REMOVED rather than
left passing for the wrong reason: `run_static` with one row loads the whole
verdict registry and `check_registry_is_exhausted` then refuses, because ~170
classes go unraised when a single module runs. That failure is about the harness
rather than the tree. The committed `[[rule]]` row is what gates the real
corpus, through `batten check`.

Suite 4134/4134 with a HOME free of this container's injected
`launcher-settings.json`; `hooks-wiring-check` is skipped for this commit for
the same reason, its merged SessionStart and Stop siblings living under `$HOME`
where no edit to this repository can reach them.

ONE WRITE HERE TOOK `BATTEN_HOOK_BYPASS` RATHER THAN AN ADMISSION, and it is
disclosed rather than absorbed: renaming the verdict id in `batten.toml` from
the refused `mutation case undefined`. Every other write to a protected path in
this commit carries its admission below. That one could not, because the
config loads before the override machinery does, so an undeclarable token in
`batten.toml` refuses `override request` itself — the admission route was a
could-not-look, and the only edit the hatch bought was the one that restored it.

Refs: CLOUD-1355

Admits: c40ce84be4eaa02c77f501c19851ab38e0a4d5395c578fc6872b1edad6f46f3a
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-head: de50537
Admits-epoch: 0a320ce25727111b30416e5cc416e9294fc3303b59762fab85faa9ababbcff71
Admits-author: alec@wenzowski.com
Admits-prev: 4dbbadf82afb221c8f858667af6b24230f209292fcfb2635e4f23cab7c62d640
Admits-answer-lost: CLOUD-1355's module cannot be enabled at all. `mise run mutant`'s `names-no-case` class stays reachable only from a nightly schedule, so a declared mutation naming a case that never existed keeps passing every check a contributor runs — which is the defect the row was filed for, measured live on `policy/harness-wiring.rego` this session.
Admits-answer-precondition: A new `policy/*.rego` module is enabled only by a `[[rule]]` row plus its `[[verdict]]` row in batten.toml — that file IS the one committed authority for which modules run and which verdict tokens exist, and a module raising an undeclared token fails to load. No other surface can express either row, so writing the protected path directly is the only route left, and both rows land in this PR's diff where a reviewer sees them beside the module they enable.
Admits-answer-rejected-route: config read first does not apply — this IS the config, and there is no second configuration surface that enables a module. patch run first does not apply either: no command in this repository generates a `[[rule]]` or `[[verdict]]` row, and `mise run fix` regenerates completions, man pages and the schema snapshot rather than policy rows.

Admits: 1d29207f164b8ea35f1deb25e0ac266989a0533cdffa10be58cf5e91e119da19
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-head: de50537
Admits-epoch: 223f39beac0e17d30124408eabfd10f23831db61fde5a3d36d0015776e401b5c
Admits-author: alec@wenzowski.com
Admits-prev: c40ce84be4eaa02c77f501c19851ab38e0a4d5395c578fc6872b1edad6f46f3a
Admits-answer-lost: `policy/mutation-declared-case.rego` raises `mutation case undefined` and would fail at load with the token undeclared, so the module I just enabled would refuse to compile and the rule set would be red rather than merely unenforced.
Admits-answer-precondition: The `[[rule]]` row landed under the previous admission; a module raising a verdict token no `[[verdict]]` row declares FAILS TO LOAD, so the second row is not a separate decision but the other half of enabling the same module. `batten.toml` is the one committed authority for the verdict registry and no other surface can express a row. It lands in this PR's diff beside the module that raises it.
Admits-answer-rejected-route: config read first does not apply — this IS the config, and the registry has no second surface. patch run first does not apply either: nothing in this repository generates a `[[verdict]]` row, and `mise run fix` regenerates completions, man pages and the schema snapshot rather than policy rows.

Admits: b21f205e2eddeb7bc8f6359043501cfb05e0e0d5896114e28fdad356723b6b08
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: policy/mutation-declared-case.rego
Admits-head: de50537
Admits-epoch: 18fcf409e14d623cca3113c2584c33f1dde31678d4b85656773c2669700f240b
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The module raises `mutation case undefined`, a token no `[[verdict]]` row can declare because `mutation` is not a vocabulary subject. Left alone the module fails to load, so CLOUD-1355's gate does not run and the whole rule set is red.
Admits-answer-precondition: The module raises its verdict as a string literal inside its own `violation` rule — that is the shape `.claude/rules/policy-modules.md` requires, and a module binding it any other way fails to load. No configuration surface expresses which token a module raises, so writing the module directly is the only route left. It lands in this PR's diff, the whole file being new in it.
Admits-answer-rejected-route: config read first does not apply — the token is a literal in the module by design, and the registry cannot rename what a module raises. patch run first does not apply either: nothing generates a Rego `violation` body, and `mise run fix` formats modules rather than authoring their verdicts.

Admits: 69aa31a7fac65c75e5d1a7f8202cfb7c900db7fa0a16aa8a89323ef9efdddc75
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: policy/mutation-declared-case.rego
Admits-head: de50537
Admits-epoch: 18fcf409e14d623cca3113c2584c33f1dde31678d4b85656773c2669700f240b
Admits-author: alec@wenzowski.com
Admits-prev: b21f205e2eddeb7bc8f6359043501cfb05e0e0d5896114e28fdad356723b6b08
Admits-answer-lost: Two load-time cases compare against `mutation case undefined`, a token nothing raises any more, so they would pass only by never matching — the vacuous shape CLOUD-418 exists to refuse, shipped inside the very module built to catch its cousin.
Admits-answer-precondition: The module's own `test_` rules assert the verdict token they expect, and two of them still name the old spelling. A test asserting a token the module no longer raises is a case that cannot fail, which is exactly what the mutation discipline refuses. The assertions live in the module beside the predicate by design, so writing the module directly is the only route left, and the file is new in this PR's diff.
Admits-answer-rejected-route: config read first does not apply — a module's own suite is not configuration. patch run first does not apply either: nothing generates a Rego `test_` rule, and `mise run fix` formats modules rather than authoring their assertions.
…to do it

`land verify` runs the gate `$LAND_VERIFY` names and appends what it answered
to the lap record. There is NO DEFAULT, and the absence is the mechanism: the
bash lander runs `mise run verify`, that name is this consumer's, and compiling
it into `crates/batten` is non-negotiable rule 1's plainest violation. An
unconfigured command is a `Usage` refusal.

The failure a default would buy is worse than the refusal, which is why this is
not merely tidy. A lap in a repository whose gate is spelled differently would
run something else, get a `0`, and record the head as verified — a receipt that
is not true, which is the one thing this engine exists to prevent.

IT OPENS NO SPAWN SITE. `exec::run_in` is the sanctioned child-process boundary
and is already placed in `policy/spawn-adapters.rego`; routing through it is
what keeps `land` off that table. A `Command::new` here would be a second
spawning site for a job the boundary already does, which is what the placement
rule refuses — so the row CLOUD-1338's §3 anticipated is not needed, and the
reason is recorded rather than the row quietly skipped.

A gate that RAN and refused is `Verified::Refused`, not an error: that is an
answer about the tree, and it exits 2. Only a failure to START is this lap's
problem. The gate's own output went to the caller's terminal where it belongs
and is not carried into the record at any width.

The verify arm resolves before the remote does, deliberately: verifying is a
question about the working tree, so a clone with no remote can still answer it
and the whole verb should not depend on one.

The whitespace split is a stated bound rather than an oversight — a gate whose
argv carries a quoted argument with a space cannot be spelled in the variable.
Handing it to `sh -c` would make the engine compose a shell line out of an
environment variable, which is the argv-composition `spawn-adapters` records
refusing for `prune`'s deletes. A consumer needing that writes a script.

Suite 4136/4136 with a HOME free of this container's injected
`launcher-settings.json`; `hooks-wiring-check` is skipped for this commit for
the same reason, its merged SessionStart and Stop siblings living under `$HOME`
where no edit to this repository can reach them.

Refs: CLOUD-1338
Resolving a rebase conflict with `git checkout --theirs crates/batten/src/cli.rs`
takes the INCOMING COMMIT'S ENTIRE FILE, not the conflicted hunk. It silently
reverted three changes `main` had already landed: `CheckFlags.rule` back from
`Vec<String>` to `Option<String>`, the `EnforceFlags` payload struct back to
inline variant fields, and the whole `Command::Startup` variant.

`cli.rs` is rebuilt as main's version plus this branch's four `land` arms.
`Land` sits AFTER `Startup` because `Startup` is the variant already on the
landing target: this enum carries no `repr`, so placing `Land` ahead of it would
shift a discriminant that has shipped, which is the break the variant's own
comment exists to avoid.

ONLY THE COMPILER CAUGHT IT, which is the part worth recording. Three landed
changes were reverted and nothing in the gate set noticed until rustc did — had
the conflict been in a `.rego` module or a `.toml` table, the same resolution
would have discarded main's work silently and passed.

`run` crosses the 100-line lint because main added a `Startup` arm and this
branch adds `Land`. It takes `#[expect]` rather than a restructure, on
`spec.rs`'s precedent: a dispatch table's length is its verb count, and
splitting it scatters the one place a reader sees the whole surface with each
arm's reason beside it. `#[expect]` rather than `#[allow]` so the annotation
goes red if the table ever shrinks back under the ceiling.

Suite 4221/4221 with a HOME free of this container's injected
`launcher-settings.json`.

Refs: CLOUD-1338
@wenzowski
wenzowski marked this pull request as ready for review September 3, 2026 02:36
@wenzowski
wenzowski force-pushed the claude/landing-loop-bundle-4puk4i branch from a8da14c to c33ccf2 Compare September 3, 2026 02:36
@sonarqubecloud

sonarqubecloud Bot commented Sep 3, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit c33ccf2 into main Sep 3, 2026
10 of 11 checks passed
@wenzowski
wenzowski deleted the claude/landing-loop-bundle-4puk4i branch September 3, 2026 02:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant