Lane honesty: four ways GET /lane and the trail reported the wrong thing #115

Merged
triform-admin merged 4 commits from agent/lane-honesty into main 2026-08-03 23:19:46 +00:00
Contributor

Stacked on agent/lane-stages (PR #99). Targeting main because CI only fires on PRs into main; merge after #99, or retarget once #99 lands. The diff below includes #99 commits.

Four defects that share one property: every decision the lane made was correct, and every surface an author or operator reads was wrong. They share files, so this is one change.

1. A member enqueued during a build was invisible for the whole build

enqueue returns the instant the event is in the mpsc channel, and nothing reads that channel until the worker returns from pump — for a real lane, ~45 minutes. Observed: POST /lane answered {"detail":"queued \pr-6956`","ok":true}andqueue_depth` stayed 0 across 36s of polling. An author reads that as "the lane never got it" and re-submits.

The host now keeps its own set of accepted-but-not-yet-stepped ids and reports the union, de-duplicated against the lane's own queue/in_flight/ejections. The merge is at read time, not publish time — the worker may not republish for the length of a build, so a withdraw in that window has to take effect on the next read. queued names them.

2. /lane reported phase=idle while the lander ran

Not fixable the way the build case was. By the time LandAndPublish executes, the lane is genuinely Idle — green verdict in, in_flight emptied — while the lander moves the trunk for up to 7200s. The phase is honest and still misleading.

pump_observed now brackets every blocking action with a LaneActivity, and the snapshot carries activity + landing beside phase. It reports the driver's activity; it does not falsify the phase.

3. materialize could fail with a bare os error 2 naming nothing

Observed: lane-build generation=13 outcome=infra reason=candidate tree could not be materialized: No such file or directory (os error 2). The candidate root, the scratch parent, the repo, and a git that cannot be spawned all produce exactly those bytes, with different fixes. Both fallible filesystem steps now name the operation and the path (MaterializeError::infra_at, preserving io::ErrorKind), and git() annotates spawn failures with the cwd it tried.

4. The lane spun a generation every ~30s against an unreachable preview daemon

51 wasted generations in one night. The backoff was not missing — it could not fire. LaneState::now moves only on LaneEvent::Tick, and the host's ticks sit unread in the channel for the whole of a blocking action because the worker is inside execute. So every deadline the outcome computes is anchored at the moment the attempt started. Pointing the preview slot takes ~24-35s against a 30-tick backoff, so infra_retry_after = started + 30 was already in the past when written. On the land path a 7200s budget against a 30-tick backoff can never delay anything.

The driver now re-syncs the lane's clock from the host's own tick stream immediately after each blocking action, before the outcome is applied. Deliberately advance_clock, not a Tick: a Tick also runs maybe_start_build, and after a failed land the phase is already Idle with the members re-enqueued — so a Tick there would start the next build before LandFailed installs the backoff. The retry is paced, never removed.

Tests

Defect Test
1 lanehost::a_member_enqueued_during_a_build_is_visible_immediately — asserts on the FIRST read, no polling loop, so a merely-shorter window cannot pass
1 lanehost::withdrawing_a_member_still_in_the_channel_removes_it_from_the_snapshot
1 lane_policy::the_lane_reports_who_is_queued_not_just_how_many
2 lanehost::the_snapshot_says_landing_while_the_lander_runs — also asserts phase stays idle, and that it returns to settled
2 lanehost::the_snapshot_says_building_while_the_legs_run (regression guard)
3 lanetree::a_scratch_dir_that_cannot_be_created_names_the_path_and_the_step (real ENOTDIR)
3 lanetree::a_git_that_cannot_be_spawned_names_the_directory (real ENOENT)
3 lanedrv::materialize_context_tests — 3 tests pinned against the exact generation-13 string
4 lane_policy::an_infra_backoff_is_measured_from_the_failure_not_the_attempt — attempt takes LONGER than the backoff; asserts the boundary tick
4 lane_policy::advancing_the_clock_does_not_itself_start_a_build — proven by stepping the machine both ways, never by comparing two constants

Validated against a Rust-faithful Python port of LaneState::step, pump_observed and LaneSnapshot before pushing (cargo is hook-blocked; CI is the only build). The port measured OLD=9 vs NEW=5 generations per 300 wall-seconds against a failing preview, and caught two things review did not: a mis-measured land assertion, and the publish-time-vs-read-time merge bug in defect 1.

All 9 lane-attribution-mutation-test.sh anchors verified still present and uniquely-matching after the lane.rs edits.

🤖 Generated with Claude Code

**Stacked on `agent/lane-stages` (PR #99).** Targeting `main` because CI only fires on PRs into main; merge after #99, or retarget once #99 lands. The diff below includes #99 commits. Four defects that share one property: every decision the lane made was correct, and every surface an author or operator reads was wrong. They share files, so this is one change. ### 1. A member enqueued during a build was invisible for the whole build `enqueue` returns the instant the event is in the mpsc channel, and nothing reads that channel until the worker returns from `pump` — for a real lane, ~45 minutes. Observed: `POST /lane` answered `{"detail":"queued \`pr-6956\`","ok":true}` and `queue_depth` stayed 0 across 36s of polling. An author reads that as "the lane never got it" and re-submits. The host now keeps its own set of accepted-but-not-yet-stepped ids and reports the union, de-duplicated against the lane's own queue/in_flight/ejections. The merge is at **read** time, not publish time — the worker may not republish for the length of a build, so a `withdraw` in that window has to take effect on the next read. `queued` names them. ### 2. `/lane` reported `phase=idle` while the lander ran Not fixable the way the build case was. By the time `LandAndPublish` executes, the lane is genuinely `Idle` — green verdict in, `in_flight` emptied — while the lander moves the trunk for up to 7200s. The phase is honest and still misleading. `pump_observed` now brackets every **blocking** action with a `LaneActivity`, and the snapshot carries `activity` + `landing` beside `phase`. It reports the driver's activity; it does not falsify the phase. ### 3. `materialize` could fail with a bare `os error 2` naming nothing Observed: `lane-build generation=13 outcome=infra reason=candidate tree could not be materialized: No such file or directory (os error 2)`. The candidate root, the scratch parent, the repo, and a `git` that cannot be spawned all produce exactly those bytes, with different fixes. Both fallible filesystem steps now name the operation and the path (`MaterializeError::infra_at`, preserving `io::ErrorKind`), and `git()` annotates spawn failures with the cwd it tried. ### 4. The lane spun a generation every ~30s against an unreachable preview daemon 51 wasted generations in one night. **The backoff was not missing — it could not fire.** `LaneState::now` moves only on `LaneEvent::Tick`, and the host's ticks sit unread in the channel for the whole of a blocking action because the worker is inside `execute`. So every deadline the outcome computes is anchored at the moment the attempt *started*. Pointing the preview slot takes ~24-35s against a 30-tick backoff, so `infra_retry_after = started + 30` was already in the past when written. On the land path a 7200s budget against a 30-tick backoff can never delay anything. The driver now re-syncs the lane's clock from the host's own tick stream immediately after each blocking action, before the outcome is applied. Deliberately `advance_clock`, **not** a `Tick`: a Tick also runs `maybe_start_build`, and after a failed land the phase is already `Idle` with the members re-enqueued — so a Tick there would start the next build before `LandFailed` installs the backoff. The retry is paced, never removed. ### Tests | Defect | Test | |---|---| | 1 | `lanehost::a_member_enqueued_during_a_build_is_visible_immediately` — asserts on the FIRST read, no polling loop, so a merely-shorter window cannot pass | | 1 | `lanehost::withdrawing_a_member_still_in_the_channel_removes_it_from_the_snapshot` | | 1 | `lane_policy::the_lane_reports_who_is_queued_not_just_how_many` | | 2 | `lanehost::the_snapshot_says_landing_while_the_lander_runs` — also asserts `phase` stays `idle`, and that it returns to `settled` | | 2 | `lanehost::the_snapshot_says_building_while_the_legs_run` (regression guard) | | 3 | `lanetree::a_scratch_dir_that_cannot_be_created_names_the_path_and_the_step` (real ENOTDIR) | | 3 | `lanetree::a_git_that_cannot_be_spawned_names_the_directory` (real ENOENT) | | 3 | `lanedrv::materialize_context_tests` — 3 tests pinned against the exact generation-13 string | | 4 | `lane_policy::an_infra_backoff_is_measured_from_the_failure_not_the_attempt` — attempt takes LONGER than the backoff; asserts the boundary tick | | 4 | `lane_policy::advancing_the_clock_does_not_itself_start_a_build` — proven by stepping the machine both ways, never by comparing two constants | Validated against a Rust-faithful Python port of `LaneState::step`, `pump_observed` and `LaneSnapshot` before pushing (cargo is hook-blocked; CI is the only build). The port measured OLD=9 vs NEW=5 generations per 300 wall-seconds against a failing preview, and caught two things review did not: a mis-measured land assertion, and the publish-time-vs-read-time merge bug in defect 1. All 9 `lane-attribution-mutation-test.sh` anchors verified still present and uniquely-matching after the `lane.rs` edits. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
iggy added 61 commits 2026-08-03 07:17:44 +00:00
The build lane runs a real release build — 39 minutes measured, 66% of it
in the server compile alone. Today a type error costs that entire cycle,
because the check engine has no way to say "prove the cheap thing first".

Three additions, all opt-in:

  stage: N       checks group by stage; stages run in ascending order and a
                 stage starts only if every earlier stage was green. Default 0
                 puts every existing check in one stage — an unstaged manifest
                 behaves exactly as before.

  fail_fast      (profile) stop on the first REQUIRED red and kill what is
                 still in flight. Off by default: for a governance profile you
                 want the full picture, and reporting one red while hiding nine
                 makes a developer fix them one round-trip at a time. It earns
                 its keep only where the remaining work is expensive and certain
                 to be discarded.

  target_key: K  which CARGO_TARGET_DIR a leg compiles into. This is the one
                 that makes parallelism real: under a warm target EVERY command
                 check was a single mutual-exclusion class, so declaring two
                 compile legs in parallel bought nothing — they serialized on
                 cargo's own .cargo-lock. Distinct keys give distinct dirs.
                 The deploy this models has always used separate target dirs
                 for its native and wasm builds; this closes the gap.

Honesty properties the tests pin, because they are where this could quietly
go wrong:

  * A skipped check is REPORTED, not dropped. A caller seeing fewer results
    than checks cannot distinguish a truncated run from a shorter pipeline,
    and a lane deciding whom to eject would be reasoning from a silently
    incomplete picture.
  * A cancelled check reports SKIPPED, never a red or a timeout of its own.
    We killed it on purpose; blaming it would send someone debugging a
    failure that never happened.
  * Skips are never cached — a skip describes the RUN, not the CHECK, so
    caching one would keep a check skipped after the failure was fixed.
  * A non-required red never trips fail-fast. Cancelling a build over a
    failing advisory would silently promote it to a gate.
  * target_key is sanitised, not trusted: it becomes a path component, and
    a manifest must not be able to escape the parent with `../`.

Cancellation reuses the existing kill_process_tree path rather than inventing
a second one — the grandchild-reaping problem is identical to a timeout's.

Note the rollout order this obliges: reject_unknown() makes an unrecognised
key a HARD error, so the daemon must ship these keys and the fleet must roll
BEFORE any repo declares them. A repo that declares early reds the manifest
for every agent pushing there, not just its author.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(lane): a real candidate tree, and an artifact that actually publishes
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / build (pull_request) Successful in 2m30s
ci / test (pull_request) Failing after 2m39s
ci / fmt (pull_request) Successful in 8m8s
ci / lane policy mutation proof (pull_request) Successful in 8m9s
ci / clippy (pull_request) Successful in 13m36s
953a0b8d77
Two gaps that made the merged lane unusable in practice.

1. NO CandidateTree implementation existed outside tests.

   `grep 'impl CandidateTree' crates/` returned exactly one hit, in
   lane_reusability.rs, and it returns a hardcoded path without touching
   the filesystem. So the lane could be driven by a test vector and by
   nothing else.

   `lanetree::GitCandidateTree` is the real one: `git worktree add
   --detach <scratch> <base>`, then `git merge --no-ff` each member in
   submission order. It MERGES rather than overlaying files, because
   members carry commit shas and "base + every member" is the entire
   claim the lane makes — a tree assembled any other way would let the
   lane report a verdict about a candidate that was never assembled.

   A conflict returns Err, i.e. infrastructure, never a red. A member
   that cannot merge has not failed a build; it has failed to produce a
   candidate. Blaming it would be arbitrary anyway — the other side of a
   conflict is equally "responsible" — and wrong attribution is how a
   team learns to route around its own gate. The failing member IS named
   in the error, since "B conflicts when applied after A" is actionable
   and "the candidate conflicted" is not.

2. The driver hardcoded `Green { artifact: None }`.

   So PointerLander always took its "nothing to publish" branch and
   `.cargoless/latest-green` NEVER advanced. Every lander test passed —
   the lander was correct — and the lane still published nothing. That
   looks exactly like a working lane until someone reads the pointer.

   The seam was untested because RecordingLander ignored its artifact
   argument: every test asserted WHO landed, none asserted WHAT shipped.
   It now records the payload, and two tests pin the seam in both
   directions — an artifact reaches the lander, and a check-only lane
   hands it None rather than an empty string. `Some("")` would advance
   the pointer to nothing: a silent rollback wearing a success's clothes.

   ProfileLegRunner grew `artifact_path`, read ONLY on green. A red build
   may have left a stale artifact in a warm target dir, and publishing
   that is precisely what "never publish red" exists to prevent.

LegOutcome also carries per-leg reports now. ProfileLegRunner was
discarding ProjectCheckReport.results — the per-check id/tree/duration —
which is exactly the visibility a staged lane needs to say "stage 1
rejected this in 4 minutes, stages 2-3 never ran" instead of "the build
failed".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ci: re-trigger — the PR-open run was created already-skipped
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 8m3s
ci / clippy (pull_request) Successful in 9m3s
ci / test (pull_request) Failing after 10m6s
ci / build (pull_request) Successful in 11m29s
ci / lane policy mutation proof (pull_request) Successful in 13m33s
5ee32e8647
Forgejo created run 131593 for PR #99 with every job at status=5
(skipped) and no runner ever assigned. Not the coalescer (its log shows
only a tf-multiverse dev-staging-build entry) and not the pruner. One
skipped run in 30 days of this repo's history, so this is a one-off at
run creation rather than a rule worth working around.

A branch push produces a pull_request_sync run, which has never skipped
here (10 success / 6 running / 1 cancelled, 0 skipped).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ci: re-trigger after clearing a wedged run on main
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / build (pull_request) Successful in 2m45s
ci / test (pull_request) Failing after 2m45s
ci / lane policy mutation proof (pull_request) Successful in 2m24s
ci / clippy (pull_request) Successful in 6m13s
ci / fmt (pull_request) Successful in 9m6s
9390a83b77
Run 130788 (main @adef512) sat at status=1 for 5h with all six jobs
'running' but their action_task heartbeats dead since 14:51 — the
runner pods rotated mid-job and nothing reaps a RUNNING job with a dead
heartbeat (the stale-action-job-reaper is dry-run AND only targets
waiting/blocked jobs whose parent run is terminal).

concurrency_type=1 serializes per group, so every subsequent cargoless
run was created already-SKIPPED behind it. That is why PR #99's first
two runs skipped without ever being assigned a runner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(lane): host the lane on a worker thread, and a real readmit primitive
Some checks are pending
ci / build (pull_request) Waiting to run
ci / test (pull_request) Waiting to run
ci / lane policy mutation proof (pull_request) Waiting to run
ci / fmt (pull_request) Waiting to run
ci / clippy (pull_request) Waiting to run
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Waiting to run
8bd4f0a9e4
Two things the lane needed before a daemon could serve it.

1. LaneHost — a worker thread owning the LaneState.

   `LaneDriver::pump` runs the entire build inside itself, which for a
   real lane is tens of minutes. That is the right shape for correctness
   (one action at a time, no concurrency to reason about) and exactly
   the wrong shape to call from an HTTP handler. The host takes events
   over a channel and pumps them one at a time, so the driver's
   serialization is preserved while callers return immediately.

   Readers get an immutable SNAPSHOT rather than a lock on the lane. If
   `GET /lane` shared the lock the worker holds for a whole build, the
   endpoint would block for that build — and the endpoint exists
   precisely so someone whose change stopped moving can find out why.
   A test pins this: it parks the worker inside a build and asserts a
   snapshot still returns in under 2s.

2. LaneEvent::ForceReadmit — the escape hatch `POST /lane/readmit` needs.

   There was no way to lift an ejection by hand. `HeadMoved` at the same
   head does not do it, correctly: that path asks "does this change
   touch a failing file?" and answers no, which is the right answer to a
   different question. The operator's question is "I have evidence the
   attribution cannot see" — a dependency bump, a toolchain change, a
   red that was never this member's fault.

   Ejection now also retains the member's `changed_files`. Without that
   a re-admitted member returns with an empty changed set and is
   silently UNATTRIBUTABLE for every build it subsequently rides: a red
   it caused would land as "could not attribute" and hold the whole
   queue instead of ejecting it again. Laundering a member out of
   accountability is worse than refusing the re-admission, so a test
   asserts a re-admitted member is still blameable for its own file.

   Force-readmitting an id that is not ejected reports rather than
   silently succeeding — an operator who typos an id must not come away
   believing they unblocked something.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(serve): the daemon actually hosts a build lane
Some checks are pending
ci / build (pull_request) Waiting to run
ci / test (pull_request) Waiting to run
ci / lane policy mutation proof (pull_request) Waiting to run
ci / fmt (pull_request) Waiting to run
ci / clippy (pull_request) Waiting to run
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Waiting to run
3787f7d778
`POST /lane/enqueue` has been answering "build lane not enabled on this
daemon" since the lane merged, because nothing implemented the service
methods — the routes, wire types, policy, driver and docs all shipped
and the host did not. This adds it.

`ServeVerdictState::with_lane(repo, state_dir, base_ref, profile,
artifact_path)` builds a GitCandidateTree + ProfileLegRunner +
PointerLander and spawns a LaneHost. Opt-in and off by default: a lane
merges and publishes, so a daemon must never acquire one as a side
effect of a default. The caller names the profile because the legs are
the PROJECT'S — that is what keeps the lane reusable rather than a
tf-multiverse feature.

The three service methods keep the contract the trait defaults
established:

  * A laneless daemon still ERRORS on enqueue rather than accepting.
    Answering "queued" when no lane exists leaves the caller waiting
    forever for a build that will never run.
  * `lane_snapshot` still returns None on a laneless daemon, so
    `GET /lane` 404s instead of reporting an empty lane. "No lane here"
    and "a lane with nothing in it" are different answers.
  * `id` and `head` are required and NOT defaulted — a member with no
    identity cannot be attributed, ejected or reported on, and an
    anonymous candidate must never enter a queue that can move the
    trunk. `changed_files` stays optional: a caller that cannot compute
    a diff still gets queued and accepts unattributable reds.

The snapshot reports each ejection's `kind` (attributed vs
unattributed) alongside its files, because the two are cleared by
different things and an author needs to know which one they have.

One real bound fix: `mpsc::Sender` is `Send` but not `Sync`, and
`VerdictService: Send + Sync` shares the service across connection
threads. The sender is now behind a Mutex held only for the send — a
pointer hand-off, never a build — so it cannot become the contention
point the snapshot design exists to avoid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ci: fresh run — my DB un-skip left the old runs unschedulable
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 2m39s
ci / build (pull_request) Successful in 13m30s
ci / clippy (pull_request) Failing after 13m11s
ci / lane policy mutation proof (pull_request) Successful in 19m51s
ci / test (pull_request) Failing after 19m54s
ee1fd5423c
I set two runs from status=5 (skipped) to 6 (waiting) with a direct
UPDATE. Forgejo's scheduler skipped them for an hour while giving 63
slots to the other repo, because a healthy waiting run also carries
`started` — mine had started=0, a state the pick query never
transitions out of. The jobs sat first in the FIFO with no task rows and
were simply never claimed.

Same lesson as the requeue attempt: the DB is a diagnosis surface, not a
scheduling interface. Only a push creates a run the scheduler will run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ci: re-trigger now the two zombie runs are cancelled
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / build (pull_request) Successful in 1m34s
ci / clippy (pull_request) Failing after 1m31s
ci / fmt (pull_request) Successful in 2m39s
ci / lane policy mutation proof (pull_request) Successful in 2m53s
ci / test (pull_request) Failing after 11m46s
b5673e9969
131697/131749 sat at status=6 with started=0 — unschedulable, but still
occupying the ci.yml concurrency lane, so every newer run was created
already-SKIPPED behind them. Cancelled both; this push gets a run that
can actually be picked up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test(lane): five more mutations — prove the staging engine's tests have teeth
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 1m52s
ci / clippy (pull_request) Failing after 1m45s
ci / lane policy mutation proof (pull_request) Failing after 2m6s
ci / build (pull_request) Successful in 2m14s
ci / test (pull_request) Failing after 13m25s
8328552c6f
The stage/fail_fast/target_key tests I added are worth exactly as much as
their ability to fail. This repo has shipped machinery that exits 0 while
doing nothing more than once, and a suite that passes either way converts
"unverified" into "verified" — worse than no suite.

So the existing harness grows five mutations against project_checks.rs,
each mapping onto a property the feature claims:

  5. stages do not gate      — a red stage no longer halts the next, so
                               the expensive release build runs for a
                               candidate the cheap check already rejected.
                               Without this the whole feature is decoration.
  6. fail_fast never trips   — in-flight work runs to completion on a
                               candidate that cannot land.
  7. fail_fast over-trips    — cancels on a NON-required red, silently
                               promoting every advisory into a gate.
  8. target_key ignored      — every leg back on one CARGO_TARGET_DIR, so
                               legs declared parallel serialize on cargo's
                               .cargo-lock and the parallelism is imaginary.
  9. skipped reports green   — a check that never ran reports GREEN, so a
                               gate reads "unknown" as "passed". Fails OPEN,
                               which is the direction that actually hurts.

These mutate a different file and run a different suite than the four
lane.rs ones, so they get their own backup and runner rather than being
forced through the existing `mutate`. The EXIT trap now restores both
files on every path — a harness that leaves a mutated source behind would
be a far nastier bug than the one it hunts.

All five anchors verified against the current source before committing; a
mutation that fails to APPLY is still reported as a survivor, so a moved
pattern shows up as a loud failure rather than a silent skip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test(lane): driver-level coverage for a multi-member queue
Some checks failed
ci / fmt (pull_request) Successful in 1m10s
ci / clippy (pull_request) Failing after 43s
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / lane policy mutation proof (pull_request) Failing after 2m25s
ci / test (pull_request) Failing after 2m26s
ci / build (pull_request) Successful in 9m20s
19c876091a
The operator asked directly: "have you tested it with queueing a few
different ones as well?" The honest answer was no — `lane_policy.rs`
covers multi-member cases at the PURE POLICY level, but only one
driver-level test involved more than one member, and none covered two
independent reds or re-admission.

Four tests, each an outcome someone would actually hit:

  * three members, TWO independent reds owned by different members —
    both owners ejected, innocent third ships. Catches the naive
    implementation that ejects the first owner it finds, and the timid
    one that gives up and holds everyone.

  * a member arriving mid-build QUEUES rather than preempting, and gets
    its own candidate. Driver-level on purpose: the policy test proves
    no second build is *decided*, this proves none is *started*, which
    is the part that would burn a runner. On a real lane a cancelled CI
    run is permanently not-green, so preempting strands work that was
    about to pass.

  * an ejected member returns when its new head touches the file that
    failed, and actually ships. Without this an ejection is a dead end
    rather than a hold.

  * and stays out when its push touches nothing implicated — asserted on
    the CANDIDATE COUNT, not just the ejection, because the real cost of
    getting this wrong is a build. At tens of minutes each, a README
    edit buying a slot turns the lane into a treadmill.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(serve): actually enable the lane — with_lane had no caller
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 5m16s
ci / clippy (pull_request) Successful in 5m17s
ci / lane policy mutation proof (pull_request) Failing after 10m6s
ci / test (pull_request) Failing after 10m8s
ci / build (pull_request) Successful in 10m9s
c7faa56683
The `build` job's log carried a warning I would otherwise have shipped:

    warning: method `with_lane` is never used
     --> crates/cargoless/src/serveapi.rs:1490:12
     = note: `#[warn(dead_code)]` on by default

Which is exactly right, and worse than it looks: clippy runs with
`-D warnings` here, so this was a red waiting to happen — and more to
the point, a lane nothing can turn on is the "machinery that exits 0
while doing nothing" pattern this repo keeps re-learning. I wired the
daemon to HOST a lane and then gave no way to ask for one.

Three env vars at the serve-loop's construction site:

    CARGOLESS_LANE_PROFILE   the cargoless.checks.yaml profile to run
    CARGOLESS_LANE_BASE      base ref for candidates (default: main)
    CARGOLESS_LANE_ARTIFACT  artifact to publish on green; unset = a
                             check-only lane that proves the merged tree
                             compiles and leaves the pointer alone rather
                             than advancing it to nothing

Opt-in and off unless asked. A lane merges and publishes, so a daemon
must never acquire one as a side effect of starting up — the operator
names the profile, because the legs are the PROJECT'S build and that is
what keeps the lane reusable rather than a tf-multiverse feature.

It announces itself on boot. A lane that can move the trunk must be
visible in the log rather than inferred from behaviour, same reasoning
as the resolved-caps lines just below it.

Not written as a let-chain: this workspace is Edition 2024 / MSRV 1.85
and `if let ... && cond` does not compile here.

Incidental finding while chasing this: `build` and `fmt` had 0 successes
in 10 attempts over 6h while test got 7 and clippy 4 — but their logs
both end `🏁 Job succeeded` with `Finished dev profile in 44.21s`. They
PASSED; the runner never wrote the result back. The DB status is the
unreliable part, not the build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ci: re-run — clippy/build/fmt orphaned in setup on run 131987
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 1m28s
ci / lane policy mutation proof (pull_request) Failing after 5m43s
ci / clippy (pull_request) Successful in 5m54s
ci / build (pull_request) Successful in 9m32s
ci / test (pull_request) Failing after 9m32s
b05df86c11
Those three sat at status=1 with 0 of 4 steps complete, ~150-450 bytes
of log, heartbeats 6+ min stale, no container on any runner, and the
docker pool nearly idle at 4/24. They died in the setup step and never
recorded a verdict.

Distinct from the earlier class where a job finished and the status was
simply never written back: those had a full .log.zst ending 'Job
succeeded'. These have no completed step at all.

test (608s) and the lane policy mutation proof (606s) both passed on
this same commit, so this is scheduler flakiness, not the tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
docs(lane): how to actually switch the lane on
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / build (pull_request) Successful in 2m0s
ci / fmt (pull_request) Successful in 2m7s
ci / clippy (pull_request) Successful in 2m7s
ci / lane policy mutation proof (pull_request) Failing after 2m13s
ci / test (pull_request) Failing after 5m30s
21b63c568c
The runbook explained how to declare legs and how to read a stuck member,
but not the one thing an operator has to do first: turn the lane on. That
gap existed because until `c7faa56` there was no way to — `with_lane` had
no caller.

Documents the three env vars, and leads with the check-only mode
(`CARGOLESS_LANE_ARTIFACT` unset) as the safe way to start: it proves the
merged tree compiles and cannot move `.cargoless/latest-green`, so a
wrong profile costs build minutes and nothing else.

Says to confirm from the boot line rather than from the environment. A
knob whose effect is invisible is how this fleet has repeatedly shipped
machinery that exits 0 while doing nothing.

Also records the manifest-key staging rule in the operator doc rather
than only in commit messages, including the property worth checking
before staging anything: without `stage:` the cheap legs run ALONGSIDE
the expensive ones — more work, never less coverage. A staged state that
ran FEWER checks until the key landed would fail open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(lane): the lane never built — nothing drove its clock
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 1m10s
ci / lane policy mutation proof (pull_request) Failing after 2m6s
ci / test (pull_request) Failing after 8m53s
ci / build (pull_request) Failing after 8m58s
ci / clippy (pull_request) Failing after 10m1s
b51d8ded8b
An audit of what has actually run against real I/O found three defects,
one of which meant the feature did not work at all.

1. NOTHING CALLED LaneHost::tick.

   `LaneState`'s clock advances ONLY on `LaneEvent::Tick` (lane.rs:457).
   Nothing anywhere called `tick` — not production, not one test. With
   `capture_window_ticks: 60` as the default, `now` stays 0 forever, the
   window never elapses, and `maybe_start_build` returns early every
   time. A freshly enabled lane would accept submissions, report
   "queued", and build NOTHING until a 10th member arrived and tripped
   the max_members path.

   That is the worst failure shape available: it looks like a broken
   transport or bad auth, not a missing heartbeat. Every test that
   starts a build sets `capture_window_ticks: 0` or drives Tick by
   hand — the DEFAULT config was used by production and by no test.

   Ejection TTLs lapse on the same signal, so `eject_ttl_ticks: 3600` —
   the backstop guaranteeing nothing is stuck forever — never fired
   either.

   Now driven from the serve loop beside the existing activity tick.

2. AN UNKNOWN LANE PROFILE WAS A QUEUE-WIPING FOOTGUN.

   `profile_for` falls back to `include: ["*"]` with a 12-SECOND budget
   for a name it does not recognise, and `"*"` matches every check
   regardless of tier. So a typo in CARGOLESS_LANE_PROFILE selects the
   WHOLE manifest — including any 25-80 minute release build — times
   nearly all of it out, and hands the lane ~130 error diagnostics
   pinned at `cargoless.checks.yaml:1:1`.

   Those are attributable to nobody, so the lane ejects the entire queue
   as `Unattributed` on its first build: the gate appearing to decide
   everyone is guilty, on evidence that is a scheduler artifact. With
   defect 1 unfixed the TTL would never lapse it either.

   The daemon now REFUSES to start on an unknown profile and names the
   declared ones. `profile_for` is left alone — it is shared with
   `checks run --profile x`, where the fallback is merely slow.

3. THE LANDER DID NOT FOLLOW THE ARTIFACT SETTING.

   `with_lane` hardcoded `PointerLander` even for a check-only lane.
   Harmless today (it takes the no-artifact branch) but one refactor
   from advancing a pointer to nothing. Adds `ReportOnlyLander` and
   selects it when no artifact is declared, so the safe configuration is
   also the default one and the two cannot disagree.

Plus the test file this whole audit was shaped around: tests/lane_real_io.rs.

`ProfileLegRunner` — the ONLY production LegRunner, the type that turns
a candidate tree into a verdict — had zero coverage of any kind, and no
test at any level composed two real components. The tested sets were
{real git tree, no legs}, {fake tree, fake legs, real host} and {real
subprocesses, no lane}. The shipped set is all four real.

Six tests, no fakes: real git repo, real worktree, real merges, real
bash subprocesses, real diagnostics parsed back. They pin that a red
carries REAL file paths (without which attribution silently degrades to
"blame nobody, hold everyone"), that a missing artifact on green is
infra rather than a false accusation, that the full pipeline advances
the pointer, and that a report-only lane really does not.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
docs(lane): shadow-run procedure, and how to tell a stalled clock from a queue
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 50s
ci / test (pull_request) Failing after 1m17s
ci / lane policy mutation proof (pull_request) Failing after 2m5s
ci / clippy (pull_request) Failing after 6m24s
ci / build (pull_request) Failing after 7m20s
9661a4287b
Three additions the audit made necessary.

**Confirm it is actually building.** The lane's window is driven by a tick
from the serve loop, and until this branch NOTHING drove it — the lane
would accept submissions, report "queued", and never build. That reads as
a broken transport, not a stalled clock, so the doc now says to enqueue
one member and watch `GET /lane` move. A lane idle with a non-zero queue
for longer than the window is not waiting.

**A wrong profile name refuses to boot.** Worth stating because the
failure it prevents is counter-intuitive: an unrecognised name inherits a
fallback that runs every check under a 12-second budget, and the flood of
timeout diagnostics is attributable to nobody, so the lane ejects the
whole queue on its first build.

**Shadow-running.** Not ceremony — the leg runner is the seam between the
queue and a real build, and until it has run against a given project
nobody knows what it does there. Names the three comparisons worth making
(agreement, wall-clock-to-red, attribution correctness) and says plainly
that a shadow lane nobody reads is just a runner burning CPU.

Also flags the green-with-no-artifact shape: reported as infrastructure
rather than a red, holding the queue rather than blaming anyone — correct,
but it looks like a stuck lane if you do not know to expect it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(serve): ManifestError has no Display — render its fields
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / lane policy mutation proof (pull_request) Failing after 1m6s
ci / clippy (pull_request) Successful in 2m3s
ci / fmt (pull_request) Successful in 3m48s
ci / test (pull_request) Failing after 15m27s
ci / build (pull_request) Successful in 15m32s
553d4bc7a4
CI caught a real compile error in the lane boot-validation I added:

    error[E0277]: `ManifestError` doesn't implement `std::fmt::Display`
     --> crates/cargoless/src/servedrv.rs:477:63

`{e}` does not compile for it. `{:?}` would compile and print a struct
dump, which is the wrong fix — the type carries `path`, `line` and
`message` as public fields, so it can render as `path:line: message`,
the shape every other tool in this repo uses and the one an operator can
act on without decoding Rust syntax.

Note what this says about the earlier verdict: `test: SUCCESS` on this
tree was the LIB tests passing. The binary did not compile. A green
`test` job is not a green build, and the two are separate CI jobs for
exactly this reason.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(test): the traversal test rejected correct output
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / lane policy mutation proof (pull_request) Failing after 3m5s
ci / test (pull_request) Failing after 10m26s
ci / build (pull_request) Successful in 10m27s
ci / clippy (pull_request) Successful in 11m50s
ci / fmt (pull_request) Successful in 17m20s
a4554e74e0
`a_traversing_target_key_cannot_escape_the_parent_dir` asserted
`!seen.contains("..")`. But `../../etc` sanitises to the leaf
`.._.._etc` — which still contains ".." as a SUBSTRING while being an
ordinary directory name. The dots are no longer path components, which
is exactly what the sanitiser is for.

So the test reddened on working code. That is the worse kind of wrong
test: the natural way to make it pass is to weaken the sanitiser until
the output stops containing dots, which would trade a correct
implementation for a green check.

Now asserts the property that actually matters — containment — twice:
the resolved path starts with the run root, and no path COMPONENT is
`ParentDir`. Both hold regardless of how the leaf is spelled.

Found by the mutation harness refusing to run: "FAIL: the engine suite
is red BEFORE any mutation — fix that first". That guard earned its
keep; without it the harness would have reported mutation results from
a suite that was already broken.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test: a surviving mutation — the advisory test proved nothing
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 1m31s
ci / clippy (pull_request) Successful in 1m47s
ci / lane policy mutation proof (pull_request) Failing after 3m17s
ci / test (pull_request) Failing after 17m46s
ci / build (pull_request) Successful in 30m31s
498aa383e3
The mutation harness reported:

    SURVIVED — fail_fast trips on a non-required red
        the suite passes on code that cancels a build over a FAILING
        ADVISORY, promoting non-required checks into gates

It was right. `a_non_required_red_never_trips_fail_fast` had the advisory
exit INSTANTLY while the required check slept 1s — so a fail_fast that
wrongly counted non-required reds would fire, but the required check had
usually already finished. The assertion passed either way and the test
proved nothing about the property it is named for.

Sleep is now 6s, so a wrongful cancel lands mid-flight and leaves
`real.out` unwritten. The sibling `without_fail_fast_a_red_does_not_cancel_anything`
had the identical weakness and gets the same margin — it guards fail_fast
staying OPT-IN, which fails the same way for the same reason.

Worth stating plainly: this is the mutation harness earning its keep. Two
timing-shaped holes in tests I wrote yesterday, both invisible to a
passing run, both found by deliberately breaking the code and watching
the suite not notice.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(lane): GET /lane reported "idle" for the entire duration of a build
Some checks failed
ci / test (pull_request) Successful in 1m8s
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / clippy (pull_request) Successful in 1m6s
ci / fmt (pull_request) Successful in 2m26s
ci / lane policy mutation proof (pull_request) Failing after 5m9s
ci / build (pull_request) Successful in 11m59s
9045564a5a
A failing test turned out to be reporting a real product defect, not a
bad assertion.

`LaneHost`'s worker published its snapshot only AFTER `pump` returned.
But `pump` runs the whole build inside itself — tens of minutes for a
real lane — and the transition that flips the phase to `Building` happens
on the line just before that blocking call. So for the entire duration of
every build, `GET /lane` answered:

    {"phase": "idle", "queue_depth": 0, "in_flight": []}

That is precisely the window the endpoint exists to explain. An author
whose change stopped moving would look, see "idle", and reasonably
conclude the lane never received their submission — or was broken. The
one question the product surface is for, answered wrongly, for the whole
time it matters.

`LaneDriver::pump_observed` takes a callback fired after every state
transition and before the actions it produced are executed. `pump` stays
as a wrapper so existing callers are untouched. The host publishes from
that callback, and once more after, so a terminal state is never lost.

The test also had a genuine race — it asserted immediately after the legs
signalled, which is a few instructions before the worker publishes. It
now polls briefly, and asserts the two properties separately: that the
read does not BLOCK (the reason snapshots exist at all) and that it
REFLECTS the running build (the reason this commit exists).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(lane): infra failures back off, cap, and eject instead of hot-looping
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 2m9s
ci / lane policy mutation proof (pull_request) Failing after 2m19s
ci / build (pull_request) Failing after 11m58s
ci / test (pull_request) Failing after 27m2s
ci / clippy (pull_request) Failing after 26m51s
f04011ebb2
Found by the first real shadow deployment, not by a test. Every candidate
failed to materialize (the daemon could not reach the members' head commits)
and the lane retried about once every 2.5 seconds, indefinitely, while
GET /lane reported a steady `phase=building` — indistinguishable from a slow
compile. The `Report` action carrying the reason is a no-op in the driver's
`execute`, so the reason went nowhere and the loop was invisible.

Three changes, one defect:

* `infra_backoff_ticks` (default 30) — requeued members are not eligible again
  until it elapses. Without it the retry runs the instant the failure is
  reported and spins as fast as the failure returns.
* `infra_max_attempts` (default 5) — retrying forever assumes every infra
  failure is transient. Some are permanent from the lane's side: an unreachable
  commit never becomes mergeable by waiting, and retrying it burns the machine,
  reports nothing actionable, and blocks every later submission behind a build
  that cannot succeed.
* `EjectReason::Infrastructure` — a THIRD variant, deliberately not folded into
  `Unattributed`. Unattributed says "your tree is red and we cannot tell whose
  change did it"; the code is implicated. This says nothing compiled, so nothing
  was judged. An author who reads the wrong one goes hunting a bug that was
  never diagnosed.

The streak resets on any non-Infra outcome, so an occasional transient cannot
accumulate across hours and eject a good member.

Also fixes a genuinely vacuous test. `a_non_required_red_never_trips_fail_fast`
used a plain `command` check, whose failure routes through `diag()` and is
stamped Severity::Warning when required:false — so `result.tree` was never Red
and `fail_fast && result.required && …` was unreachable. The mutation harness
was right to keep reporting SURVIVED: under that fixture the mutant was
equivalent. Switching the advisory to `output: cargo-json` (whose severity comes
from cargo's own `level`, never from `check.required`) makes the required:false
+ Red state reachable, and a fixture-precondition assertion now fails loudly if
anyone regresses it rather than letting the test quietly stop testing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test(lane): mutation-prove the infra retry policy
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / build (pull_request) Failing after 2m0s
ci / test (pull_request) Failing after 1m52s
ci / clippy (pull_request) Failing after 3m30s
ci / fmt (pull_request) Successful in 4m53s
ci / lane policy mutation proof (pull_request) Failing after 5m21s
1c3ad3555f
Four mutations for the defect the shadow deployment found. The hot loop shipped
because no test asserted that a retry WAITS, so the harness must now prove these
tests would catch it coming back:

* no backoff — the exact code that shipped, retrying ~every 2.5s forever
* unbounded attempts — never gives up on a permanently-broken candidate, so the
  queue is blocked indefinitely by a build that cannot succeed
* streak survives a good build — an occasional transient eventually ejects an
  innocent member for failures spread across unrelated builds
* infra ejection reported as Unattributed — the two mean opposite things to the
  author reading them, and conflating them sends someone debugging a failure
  that was never diagnosed

Each verified to actually apply against the current source; a mutation whose
pattern has moved is reported as a survivor by design, and all four were checked
through the same heredoc expansion the harness uses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(lane): readmit an infrastructure-ejected member on any new head
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 2m49s
ci / clippy (pull_request) Failing after 2m17s
ci / lane policy mutation proof (pull_request) Failing after 6m43s
ci / test (pull_request) Failing after 7m12s
ci / build (pull_request) Failing after 13m37s
5e79f45f9a
E0004 from the new EjectReason::Infrastructure variant — readmission_decision
was non-exhaustive. Not a mechanical wildcard: this arm decides when a member
held by a daemon-side fault comes back.

Any new head readmits, for the same reason Unattributed does but more strongly.
Unattributed gates on nothing because we could not identify the failing files;
here there are no failing files at all, because nothing ever compiled. Gating
would strand a member for a fault that was never theirs.

The TTL lapse in expire_ejections applies too, so a member does NOT need its
author to push anything to escape once an operator clears the underlying cause.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
docs(operator): record the lane's first shadow run and what it found
Some checks failed
ci / build (pull_request) Failing after 1s
ci / lane policy mutation proof (pull_request) Failing after 1s
ci / clippy (pull_request) Failing after 0s
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 7m2s
ci / test (pull_request) Failing after 16m53s
fc6891e02d
Two defects before the lane produced a single verdict: the daemon could not
reach the members' commits (witness template fetches only `dev`), and an
infrastructure failure retried forever while reporting a phase indistinguishable
from a slow compile.

Includes the three things any new pod in cargoless-builder needs (a
name-matched NetworkPolicy — the default-deny gives a 134s timeout, not a
refusal; PR head refs; a complete clone), and the git verification trap that
made a correct candidate look empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(lane): borrow the ejection reason instead of partially moving it
Some checks failed
ci / build (pull_request) Failing after 1s
ci / test (pull_request) Failing after 1s
ci / lane policy mutation proof (pull_request) Failing after 1s
ci / fmt (pull_request) Failing after 0s
ci / clippy (pull_request) Failing after 1s
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
863a17efea
E0382 in a_persistent_infra_failure_stops_retrying_and_ejects: the let-else
destructured `reason` by value, moving the inner String out, and the test then
calls describe() and fingerprints() on it. Bind by reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(lane): a verdict trail that outlives the candidate worktree
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 2m28s
ci / lane policy mutation proof (pull_request) Failing after 4m5s
ci / clippy (pull_request) Successful in 6m35s
ci / test (pull_request) Failing after 8m47s
ci / build (pull_request) Successful in 8m49s
5c9f53ddb8
The first real shadow build compiled for 76 minutes and reported its verdict
NOWHERE. GET /lane shows only current state, and CandidateTree::release removes
the candidate worktree — and with it the target dir and every artifact — the
instant the build ends. Afterwards there was no way to tell green from red from
inside the pod, so the lane-vs-dev-staging-build comparison the shadow run
exists to produce could not be made at all.

The data was already there and thrown away: LegOutcome.legs carries an id,
tree, required and duration_ms per leg, and run_build discarded it through two
`..` patterns. Now it is bound and written.

Shape copied from tf-multiverse's scripts/ci/_witness_leg_obs.sh, which solved
exactly this for the witness tier — one greppable [cargoless:obs] line per leg
plus one per build outcome, appended to <state_dir>/lane-runs.log beside the
witness's own witness-legs.log. Same vocabulary means an operator already knows
how to read it.

Deliberately NOT a change to LaneBuildOutcome: that enum has 35 construction
sites and threading leg reports through it would churn every test for an
observability goal. The driver is already the I/O half by design, so the write
belongs there and lane.rs stays pure.

Best-effort by contract. A trail is evidence ABOUT a build, never a
precondition FOR one — an unwritable path must not turn a green candidate into
a failure, or the observability becomes the outage, failing closed on exactly
the disk-pressure days the evidence is most wanted. Asserted by
an_unwritable_trail_never_fails_a_build.

The main test deletes the entire repo before reading the log — strictly harsher
than what release() does — because "the verdict outlives the tree it was
computed from" is the property, not "a line was written".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
docs(operator): record the third shadow defect and what it says about the pipes
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 1m42s
ci / clippy (pull_request) Successful in 1m42s
ci / test (pull_request) Failing after 3m19s
ci / build (pull_request) Successful in 3m21s
ci / lane policy mutation proof (pull_request) Failing after 5m50s
e8bfca1d51
The corrected run compiled for 76 minutes and left no readable verdict — GET
/lane shows only current state and release() destroys the worktree, so ten
minutes later green and red were indistinguishable from inside the pod. Fatal
to the exercise: an unreadable verdict cannot be compared against
dev-staging-build, which is the only reason to shadow anything.

Also records the pattern behind all three defects: each was a rediscovery of
something this repo had already solved (the merge train fetches PR heads, the
preview tier has the non-latching ENOSPC retry cap, the witness persists
per-leg lines, build-portal.sh cures the wasm-bindgen skew, and the preview
tier already builds an unmerged ref in isolation). Plus the two facts that
followed from comparing them: the lane's cfg-cone gap, and that compiling
unmerged code inside a token-holding daemon is the escalation
merge-train-candidate.yml exists to prevent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(checks): the advisory fixture emitted nothing — unquote_yaml order
All checks were successful
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / build (pull_request) Successful in 2m6s
ci / test (pull_request) Successful in 2m25s
ci / fmt (pull_request) Successful in 3m21s
ci / clippy (pull_request) Successful in 3m22s
ci / lane policy mutation proof (pull_request) Successful in 4m54s
811903b5d3
The fixture-precondition assertion added with the cargo-json advisory did its
job on the first CI run: it reported the advisory as Green when the test needs
it genuinely Red, instead of passing vacuously.

Cause is in the fixture, not the engine. `unquote_yaml` (yamlscan.rs:517)
replaces `\n` BEFORE `\\`, so the `\\n` written in the manifest string became a
real newline rather than a literal backslash-n. That split
`printf '%s\n' '{...}'` across two lines, printf got a broken format string,
and no parseable cargo JSON ever reached stdout — so there was no error
diagnostic, so the tree was Green.

`echo` instead: it supplies the trailing newline itself, so the escape was
never needed. Verified by porting unquote_yaml's exact replace ORDER and
running the resulting argv: no real newline, exit 1, one line that parses as
reason=compiler-message level=error with the span intact.

This is the second time this test's fixture, not its assertions, was the
problem — which is precisely why the precondition assertion is there rather
than trusting that a red fixture stays red.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(lane): DispatchLegRunner — compile the candidate somewhere unprivileged
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / clippy (pull_request) Successful in 4m0s
ci / fmt (pull_request) Successful in 5m27s
ci / build (pull_request) Successful in 6m29s
ci / test (pull_request) Failing after 6m51s
ci / lane policy mutation proof (pull_request) Successful in 7m42s
5ea52cd8fb
Running the legs in the daemon is a privilege escalation, and on tf-multiverse
it is a demonstrated one. Verified 2026-07-31 from inside the `serve` container
of cargoless-serve-witness:

    $ id -u                                        # 1000
    $ [ -r /workspace/tf-multiverse/.git/config ]  # READABLE
    $ git push --dry-run origin HEAD:refs/heads/probe
      * [new branch]                               # PUSH RIGHTS

`env | grep FORGEJO` in that container returns nothing, which makes it look
unprivileged — the token is injected into repo-bootstrap and repo-sync. But
those sidecars write it into remote.origin.url on the SHARED volume, and serve
reads it as the same uid. Check the file, not the environment.

Since `cargo` executes build.rs and proc-macros from the tree it compiles, and
that tree is the candidate merge of unreviewed code, "we only compile it, we
don't run it" is false and the blast radius is push access to the trunk.

tf-multiverse already solved this: merge-train-candidate.yml compiles an
unmerged train commit holding no deploy key, no kubeconfig, no Plane credential
and no merge token, with the contract pinned by
merge-train-candidate-unprivileged-test.sh. DispatchLegRunner is the cargoless
half of that shape — publish the candidate on a ref, hand it to an external
builder, parse the cargo JSON it reports back.

Cargoless stays forge-agnostic: it pushes a ref and runs a command. What that
command does — dispatch a workflow, submit a Job, ssh a builder — is the
operator's business, exactly as LaneLander is for landing. A red still carries
real file paths, so attribution survives the move out of process.

Reuses project_checks::kill_process_tree (now pub(crate)) rather than
hand-rolling a kill: it sweeps setpgid escapees as well as the process group,
which is what stops a timed-out build leaking grandchildren to init. That needs
the child to be a session leader, so the spawn does setsid too — process_group
alone would leave the sweep inert.

Tests assert the security property rather than describing it: a candidate
carrying a build.rs-shaped payload must leave no mark, and a mutation that runs
the dispatcher inside the tree fails that assertion. Logic validated end to end
by a Python port of the same git/subprocess sequence (8/8, mutant caught).

Not yet wired to a daemon env knob — that lands with the tf-mv adapter, so this
commit adds capability without changing any running behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(lane): assert the CANDIDATE sha, and create parent dirs in the branch helper
All checks were successful
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 1m24s
ci / clippy (pull_request) Successful in 6m22s
ci / test (pull_request) Successful in 11m17s
ci / lane policy mutation proof (pull_request) Successful in 11m19s
ci / build (pull_request) Successful in 11m20s
944ee71a7e
Two test bugs from the DispatchLegRunner tests, neither in the runner.

1. I asserted the dispatcher is handed the MEMBER's sha. It is handed the
   candidate's, and that is the contract: the candidate is a --no-ff merge of
   every member onto the base, so it is a new commit that exists nowhere else.
   Asserting the member's sha would have been asserting that the builder
   compiles the unmerged branch — precisely what the lane exists not to do.
   Now asserts the opposite (sha differs from the member's), that the ref is
   addressed by the sha it carries, and that the ref on the remote resolves to
   that same commit — so the builder cannot compile a different tree than the
   lane judged.

2. `branch()` wrote a nested path with fs::write, which does not create
   parents; `src/broken.rs` gave an ENOENT unwrap panic inside the helper,
   nowhere near the caller's intent. Creates the parent first.

The Python port was updated to match and had the same flaw in reverse — it
committed the payload onto main, making the merge a fast-forward and the
"candidate is a distinct commit" property vacuous. Member now goes on its own
branch. 8/8 with the corrected contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(lane): CARGOLESS_LANE_DISPATCH — select where the legs run
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / clippy (pull_request) Successful in 2m15s
ci / fmt (pull_request) Successful in 2m53s
ci / test (pull_request) Failing after 3m55s
ci / build (pull_request) Successful in 3m55s
ci / lane policy mutation proof (pull_request) Successful in 4m51s
8b21fec2cf
DispatchLegRunner shipped as capability with nothing able to select it. This
wires it:

  CARGOLESS_LANE_DISPATCH             argv of the dispatcher (unset = in-process)
  CARGOLESS_LANE_DISPATCH_REMOTE      default "origin"
  CARGOLESS_LANE_DISPATCH_REF_PREFIX  default "refs/heads/lane-candidate"

Split on whitespace as an ARGV, never handed to a shell. Making the dispatcher
command a shell string would add an injection surface to a feature whose entire
purpose is to stop executing untrusted input.

In-process stays the default because it is the zero-config one — a lone
developer has no builder to dispatch to. It is not the SAFE one for a daemon
that can reach a credential, and the boot line now says which you got:
`where=in-process (compiles candidate code in THIS pod)` vs
`where=dispatched:<cmd> remote=… ref_prefix=…`. A knob whose effect is
invisible is dead machinery.

REFUSES the contradictory pair. DispatchLegRunner always reports
`artifact: None` (the build happened elsewhere), so combining it with
CARGOLESS_LANE_ARTIFACT would leave PointerLander taking its "green with
nothing to publish" branch forever: no error, no pointer movement, and an
operator watching a publishing lane publish nothing. Exits 2 at boot with the
fix in the message, matching the unknown-profile check. The library-level
assert in with_lane stays as the invariant for other callers.

The runner is boxed rather than monomorphised: LaneHost::spawn is generic over
runner AND lander, so choosing both at boot would need four spawn bodies today
and eight the next time either grows. LegRunner is object-safe, and one vtable
hop per build — measured in tens of minutes — is not a cost worth
combinatorics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(lane): EX_TEMPFAIL from a dispatcher means infra, never a code red
All checks were successful
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / test (pull_request) Successful in 2m57s
ci / build (pull_request) Successful in 4m6s
ci / fmt (pull_request) Successful in 5m53s
ci / clippy (pull_request) Successful in 5m53s
ci / lane policy mutation proof (pull_request) Successful in 8m56s
2308f0afed
A remote build that produced no verdict — cancelled, runner vanished, queue
timed out — must not read as "your code is broken". Without this the only
signal is "non-zero", so an infrastructure fault ejects whichever member
happened to be aboard: the fastest way to teach a fleet to distrust its own
gate.

Exit 75 (EX_TEMPFAIL, which sysexits.h already means and shell authors reach
for unprompted) now returns Err from DispatchLegRunner, which the driver
classifies as Infra — members stay queued, nobody is blamed, and the retry
backoff applies.

run_to_completion carries the exit code out for this. `code()` is None on a
signal death, which is also not a verdict but is already not-success; only an
explicit 75 is special-cased.

scripts/ci/lane-dispatch.sh in tf-mv (agent/lane-dispatch-hook) emits exactly
this on cancelled/timeout, so the two halves agree by construction rather than
by comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(lane): PreviewLegRunner — gate on "live on the preview", not just "compiles"
All checks were successful
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 3m39s
ci / clippy (pull_request) Successful in 5m3s
ci / test (pull_request) Successful in 6m8s
ci / build (pull_request) Successful in 6m9s
ci / lane policy mutation proof (pull_request) Successful in 6m55s
0b62386369
Goal step: new MRs roll through a serial queue, coalesce onto a preview slot,
and merge once checks are done AND the candidate is live there. This is the
preview half.

A build proves the tree compiles. A preview proves it BOOTS AND SERVES — the
app came up, its health endpoint answered, and the never-serve-red promote
actually flipped to this candidate. Only the second is what "ready to merge"
means for a deployed service.

Adds no second build path. Reuses the app-serve tier wholesale: worktree per
instance, exact-sha checkout, mid-build HEAD-move detection, warm per-lane
target dir, ENOSPC classification and self-heal, bundle pruning, health-gated
promote at a single site, TTL reaping. Re-deriving any of that is how this lane
already re-solved four problems the repo had solved.

The gate is GET /app (auth-exempt, so polling needs no token):
  serving_sha == candidate  => green and live
  last_red_sha == candidate => red, carrying last_red_reason
anything else is "not yet". A timeout is Err/infra, never a red — "we stopped
looking" is not a verdict about anyone's code.

ONE fixed slot name: a serial queue has one staging area, so the slot is a
position, not a per-candidate resource. Re-POSTing /instances re-points a live
preview and renews its TTL rather than churning it — exactly what a queue wants.

Verified against the real deployment: the preview repo-sync mirrors
`+refs/heads/*`, so a candidate published outside refs/heads/ would be
invisible and the instance would silently never bind. The default ref_prefix is
under refs/heads/ for that reason, now stated rather than lucky.

Also replaces with_lane's widening `Option<(Vec,String,String)>` with a LegPlan
enum. Three destinations was the point where a tuple stopped being readable and
illegal combinations stopped being visible; the enum makes
"remote build + local artifact path" unrepresentable rather than merely
refused. servedrv still refuses the ambiguous env pairs at boot with the fix in
the message.

Verdict logic validated by a port against the real /app shape (7/7), including
the two that would be false greens: a STALE serving_sha is not green, and a red
on a different sha is not our verdict.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(lane): CommandLander — the auto-merge step, via CARGOLESS_LANE_LAND
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 2m30s
ci / clippy (pull_request) Successful in 7m9s
ci / build (pull_request) Successful in 8m51s
ci / test (pull_request) Failing after 9m27s
ci / lane policy mutation proof (pull_request) Successful in 10m7s
3ea460d466
Closes the goal's last link: serial queue -> coalesce onto a preview slot ->
green and live -> MERGE.

Landing on a forge is not one API call, and tf-multiverse's
scripts/merge-train-controller already does it with parts that were each
earned: a k8s Lease (replicas:1 is not a lock — a rolling update starts the new
pod before the old exits), ONE EXIT trap doing worktree + scratch + lease
release (traps REPLACE rather than append, so a second one silently drops the
release and wedges the lane for a full TTL), `git push --force-with-lease` as
the compare-and-swap, and a per-member `manually-merged` reconcile ordered
AFTER the queue retraction.

Re-implementing that here would be a second copy of a correctness-critical path
to keep in sync — and this lane has already paid for re-deriving solved
problems four times. So cargoless stays forge-agnostic and runs a command,
exactly as DispatchLegRunner does for building.

A failed land is Err, never Ok-with-a-sad-message. The driver re-enqueues on
Err, and these members are GREEN: reporting a lost CAS race as success would
drop verified work silently, which looks identical to the lane doing nothing.
Asserted by a_failed_land_requeues_the_members_instead_of_losing_them.

Unset = lands nothing. A lane that can move the trunk should require someone to
say so out loud, and the boot line now reads
`land=AUTO-MERGE via ...` or `land=<none: reports only, lands nothing>` beside
`where=`.

Landers are boxed for the same reason runners were: three landers x three
runners would be nine monomorphised spawn bodies. Landing runs once per green
build, so the vtable hop is free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(lane): hand the preview the ref IT can resolve, not the one we pushed
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 2m22s
ci / clippy (pull_request) Successful in 2m44s
ci / build (pull_request) Successful in 3m4s
ci / test (pull_request) Failing after 3m6s
ci / lane policy mutation proof (pull_request) Successful in 12m35s
415d6ecaca
Found in production: the `lane` slot was created, the candidate refs WERE
mirrored (6 of them), and the slot still sat phase=idle forever with no error
recorded anywhere.

We publish `refs/heads/lane-candidate/<sha>` on the forge. The preview daemon
resolves refs in its OWN clone, where a `+refs/heads/*:refs/remotes/origin/*`
mirror lands that ref at `refs/remotes/origin/lane-candidate/<sha>`. Asking it
for the `refs/heads/` name gives `fatal: Needed a single revision`; the daemon's
ref poller treats an unresolvable ref as "not yet" and silently skips, so the
slot never builds and nothing is logged. Verified both halves by hand in the
preview pod.

POST /instances now sends the remote-tracking name. The published name is
unchanged — a builder fetching from the forge still wants refs/heads/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chore: re-roll CI — test job 222404 was CANCELLED (status=3), not red
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 7m53s
ci / test (pull_request) Failing after 9m16s
ci / clippy (pull_request) Successful in 9m41s
ci / build (pull_request) Successful in 12m45s
ci / lane policy mutation proof (pull_request) Successful in 14m27s
436e812958
A cancelled Forgejo job is permanently not-successful, so the required
context can never go green without a new run. build/clippy/fmt were all
green on the same sha; the cancelled job produced an empty log, which is
the infra signature rather than a code failure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(lane): an unmergeable member is ejected, not called infrastructure
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / lane policy mutation proof (pull_request) Failing after 2m34s
ci / fmt (pull_request) Successful in 4m53s
ci / clippy (pull_request) Failing after 4m54s
ci / build (pull_request) Failing after 10m27s
ci / test (pull_request) Failing after 10m27s
7714b9f448
A member that cannot be merged onto the base used to report
`LaneBuildOutcome::Infra`, because every `materialize()` failure did — the
trait returned `io::Result`, so a merge conflict and an ENOSPC were the same
value. `Infra` ejects nobody by design ("nothing was compiled, so nothing was
judged"), which is right for a runner dying and wrong here: the conflicting
member never left the queue, and every subsequent candidate re-included it.

Observed against tf-multiverse on 2026-08-02. Generations 2, 3, 4 and 5 each
died identically:

    lane-build generation=N outcome=infra
      reason=candidate tree could not be materialized: member `pr-10462`
      (77377ef0) could not be merged onto the candidate: git [...] exited 1

`git merge-tree` confirmed a genuine conflict — five markers against dev — and
two other real PRs sat behind it for the whole time. The lane livelocked.

This is the mirror image of the misattribution `EjectReason` was built to
prevent. Instead of blaming an innocent member we exonerated a guilty one, and
paid with a queue that could never drain.

## The change

`CandidateTree::materialize` now returns `Result<PathBuf, MaterializeError>`:

  * `Conflict { id, files, reason }` — git named the member before we inferred
    anything. Reported as the new `LaneBuildOutcome::Conflict`, which ejects
    that member alone and requeues everyone else at the front WITHOUT the infra
    backoff, because the next candidate is genuinely different (it no longer
    contains the conflicting member) rather than a retry of the same one.
  * `Infra(io::Error)` — fetch failed, worktree could not be created, disk
    full. Unchanged behaviour: nobody's fault, everyone stays queued.

The ejection is `Attributed` with the conflicting paths, so
`readmission_decision`'s existing files-based gate applies unchanged: a new
head touching one of those paths readmits, anything else stays out. When git
cannot report the paths it falls back to `Unattributed`, which still ejects and
readmits on any new head — the honest answer when we cannot say which files to
watch, and still not a livelock, because the member is out until its head moves.

`GitCandidateTree` reads the unmerged paths BEFORE `merge --abort`; the abort
clears the index and with it the only record of which files collided.

## Why a conflict is the member's fault

The other side of a conflict is equally "responsible" for the *content* — that
was the original reasoning, and it is why this was classified as
infrastructure. But it is not responsible for the *decision*: the base is what
everyone else already agreed on, so the member that cannot apply to it is the
one that has to move. That is exactly the judgement a merge queue exists to
make.

## Test

`an_unmergeable_member_is_ejected_and_the_queue_keeps_moving` builds a real
conflict in a real git repo and asserts three things. The third is the
regression guard the old code would have failed: after the ejection, a fresh
tick must NOT put the conflicting member back in flight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(lane): give the trail match a real Conflict arm (E0004)
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 5m54s
ci / test (pull_request) Failing after 5m58s
ci / lane policy mutation proof (pull_request) Successful in 6m22s
ci / clippy (pull_request) Successful in 9m39s
ci / build (pull_request) Successful in 10m27s
76570b0303
The `lane policy mutation proof` job caught a non-exhaustive match: the
per-build trail writer in `lanedrv.rs` matches `&LaneBuildOutcome` and did not
handle the new `Conflict` variant.

Written as a genuine arm rather than `_ => {}`. Today a conflict is detected
while materialising, which returns early and writes its own `outcome=conflict`
line, so this arm is unreachable — but if a future path ever produces a
conflict *after* materialisation, a wildcard would silently swallow the verdict
and the trail would lose it. A verdict outliving the tree it was computed from
is the reason this trail exists.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chore: re-roll CI — known ~1/357 warm-target flake
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 3m43s
ci / clippy (pull_request) Successful in 3m43s
ci / test (pull_request) Failing after 5m7s
ci / build (pull_request) Successful in 5m10s
ci / lane policy mutation proof (pull_request) Successful in 5m40s
4c8f9518eb
`serveapi::tests::resolve_warm_target_contended_key_goes_cold_until_release`
failed alone: 340 passed, 1 failed. That is the documented CGLS-26 flake (a
poisoned mutex under load), not a regression — and it aborted the run inside
the `cargoless` unit tests, before `cargoless-core`'s integration tests, so
it also left the new lane-conflict test unverified.

fmt is already green on this sha; build/clippy/mutation-proof were still
running when the test job failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ci(test): --no-fail-fast so one flaky binary stops hiding every other crate
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 4m6s
ci / clippy (pull_request) Successful in 4m7s
ci / lane policy mutation proof (pull_request) Successful in 4m18s
ci / build (pull_request) Successful in 4m29s
ci / test (pull_request) Failing after 4m30s
2d08cb3dea
`cargo test --workspace` stops at the first test binary that fails. The known
~1/357 warm-target flake lives in the `cargoless` unit tests, which run BEFORE
`cargoless-core`'s integration tests — so every time it fires, the entire lane
suite silently never executes.

That happened twice consecutively on 2026-08-02 (4c8f951 and 76570b0). Both
runs reported `340 passed; 1 failed` from a single binary:

    Running unittests src/main.rs (target/debug/deps/cargoless-...)

and nothing else. On both occasions the job's red said nothing whatsoever about
the crate the change actually touched, and a new lane regression test never ran
even once.

A flake that hides real signal is worse than a flake. `--no-fail-fast` costs
one extra run of the remaining binaries when something is red, and buys an
honest answer about every crate on every run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test(lane): two lander tests set a zero capture window and never passed
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 2m57s
ci / build (pull_request) Successful in 5m10s
ci / lane policy mutation proof (pull_request) Successful in 7m2s
ci / clippy (pull_request) Successful in 7m10s
ci / test (pull_request) Has been cancelled
cd37d239a8
Both were added in 3ea460d alongside `CommandLander` and have failed in CI ever
since — `3ea460d` and `415d6ec` both show `test=FAILURE`. Nobody saw it because
the run aborted earlier, in the `cargoless` unit tests, on the known
warm-target flake; `--no-fail-fast` (2d08cb3) is what finally let the lane
suite run and surfaced them.

Neither is a product bug. Both set `capture_window_ticks: 0`, which is wrong
for what they assert.

**a_green_candidate_is_handed_to_the_lander_with_its_roster** wants ONE
candidate carrying both members. With a zero window the window is already
expired when the first enqueue is pumped, so `a` builds alone; `b` arrives
while the lane is Building and rides a second candidate. The lander script
rewrites the same file on each invocation, so the roster ends up holding only
`b`, and the assertion fails on `a` for a reason unrelated to rosters. Fixed
with a window of 5 and an explicit tick to close it — which is also what makes
the coalescing deterministic rather than a race.

**a_failed_land_requeues_the_members_instead_of_losing_them** hit something
sharper. With a zero window the driver spins: Enqueue → StartBuild →
BuildFinished → LandAndPublish → lander Err → Enqueue → … Each turn is several
lane events, so `pump`'s MAX_STEPS backstop (64) cuts the loop at an arbitrary
point. `on_build_finished` does `mem::take(&mut self.in_flight)`, so a cut
between that take and the re-enqueue being applied leaves BOTH `queue_depth()`
and `in_flight()` at zero — the member looks lost when it is not. Same fix: a
real window, so the requeued member comes to rest in the queue where the
assertion can see it.

That spin is worth knowing about beyond this test: a lander that fails
persistently will retry as fast as the driver can loop, bounded only by
MAX_STEPS per pump. The lane has an infra backoff for build failures and
nothing equivalent for landing failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(lane): a failing lander gets the infra backoff instead of spinning
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / test (pull_request) Failing after 5m46s
ci / clippy (pull_request) Successful in 6m13s
ci / lane policy mutation proof (pull_request) Failing after 6m22s
ci / build (pull_request) Successful in 6m50s
ci / fmt (pull_request) Successful in 11m19s
c86d823cf4
The driver turned a lander `Err` straight into `LaneEvent::Enqueue` for every
member. `pump` feeds follow-up events back into the lane, and `maybe_start_build`
then starts the next candidate immediately — so a lander that keeps failing made
the lane rebuild the same tree as fast as it could loop, each turn a real
multi-minute build holding the preview slot. The only bound was `MAX_STEPS = 64`
per pump, which is a runaway backstop, not a policy.

The realistic trigger is the base moving and the forge's compare-and-swap
rejecting the push, which on a busy trunk persists for many minutes. Arming
`CARGOLESS_LANE_LAND` is what puts a real forge behind that path, so this is
worth having before, not after.

The build path has had pacing since the first deployment, and its comment says
exactly why: "The backoff is what stops this being a hot loop. Without it the
retry runs the instant `maybe_start_build` is reached, hits the same broken
condition, and spins as fast as the failure returns." A failed land is
infrastructure by the driver's own reasoning — the build was GREEN, so nobody's
code is at fault — so it should inherit the same pacing rather than get a second
mechanism.

New `LaneEvent::LandFailed { reason, members }` sets `infra_retry_after` and
counts toward `infra_failures`. It is emitted BEFORE the re-enqueues: every
`Enqueue` ends in `maybe_start_build`, so with a zero capture window the very
first one would otherwise start the build before any backoff existed. It carries
the member ids for the same reason — they are not in the queue yet when it runs.

Members still requeue exactly as before. Losing green work to a push race stays
the worst available outcome; this only changes how fast the retry comes.

Test: a lander that always fails, with `capture_window_ticks: 0` — the worst
case, where nothing else would hold the member back. Asserts the member survives,
that it is WAITING rather than building (this was `Building` in a loop before),
and that the failure is reported with its retry budget instead of being silent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test(lane): anchor the hot-loop mutation so it cannot drift onto a lookalike
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / clippy (pull_request) Successful in 5m17s
ci / fmt (pull_request) Successful in 5m45s
ci / lane policy mutation proof (pull_request) Failing after 8m0s
ci / build (pull_request) Successful in 9m23s
ci / test (pull_request) Successful in 9m25s
221c0549b6
The `lane policy mutation proof` job reported "SURVIVED — infra retry has no
backoff (the hot loop)" on c86d823, which read as the suite going blind to the
regression that once shipped. It was not. The mutation was being applied to a
DIFFERENT line than the one it names.

`LandFailed` (c86d823) sets `infra_retry_after` with a byte-identical two-line
statement, and it appears earlier in lane.rs — line 573 versus 883. The mutation
used a bare `replace(..., 1)` on that string, so it neutered the land backoff
and left the infra rung fully intact. Nothing failed, and the proof correctly
reported a survivor while naming the wrong cause.

A mutation that can drift onto a lookalike is worse than no mutation: it reports
on code nobody meant to test, and the failure message actively misleads about
where the gap is. Anchored on `infra_failures.saturating_add(1)` plus the "GIVE
UP eventually" comment, which is unique to the infra arm.

Verified before pushing: the anchor occurs exactly once, the mutation applies,
and the mutated site is after the `LandFailed` arm rather than inside it.
`an_infra_failure_does_not_retry_before_its_backoff` in lane_policy.rs is the
test that must now fail under it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(lane): a lapsed TTL requeues the member instead of dropping it
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 2m11s
ci / test (pull_request) Successful in 2m36s
ci / build (pull_request) Successful in 2m37s
ci / clippy (pull_request) Successful in 3m20s
ci / lane policy mutation proof (pull_request) Failing after 3m42s
1d2f206f04
`expire_ejections` removed the ejection and pushed `LaneAction::Readmit`, but
never called `admit`. So a member whose TTL lapsed left `ejected`, never
reached `queue`, and was simply gone — while the log said "re-admitted".

Observed in production today. Three members ejected `infrastructure` by a
preview outage hit their TTL and vanished: `queue_depth: 0`, nothing building,
and no trace in `GET /lane` that anything had been lost. The lane looked idle
and healthy with three real PRs silently dropped.

The TTL is the BACKSTOP for a member the attribution stranded — the last thing
standing between a wrong ejection and a PR that never moves again. A backstop
that discards what it was protecting is worse than none, because the reported
outcome is identical either way: `Readmit` is emitted on both paths.

`on_force_readmit` already did this correctly, reconstructing the member from
`Ejection`'s retained `head` and `changed_files`. `expire_ejections` now does
the same. Those fields exist for exactly this.

Why no test caught it: `ttl_expiry_readmits_as_a_backstop` asserted only that
`ejection("A")` was gone and that a `Readmit` action was emitted — both true of
the broken code. It now also asserts the member is back in the lane, with its
head and changed set intact. Without `head` the candidate cannot be built;
without `changed_files` every later red it rides is unattributable and holds the
whole queue instead of ejecting one member.

Added a mutation for the class, since this is the second lane bug that shipped
because a test checked the announcement rather than the effect. Anchored on a
string verified to occur exactly once — including after rustfmt, which is what
would silently have broken it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test(lane): the hot-loop mutation now actually breaks the backoff
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / build (pull_request) Successful in 3m56s
ci / clippy (pull_request) Successful in 4m47s
ci / test (pull_request) Failing after 4m51s
ci / fmt (pull_request) Successful in 5m37s
ci / lane policy mutation proof (pull_request) Successful in 6m9s
5c1d2999ee
Third attempt, and the first that changes behaviour. Both earlier ones reported
"SURVIVED — infra retry has no backoff" and both blamed the test suite, which
was innocent each time:

  1. 7714b9f..c86d823 — a bare replace on the two-line assignment DRIFTED.
     `LandFailed` sets `infra_retry_after` with byte-identical text and appears
     earlier in lane.rs, so `replace(..., 1)` mutated that one and left this
     rung untouched.
  2. 221c054 — anchoring on `infra_failures.saturating_add(1)` injected
     `infra_retry_after = None` at the TOP of the infra arm. The real assignment
     happens later in the same arm and simply overwrote it. The mutation applied
     cleanly and changed nothing.

Now anchored on the comment directly above the assignment ("failure returns."),
which is unique to this rung, and it REPLACES the assignment rather than adding
one before it.

Verified four things before pushing, rather than the two I checked last time:
the anchor occurs exactly once; the mutation applies; **the field is not
re-assigned later in the same arm** (the check that would have caught attempt 2);
and the mutated offset falls after the `LandFailed` arm (the check that would
have caught attempt 1).

A mutation that edits the wrong line, or the right line at a point where the
edit is overwritten, is worse than no mutation: it accuses the suite of a
blindness it does not have, and sends you writing a redundant test for a rung
that is already covered. The proof's own contract is "prove the tests can
fail" — a mutation has to be held to the same standard.

The new "TTL expiry drops the member" mutation added in 1d2f206 is caught, so
that rung is genuinely covered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chore: re-roll CI — known warm-target flake, all lane tests green
All checks were successful
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 1m57s
ci / clippy (pull_request) Successful in 1m57s
ci / lane policy mutation proof (pull_request) Successful in 4m24s
ci / build (pull_request) Successful in 5m42s
ci / test (pull_request) Successful in 7m25s
e4ee5bd14b
`resolve_warm_target_contended_key_goes_cold_until_release` alone: 340 passed,
1 failed. The documented CGLS-26 flake, not a regression.

Everything this branch adds passed in the same run, which is only visible
because of the --no-fail-fast added in 2d08cb3:

  ttl_expiry_readmits_as_a_backstop ... ok
  a_failing_lander_backs_off_instead_of_spinning ... ok
  an_unmergeable_member_is_ejected_and_the_queue_keeps_moving ... ok

build and clippy are already green on this sha.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(lane): a member that landed mid-queue is ejected, not merged empty
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / test (pull_request) Failing after 2m2s
ci / lane policy mutation proof (pull_request) Successful in 3m47s
ci / build (pull_request) Successful in 6m8s
ci / fmt (pull_request) Successful in 8m48s
ci / clippy (pull_request) Successful in 8m49s
304ad5af23
A candidate build takes minutes. In that window a member can LAND —
merged by hand, or carried by an earlier candidate. Its head is then
already an ancestor of the base, and `git merge --no-ff` does not fail:
it writes an EMPTY commit and returns 0.

Verified against real git:

    merge rc=0 ; commits added: 0

So the candidate builds, goes GREEN, and the lander is handed a roster
naming a PR that is already closed. While landing was report-only that
was untidy. With auto-merge armed it is a real merge API call against a
merged PR — precisely what makes an auto-merger untrustworthy.

`GitCandidateTree::materialize` now checks `git merge-base --is-ancestor
<head> HEAD` before merging each member and returns a new
`MaterializeError::Stale`. HEAD rather than the base ref, so it also
catches a member already carried by an EARLIER member of the same roster.

Fails CLOSED: if git cannot answer, the check returns false and the merge
proceeds. A false negative costs one empty commit; a false positive drops
live work.

The driver ejects it via the existing `Conflict` outcome with no files,
which the state machine already classifies `Unattributed` — one ejection
path, not two. The trail line says `outcome=stale` so it is never
confused with a genuine merge conflict.

Ejected by NAME rather than silently skipped: a green candidate that
never contained the member must not look like one that did.

Test asserts all three properties — the landed member is ejected, not as
Infrastructure (infra ejects nobody, so it would ride every future
candidate), and the unmerged co-rider survives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chore: re-roll CI — known warm-target flake, not the stale-member change
All checks were successful
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 5m19s
ci / build (pull_request) Successful in 7m6s
ci / test (pull_request) Successful in 7m26s
ci / clippy (pull_request) Successful in 9m8s
ci / lane policy mutation proof (pull_request) Successful in 9m21s
90180761ae
`test` red on 304ad5a with build/clippy/fmt/mutation-proof all green. The
log names it:

  test serveapi::tests::resolve_warm_target_contended_key_goes_cold_until_release ... FAILED

That is the documented ~1/357 CGLS-26 flake (a poisoned mutex in the
warm-target env lock), not a regression from the stale-member fix — which
touches lanetree/lanedrv only and has its own passing test.

Re-roll rather than diagnose, per the recorded disposition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(lane): retry the preview point — a restarting daemon killed 4 candidates
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / build (pull_request) Successful in 4m37s
ci / fmt (pull_request) Successful in 5m29s
ci / test (pull_request) Failing after 5m33s
ci / lane policy mutation proof (pull_request) Successful in 6m33s
ci / clippy (pull_request) Successful in 7m7s
cec1c42ceb
The single POST that points the preview slot decides whether a 20-45 minute
candidate build happens at all, and it had no retry. Worse, the failure
test is `post.status` — CURL's exit code — so a connection refused during a
rolling update is indistinguishable from a permanent failure.

Measured 2026-08-02. The preview rolled twice in 90 minutes (a Flux apply
of a merged PR, then a second kill) and every candidate racing it died
instantly:

  generation=7  outcome=infra  GET /app failed: curl: (7) Failed to connect
                               ...after 27 MINUTES of real compilation
  generation=8  outcome=infra  could not point preview slot "lane"
  generation=9  outcome=infra  could not point preview slot "lane"
  generation=11 outcome=infra  could not point preview slot "lane"

Each one a fresh ejection and backoff for a member whose code was never at
fault, and pr-10394 has now spent ~7 hours in this loop without landing.

Retry up to POINT_ATTEMPTS (5) with POINT_RETRY_DELAY (6s) between — about
30s of tolerance, which covers the observed gap between a preview pod being
killed and its replacement answering.

Deliberately small. This covers a daemon that is RESTARTING, not one that is
gone: a genuinely absent daemon must still surface as Infra quickly rather
than hang the lane behind a long retry. The error message now reports the
attempt count so a real outage is still legible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chore: re-roll CI — the warm-target flake pair, not the point-retry change
All checks were successful
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 3m38s
ci / clippy (pull_request) Successful in 3m39s
ci / lane policy mutation proof (pull_request) Successful in 9m4s
ci / test (pull_request) Successful in 9m6s
ci / build (pull_request) Successful in 9m8s
70f97e920d
`test` red on cec1c42 with build/fmt/mutation-proof/ra-harness green.
339 passed, 2 failed, and the two are the documented pair:

  serveapi::tests::warm_flock_second_acquire_contended_until_release
  serveapi::tests::resolve_warm_target_contended_key_goes_cold_until_release

That is the known ~1/357 CGLS-26 flake (a poisoned mutex in the warm-target
env lock); the two fail together. Neither touches PreviewLegRunner, which is
the only thing cec1c42 changed.

Re-roll rather than diagnose, per the recorded disposition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(lane): wait for a busy preview slot instead of burning a generation
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / lane policy mutation proof (pull_request) Successful in 3m21s
ci / build (pull_request) Successful in 6m50s
ci / test (pull_request) Failing after 6m52s
ci / clippy (pull_request) Successful in 7m39s
ci / fmt (pull_request) Successful in 7m40s
b97d8d7508
Nothing coordinated the lane with the slot it builds on. PreviewLegRunner
POSTed /instances without ever reading the slot's phase, and the daemon
accepts a re-point while a build is in flight. Meanwhile an infra-ejected
member is auto-requeued the moment its TTL lapses — correct, but on a timer
that knows nothing about the slot.

Observed 2026-08-02 20:07Z: an ejection due to expire in ~3 minutes while
the slot had 10-20 minutes of `triform_physics` left, so the readmit was
guaranteed to land mid-build. Not dangerous — it fails infra and backs off
— but it burns a generation every time, which is why the counter climbed
past 11 today with no progress.

A busy slot is not an infrastructure FAILURE, it is a queue. Wait for it.

Bounded at SLOT_FREE_TIMEOUT (45 min, sized to the 20-45 min tf-multiverse
compiles actually observed) so a slot wedged `building` forever still
surfaces as Infra rather than hanging the lane. Polls every 20s: this is a
tens-of-minutes wait, so a tight loop would be noise.

`slot_is_building` FAILS OPEN by construction — unparseable JSON, an absent
slot, or a missing `phase` all read as not-busy, so a bad snapshot can never
wedge the lane waiting for something it cannot see. The point attempt below
has its own retry and reports the real error.

Busy phases are `building|queued|probing|probing+serving`, verified against
phase_label() in appsvc.rs rather than recalled; `idle` and `serving` are
free. Only OUR slot counts — dev compiling must not serialise the lane.

Four tests cover: each busy phase blocks, each free phase does not, six
unreadable-snapshot shapes all fail open, and another slot's build does not
block us. Logic validated against all 13 vectors before commit (local
compilation is hook-blocked by design).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chore: re-roll CI — warm-target flake again, slot_free_tests all passed
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 1m33s
ci / clippy (pull_request) Successful in 1m33s
ci / lane policy mutation proof (pull_request) Successful in 3m13s
ci / test (pull_request) Failing after 3m30s
ci / build (pull_request) Successful in 4m47s
da676e8acc
`test` red on b97d8d7 with the other five jobs green. 340 passed, 1 failed,
and the one is the documented flake:

  serveapi::tests::resolve_warm_target_contended_key_goes_cold_until_release

Note the count: 340 passed vs 339 on the previous run — the four new
slot_free_tests ran and passed. The failure does not touch PreviewLegRunner.

Re-roll rather than diagnose, per the recorded disposition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(lane): a slot SETUP failure is infrastructure, not a code red
All checks were successful
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / build (pull_request) Successful in 2m37s
ci / fmt (pull_request) Successful in 5m6s
ci / clippy (pull_request) Successful in 5m6s
ci / lane policy mutation proof (pull_request) Successful in 24m17s
ci / test (pull_request) Successful in 24m20s
36247926d8
The preview slot reports setup failures through the same `last_red_reason`
field as compile failures, and the leg turned every one of them into
TreeState::Red. So on 2026-08-02, after a PVC fault removed the lane slot's
worktree directory, the next candidate produced:

  last_red_reason=git checkout 8b6af9d3... failed: fatal: cannot change to
                  '/workspace/cargoless-state/app/lane/worktree':
                  No such file or directory

and the lane ejected pr-10394 — a member whose code compiles fine — in
TWELVE SECONDS. No tf-multiverse build can go red that fast; the elapsed
time alone said it was not a verdict.

A checkout/setup failure is OUR problem. It must report Infra so everyone
stays queued and the lane retries, instead of a false accusation that also
leaves the real fault unnoticed. Same rule as the already-tested
`a_missing_artifact_on_green_is_infrastructure_not_a_red`.

reason_is_infrastructure FAILS TOWARD RED by design. A false
"infrastructure" verdict would keep a genuinely broken candidate queued
until it landed, which is worse than a false accusation — an accusation at
least stops the merge. So it matches only phrases that cannot plausibly come
from a compiler or a failing test: git checkout / cannot change to / no such
file or directory / worktree creation / ENOSPC / fetch / repository not
found.

Two tests. One asserts the exact reason string from the incident classifies
as infrastructure, plus four other setup failures. The other pins the
fail-toward-red direction across seven real red reasons — including
"respawn ... failed health probe: no 200 on /health", which IS a code
verdict (the tree does not boot) and must stay Red.

All 12 cases validated against a faithful port before commit; local
compilation is hook-blocked by design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(lane): the land budget must outlive the lander it delegates to
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 2m13s
ci / clippy (pull_request) Failing after 2m12s
ci / test (pull_request) Failing after 4m14s
ci / build (pull_request) Successful in 4m31s
ci / lane policy mutation proof (pull_request) Successful in 5m11s
745619fa4d
The lane produced five green candidates on 2026-08-02 and merged nothing.
Every one was killed at exactly 600s.

`CommandLander::timeout` was `600s`, justified by "landing is a push plus N
PR reconciles — seconds, not minutes." That is true of a lander that lands
itself. The one actually configured is `scripts/ci/lane-land.sh`, which
delegates to `scripts/merge-train-controller --land` — and the controller
does not just push. It re-derives the candidate, publishes a merge-train
ref, DISPATCHES A CANDIDATE BUILD and waits for the verdict, under its own
`TRAIN_BUILD_MAX_WAIT_SECS` of 5400.

A parent budget below the delegate's own ceiling cannot ever observe an
outcome. It SIGKILLs a healthy land mid-wait and reports infrastructure
failure, the driver re-enqueues, and the lane rebuilds the same members
forever. The comment was not wrong about landing; it was describing a
different lander than the one we run.

7200s, above the delegate's 5400 with room for the forge round-trips that
bracket it, overridable via CARGOLESS_LANE_LAND_TIMEOUT_SECS. This does not
remove the timeout — it makes it mean what it says. The invariant to keep is
parent > delegate.

The second half is why this took a day to find. The reason was DISCARDED:
`execute()` returns `Vec::new()` for `LaneAction::Report` and nothing wrote
a trail line for a failed land, so lane-runs.log read

    lane-build generation=1 outcome=green
    lane-build-start generation=2 members=pr-10394@4fc9c6c5

with nothing in between — a green candidate silently rebuilding, which is
indistinguishable from a lane that never tried to land. I diagnosed it from
Forgejo status timestamps, not from our own trail, which is exactly what the
trail (task 32) exists to prevent. Both outcomes now write a line: a land is
the only step that moves the trunk and has the same claim on the trail as
the verdict that authorised it.

Timestamps, UTC: green + merge-train/queued at 22:15:17, slot heartbeat
moves 22:25:39. 600s, to the second. Same shape at 21:27, 13:08, 12:55,
12:07.

The parse half is split out as a pure fn so its five tests need no
environment: `set_var` is unsafe in Edition 2024, and this crate already
carries a known env-lock flake from exactly that pattern (CGLS-26
warm-target). Tests assert against the controller's real 5400 rather than a
copy of our own default, so lowering it back toward the delegate fails. All
9 vectors validated against a Rust-faithful port before commit (local
compilation is hook-blocked by design).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(app-serve): rebuild an instance worktree that vanished mid-life
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / clippy (pull_request) Failing after 2m36s
ci / fmt (pull_request) Successful in 2m39s
ci / test (pull_request) Successful in 3m56s
ci / build (pull_request) Successful in 3m56s
ci / lane policy mutation proof (pull_request) Successful in 4m22s
e7cad39180
`ensure_instance_worktree` runs at boot (manifest instances) and at instance
creation (runtime previews). Nowhere else. So a worktree destroyed while the
daemon keeps running is never recreated, and every subsequent build of that
instance fails identically and permanently:

  git checkout <sha> failed: fatal: cannot change to
  '/workspace/cargoless-state/app/lane/worktree': No such file or directory

Observed 2026-08-02: the preview pod self-deleted mid-build and took the
`lane` slot's worktree with it — gone from disk AND from `git worktree list`,
while dev/feature-x/merge survived because their boot path re-ran on the
replacement pod. `lane` is a runtime preview, so nothing re-ran for it.

The build lane then burned five generations in four minutes against a fault
no retry could ever clear. Every one was correctly classified `outcome=infra`
"not a code verdict" (so the innocent PR was never blamed — that part
worked), but correct classification of a permanent fault is still a permanent
fault. The slot cannot heal itself and nothing else was going to heal it; I
recovered it by hand, which is not a fix.

Recreate at the checkout site instead. That is the ONE place that requires
the tree to exist, so it is the one place that cannot forget to check —
whereas every future caller of the build path would otherwise have to
remember. Boot-time setup stays: it keeps the first build fast and surfaces a
broken repo early. It is simply no longer the only line of defence.

The repo root is DERIVED, not threaded through. `RealHooks::checkout` is
handed only the worktree, and adding a repo parameter would change the
`BuildHooks` signature for every fake in the suite to serve one recovery
path. Instead we read the layout — `<state_dir>/app/<name>/worktree` — and
ask git for the common repository via any SURVIVING sibling instance. With no
sibling left there is nothing to derive from, and the honest answer is a
reported failure: guessing a path would either do nothing or, worse, create a
worktree of the wrong repository.

`git worktree prune` before `add` is load-bearing, not defensive. git still
holds an administrative record for the deleted tree and `add` refuses a path
it believes is registered — with "already exists", about a directory that
plainly does not. The second test asserts recovery works twice in a row, so
the stale record is really being cleared rather than tolerated once by luck.

Four tests: the regression (deleted mid-life is rebuilt, and is a real
checkout rather than an empty dir), repeatability, no-surviving-sibling
reports rather than guesses, and a healthy worktree is left untouched — that
last one matters because rebuilding a live tree would discard an in-progress
build's state. Each skips cleanly when git is absent rather than failing
spuriously, and git identity is set locally because a CI box may carry no
global config. Logic validated against a real git repo via a faithful port
before commit; local compilation is hook-blocked by design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(deploy): add the missing build job for the cargoless-appserve image
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 2m54s
ci / clippy (pull_request) Failing after 4m52s
ci / build (pull_request) Successful in 5m18s
ci / test (pull_request) Successful in 5m18s
ci / lane policy mutation proof (pull_request) Successful in 10m31s
12268b5137
There was no way to build this image, and that cost six weeks.

`deploy/jobs/` held only build-serve-image.yaml. Meanwhile
deploy/cargoless-appserve.Dockerfile and the egress NetworkPolicy
(app=cargoless-appserve-image-builder) had both existed for months. With no
job to render, nobody rebuilt the image: on 2026-08-03 the running
cargoless-preview reported `cargoless 0.4.0 built=1782303203` — 2026-06-24.

That is not cosmetic. app-serve is a DIFFERENT BINARY PATH from
cargoless-serve. `RealHooks::checkout` in crates/cargoless-core/src/appbuild.rs
— the preview build worker — runs from this image, not from the lane/witness
daemon. A fix shipped to cargoless-serve never reaches it. I proved that the
expensive way: built the worktree self-heal (e7cad39) into cargoless-serve,
rolled it, watched the preview keep failing with the exact error the fix
removes, and hand-repaired the same worktree twice before checking
`cargoless --version` in the target pod.

The job mirrors build-serve-image.yaml with the two corrections that file
STILL lacks, both of which my notes say to apply by hand every single time:

  1. The git context is FORGEJO, not github.com. The serve template points at
     `git://github.com/TriformAI/cargoless.git`; this repo lives on
     forgejo.triform.dev, the clone fails, and the egress NetworkPolicy only
     permits the forge anyway. Also needs GIT_USERNAME/GIT_PASSWORD from
     forgejo-cargoless-auth, since the repo is not public.
  2. triform-1 is excluded via nodeAffinity. It is the single control-plane
     host and workload-triform1-exclusion-audit is AUDIT-mode, so a
     12-CPU/24Gi build schedules there and only warns.

The header records the two traps that actually bite:
  - substituting only ONE placeholder — kaniko then pushes to a tag literally
    named `__IMAGE_TAG__` and reports success. The usage block asserts
    `grep -oE '__[A-Z_]+__'` prints nothing before apply.
  - renaming the pod label — it is what cargoless-appserve-builder-egress
    selects, so a rename silently removes egress and kaniko hangs on the clone
    with no useful error.

And it states the thing I got wrong: building is not deploying, and asserting
the DEPLOYMENT shape is not asserting the running binary. Verify with
`cargoless --version` in the target pod; if the build timestamp did not move,
the roll did not take. cargoless-preview is Flux-managed, so a live
`set image` may be reverted within ~60s — the tag belongs in the tf-multiverse
manifest.

Verified: YAML parses as a Job with 7 kaniko args, 3 env vars, the correct
builder label and the triform-1 exclusion; a real sed render leaves zero
`__PLACEHOLDER__` strings. The equivalent hand-rendered job built
cargoless-appserve:appserve-e7cad39 successfully in-cluster before this was
committed, so the recipe is proven, not proposed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(app-serve): keep the ERROR in a red build's reason, not the last 20 lines
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / clippy (pull_request) Failing after 3m51s
ci / fmt (pull_request) Successful in 7m9s
ci / lane policy mutation proof (pull_request) Successful in 8m47s
ci / build (pull_request) Successful in 10m32s
ci / test (pull_request) Failing after 10m32s
229c46571f
A red build's reason was `tail_lines(&format!("{stdout}\n{stderr}"), 20)`. That
makes real reds undiagnosable, because rustc prints warnings AFTER the error
that stopped the build, then a one-line summary.

Observed 2026-08-03 on a broken `triform-physics` on dev: 63 warnings, so the
last 20 lines were GUARANTEED to be warnings plus

  error: could not compile `triform-physics` (lib) due to 1 previous error;
  63 warnings emitted

The 1039-char reason stored on the slot began mid-warning at line 1587 and
contained no diagnostic at all. I could see WHICH FILES were implicated
(app_ops.rs:630, lifecycle.rs:533, from warning bodies) but never WHAT WAS
WRONG — on the one build where it mattered, because that red was falsely
ejecting innocent PRs from the build lane and I needed to prove the base was
broken, not the member.

There was nowhere else to look. No build log is written to disk, the app-serve
container log does not carry it, and NO CI WORKFLOW COMPILES THIS CRATE — the
preview is the only thing that builds the debug server. The truncated string
was the only evidence in existence and it had discarded the error.

So anchor on the first line that OPENS an error and keep the window after it.
rustc emits the diagnostic body (`-->`, the source excerpt, `= help:`)
immediately following that line, which is exactly what a human needs.

`opens_an_error` deliberately REJECTS the trailing summary: `error: could not
compile ...` and `error: aborting due to N previous errors` are not
diagnostics, and anchoring on them would keep the precise useless line the old
code kept. It accepts `error:` and `error[E1234]:`, trims leading whitespace
(cargo indents sub-crate output), and rejects warning/note/help.

Falls back to `tail_lines` when no error line is found — a step can fail
without rustc (missing binary, shell error) and the tail is right for those.

Five tests, the first reproducing the exact 2026-08-03 shape (error, then 63
warnings, then the summary) and asserting the excerpt LEADS with `error[E0308]`
and carries `app_ops.rs:630`. Plus: the summary is not an anchor, a non-rustc
failure keeps its tail, an indented error still anchors, and empty input does
not panic (the slice arithmetic is the risk).

Logic validated against a Rust-faithful port before commit — old capture
contained E0308: false; new capture: true. Local compilation is hook-blocked by
design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(lane): don't blame a member for a red the base already has
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / lane policy mutation proof (pull_request) Successful in 5m3s
ci / fmt (pull_request) Successful in 7m47s
ci / build (pull_request) Successful in 11m7s
ci / clippy (pull_request) Failing after 8m31s
ci / test (pull_request) Successful in 12m13s
a4c24d36b2
The lane ejected FOUR members in 90 minutes on 2026-08-03 for a defect none
of them caused, and one of them — pr-10572 — changes a single YAML file and
no Rust at all. It was ejected for `error: could not compile triform-physics`.
A YAML change cannot break a Rust compile. The accusation was impossible on
its face and the lane made it anyway, twice.

Root cause upstream: a commit added a `debug!()` call without adding `debug`
to the file's `use tracing::{...}`. The base stopped compiling. The candidate
is base + members, so every candidate inherited that failure, and the lane
attributed each one to whichever member happened to be aboard.

Nothing in the lane could tell the two apart, because from the candidate's
own red they are INDISTINGUISHABLE. The evidence needed was already on hand:
the base slot builds the base with NO candidate merged in, and it is in the
SAME `GET /app` snapshot the runner has already fetched. Base red + candidate
red with the same failure => infrastructure, eject nobody. One map lookup, no
extra request.

Compared on the trailing 60 chars, not the whole string. The two builds run
at different shas in different worktrees, so their reasons diverge in leading
path and step detail while ending in the same compiler verdict — `error:
could not compile X (lib) due to N previous errors; M warnings emitted`. That
tail is what identifies the failure; the head is noise. Both real fixtures
are in the tests and they DO differ up front, which is the point.

Fails toward attributing, deliberately. Every "cannot tell" shape — no such
slot, missing field, unparseable snapshot — yields empty, and empty is never
a match: no evidence must not read as agreement. A missed base-red costs one
false ejection, which the lane already survives; a wrongly-suppressed red
would let a broken member land.

Off unless `CARGOLESS_LANE_BASE_SLOT` names a slot, so a project with no
base-only slot is unaffected. On tf-multiverse that is `dev`.

Six tests, built on the real production strings from the incident: the false
ejection is caught; the comparison survives differing leading detail; a
genuine member fault is still attributed; empty never matches; the named slot
is read (not the first one); and five unreadable-snapshot shapes each still
let the red through.

Logic validated against a Rust-faithful port before commit — all nine
assertions pass, including the five-shape unreadable sweep. Local compilation
is hook-blocked by design.

Does NOT fix the timing tell recorded alongside this: a red returning in 6
minutes when a real build takes 90 is base-shaped, and the lane still does
not know that. This guard is the evidence-based half; the cheap heuristic is
separate work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(lane): the budget test asserted two constants, so clippy folded it away
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / test (pull_request) Failing after 2m38s
ci / fmt (pull_request) Successful in 5m25s
ci / build (pull_request) Successful in 5m54s
ci / clippy (pull_request) Successful in 8m10s
ci / lane policy mutation proof (pull_request) Successful in 9m38s
2e6ca587f1
`ci / clippy` has been RED on this branch since 745619f — four commits — and
the failure is mine, not a flake:

  error: `assert!(true)` will be optimized out by the compiler
    --> crates/cargoless-core/src/lanedrv.rs:1829:9
     = note: `-D clippy::assertions-on-constants` implied by `-D warnings`

Both budget tests compared `LAND_TIMEOUT_DEFAULT_SECS` against a literal. Two
constants resolve at compile time, so the assertion is `assert!(true)` — the
lint is correct and the tests proved nothing at runtime.

Assert on `parse_land_timeout(None)` instead: the budget the code actually
resolves when the env var is unset. Clippy cannot fold a function call, and
the test now covers strictly more — the constant being right AND the fallback
path returning it. A default that is reachable but not returned would have
passed the old test and failed this one.

I read the four reds as "clippy flake" from the status API alone and pushed
past them. `status=cancelled` on the API row is not the verdict — the log on
the forge disk had the diagnostic all along, exactly the shape of the
truncation bug I fixed one commit earlier. The `test` red on the same commits
IS the known warm-target env-lock flake (1 of 341, serveapi.rs:8462), which is
what made the pair look uniformly infrastructural.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chore: re-roll CI — warm-target env-lock flake on 2e6ca58, 5/6 green
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / test (pull_request) Failing after 1m54s
ci / fmt (pull_request) Successful in 1m36s
ci / clippy (pull_request) Successful in 5m24s
ci / build (pull_request) Successful in 7m1s
ci / lane policy mutation proof (pull_request) Successful in 19m1s
c5c4c20ed1
`ci / test` red naming resolve_warm_target_contended_key_goes_cold_until_release
(1 of 341, serveapi.rs:8462) — the known ~1/357 env-lock flake, re-roll not
diagnose. build/clippy/fmt/mutation-proof/RA-harness all success on the same sha.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(lane): POST /lane/withdraw — a member could enter the lane but never leave
All checks were successful
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / clippy (pull_request) Successful in 4m42s
ci / fmt (pull_request) Successful in 6m59s
ci / lane policy mutation proof (pull_request) Successful in 10m56s
ci / test (pull_request) Successful in 10m59s
ci / build (pull_request) Successful in 11m3s
4d0796ac9c
`LaneEvent::Withdraw` has existed and been tested since the state machine was
written. Nothing could reach it: there was no verb on either transport. So a
member entered the lane and left only by building green or being ejected by a
red it caused — and a member that can do NEITHER rebuilds forever.

pr-10394 is that member. It is `failure` on "dev portal SSR check", a REQUIRED
context in dev's branch protection, so the forge refuses the merge however green
the candidate builds. The lander said so in as many words —

    [train] single-member train red: PR #10394 is the cause

— and the lane immediately started generation 15 with the same member, its
third build. Each costs ~45 minutes on the single preview slot, which is the
entire throughput of the queue. The only way to stop it was to restart the
daemon, which discards the whole in-memory queue including innocent members.

The verb is the small half. The real bug is that `Withdraw` cleared `queue` and
`ejected` but NOT `in_flight`, which reads as complete and is not:
`on_build_finished` takes `in_flight` and requeues whatever it finds there on
any non-green outcome. A member withdrawn mid-build therefore came BACK when
the build it was withdrawn from ended — minutes later, unobserved, in exactly
the case the verb exists for. Validated against a Rust-faithful port: without
the `in_flight` line the withdrawn member ends up EJECTED by that build; with
it, gone.

It deliberately does NOT cancel the running build. The candidate tree is
materialised and the compile is already paid for; killing it would waste that
work and deny a verdict to any other member aboard. The build finishes and is
attributed to whoever is left — and if that is nobody, the outcome applies to
nobody and the lane goes idle.

`LaneHost::withdraw` does NOT pre-check the snapshot the way `readmit` does.
The snapshot is one pump behind, and a member enqueued during a build sits in
the channel where the snapshot cannot see it at all, so a "not found" refusal
would make the verb useless in precisely the situation that motivates it. The
lane answers authoritatively; an unknown id is a harmless no-op.

Added to BOTH transports because transport/unix.rs's match is exhaustive on
purpose — its comment says a new verb should fail to compile in both until
someone decides what it means there, rather than silently working over HTTP
only. It should.

Two tests, both on the path that was broken: a member withdrawn mid-build is
absent after that build ends (and does not acquire an ejection — it is gone,
not blamed), and withdrawing one of several leaves the others owning the red
they caused. The pre-existing test only covered withdrawing an already-ejected
member, which is why this survived.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(lane): the lane did the right thing and reported the wrong thing, four ways
Some checks failed
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / clippy (pull_request) Successful in 2m11s
ci / build (pull_request) Successful in 8m23s
ci / fmt (pull_request) Successful in 8m23s
ci / test (pull_request) Failing after 8m29s
ci / lane policy mutation proof (pull_request) Successful in 11m2s
c2aeb693a3
Four defects that share one property: every decision the lane made was
correct, and every surface an author or operator reads was wrong. A gate
whose status surface lies is worse than one with no surface, because the
lie gets acted on.

1. A MEMBER ENQUEUED DURING A BUILD WAS INVISIBLE FOR THE WHOLE BUILD.
   `enqueue` returns the instant the event is in the mpsc channel, and
   nothing reads that channel until the worker returns from `pump` — for
   a real lane, ~45 minutes. Observed: `POST /lane` answered
   `{"detail":"queued `pr-6956`","ok":true}` and `queue_depth` stayed 0
   across 36s of polling. An author reads that as "the lane never got
   it" and re-submits, which is the correct inference from what we told
   them.

   The host now keeps its own set of accepted-but-not-yet-stepped ids
   and reports the union, de-duplicated against the lane's own
   queue/in_flight/ejections. The merge happens at READ time, not at
   publish time: the worker may not republish for the length of a build,
   so a `withdraw` in that window has to take effect on the next read
   rather than the next republish. `queued` names them, because
   `queue_depth: 3` still cannot be reconciled against your own PR.

2. `/lane` REPORTED `phase=idle` WHILE THE LANDER RAN.
   Same family, worse consequence, and NOT fixable the way the build
   case was. By the time `LandAndPublish` executes, the lane is
   genuinely `Idle` — green verdict in, `in_flight` emptied — while the
   lander moves the trunk for up to 7200s (the real one delegates to a
   merge-train controller that waits on its own candidate build). The
   phase is honest and still misleading, so no amount of
   transition-watching would have helped.

   `pump_observed` now brackets every BLOCKING action with a
   `LaneActivity`, and the snapshot carries `activity` + `landing`
   beside `phase`. It reports the driver's activity; it does not
   falsify the phase.

3. `materialize` COULD FAIL WITH A BARE `os error 2` NAMING NOTHING.
   Observed: `lane-build generation=13 outcome=infra reason=candidate
   tree could not be materialized: No such file or directory (os error
   2)`. The candidate root, the scratch parent, the repo and a `git`
   that cannot be spawned all produce exactly those bytes, with
   different fixes. Both fallible filesystem steps now name the
   operation and the path via `MaterializeError::infra_at`, preserving
   `io::ErrorKind`; `git()` annotates spawn failures with the cwd it
   tried (a missing cwd, not only a missing binary, yields `os error 2`
   — the PVC-fault shape).

4. THE LANE SPUN A GENERATION EVERY ~30s AGAINST AN UNREACHABLE PREVIEW
   DAEMON. 51 wasted generations in one night.

   The backoff was not missing — it could not fire. `LaneState::now`
   moves only on `LaneEvent::Tick`, and the host's ticks sit unread in
   the channel for the whole of a blocking action because the worker is
   inside `execute`. So every deadline the outcome computes is anchored
   at the moment the attempt STARTED. Pointing the preview slot takes
   ~24-35s (5 attempts, 6s apart) against a 30-tick backoff, so
   `infra_retry_after = started + 30` was already in the past when it
   was written, and the drained tick backlog cleared it on arrival. On
   the land path it is starker: a 7200s budget against a 30-tick backoff
   can never delay anything.

   `LaneDriver` now re-syncs the lane's clock from the host's own tick
   stream immediately after each blocking action, before the outcome is
   applied. Deliberately `advance_clock`, NOT a `Tick`: a Tick also runs
   `maybe_start_build`, and after a failed land the phase is already
   `Idle` with the members re-enqueued — so a Tick there would start the
   next build before `LandFailed` installs the backoff, reintroducing
   the exact hot loop that event was added to prevent. The retry is
   paced, never removed.

TESTS — which pins which defect

  DEFECT 1
    lanehost::tests::a_member_enqueued_during_a_build_is_visible_immediately
      Asserts on the FIRST read after the POST, with no polling loop, so
      a fix that merely shortened the window cannot pass.
    lanehost::tests::withdrawing_a_member_still_in_the_channel_removes_it_from_the_snapshot
      The read-time-merge property. Found by writing the test.
    lane_policy::the_lane_reports_who_is_queued_not_just_how_many
      `queued`/`queue_depth` describe the same set; in-flight members
      leave the queue, which is what the host de-duplicates against.

  DEFECT 2
    lanehost::tests::the_snapshot_says_landing_while_the_lander_runs
      Blocks inside the lander and asserts `activity=landing` + the
      roster, AND that `phase` stays `idle` — the fix reports the
      activity, it does not falsify the phase. Also asserts it returns
      to `settled`, since a successful land emits no follow-up event.
    lanehost::tests::the_snapshot_says_building_while_the_legs_run
      The case that already worked, pinned against regression.

  DEFECT 3
    lanetree::tests::a_scratch_dir_that_cannot_be_created_names_the_path_and_the_step
      Real ENOTDIR (scratch parent inside a regular file), not a
      constructed error.
    lanetree::tests::a_git_that_cannot_be_spawned_names_the_directory
      Real ENOENT from a missing cwd.
    lanedrv::materialize_context_tests — three tests, pinned against the
      exact generation-13 string, including that `io::ErrorKind`
      survives the annotation.

  DEFECT 4
    lane_policy::an_infra_backoff_is_measured_from_the_failure_not_the_attempt
      Attempt takes LONGER than the backoff — the real preview-point
      case. Asserts the boundary tick, so a backoff that is present but
      effectively zero fails. Asserts the retry still happens.
    lane_policy::advancing_the_clock_does_not_itself_start_a_build
      The Tick-vs-advance_clock distinction, proven by stepping the
      machine both ways from the same state — never by comparing two
      constants, which `clippy::assertions_on_constants` folds away.

Validated against a Rust-faithful Python port of `LaneState::step`,
`pump_observed` and `LaneSnapshot` before pushing (cargo is hook-blocked;
CI is the only build). The port measured OLD=9 vs NEW=5 generations per
300 wall-seconds against a failing preview, and caught two things review
did not: a mis-measured land assertion of mine, and the
publish-time-vs-read-time merge bug in defect 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(lane): defect 2 had a durable half — the trail never said a land STARTED
Some checks failed
ci / fmt (pull_request) Failing after 3s
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / build (pull_request) Successful in 6m50s
ci / lane policy mutation proof (pull_request) Successful in 6m44s
ci / test (pull_request) Successful in 6m49s
ci / clippy (pull_request) Successful in 6m40s
b40f9bc991
`GET /lane` now reports `activity: landing` while the lander runs, but a
snapshot dies with the pod and the TRAIL is what is left afterwards. And
the trail had exactly the same gap: `run_build` writes
`lane-build-start` before blocking, `execute`'s land arm wrote nothing
until the land returned.

So a daemon rolled or killed during a land left `outcome=green` followed
by silence — a record indistinguishable from a lane that never tried to
land at all. That is the same shape that hid a 600s lander timeout for a
full day on 2026-08-02: five greens, five kills, and the only evidence
was Forgejo status timestamps.

A land is the only step that moves the trunk and it can run for up to
7200s. It gets a start line naming the roster by `id@head`, for the same
reason a build does — and because `in_flight` is already empty by that
point, so nothing else can answer "who was being landed".

TEST
  lane_real_io::the_verdict_and_per_leg_timings_outlive_the_candidate_worktree
    Extended. Asserts the start line exists, names the roster by id@head,
    and PRECEDES the outcome line — a start written after the land would
    prove nothing about the window it exists to cover. Read after the
    whole repo is deleted, like the rest of that test.

Also folds in two cosmetic fixes to the previous commit: two intra-doc
links pointed at a private const (`LAND_TIMEOUT_DEFAULT_SECS`), and one
`&x == y` comparison is written `x == *y`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chore: re-roll CI — fmt hit a Docker image-pull failure on b40f9bc
All checks were successful
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / fmt (pull_request) Successful in 2m14s
ci / clippy (pull_request) Successful in 1m1s
ci / build (pull_request) Successful in 5m42s
ci / test (pull_request) Successful in 13m36s
ci / lane policy mutation proof (pull_request) Successful in 13m37s
70a73742fb
The fmt job's whole log is five lines ending in:

  failed to create container: 'Error response from daemon:
  No such image: rust:1.85-bookworm'

It never ran rustfmt. build / test / clippy / lane-policy-mutation all
went GREEN on the same commit using the same image, which is what rules
out a real formatting fault — and `rustfmt --edition 2024 --check` over
all eight changed files exits 0 locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
iggy changed target branch from main to agent/lane-stages 2026-08-03 07:54:22 +00:00
Merge remote-tracking branch 'origin/agent/lane-stages' into agent/lane-honesty
All checks were successful
ci / rust-analyzer latency harness (S1 / AC#2) (pull_request) Has been skipped
ci / build (pull_request) Successful in 1m54s
ci / fmt (pull_request) Successful in 6m16s
ci / test (pull_request) Successful in 9m52s
ci / clippy (pull_request) Successful in 9m25s
ci / lane policy mutation proof (pull_request) Successful in 17m49s
d0aa0156ba
triform-admin changed target branch from agent/lane-stages to main 2026-08-03 21:52:06 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
triform/cargoless!115
No description provided.