Changes touching this path
- loot-first in Rust: land policy behind a Forge seam (#218) — new loot-first bin crate (ledger/forge/policy/orchestrator), in-process Workspace reads via loot-cli lib split, typed pr-map owner + shared ferry::WipState; every policy decide-tested against a fake forge. Build+test only; shadow-run, real land, and ps1 deletion remain operator-gated.
58c28888 · dbf3dbe6… - #243 D2: loud mirror-drift guard on status/review/land
Warn when the loot mirror's projected `main` has drifted from the
checkout's real `origin/main` — the signal that would have stopped PR
#241 before it projected a revert of landed work.
- policy.rs: pure, unit-tested classifier. `Ancestry {Same, MirrorBehind,
Diverged}` + `mirror_drift_warning(mirror, origin, ancestry)` rendering
the mandated wording ("loot mirror is behind origin/main (X vs Y) —
reconcile before landing"), with the loudest "DIVERGED — do NOT land"
for the #241 shape. No "ahead" variant: an unpushed-ahead mirror tip is
not an object the checkout holds, so it folds into Diverged.
- orchestrator.rs: best-effort, side-effect-free reader (`mirror_drift`
/ `warn_if_drifted`) — reads the local `refs/remotes/origin/main` only
(no network), classifies via `git merge-base --is-ancestor`, and prints
loudly to stderr in `status`, `review`, and `land`. A guard never fails
its host command.
Deliverable 1 (the one-time mirror/dock re-baseline) is not in this
commit — it needs a clean fork-drop/adopt path loot does not yet expose.
cadc5ebe · dbf3dbe6…diff - drift guard: a mirror merely AHEAD of a stale origin/main is quiet, not DIVERGED (#273)
The guard fired the loudest warning it has — "DIVERGED — do NOT land" — on the
single most common healthy path: between a land and the checkout's next
`git fetch`. A guard that cries wolf on the normal path is one the operator
learns to scroll past, which is how the real #243 case (a lane projecting
backward over landed work) would eventually slip through. Its value is its
rarity.
The pure policy half was already correct and unit-tested; the bug lived entirely
in the impure oracle that computed `Ancestry`, which had no tests at all.
- `Ancestry::MirrorAhead`, probed by asking BOTH directions. Each direction is
asked of the repo guaranteed to hold both commits when that answer is true:
"ahead" of the mirror (which always holds its own lineage — the checkout may
never have fetched the tip, which is why "ahead" collapsed into "diverged" by
construction), "behind" of the checkout. Ahead is quiet — it is the normal
post-land state.
- `land`/`tag` refresh `main` from the remote before judging; `status`/`review`
stay local and cheap. This pays for `MirrorAhead` being quiet: a stale
tracking ref could otherwise hide a `main` that moved under us at exactly the
two verbs that must not miss it. Falls back to the tracking ref, with a note
rather than in silence, when origin is unreachable.
- `is_ancestor` captures output, so a missing object no longer sprays a raw
`fatal:` that reads like a real failure.
The refresh FETCHES rather than `ls-remote`s, deviating from the ticket's
suggested fix 2: ancestry needs operands git can walk, and a bare sha is not one
— the checkout may never have seen that commit, so the behind-probe would fail
into a false Diverged on precisely the fresh break-glass push the refresh exists
to catch. Caught in review; regression-tested.
Behind and diverged still warn exactly as loudly. `mirror_ancestry` and the
refresh now have direct tests over real temp repos — the previously untested
half — covering same/ahead/behind/diverged/missing-object, plus the case the old
doc claimed was undecidable (ahead when the checkout lacks the mirror's tip).
Verified live against the ticket's repro: the old binary says DIVERGED, the new
one is quiet on identical state, while forged diverged/behind states still warn.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0c808aa8 · dbf3dbe6…diff - review refs carry the position, not the dock (#281)
Every lane's home dock is main, so dock-named review refs made N
concurrent lanes share one review/main branch: the second lane's
ferry --with-wip force-pushed over the first's in-flight PR head,
and either position's reap pass could misjudge (and retire) the
other's live entry, since liveness reads the positional working
pointer a foreign position cannot see.
The review lane is now keyed by its owner position end to end:
- ferry projects review/<lane-id> from a lane, review/<dock> on the
primary; the wip and pr-map ledgers gain an owner column (- =
primary; pre-#281 short rows parse as primary-owned) and the
review line carries owner=.
- Reap is owner-scoped: only the owner judges liveness; a foreign
pass reaps exactly the entries whose owner lane is gone from the
registry, so an abandoned lane's review ref dies with it instead
of leaking.
- land derives the collapse branch from the pr-map lane's owner and
refuses to run from any other position: it finalizes the current
position's working change, and the dock guard cannot catch that
mismatch when every lane's dock is main.
Docs: ADR 0033 amendment, concurrent.md (review projection is now
genuinely parallel-safe), workflow.md, CONTEXT.md review-lane entry.
aa68437a · dbf3dbe6…diff - Workspace accepts its clock; lane flag-gate goes pure so cmd_* tests never touch a real .loot (#322)
20ac82e1 · dbf3dbe6…diff - loot-first land: report the provisional-branch reap instead of swallowing it (#625)
ed9ad0c2 · dbf3dbe6…diff - a land from the primary refuses when the git checkout is parked on another branch, so a projection can no longer write across someone else's work (#618)
Level A of #618, first half. `reconcile_onto` materializes the reconciled tree
over the running position's disk; from the primary that disk is the shared
checkout, the one position that also carries a .git. Parked on another session's
branch it takes landed content across their working copy — observed live, the
primary ended up holding main's content against research/tracker-prior-art, and
only luck (their work was committed and pushed) made it harmless.
`projection_target` (policy.rs) decides from two facts: the position running the
land, and the branch its checkout is on. A lane's projection writes its own tree
and leaves the primary for its own `loot adopt` (#265), so the guard is silent
there rather than crying wolf on the normal way to work. Off-branch from the
primary refuses, naming the branch; --allow-off-main is the break-glass. An
unreadable branch (detached HEAD, no .git, a git that failed) proceeds but says
the guard did not run, so a broken git cannot mute it silently.
The refuse/proceed decision is carried out on Gate::Proceed's new `note` rather
than re-derived by the caller, so what gets reported cannot drift from what was
decided.
Level A's second half — refuse a primary tree dirty with content that is not
this land's — is deliberately absent: from the primary the implicit snapshot
(ADR 0030) has already captured that dirt into the landing change, and dirt
arriving after the review round is already refused by review_currency (ADR
0033). The residual, recorded in policy.rs: a land with no reviewed_version
takes Currency::Current unconditionally, so that path is still unguarded.
Levels B and C remain open.
c1148309 · dbf3dbe6…diff - every loot-first failure reaches the exit status, and a land whose fast-forward is refused no longer reports a landing (#832)
f641cfe4 · dbf3dbe6…diff - a land runs from a lane: the position gate replaces #618's checkout guard, and level A is deleted rather than kept beside it (#932, ADR 0050)
b629f752 · dbf3dbe6…diff - a skipped gate leaves a mark, and landed main gets a canary: the single test gate stops being silent (#642)
Three questions, decided.
1. A test/lint CI workflow — YES, but exactly one, and it runs on landed main
rather than on PRs. The pre-land gate IS the design real gate (loot is the merge
authority, git main is a projection), so a job re-running the same suite on every
review round duplicates it, adds latency, and covers only paths nobody takes.
What it covers instead is SILENCE. The ticket named three ways past the gate that
leave no test run behind; a fourth appeared today and cost a morning — #920 and
#921 each passed their own gate and together left main red, and nobody learned
until the NEXT land failed on a breakage it had not caused. #939 closed that
window; this says so in minutes if anything reopens it. It uploads no artifacts,
so it does not touch the quota blocking releases (#835, #909), and docs-only
pushes are skipped, which is what --skip-tests is documented for.
2. clippy — NO, not yet. ~30 pre-existing warnings mean -D warnings lands
permanently red and warn-only lands permanently yellow, which teaches everyone to
ignore the one signal the workflow exists to make trustworthy. Same argument that
kept an audit gate out of site/ (#916). It wants a cleanup pass and then its own
decision.
3. --skip-tests recording itself — YES, on the verdict line, as tests=ran or
tests=SKIPPED. Shouted like relay=FAILED and branch=FAILED (#519, #625), because
the field is saying the change above it was never proved to build, plus a block
below that cannot be scrolled past. The block also names the thing that is easy
to forget: the perf gate skips WITH it, so such a land writes no point and the
previous one stays the baseline. Before this the only trace of a skip was one
line minutes earlier, above everything anyone was watching for.
Not the commit trailer the ticket offered as the alternative: the projected
commit trailer set is part of the ferry ingest contract, and adding to it is a
wire-adjacent change that wants its own ticket rather than riding this one.
49e4554a · dbf3dbe6…diff - a land gates the tree it publishes: the merged-tree gate runs inside the ferry pass, so a lane behind landed main queues and converges instead of being refused (#1013)
4d9d4c0f · dbf3dbe6…diff - an offline land skips the site gate loudly instead of refusing, the verdict grows a site= field read like relay=, and only a provable npm network failure ever earns the skip (#1251)
3326a228 · dbf3dbe6…diff - the land verdict becomes a value: one Verdict struct renders the line and its operator blocks so every combination is a table test, and the ferry re-gate closure body gets a name without leaving the pass (#1293)
a89e5aca · dbf3dbe6…diff - a --skip-tests land that converges stops publishing a merged tree no gate ever saw, because the eight gates are one table now and each declares when it runs, so the re-gate asks that table per gate instead of reading the perf gate's return value as if it spoke for all eight (#1408)
a998f52b · dbf3dbe6…diff - every land now states which versions it published that the lander did not write, and refuses nothing - ADR 0092's detection half, part 1 of 2 of this ticket. a version is foreign when its author is not in the lander's own key set, which is new: loot_identity::own_signing_keys reads the active id.pub plus every id.pub.rotated archive that loot id rotate writes, exposed as Workspace::own_signing_keys beside author_pubkey rather than instead of it, because the two answer different questions. carry_line's Foreign asks may I RE-AUTHOR this and only the current key can sign, while the gate asks did I WRITE this and authorship is historical, so a rotated operator's own change is foreign to the carry and theirs to the gate and both are right - each site now names its own question and points at the other, since unifying them breaks a caller in either direction. the judged set is the ancestor closure of the landed change minus the closure of the main it was judged against, read BEFORE the pass because by verdict time main already includes the landed change and the difference would be empty on every land, and it is the whole closure rather than carry_line's first-parent chain because a stack carrying a merge of its own would otherwise have one side waved through. an absent author is foreign, which is carry_line's answer too, so the two predicates disagree only about which keys are mine. the count rides the verdict line as foreign= and a block names the versions, since the count says that and the next question is which. foreign= sits immediately BEFORE pusher= and not after it, which I got wrong first: pusher= carries an arbitrary percent-encoded path and is designed as the tail, workflow.md says the path comes last, and five pusher tests read it with ends_with - appending after it broke exactly those, while inserting before it breaks only a fixed-index reader and the repo has none, and those five pass unmodified. the key set reads only id.pub.rotated and never the private id.rotated spelling, which holds a secret key and has no business answering a question about authorship. a machine that pulled rather than rotated holds no archive, so there your own pre-rotation work reads as foreign, which fails toward asking for an approval nobody owed and never toward waiving one, and the block and workflow.md both say so. red under mutation, counts read each time: the key set ignoring the archive (52 passed and 1 failed), the filter admitting the private archive spelling (52 passed and 1 failed), and an unauthored change read as the lander's own (202 passed and 1 failed), each restored to 53, 53 and 203. ADR 0092's status now says which sections are built, because section 5 - the gating column, review/approve and the refusals - is part 2 and until it lands detection reports and nothing refuses, which is that section's decided default anyway. no migration, no wire or format byte moves, and nothing on a host moves, so this owes no deploy. the workspace suite is green (4189 passed over 133 binaries) (#2178)
8cb77a56 · dbf3dbe6…diff - a land that publishes work you did not write now refuses when the repo asks it to, ADR 0092's gating half and part 2 of 2 of this ticket: a require line in the .lootpipeline on main names a step whose pass or the approval every foreign tip must carry, and with no such line the land stays advisory as decided. the syntax is a directive rather than the column section 5 named, the operator's choice, because a step line's command swallows the rest of the line so a column would have re-laid every step line in a grammar map #2099 owns - a require line has two words, every two-word line was already refused as a malformed step, so it changes the meaning of no line that parsed before, and it can express the approval, which was never a step. only the owner's own keys satisfy require approval, as review/approve: a trusted runner's passes count and its approval does not, or the machine that ran the tests could also sign the second pair of eyes. a require line that names no step, names a step not on change, or repeats refuses the land rather than reading as nothing required, because a typo there is a gate the owner meant to declare. the requirement is read at the judged main and never from the stack being landed, a sealed or burned landed file refuses, the check runs before the break-glass branch so --skip-tests cannot reach it, and own work returns before reading the file at all. only the foreign tips must carry the roles, since a pass on a version certifies the tree at that version, and a role signed by a key this machine does not trust gets its own refusal and repair because re-running would change nothing. not proved end to end: no test drives a real land with a second identity and a projected main, so the decision logic is pinned as pure functions and which copy is read and where the check sits are pinned by censuses on the source. red under mutation, counts read each time: the approval accepting any trusted key (34 passed and 1 failed), untrusted read as missing (33 passed and 2 failed), the landed-copy census fed the working tree (0 passed and 1 failed), the check moved after the break-glass (0 passed and 1 failed), and a require naming no step dropped silently (21 passed and 1 failed), each restored to 35, 35, 1, 1 and 22. ADR 0092 records the directive and what building it found, and workflow.md says how to turn gating on. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy, though the primary's release binaries must be rebuilt before a land enforces it. the workspace suite is green (4208 passed over 134 binaries, 9 ignored) (#2178)
946e7f14 · dbf3dbe6…diff
Renames are not followed. loot's tree maps a path to an address, so a rename is a delete and an add. This list is the history of the name, not of the bytes.