Changes touching this path

  • day 0: loot hosts loot f4c30e75 · dbf3dbe6…
  • evidence: crew minted and verified (#86) 1fada823 · dbf3dbe6…diff
  • embargo CLI (#88) + attack demo (#89) + section-B evidence + maroon propagation fix + drive setup c670cc2b · dbf3dbe6…diff
  • catch up: GB1 loot ferry lands (PR #114) 6816f50d · dbf3dbe6…diff
  • (working change) 02306b53 · dbf3dbe6…diff
  • Ferry drive binding: close the uncaptured-WIP clobber hole; ritual runs the bridge (#115) ferry_capture now snapshots unconditionally: disk edits that never saw a `loot status` had no working change, so the adopt path's full-tree materialize silently overwrote them. A capture snapshot identical to the anchor is dropped from the graph again (new DagRepo::drop_working over remove_head), so a quiet pass mints no redundant change and leaves no stray head for reconcile or the next pass's anchor derivation. tools/loot-day.ps1 now runs the daily ritual through the bridge (ADR 0028): capture day WIP when git sees it (-ForceSnapshot for git-ignored loot paths like docs/pitch), sync the local-only private mirror at .loot/git-mirror/mirror.git, one `loot ferry` pass, then the relay push and drive-log entry as before. Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com> 481fbc26diff
  • Ferry: recognize pulled content, or the co-located mirror walks off main (#116) In the co-located binding (the dogfood repo), git pull writes incoming content to disk before ferry runs, so the pre-ingest capture snapshotted it as a spurious working change, merged it with the identical ingested line, projected both, and moved the mirror main ahead of the checkout - breaking the next day's mirror-sync fast-forward push. The pass now ingests first (graph-only), then captures against BOTH the pinned anchor and the incoming target, forking explicitly from the anchor (snapshot_from) so a pre-dock home capture cannot fold the fresh ingested head in unclassified. Tree equality is by CONTENT (DagRepo::same_tree_content): sealing mints a fresh key+nonce per write (#98), so the capture and the ingest give the same bytes different addresses. New regression: pulled_content_is_not_recaptured (pure ingest, no merge, mirror main stays at the native commit, single head after reload). Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com> 28d855d0diff
  • resolve conflict at crates\loot-cli\src\workspace.rs 8a6c124a · dbf3dbe6…diff
  • catch up: sign-resolutions fix lands (PR #118) 8c37c7c7 · dbf3dbe6…diff
  • Fix #128: pull/apply collapses a concurrent two-writer fork (#129) Engine `apply_sync` ingests a peer's divergent tip as a *sibling head* and classifies per-path outcomes, but never merges tips — so a keyholder that had also advanced its own line was left on two heads with a working tree showing only its own side (the peer's content in the graph but never materialized). No CLI verb converged it (`dock merge` is docks-only). This is the gap the concurrent-agents proof surfaced under a real run. Add `Workspace::converge_heads(base)` — the peer-side analogue of `merge_dock` (ADR 0011: keyholders collapse forks on pull+apply). It folds every other head into our line via `merge_tips`, signs each merge so it travels, then materializes the merged tree (visibility-aware: a sealed path the identity can't open stays relayed, not dropped). `cmd_pull` calls it after the batch loop, passing the pre-pull head as our side, and now also honors `--porcelain`/`--json` like the other reconciliation verbs. Proven end to end by docs/evidence/scripts/concurrent-agents-demo.ps1 — both acts pass: Act 1 (local docks → harbor, real conflict + resolve + buoy), Act 2 (two identities push concurrently, relay DAG forks, agent's pull collapses it; public converges with no side dropped, restricted path relays R). Regression test: converge_heads_collapses_a_two_writer_fork_no_side_dropped. Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com> bb4c849fdiff
  • S1: implicit auto-snapshot on mutating verbs + demotion guard (#144) (#156) Flip the snapshot trigger from explicit to implicit (ADR 0030): every mutating verb captures the working tree first, so edits are never lost between commands and no manual `loot status` is needed. Read-only verbs still never snapshot. - Add `implicit_snapshot` + `SnapshotOpts` (--allow-demote repeatable, --no-snapshot/--ignore-working-copy) and wire it into `new`, `describe`, `grant` (both forms), `maroon`, `migrate`. A `positionals` helper strips the `--allow-demote <path>` value so it is never read as a verb positional. - `loot new` now captures pending edits before finalizing via `Workspace::finalize_capturing`, dropping an empty/tip-duplicate capture so a bare `new` mints no empty signed change. `working_message` preserves a described name across an implicit capture. - Demotion guard (#62) rides the implicit snapshot and is now a typed `RepoError::Demotion { paths }` (matchable, not a Backend string); the message is verb-agnostic ("re-run with --allow-demote"). - Docs: CONTEXT.md working-change + .lootattributes notes; USAGE. Verified end-to-end on the built binary (new/grant capture without status; demotion aborts + --allow-demote overrides; --no-snapshot skips) plus 6 new tests. Full workspace test suite + clippy green (no new warnings). Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com> 8dc00304diff
  • S0: stable-change-id data model + FORMAT_MAJOR 6 (#143) Implement the durable change-id data model per ADR 0029 — the keystone the jj-ergonomics trio (map #142) builds on. Two ids per change: the existing content-derived **version id** (`ChangeNode.id`, unchanged role: dedup, DAG edges, sync addressing) and a NEW random 16-byte **change id**, a durable handle stable across a working change's re-snapshots. - `ChangeNode` gains additive `change_id: Option<[u8; 16]>`; never folded into any hash. - The Workspace mints a fresh change id when a change begins and carries it across every re-snapshot (`snapshot_allowing` reads the prior working node's id before dropping it; `record_carrying` carries, `record` mints when authored). Keyless/bridge/legacy changes stay `None`. - Finalize signs over `version_id ‖ change_id` (new `change_signing_message`); `verify_authored_change` checks the same. A legacy change (`change_id = None`) signs over the version id alone, so pre-v6 signatures still verify unchanged. All four CLI finalize paths widened (finalize_working, sign_change, resolve). - Wire/durable codecs carry the change id after author+sig, gated on major >= 6 (`put_change_id`/`read_change_id`); idempotent on re-receipt. - FORMAT_MAJOR 5 -> 6; legacy decodes as `None`, no backfill. v6 goldens added, v5 kept as decode-compat. Parents, dedup, sync addressing, convergence: unchanged (they key on the version id). No display/verb changes — that is S2. Tests: change id stable across re-snapshots while version id rewrites; keyless mints none; v6 signed-over-both-ids verifies through apply; relabelling the change id after signing is rejected; v6 bundle/graph round-trip; v<=5 loads as legacy. Verified end-to-end via the CLI (alice bundles a v6 signed change, bob applies and verifies). f253ce09 · dbf3dbe6…diff
  • Merge pull request #157 from Connor-Miller/s0-stable-change-id-format-6 S0 — Stable-id data model + FORMAT_MAJOR 6 (keystone) (#143) Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com> 419d6996diff
  • S2: reconcile the verb surface — read-only status, columnar log, eager change ids (#145) Reconciles the daily verb surface to ADR 0030, consuming S0's durable change_id (#143) and S1's implicit snapshot (#144). - status is now READ-ONLY: it recomputes the pending delta live and never persists a snapshot; -m is dropped (naming is describe's job). It shows the working change's durable change_id alongside a live, non-durable version id (a plaintext content fingerprint, distinct by construction from the sealed snapshot id — Seam #1). - new is the finalize/sign boundary and eagerly mints + prints the *next* change's durable handle, so a fresh change has a name from birth. Adds the convenience `new -m`. init mints the first change's handle too. - log/status go columnar: change · version · message · vis · author, with the change id as reverse-hex LETTERS and the version id as hex DIGITS (ADR 0029) so the two ids disambiguate at a glance. The working change is rendered once, as a live row shared with status (log agrees with status). - status porcelain/JSON gain an `@` change-identity header carrying change_id + version id (hex); the ~ path rows and status chars stay a frozen contract. Engine: snapshot_assigning carries an eagerly-minted handle onto a fresh change's first version; working_preview computes the live, non-durable version id + emptiness without sealing or recording; mint_next_change_id gates on authorship. Store persists a per-dock next-change handle. bd5d6a8f · dbf3dbe6…diff
  • Merge pull request #159 from Connor-Miller/s2-verb-surface Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com> 42d3411bdiff
  • S4: operation log + undo (#146) Implement ADR 0031: an append-only, repo-wide, local-only operation log (`.loot/ops`) backing `loot undo` / `loot op log` / `loot op restore`, the safety net that makes ADR 0030's implicit auto-snapshot safe to trust. Core (`loot_core::oplog`): - `Operation` captures the resulting **view** — change-graph heads, each dock's working/tip pointers, the conflicts set, and the ambient-dock pointer — as raw pointer-file bytes, so restore is a pure pointer reset that never touches the object store or the append-only graph (nothing is ever deleted). - `record` appends one op per view-changing command; `undo` steps the view back one op and appends a compensating op (the log grows on undo, so redo lands); `op restore <n>` jumps to any op. A 1-based ordinal is the durable ref; a `pos` field walks the history back one step at a time. - Barriers: `push`/`grant`/`maroon`/`pull-grants` are recorded non-undoable; undo refuses to cross one and names the real remedy (keyring/manifest are one-way state a view reset cannot retract). CLI: - `loot undo`, `loot op log`, `loot op restore <n>` verbs + USAGE. - One `record_op` per view-changing command; read-only verbs record nothing. loot has no standalone snapshot op — S2 made `status` read-only, so every capture rides a mutating verb and that verb is the one op. - undo/restore reload from the restored files and re-materialize the ambient dock (writing the restored tree, pruning what the step removed). The oplog never enters a bundle (bundle serializes changes/objects/keys, never reads `.loot/ops`) — asserted by test. Tests cover append-on-undo, walk-back, barrier refusal, absent-pointer round-trip, and full CLI undo/redo. clippy clean. ce2f96a7 · dbf3dbe6…diff
  • Merge pull request #162 from Connor-Miller/s4-oplog-undo Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com> 4204625fdiff
  • S3: divergent change — marker + loot abandon (#147) Surface and collapse a divergent change per ADR 0029/0030 — the last slice of the jj-ergonomics trio (map #142). A divergent change is one durable change id carrying more than one live version id (two writers rewriting one change id); it is data, not an error, and is detected per change id, not by head-counting. - Engine: `divergent_change_ids(abandoned)` scans every node (a divergence can sit under a single graph head, e.g. as merge parents, with identical trees) and returns change ids with >1 live version; `versions_of_change`; `abandon_head` drops a version from the live heads. `record_carrying` is now public — the amend primitive that makes divergence exist (and constructs it in tests). - `log`/`status` render a trailing `!` on a divergent change id and list each version (shared `change_col` helper). A log whose only multi-head reason is one divergent change stays the flat listing — routed by distinct change *lines*, not head count, so the "run `loot apply` to converge" branch never mis-claims a divergence apply cannot collapse (Act 3 reproduces flat, as in the prototype). - `loot abandon <version-id>` drops a version, leaving the other live version(s) under the change id. Nothing is deleted — the version stops being a live head and joins a local-only `.loot/abandoned` set the live view filters. It refuses a non-divergent change (never hides a change's sole version), and is one undoable operation: the oplog View now also captures `.loot/abandoned`, so `loot undo` brings the version back. `resolve`/`dock merge` are untouched. Tests: engine detection + abandon; workspace abandon→collapse→undo and the non-divergent refusal; store abandoned round-trip; `change_col` `!` rendering. Live-verified Act 3 (flat `!` listing → abandon → undo restores). clippy clean. 43d5cd72 · dbf3dbe6…diff
  • Merge pull request #168 from Connor-Miller/s3-divergent-abandon Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com> dd573561diff
  • R6: structural snapshot-before-mutate - the Snapshotted handle (ADR 0030, #182) 6f0c24b7 · dbf3dbe6…diff
  • R1: Workspace becomes the CLI's only face of the engine (#177) 06340e98 · dbf3dbe6…diff
  • R2: one home for reconcile orchestration - Workspace::reconcile_onto (#178) b6c013fb · dbf3dbe6…diff
  • review fixes: retire with_repo, collapse forwarders, reconcile_* renames, fork-view naming (#177/#178/#182 + code-review) b071df66 · dbf3dbe6…diff
  • loot edit: amend a finalized change; supersession travels as signed predecessors (ADR 0032, #171) Implement the amend model: `loot edit <change-id>` reopens a finalized tip change as the working change - a sibling (parent = its parent, tree carried address-for-address, durable handle kept) whose `predecessors` names the reopened version - so once `loot new` signs the amend, the claim that X-prime replaces X is signed data that travels, not a local-only abandon. - Format: FORMAT_MAJOR 6 -> 7 (ADR 0019). ChangeNode.predecessors: Vec<Oid> rides the bundle + durable graph after the change id, canonically sorted, empty = ordinary; folded into the version-id computation (a no-op amend still mints a distinct version) AND into the finalize signature (version_id || change_id || predecessors) - ingest trusts received ids, so stripping/forging a supersession claim on the wire must break the signature directly. v7 reads v<=6 as predecessors-empty; goldens updated, v5/v6 kept decode-compat. - Liveness (amends ADR 0029's definition): superseded - named as a predecessor by any in-graph same-cid version, regardless of that supersessor's own abandoned/superseded state - joins abandoned as a live-view filter in divergence detection, versions_of_change, and log/status rows. Abandon means kill, never revert. - Converge: converge_heads drops superseded heads before collapsing forks (a solo amend lands at peers as a clean replacement, never content-merged with the version it replaced); dock merge adopts an amend of our tip as a fast-forward and treats the mirror case as a no-op (supersedes() requires the claim to sit ON the other line). - The verb: a named Workspace mutation; refuses on an in-progress or uncaptured working tree (the documented ADR 0030 exception - edit replaces the working change and never implicit-captures), on a divergent handle (abandon first), and on descendants (tip-only v1). One undoable op (ADR 0031); output through the render String seam. - dock switch: an idle dock no longer parks a tip-duplicate working child on its tip (the finalize_capturing duplicate-drop now runs there too) - the stray polluted the tip descendants and, post-0032, would have content-merged against amends. Tests: engine liveness + canonical hashing + signature strip/forge; codec round-trips + v7 goldens; workspace edit e2e / guards / undo / dock-merge FF / converge drop. Live-verified on the built binary (edit -> amend -> new; guards; undo). 366 tests + clippy clean. 8176f2e0 · dbf3dbe6…diff
  • ADR 0033: ferry survives amends — predecessor-conditional threading, Loot-Predecessors trailer, amend-aware reap, reconcile supersession guard, land review-currency guard (#199) f4129edb · dbf3dbe6…diff
  • (working change) 64d767d4 · dbf3dbe6…diff
  • loot dock rm: remove a dock, dropping parked unsigned WIP; undoable (ADR 0022 amendment, #212) c14695b4 · dbf3dbe6…diff
  • Liveness: one loot-core home for live/superseded/divergent/parked + the head partition (#216); CONTEXT.md vocabulary; rides along: cargo-dist release config (dist-workspace.toml, release workflow, 0.1.0 manifests) from the install-prototype session (loot-site map, #206) 3630469b · dbf3dbe6…diff
  • pull_via over a SyncTransport seam: the pull pipeline gets an interface and tests; negotiation uses complete heads (#217) 68836d58 · dbf3dbe6…diff
  • 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…diff
  • resolve conflict at crates/loot-cli/src/workspace.rs dc1e4ae1 · dbf3dbe6…diff
  • (working change) 4b9a24c5 · dbf3dbe6…diff
  • One materialize chokepoint: pull/apply capture-first, converge waits (#219) pull/apply now capture uncaptured disk edits into the working change before they touch the tree, like every other mutating verb (ADR 0030 amendment). A dirty pull ingests (graph append is always safe) but DEFERS convergence — the working-change guard makes converge a no-op for that pass — and emits a note ("captured working change <id>; heads left unconverged — finalize then re-run"). A clean pull converges as before. The seam: one internal tree-write chokepoint. converge_heads evaluates dirtiness ONCE at entry (before any head is dropped, so the reference stays queryable) and the adopt/merge materialize paths refuse over uncaptured dirt rather than clobber it; undo/abandon resurface is exempt by intent. capture skips only a mid-flight transfer (anchor closure incomplete, via new engine closure_complete) — a genuine delete-all still captures, never refuses. - workspace: capture_uncaptured_edits, tree_is_dirty_over chokepoint, PullReport { outcomes, deferred }; converge_heads gates both writes on disk_dirty. - cli: cmd_pull renders the defer note + records the op; cmd_apply captures. - core: extract closure_complete (negotiation_have reuses it). - ADR 0030 dated amendment + CONTEXT.md glossary; #169 pull-over-dirty gotcha becomes a guarantee. - tests: dirty divergent pull, dirty independent-head pull (defer then converge-after-finalize), chokepoint invariant, delete-all-captures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> d5dd28e4 · dbf3dbe6…diff
  • Spawn DevX (#232): loot lanes observability, --ticket lane spawn, wayfinder claim-to-lane f0a885c2 · dbf3dbe6…diff
  • Lane finalize: advance the tip for a lane, not just an active dock (#229) A lane (ADR 0034) sits on the home dock but is born with a *seeded* tip (spawn pins it at the finalized anchor over the shared store), so `docks_active()` is false and `finalize_working` took the `else` branch, leaving the tip pinned at the spawn anchor while `heads` moved on. A land from the lane then aimed git-main at the change's parent and moved nothing. The #195 harbor guard (ADR 0036) caught this live while dogfooding the harbor — refusing rather than false-landing. Advance the tip whenever this position tracks one (a lane always does); regression test included. Follow-up to the harbor (ebabcac). Break-glass git land: the loot-first lane path is blocked by this very bug, so it cannot land itself through a lane; the next ferry ingests this commit (docs/agents/workflow.md). 7f380ca6 · dbf3dbe6…diff
  • resolve: write only the resolved path, not the whole tree (#233) `loot resolve` re-materialized the entire resolution tree onto disk, reverting the operator's uncommitted edits to unrelated files. That is the clobber capture-first (#219) exists to prevent, and the reason reconciling a multi-conflict harbor bounce demanded "resolve one conflict at a time" plus manual git surgery — the fragility #233 set out to kill. `resolve` changes exactly one path in the tree (engine `resolve` inserts just `path`), and the merge that produced the conflicts already materialized the rest of the tree. So write exactly that one path and leave every other file — unresolved sibling conflicts and unrelated edits — untouched. Also advance the tip in a lane, not only when `docks_active()`: a lane is a home dock with a seeded tip and hits the same stuck-tip class #229 fixed in `finalize_working`. Regression test resolve_does_not_clobber_unrelated_uncommitted_edits: an uncommitted edit to an unrelated file survives resolving a conflict, and a sibling conflict stays open for the same pass. 533dd2d2 · dbf3dbe6…diff
  • #243: loot abandon --head <version> — drop a fork tip The mirror re-baseline needs to walk a drifted dock off a stale fork so a re-ferry fast-forwards onto landed git main instead of *merging* the stale tree (which resurrects files deleted upstream). `loot abandon` only collapses a divergent co-version; it refuses an independent fork head ("keeps its single version"). This adds the non-divergent counterpart. `Workspace::abandon_fork` reuses `abandon`'s undoable machinery — the node is union-preserved on disk (the shared graph is an immutable node store), the abandoned set keeps it out of the live view, `loot undo` restores it — minus the divergence gate, plus two guards: refuse a version that is not a live head, and refuse dropping the dock's last live head (never empty it). Wired as `loot abandon --head <version>`; two seeded-fork tests. abc2c58e · dbf3dbe6…diff
  • #244: loot adopt <version> — settle a dock onto a landed change The re-baseline primitive #243 needs and could not do safely. A dock can end up on a divergent local line that must be *discarded* in favour of a landed change (the #243 state: the primary main dock on a stale fork while origin/main moved on). apply/converge MERGES the lines — against a stale fork that resurrects files deleted upstream; abandon --head drops a signed fork tip but not the working-change head. adopt <version> fills the gap: take the target wholesale, abandon every competing head, no merge. Composition of shipped parts (no new engine machinery), per the committed spec docs/specs/loot-adopt-target.md: - Workspace::adopt: resolve the target among live finalized changes, fence it to the harbor/main lineage (reachable from the change the mirror's main projects, read from the local ferry spine — no network, no git process), WIP-gate (refuse a dirty dock unless --discard-wip), then abandon every competing head to a fixpoint (dropping a transient ferry merge resurfaces its parents, so the whole divergent line is walked into the abandoned set), settle tip on the target, and materialize its tree via the existing resurface checkout — one undoable op (ADR 0031). - loot adopt <version-id> [--discard-wip] CLI wiring (COMMANDS + USAGE). - Amend ADR 0034 (the <version> take-wholesale arm + discard-vs-merge table) and the CONTEXT.md Adopt entry. Seven tests: settle+abandon+undo, dirty-refuse, discard-wip drop, off- lineage refuse, unsigned-target refuse, a merge-over-stale-fork fixpoint walk, and an end-to-end adopt-then-ferry asserting nothing projects (§6.5). Verified live through the CLI: rolled a drifted dock back onto a landed change, undo restored it, and the lineage guard refused an ahead-of-main target. Deliverable 3 (the §6 live reconcile that finishes #243) is an operator-run repair of live .loot state and stays a follow-up per execution order §9 (it runs after this branch lands). 01bae608 · dbf3dbe6…diff
  • fix(loot-cli): finalize advances a seeded home-dock tip (adopt/merge), not just lanes ab4147dc · dbf3dbe6…diff
  • feat(loot-cli): no-arg `loot adopt` — harbor catch-up merge (#250, ADR 0034) 22524137 · dbf3dbe6…diff
  • refactor(loot-cli): retire in-place dock switching from the CLI (#3b layer 1; layer 2 = #253) c62ec238 · dbf3dbe6…diff
  • gc roots the whole shared store and catch-up verbs ingest the harbor lineage, so a lane-landed change the primary never adopted fast-forwards instead of merging, duplicating, or being pruned (#265, #263 prevention) 4c03b44c · dbf3dbe6…diff
  • adopt catch-up refreshes a stale working capture the disk moved past, so a git-reset-onto-landed-main primary fast-forwards instead of merging (#265 dogfood follow-up) 16b6cbc7 · dbf3dbe6…diff
  • finalize refuses to sign an un-described change, and status's hint names describe -m (#174) `status`'s hint on a dirty tree pointed at `loot new`, but `new` is capture-*then*-finalize: following it signed the dirty tree in one stroke under the `(working change)` placeholder, skipped the review lane, and rode to git main as a permanent commit subject. The hint was the trigger; `new`'s willingness to sign a nameless change was the foot-gun. - The hint names `describe -m` — capture *without* finalize, the first verb on dirty work. - `finalize_capturing` refuses an un-described change (no message, or the stored placeholder). It sits *after* the capture, so edits are held and only the signature is withheld, and *below* the empty/duplicate drop, so a bare `new` on a clean tree stays a no-op. Both callers inherit it — `loot new` and `loot-first land` — so 'describe before landing' is enforced, not remembered. - Deriving a subject from changed paths was rejected: it mints plausible history nobody wrote, and loot has no changed-path concept to derive from. - The placeholder now has one mint (`working_message_or_placeholder`) and one test (`is_undescribed`); 10 stray literals collapsed onto them, so the guard cannot silently stop firing on a drifted copy. ADR 0030 amendment + CONTEXT/workflow/evidence updated. Residual (dock merge and ferry sign the operator's authored work in passing) named in the ADR and filed as #275. a8fb4684 · dbf3dbe6…diff
  • merges refuse to sign an un-described change too, closing the #174 residual (#275) #174 stopped the *deliberate* finalize from signing a nameless change, but three verbs still sealed the operator's own work in passing, to make a signed merge parent: `dock merge` and the `adopt` catch-up (via `fold_line_in`), and `ferry` over a git main that moved (via `reconcile_capture`). Only the *trigger* was mechanical — the content was authored — so the placeholder still reached main. Reproduced against the pre-fix binary: a ferry over unnamed disk work landed the literal subject `(working change)` on git main in one silent pass. Both paths now refuse an un-described merge parent, below their capture (edits held, only the signature waits) and below a redundant-capture drop (a pass with no real work to sign stays a no-op, never a nag). Two reasons the ADR gave for NOT doing this were checked and dropped: a land never reaches these paths dirty (it finalizes before it ferries), and #219's refuse-on-dirt rejection was about being forced to capture, not about being asked for a name. The merge *nodes* are untouched — they are machine-authored and already carry an honest mechanical subject. That is the line: mechanical content may be named mechanically, authored content may not. - `refuse_if_undescribed` (one rule, two messages) + `drop_capture_if_redundant` (the empty/duplicate drop, previously copied at two sites and missing at a third — which is what would have made `dock merge` nag). - Cost, documented: uncaptured dirt takes two passes, since naming *is* capturing. `loot-first review` asks for a name only when it must merge. ADR 0030 amendment (correcting the two wrong claims), CONTEXT, workflow, and concurrent.md updated. Closes #275. aced27c1 · dbf3dbe6…diff
  • reconcile_onto captures whenever it may materialize, not when git brought new commits (#280) 802cbf5f · 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
  • Judge the tip-duplicate drop by recorded manifests, so deletions count (#289) A change whose only content was DELETING files was silently destroyed at finalize: same_tree_content compared tree_at ancestry overlays, and the union resurrects an ancestor's entry for every path the child deleted, so a deletion-only capture read as content-identical to the tip and drop_capture_if_redundant ate it - describe message and all. This very bug ate its discoverer's cleanup change (#284/PR #285); the workaround that landed was riding a one-line edit alongside the deletions. same_tree_content now compares the two changes' recorded manifests (every change carries its complete tree, deletion = absence): path set AND content. An empty capture is redundant only when nothing is held to compare against - over a non-empty tip it is a delete-everything change and signs like any other work. Same judgment at the adopt catch-up's inline site, the live status row, and working_preview's tip slice (a clean tree no longer reads dirty forever once a deletion lands). Truly identical captures - the bare new on a clean tree, the co-located checkout after a git pull - still evaporate; an undescribed deletion now hits the #174 refusal instead of silently no-op'ing. ADR 0030 amended. 06d1880e · dbf3dbe6…diff
  • Run the pre-land gate in the landing position's tree, not the shared root (#287) orchestrator::paths() derived its one `root` from ws.dot() -- always the SHARED store's .loot -- so a land from a lane ran `cargo test` (and the relay push) in the primary checkout's possibly-stale tree. The #284 land was validated only because the operator had run the suite in the lane by hand. Same shared-vs-position confusion as the #229 mirror-gitdir and gh-cwd dogfood fixes. Fix: split Paths.root into `position` (Workspace::root(), new accessor: the lane dir from a lane, the checkout on the primary) and `checkout` (the shared .loot's parent). The pre-land cargo test now runs in `position` -- the tree about to be signed. relay_push decision: it now also runs in `position`, deliberately. loot push discovers .loot from its cwd and a lane's .loot points at the shared store, so the push ships the same store from either dir -- but from the position it reads the tip this land just signed (has_unsigned_tip) and records its push op in the landing position's own oplog instead of writing the primary's (ADR 0034 single-writer). The spawned binary is now resolved beside the running loot-first (one cargo build produces the pair) with the old target/release fallback, because a lane tree carries no release build of its own. The drift guard's git reads and init-hook stay on `checkout` on purpose: only the primary has a .git; run from a lane dir every probe would fail and silently mute the guard. Tests: loot-first regression pair (lane position vs checkout, primary degenerate case) built on a real spawned lane via temp-dir helpers; loot-cli locks root()-vs-dot() semantics at the Workspace seam. 46f1b598 · dbf3dbe6…diff
  • resolve conflict at crates\loot-cli\src\workspace.rs afdeeae4 · dbf3dbe6…diff
  • Fix reconcile-merge resurrection of long-deleted files (#288): a change tree is a manifest, not an ancestry overlay Live incident 2026-07-16: the #281 land's `ferry --with-wip` reconcile minted merge d3ca4b8 carrying tools/loot-first.ps1 (deleted in the #218 tail) and crates/loot-first/src/ledger.rs (moved in #232) — deleted months earlier on every line involved; neither merge parent held them. Published to origin/main, cleaned up by PR #286. Root cause (loot-core, not the bridge, and not the merge base): every recorded change carries a FULL path->address manifest — snapshot, ingest_change and merge_tips all record whole trees, and deletion is absence from the child's manifest — but ChangeGraph::tree_at/current_tree computed a tip's tree by unioning every ANCESTOR's tree child-wins (delta semantics no production node ever had). Every path ever deleted anywhere in the ancestry re-entered the computed tree forever. merge_tips fed those polluted trees to the converge classifier, which saw the same stale address on both sides (untouched) and kept it; projection then faithfully published the merge manifest. The suspicion that the merge base predated the deletions was wrong — common_ancestor_tree always returned the ancestor's exact manifest; the resurrection needed no base at all (both POLLUTED inputs re-raised the paths, base or no base). The live history had exactly two ever-deleted files, and the merge resurrected exactly those two — confirming the union mechanism. Fix: tree_at returns the change's own manifest; current_tree unions the HEAD manifests only (preserving the pre-dock multi-head view). Repro tests at all three layers, each proven red under the old semantics: change_graph (tree_at_honors_a_deletion_instead_of_unioning_the_ancestry), engine (merge_tips_does_not_resurrect_a_path_deleted_before_the_fork), and the incident-shaped ferry test (reconcile_merge_does_not_resurrect_files_deleted_on_the_spine): spine deletes a file, lines fork after it, a git-native commit lands concurrently, the reconcile merges — the deleted path must appear in neither the merged loot manifest nor the projected merge commit's git tree nor on disk. Two tests that encoded the union semantics were corrected to record full manifests (an empty/partial tree in a seeded change means delete-all, which is what those tests accidentally said). ADR 0028 gains an amendment. Out of scope, noted there: the classifier still has no deletion-vs-base rule, so a path freshly deleted on ONE side since the fork is still re-adopted from the other side of a reconcile merge — follow-up ticket to come. 77f95feb · dbf3dbe6…diff
  • resolve conflict at crates\loot-cli\src\workspace.rs db39d945 · dbf3dbe6…diff
  • Restore the #289 regression tests the #288 reconcile clobbered; re-delete the resurrected pair The #288 land (tree_at/current_tree = recorded manifests) crossed this lane mid-flight; its reconcile-merge took the landed engine.rs and ferry.rs wholesale, dropping the #289 additions there. Restored: the engine-level same_tree_content deletion pin, the end-to-end deletion-projection ferry test, and the frozen-primary fixture line in the gc-pruned adopt test (an unmaterialized dir now reads as a delete-everything edit - #289 deletions are real work). Also re-deletes tools/loot-first.ps1 and crates/loot-first/src/ledger.rs a third time: this lane's earlier reconcile merges were minted by a pre-#288 binary whose overlay merge manifests resurrected them, and the final reconcile re-added them right after the #288 land's 7bcdf8e re-deletion. The lane binary now carries both fixes, so this deletion can sign (#289) and the merge line stays a manifest (#288). 81d04c47 · dbf3dbe6…diff
  • resolve conflict at crates\loot-cli\src\workspace.rs 907d432c · dbf3dbe6…diff
  • Re-delete the two files pre-#288 sibling reconciles resurrected; carry the #288 fix line to main The #288 root fix (change pyzurxul, projected commit 9763144) is already on this line: ChangeGraph::tree_at/current_tree treated node trees as ancestry deltas while every recorded change carries a full manifest, so every path ever deleted re-entered every computed tree and merge_tips resurrected long-deleted files into reconcile merges (live incident d3ca4b8, cleaned by PR #286). tree_at now returns the change's own manifest; current_tree unions the head manifests only. Repro tests proven red at three layers (change_graph, engine merge_tips, and the incident-shaped ferry reconcile test); ADR 0028 amended. This change is the tail of the same land: while this lane reviewed, sibling lanes running pre-fix binaries reconciled and the bug fired AGAIN — merge 07d2e19 re-raised tools/loot-first.ps1 and crates/loot-first/src/ledger.rs into the shared mirror main line, and this lane's (fixed, deletion-safe) reconcile could only adopt what the incoming manifests carried. Deleting them here makes the landed tip clean, the third time these two files have been deleted (69fd3cd/4f0a34b, f0525bf, here) and the last: with the union overlay gone their deletion can no longer be forgotten. Also folds in the #287 (pre-land gate tree) and #289 (deletion-aware tip-duplicate drop) lands via conflict resolutions on ferry.rs/workspace.rs/engine.rs — both sides kept, full suite green (551 tests). 0e0fdbb6 · dbf3dbe6…diff
  • resolve conflict at crates\loot-cli\src\workspace.rs 5cfdf7b5 · dbf3dbe6…diff
  • Land the #288 fix line: delete the resurrected pair once more, clean tip The #288 root fix (tree_at/current_tree treat change trees as the full manifests they are, never an ancestry union — see change pyzurxul and the ADR 0028 amendment) rides below this change, already signed. This tip change re-deletes tools/loot-first.ps1 and crates/loot-first/src/ledger.rs — the merge with the sibling line re-adopted them one last time (the still-open one-side deletion-vs-base classifier gap; the pre-fix union could resurrect them from ANY depth, the remaining gap only from a live unmerged line) — and keeps the sibling workspace.rs resolution folded (both #287 root() and the #288 seeded-manifest test fix). Full suite green (551 tests). 51470b34 · dbf3dbe6…diff
  • Restore the #289 workspace.rs side the two-lane resolve ping-pong dropped Two lanes (#288, #289) each held workspace.rs conflicts and each resolve took its own side wholesale: the #288 land carried a workspace.rs without the #289 finalize-path changes, and the reconcile materialized it over this lane. Restored from this lane's sealed 8517905: the drop_capture_if_redundant empty-arm (an empty capture over a non-empty tip is a delete-everything change - only a fresh repo's bare new drops it), the live status row manifest judgment, the adopt catch-up inline site, the five finalize-path regression tests, and the divergence-test manifest fixture. engine.rs resolves to the landed union (direct-manifest same_tree_content + #288 tree_at + the #289 engine pin) - both fixes, one file, no losers. 5047a070 · dbf3dbe6…diff
  • primary catch-up: adopt landed main (post #287) 7e3bbe42 · dbf3dbe6…diff
  • resolve conflict at crates\loot-cli\src\workspace.rs ada48ede · dbf3dbe6…diff
  • Refuse the review catch-up fold; never strand a described WIP (#292) (#302) `loot-first review` (and `loot ferry --with-wip`) run a catch-up ferry before projecting the WIP. When git `main` moved *under the lane* (a concurrent land) while a live, described working change sat on the tree, `reconcile_onto` captured and FINALIZED that WIP as a merge parent, folded it into a "ferry: reconcile git main" change, and minted a fresh empty working change. Review then projected the empty change and reported "nothing to review" — the work signed-but-stranded: unreviewable, unlandable (land finalizes a *working* change), and with no op-log entry to undo. #257 had to land via break-glass git PR. Fix: thread `preserve_wip` into `reconcile_onto`/`reconcile_capture`, set only by the review projection (`with_wip`). Under it, a catch-up that would fold real local work refuses (REFUSE_REVIEW_STALE_ANCHOR) BEFORE finalizing, leaving the WIP a live, unfinalized working change (only the signature withheld — the capture persisted). The no-op paths (main where we left it) still project un-described WIP untouched, and a plain ferry/land (with_wip=false) keeps the existing fold. Defect 2: the review's in-process ferry never passed through cmd_ferry's op record, so an honest catch-up (a clean fast-forward onto landed main) left no op to undo. The orchestrator now records a "ferry" op after the review pass, mirroring cmd_ferry. Tests: a new reconcile_onto regression proves the described-WIP fold is refused and the WIP survives; the #288 resurrection test moves to the plain-ferry reconcile path (review no longer folds); the #275 review test now asserts the #292 refusal instead of the old name-then-fold. Full `cargo test --workspace` green. Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com> 3e83f40ediff
  • Catch up to git main 809ddfe: #260 smoke default, #271 arm64 build, loot diff (#1), ADR 0034 dock retirement (#253) 587684dd · dbf3dbe6…diff
  • ferry ingest: compose the full parent tree or refuse; roll back an aborted pass's ingest (#307) cc115440 · dbf3dbe6…diff
  • Relay: explicit 64 MiB body limit; push batches byte-capped (#309) 0ac2fa23 · dbf3dbe6…diff
  • loot diff --conflict <path>: inspect both sides of a conflict (#13) 5f50590f · dbf3dbe6…diff
  • loot status: show new/modified/deleted vs previous change (#7) eee3a1d5 · dbf3dbe6…diff
  • #315: route log/attest/abandon through resolve_selector (one #305 selector grammar) - abandon: swap the hand-rolled resolve_live_version call for resolve_selector, so @ / HEAD / HEAD~n / a version-or-change-id prefix all name an abandon target, same as loot diff already accepts. - attest: retire resolve_change (its bespoke version-hex-prefix match) in favor of resolve_selector. The returned Oid is a version id, exactly what Attestation records under change_id throughout the engine (attestations_for/buoy_resolution are keyed by version id) -- no version->change-id remap is needed or type-correct here. Keep the ADR 0018 guard that refuses the still-mutable working change, now checked post-resolve (@ legitimately resolves, since diff wants that too; attest alone must still refuse it). - log: add Workspace::ancestors_of, a full multi-parent ancestor walk (unlike walk_single_parent's HEAD~n rule, it never refuses at a merge). `loot log <selector>` resolves the selector and filters the rendered view down to that version's ancestry; the live working row only survives when the selector names it directly (`loot log @`). No selector -> unchanged full-history behavior. New Workspace-level tests prove resolve_selector composes with attest, abandon_fork, and the new ancestors_of walk; cmd_* stay untested directly per the existing loot-cli idiom (a full invocation would walk cwd up to a real .loot). 3f7ca995 · dbf3dbe6…diff
  • #175: lock the fresh-empty-working row's distinct identity with a regression test 56aceb77 · dbf3dbe6…diff
  • Workspace accepts its clock; lane flag-gate goes pure so cmd_* tests never touch a real .loot (#322) 20ac82e1 · dbf3dbe6…diff
  • Position module: one home for tip / dock / anchor resolution (#324) 9788465b · dbf3dbe6…diff
  • Pure decide/execute split for reconcile_onto (#325) c23c23bd · dbf3dbe6…diff
  • loot log --path <file>: filter history to a path (#6) f3de23a6 · dbf3dbe6…diff
  • Add loot verify: object-store integrity check (#19) - rehash every loose object against its address, report corrupt and missing by address, exit 1 on problems; load-free so a corrupt store can still be diagnosed aeb147b3 · dbf3dbe6…diff
  • resolve conflict at crates\loot-cli\src\workspace.rs 0a42dc37 · dbf3dbe6…diff
  • Fix pr-map ledger lost-update: serialize writes under pr-map.lock (#336) review and land both did read -> mutate -> whole-file rewrite of .loot/git-mirror/pr-map with no lock, and land's read happens minutes before its write (tests, ferry, push) - a land clobbered all three rows sibling reviews recorded mid-flight (live, 2026-07-18). Every ledger write now funnels through one door, update_pr_map: take git-mirror/pr-map.lock, re-read fresh, apply only this operation's own row add/remove, replace atomically. Early reads in review/land are lookups only, never written back. The ledger lock is deliberately separate from the harbor lock: reviews must not queue behind a land's git-main section, and land's ledger close-out runs after harbor release anyway. The write is store::atomic_write like every replaced git-mirror spine file (#307), and readers (orchestrator + loot lanes) use store::read_replaced so the Windows rename-replace window (#293 tail) cannot read as an empty ledger. HarborLock::acquire_contending carries a caller-supplied contended message so a wedged ledger never claims to be a wedged harbor. ADR 0034 amended; concurrent.md ownership note refined. 11addd8f · dbf3dbe6…diff
  • loot embargo-status <path>: report embargoed/revealed/not-embargoed (#15) cb471c93 · dbf3dbe6…diff
  • Bounced-land resolutions inherit the described ours-line subject (#337) fbcb501d · dbf3dbe6…diff
  • loot embargo-status <path>: report embargoed/revealed/not-embargoed (#15) (conflict resolution: crates/loot-cli/src/workspace.rs) 0f4f4ca9 · dbf3dbe6…diff
  • Build the mis-seal gate: secret-name refusal + first-seal summary (#343) 12d2ea42 · dbf3dbe6…diff
  • Grant expiry: optional expires_at on GrantEntry and tag-3 wire (#20) a01c3d53 · dbf3dbe6…diff
  • loot grants --quarantined / --trust: review and trust quarantined senders (#12) 86c46a0d · dbf3dbe6…diff
  • Build loot burn: destroy + tombstone, no resurrection (#344) 0d1cc5af · dbf3dbe6…diff
  • Mis-seal gate covers loot edit and audited signing chokepoints (#353) e4e3a804 · dbf3dbe6…diff
  • loot id rotate: new keypair, expiry-preserving re-grant wave, archived old key (#16) 2b16256f · dbf3dbe6…diff
  • pull_via: one metadata-only fetch when wants is empty (#370) A change whose objects the puller already holds never ingested: the change delta only travels in fetch responses, and an empty wants list made zero fetch round-trips. Live shape (found by the #340 evidence run): a sealed grant pre-delivers the object ciphertext, so the grantee's next pull strands on nothing-new while the relay is ahead; a pure-deletion change (no new objects at all) strands the same way. pull_via now makes one metadata-only fetch(have, []) when wants is empty - the empty-wants bundle the server half already serves. Because that bundle also re-delivers every change the head-id negotiation cannot name as held, the branch keeps the applied outcomes only when the fetch actually moved the head set, so a genuinely up-to-date pull still reports nothing-new. Tests: grant-pre-delivered object ingests; pure-deletion change ingests; the existing nothing-new re-pull contract holds. 3d2e8cbd · dbf3dbe6…diff
  • ADR 0039 build: pure-projection review + carry-at-land (#362) Review mode (`ferry --with-wip` / `loot-first review`) is now a pure projection: no ingest, no dock reconcile, no mirror-main advance, no spine rewrite - it mints the provisional commit from the lane's own anchor marks and pushes only review/<position>. A lane behind git main reviews normally; REFUSE_REVIEW_STALE_ANCHOR is deleted with the fold it guarded (#292/#302), and the #349 review-mode trigger is structurally gone. Reconcile lives only at the signing verbs, and its diverged-line shape is now the carry (DagRepo::carry_line): a self-authored suffix replays onto landed main as superseding versions - same change id, same subject, single parent, stale original kept as predecessor - so landed history stays exactly one commit per change with no ferry: 1412f811 · dbf3dbe6…diff
  • Drop an unused binding the #362 land's pre-land gate warned on The carry-at-land workspace test kept a leftover `let ours` from the test it was modeled on; every cargo test run printed the warning. de601197 · dbf3dbe6…diff
  • Candidate 3: extract the visibility-policy module from Workspace CONTEXT.md calls .lootattributes "the user-facing surface of loot's thesis," but its whole machinery lived as private helper types at the bottom of the 8,759-line workspace.rs, reachable by tests only through a full Workspace. Lift it into one cohesive crate::policy module (the codebase-design review's candidate 3): - Glob + glob_match (the shared dialect: * stops at /, ** crosses it) - Ignore (.lootignore parsing + file/dir pruning) - Attributes (.lootattributes rules + visibility_for + the mis-seal gate's public_by_fallthrough consent test) - SECRET_NAMES + is_secret_name + is_catchall (ADR 0038 §1 mis-seal gate) - parse_visibility, and the ATTRS/IGNORE filenames Pure move, no behaviour change: Workspace imports these from crate::policy and consults them at snapshot and the signing seams exactly as before; the engine still receives already-resolved visibilities. workspace.rs drops ~230 lines of policy machinery; the thesis surface now has locality and one home. Full loot-cli suite green (mis-seal gate, glob-dialect, and characterization tests included). Follow-up worth doing: migrate the pure glob/is_catchall/is_secret_name unit tests from workspace.rs's test module into policy.rs so the interface is also the test surface; they currently reach across via a crate::policy import. 65596200 · dbf3dbe6…diff
  • Candidate 2: extract the lane-registry lifecycle from Workspace The lane registry lifecycle — spawn_lane/spawn_lane_as, name_lane, lane_list, lane_statuses (+ peek_lane), remove_lane, lane_gc (+ reap_entry), and the placement/id helpers (find_lane, ensure_lane_name_free, default_lane_dir, lane_key_taken, free_lane_id) — was ~340 lines inside the 8,600-line workspace.rs, the ADR 0034/0035 single-owner and heartbeat rules living as scattered methods (the exact class whose enforcement drifting caused the lane-gc near-miss). Lift it into a workspace::lanes child module (the codebase-design review's candidate 2, last Strong-strength item). Pure relocation, no interface/behaviour change: these stay Workspace methods in a child module, reaching the private position/store/root via super::*, the same shape as the engine's Custody (#323) and negotiation extractions. find_lane is pub(super) because merge_lane (kept in the parent) resolves its source through it. The lane *convergence* verbs — merge_lane and adopt_harbor — deliberately stay with the reconcile code: they are folds, not registry lifecycle. The lane report structs (SpawnedLane/LaneStatus/SweepOutcome) stay in workspace.rs so main.rs's imports are untouched. Full loot-cli suite green, lane spawn/gc/name/rm/statuses tests included. 2da6b220 · dbf3dbe6…diff
  • Candidate 5 (slice): move lane report DTOs into workspace::lanes The lane report DTOs (SpawnedLane, LaneStatus, SweepOutcome) and their registry-side free helpers (reap_lane_dir, gen_lane_handle — called only from the lane methods) still sat in workspace.rs after candidate 2 moved the lane *methods* out. Relocate them into workspace::lanes so the lane concept has full locality, and re-export the three public DTOs (`pub use lanes::{...}`) so workspace::LaneStatus etc. stay stable — main.rs's imports are untouched. loot-cli suite green (the lone describe_contention failure was the known load-dependent flake: it passed on re-run, and this change touches neither the describe/snapshot path nor any concurrency logic). Scope note on candidate 5: this is its clean, non-conflicting slice. Two parts were deliberately NOT done: - prose-into-Shape: emit.rs's human rendering is kept OUT of emit/verdict by a deliberate decision (R5/#181) because it needs the peer registry for attester names; folding prose into the shapes would re-litigate that ADR- like call and belongs in a design discussion, not a mechanical move. - the broader report-DTO migration (EditReport, PathDelta, ConflictView, AdoptReport, PullReport, StepReport, BurnReport, HistoryRow/View, …) is ~15 structs interleaved with impl-carrying types (Graph, Remotes, Snapshotted) and referenced by render.rs — a fiddly scattered move worth its own pass, unlike this cohesive lane-local block. f83cb2dc · dbf3dbe6…diff
  • Candidate 5: extract the CLI report DTOs into workspace::reports The plain-data report values Workspace verbs return for the CLI to render — WorkingRow, EditReport, DeltaClass, PathDelta, ConflictSide, ConflictView, AdoptReport, AdoptCatchupReport, PullReport, StepReport, BurnReport — were a ~210-line contiguous block in the 8,000-line workspace.rs, so a verb-output tweak meant editing the god-file even when no engine logic changed. Lift them into a workspace::reports child module and re-export (`pub use reports::{…}`) so workspace::EditReport etc. stay stable for main.rs and render.rs — zero external churn. Pure data relocation, no behaviour change; loot-cli lib (223), bin (55), emit_snapshot (3), and cli_smoke (8) suites all green. With the lane DTOs (f1bcaa6) this covers candidate 5's DTO-locality goal. A handful of report DTOs (HistoryRow/HistoryView/GraphHistory/BuoyResolution, IngestAct, SnapshotOpts) stay in workspace.rs for now — they are separated by impl-carrying types (Remotes, Snapshotted), not a clean block, and the win is already banked. Candidate 5's prose-into-Shape half is deliberately not done: emit.rs keeps human rendering out of emit/verdict by design (R5/#181, needs the peer registry) — that is an ADR-level call, not a mechanical extraction. ea9d65b8 · dbf3dbe6…diff
  • Pass-2 candidate 5: unify the key=value config codec into loot-cli::kv ferry::parse_kv and workspace::parse_config_text were byte-for-byte identical (trim, skip #/blank, split_once('=') → BTreeMap), and three call sites hand-rolled the matching `{k} = {v}\n` encode (ferry::write_kv, Config::save, GlobalConfig::save). One `kv::{parse, encode}` module now owns the dialect; ferry (git_config / git_identity_map spine files), the per-repo .loot/config, and the global config all call it. Behaviour-preserving; the module carries its own round-trip/comment-skip/ordering tests (4). loot-cli lib suite green (227, +4 kv). This is the non-blocked core of pass-1's candidate 6 — pure text, no WipState → loot-cli-ledger cross-crate coupling. 9b171a5c · dbf3dbe6…diff
  • Build the seal-WIP guard (#418): refuse a bare sync verb that seals live described WIP Graduated from #356's "Prevent + hint" resolution (map #354). After ADR 0039 (#362) made review a pure projection, the only way to strand a described working change as a PR-less signed line is a *deliberate* bare sync verb. Guard that path at its source. - New typed refusal RepoError::SealWip { subject, verb } in loot-core, a sibling of MisSeal/Demotion, on the ADR 0030/0038 guard+override pattern. Overridable with --seal-wip on both verbs. - ferry seal path: thread seal_wip through ferry::run -> reconcile_onto; the Merge-with-wip arm (the ONLY place a bare ferry seals — land pre-finalizes, adopt folds through fold_line_in) refuses unless overridden. reconcile_onto now returns the sealed subject so the caller reports the seal from the seam that decided it, not a post-state heuristic. - adopt seal path: thread seal_wip through adopt_harbor; refuse before fold_line_in when a described line diverged. The clean fast-forward and redundant-capture drop return earlier, so a no-op catch-up never trips. - Fires ONLY on a described line the sync would actually fold: an un-described one stays the #275 refusal; a break-glass ferry / no-op sync (no live described WIP) is untouched. - On override the verb prints the follow-up-round recovery recipe (SEAL_WIP_RECOVERY, one shared seam); the review "nothing to review" and land "not in the pr-map" paths print it too when sealed_unlanded_anchor() detects a signed line ahead of mirror main with no PR. - tag's ferry passes seal_wip=false — it is not a finalizer, so it refuses rather than silently seal live WIP while cutting a release. - docs/agents/concurrent.md §"One seal path remains" rewritten to describe the guard; CONTEXT.md gains the "Seal-WIP guard" vocabulary entry. Tests: guard at both seams (refuse without / seal with --seal-wip), un-described stays #275, no-wip catch-up untripped, full ferry::run break-glass + no-op pass untripped, sealed_unlanded_anchor signal. Full suite green (loot-core 337, loot-cli 243, loot-first 69); the describe_contention CPU-load flake passes in isolation. 42409371 · dbf3dbe6…diff
  • Slice 6 review fixes: stream pull, distinct setup error, clean empties (#428) Addresses the code-review findings on 4ca54a2: - pull() now STREAMS the child's stdout via spawn (was buffered via execFile then yielded once) — satisfies the read/pull streaming AC. - Missing/incompatible binary gets its own SetupError (code "setup"), distinct from generic failures and the deferred-private path; an old binary lacking `surface --json` / reading an older format maps to it too. - allowReveal is rejected with a clear error rather than silently dropped (physical slice 6 authors public content; reveal isn't mappable); guard mapping centralized in guardArgs. - Empty-repo machine output no longer string-matches the "nothing to surface" error: new Workspace::surface_tree() returns None on a headless repo, so cmd_surface emits an empty tree cleanly (no prose-scraping). - read()'s ENOENT→NotFound handling deduped into a shared streamFile helper (was repeated in the collector and the iterator). Acknowledged, kept (documented in code): the client-side overlay/baseline for status kinds (loot has no kinded-delta machine output, so capture-first can't "map directly" for status); read streams the materialized file rather than a `loot` stdout (no cat verb); the faithful status/overlay mirror of RelayRepo. Green: loot-cli 243, 32 SDK tests, tsc clean. Refs #428. 3ece1bad · dbf3dbe6…diff
  • CLI: thread typed engine errors through Workspace so slugs travel (#430 follow-up) #430 laid the CliError plumbing but the taxonomy stayed inert on live verb paths: workspace.rs methods returned Result<_, String>, stringifying RepoError before the verb's `?`, so every engine variant collapsed to code "error" at the --json boundary. Convert that boundary: impl Workspace / Snapshotted / lanes methods return Result<_, CliError>; engine (self.repo.*) sites use .map_err(CliError::from) so code() is preserved; the MisSeal/SealWip guards propagate the typed RepoError instead of .to_string(). Non-RepoError sources (io, format!, bare strings) stay code "error", messages byte-for-byte intact. Workspace::open/init stay Result<_, String> (open_repo() stamps the CLI-level no_repo slug), keeping the loot-first ripple to two boundary conversions. New emit_snapshot test proves a real RepoError::UnsupportedFormat reaches --json stderr as {"code":"unsupported_format"} on `loot apply <garbage>`, with the non-json `loot: <message>` line unchanged. Also: share loot_core::verdict::json_string (drop error.rs's byte-identical twin) and collapse the emit_snapshot run_with_code/run_stderr_with_code helpers into one run_streams. Known residual: the ferry.rs bridge path still flattens engine errors to "error" (deliberate scope boundary — git2 errors ride its String contract). cargo test --workspace green. 627897cb · dbf3dbe6…diff
  • loot bisect: binary-search history to find a regression (#390) bec6bfc5 · dbf3dbe6…diff
  • loot grep: content search across history via the key oracle (#391) e47d1412 · dbf3dbe6…diff
  • loot bisect: binary-search history to find a regression (#390) (conflict resolution: crates/loot-cli/src/workspace.rs) 12554f59 · dbf3dbe6…diff
  • loot cherry-pick + revert: apply/invert a change's delta to the current line (#392, #393) 0db6666c · dbf3dbe6…diff
  • loot duplicate: copy a change with a new change-id (#398) 36c1463b · dbf3dbe6…diff
  • loot split + squash: move/fold change content via ADR-0032 supersede (#395, #396) 9f3cbe33 · dbf3dbe6…diff
  • loot absorb: auto-distribute hunks to the nearest relevant ancestor (#399) 42de6420 · dbf3dbe6…diff
  • conflicts map stores base OID + loot resolve --tool for external 3-way merge (#400, #401) a0711731 · dbf3dbe6…diff
  • loot doctor: diagnose common setup problems (#22) 2ea3fa43 · dbf3dbe6…diff
  • Extract a Draft module: the working-change state as one state machine The CLI's local state of the working change lived in two loose Workspace fields — `working: Option<Oid>` and `next_change_id: Option<[u8;16]>` — hand-mutated at ~19 sites, each owing the invariant that they move together. Their legal combinations are a 3-state machine; the fourth (a working change *and* a pending handle) is never valid, yet two Options made it representable — `edit` from a Fresh state transiently built it, leaking the dead handle until the next finalize overwrote it. Model the state as `enum Draft { Clean, Fresh { next }, Active { working } }`, making the illegal fourth state unconstructable and dropping the pending handle on every transition into Active by construction. This is Position's precedent (#324) applied to the state half of ADR 0034's "position is place, not state": Position owns place (tip), Draft owns state. Interface: `arm` / `activate` / `take` / `clear` + `load` / `flush` + accessors `working` / `next` / `is_clean`. Minting stays in the caller, so Draft's whole dependency surface is RepoStore, like Position. The composite dances that also move the Position tip — restart-on-anchor (squash/absorb) and the finalize hand-off — are named Workspace coordinators (`restart_on`, `finalize_working`) over both modules; Draft never reaches into Position. `load` self-heals a legacy on-disk (working, pending) pair to Active. Behavior-preserving: the full workspace test suite passes unchanged. The only change is the previously-dead pending handle now cleared eagerly on →Active. e7b824f6 · dbf3dbe6…diff
  • Extract the hunk-attribution engine into a deep `hunks` module The line-redistribution algorithm behind `loot absorb` (#399) lived as five free functions (diff_hunks, apply_hunks, attribute_hunk, split/join) plus LineHunk/HunkTarget in workspace.rs, testable only through the ~200-line Workspace::absorb via a full Workspace. Move it to a new `hunks` module — the write-side twin of `blame` (the read-side line-ancestry engine they share `lcs_match`/`Attr`/`split_lines` with). The whole line algebra becomes private behind a small bytes-in/bytes-out interface: `attribute(parent_bytes, working_bytes, owners, index_of) -> (Option<PathHunks>, Vec<Stay>)` builds a per-path plan, `PathHunks::apply_at(i, base_bytes)` rebuilds one ancestor's content, and `PathHunks::targets()` reports where hunks landed. `absorb` never sees a LineHunk or a line vector now — only oids, trees, and sealed bytes; it keeps just the chain-walk and the sealing/superseding it alone can do. The payoff is testability: the splice math gets direct unit tests over crafted byte inputs (the three migrated line-function tests plus interface-level tests), instead of only through absorb's full-Workspace integration tests. `blame:: split_lines` is made pub(crate) and reused, deleting the `split_content_lines` duplicate. AbsorbStay becomes `hunks::Stay`. Behaviour-preserving: the three absorb integration tests pass unchanged. The Phase A/B rewrite is outcome-identical (the in-effect emptiness check now lives inside apply_at). 38525b23 · dbf3dbe6…diff
  • Rename the Public visibility tier to Internal (ADR 0041 §2, #480) 6969626d · dbf3dbe6…diff
  • Publish mechanism: the `published` keyword + @world grantee (ADR 0041, #481) a6758352 · dbf3dbe6…diff
  • Publish: surface the `published` visibility token across CLI/porcelain/JSON/WASM/SDK (#481 refinement 1) 082ad335 · dbf3dbe6…diff
  • Authenticate the purge lane: a purge is a signed request (#503) A wire purge carried only an oid and yielded an unauthored tombstone, and stow honored it before storing objects -- so any authenticated pusher could destroy any oid across every tenant, needing no key, only the address. ADR 0038 already called a purge event a request asking cooperating relays and peers to destroy their copy. Cooperating meant nothing: loot honored anyone. The signature now says who is asking; a per-receiver policy decides whether to honor. Peers honor registered senders and quarantine strangers; relays honor their push allowlist. Local burn is untouched and needs no key -- only propagation requires a signature, so a keyless repo burns locally and refuses to propagate, explicitly. No global entitlement predicate exists: ChangeNode.tree is a full manifest and loot duplicate copies a tree address-for-address, so authored-a-change- referencing-this-oid is satisfiable by any cloner. Both withdrawn rules have regression tests. Format 9 to 10. A v10 reader parses a legacy purge lane and drops it, so no unsigned request is honored while the rest of a v9 bundle still decodes; an older client hard-fails on v10. Destruction is structurally unreachable without verification: HonoredBurn has a private field and authorize_burn is its only constructor. Both halves are signed, with domain-separated schemes -- without tags a maroon signature re-encodes byte-for-byte as a burn tombstone at path length 31. Maroon entitlement is an exact Manifest grantor join, and a grant only records a grantor when it actually installed a key, so a crafted grant cannot plant one for content you already hold. Closes #503. 913c5cc6 · dbf3dbe6…diff
  • Stop shipping grantee nicknames on the wire (ADR 0015, #521) 55ba71eb · dbf3dbe6…diff
  • loot diff --content: unified content diffs from inside a lane (#536) 7b01b038 · dbf3dbe6…diff
  • Diagnose a premature finalize, and stop a stray ferry silencing the hint (#512) b16d77f2 · dbf3dbe6…diff
  • Deliver a relay grant inside the mutation, so a refused delivery records nothing (#546) 2aa65397 · dbf3dbe6…diff
  • Give a lane a session owner, so gc and rm refuse someone else's (#532) 47af2987 · dbf3dbe6…diff
  • Refit: plan/execute split for the change-rewrite verb family (#659) f2fdf6ed · dbf3dbe6…diff
  • burn::Burn: one home for the burn/purge concept (#662) 8e7834a7 · dbf3dbe6…diff
  • burn::Burn: one home for the burn/purge concept (#662) (conflict resolution: crates\loot-cli\src\workspace.rs) 8511c8e7 · dbf3dbe6…diff
  • emit_snapshot calls verbs in-process; test-support feature retired (#661) 8033f451 · dbf3dbe6…diff
  • GitMirror: the bridge owns its own layout (#665) b0461c4c · dbf3dbe6…diff
  • workspace/reconcile: apply_plan executor owns the four arms (#663) 4ac0277a · dbf3dbe6…diff
  • GitMirror: the bridge owns its own layout (#665) (conflict resolution: crates\loot-cli\src\workspace.rs) 7052f1d0 · dbf3dbe6…diff
  • edit guard agrees with status; premature-finalize remedies tell the truth (#682, #683) c78e9a39 · dbf3dbe6…diff
  • Sweep the rust-1.96 clippy debt; document the land-holds-the-binary hazard (#667, #681) 26cfbaa9 · dbf3dbe6…diff
  • edit guard agrees with status; premature-finalize remedies tell the truth (#682, #683) (conflict resolution: crates\loot-cli\src\workspace.rs) d8757f2c · dbf3dbe6…diff
  • loot adopt: the git-first catch-up fast-forwards content-identical work unsigned and names foreign paths (#705) 044c84ac · dbf3dbe6…diff
  • push negotiates the change lane against the relay /haves so a small land ships its delta, not 7 MB of re-sent history (#728) 2df34147 · dbf3dbe6…diff
  • loot-first: land folds a pre-finalized change so the perf point rides it (#725) d98f26f6 · dbf3dbe6…diff
  • loot-first: land folds a pre-finalized change so the perf point rides it (#725) (conflict resolution: crates\loot-cli\src\workspace.rs) acbec067 · dbf3dbe6…diff
  • push negotiates the change lane against the relay /haves so a small land ships its delta, not 7 MB of re-sent history (#728) (conflict resolution: crates/loot-cli/src/workspace.rs) 771d8e1f · dbf3dbe6…diff
  • a view step refuses over work no change records, and a primary finalize says so on stderr (#436) a3248ede · dbf3dbe6…diff
  • the relay expands a pulling client's declared heads, so a one-behind pull ships its delta and not the whole change lane (#734) e3f7604c · dbf3dbe6…diff
  • a change records when it was authored, so a projected commit stops reading '6 years ago' on GitHub (ADR 0043) loot changes carried no timestamp, so the git bridge fabricated one: BASE_EPOCH + generation, one second per ancestor depth from a 2020 epoch. All 526 commits sat inside seven minutes of September 2020. Earlier repairs (#626's floor, the missing-generation refusal) fixed ORDERING and never touched the absolute date, which is why this kept coming back. ADR 0028 inherited the no-timestamp constraint rather than choosing it. The real reason is upstream: a version id is blake3 over authored content, and a clock inside that hash gives two peers different ids for identical content, destroying the dedup and convergence of ADR 0001/0004. So authored_at rides the label seam ADR 0029 already cut for change_id -- covered by the finalize signature (no relay can restamp it) but never folded into the version id. The wasm golden vectors prove the separation held: FROZEN_VERSION_ID, FROZEN_OBJ_ADDR and FROZEN_SIGN are byte-identical, and only the version marker and one presence byte moved. Advisory, and never an ordering input: a self-reported clock is a claim, not evidence (ADR 0025). in_order/ids_topo, buoy and path_touch.ordinal are all untouched, the forge indexes nothing on it, and the projection floors it past every git parent so ancestry holds whoever's clock is wrong. Format v11: additive for readers (a v<=10 change decodes as None and an absent timestamp adds nothing to the signed message, so every existing signature still verifies), breaking for writers, so loot-cli and loot-forge go to 0.4.0 in lockstep. Forge migration 0004 stores it as bigint, not timestamptz -- signed data must round-trip bit-exact or pullers reject the change. Only new work gets real dates. Every commit on main predates v11, main is push-fast-forward-only, and backfilling would fabricate the very claim this replaces. Perf-Baseline: reset the change body grew one presence byte, plus eight where a timestamp is present, so bundle_bytes/store_bytes/wire_bytes step once at the v11 boundary eaa56d99 · dbf3dbe6…diff
  • surface's human listing derives published from the same policy its json does, so a published path stops reading internal (#764) 724ec7a2 · dbf3dbe6…diff
  • a publication survives a merge, an ingest and a resolve, so a catch-up stops silently reverting one (#783) The reported symptom was consent fatigue: every capture in the repo refused with "refusing to publish README.md", including changes touching nothing published. The cause was the opposite of the symptom -- README was not published any more, and the gate was correctly reporting a new @world transition on a publication that had been reverted. The ticket's hypothesis (an adopt/ferry fold re-sealing README from git-side content) was not the mechanism. Reading the real graph, #750's own publication change never published README at all, while it did publish both licenses. The difference is that the licenses were NEW paths, so the snapshot seal minted them @world, while README already existed and went through the merge -- where converge picked between two addresses holding identical plaintext under identical Visibility and its tie-break took the unpublished one. @world is address-neutral, outside every signature and ignored by open, key-travel and compression. That inertness is what made publication cheap; it is also why nothing carries the marker structurally. It survives only where the code writing a tree entry chooses to carry it, and four writers did not: converge, the git bridge's ingest, resolve (so a bounce quietly unpublished the conflicted path) and absorb's span rebuild. The snapshot seal learned this at #481 and nothing generalized it, so Repo::put_sealed now is the generalization. converge gains an is_published question on KeyOracle and compares publish-status as part of a path's content, which makes the existing 3-way answer publish flips correctly in both directions: a publication since the fork is an edit and wins over a stale untouched side, and a deliberate unpublish is equally an edit and still lands. On identical bytes with no base to judge from, the published side wins -- widening was consented to once, and reverting it on no evidence is the only outcome that loses something silently. The other three carry, and carry ONLY. A re-seal never originates a publication: ingest takes its policy from a git commit, which reaches loot without passing a consent seam, so a break-glass commit adding a `published` rule seals plain and leaves the transition for the capture gate to ask about in the usual place. Sealing more tightly than the rule says is always safe; more loosely is what this subsystem exists to prevent. Ask 2 -- should an unpublish need consent? No. A prompt would make consent MORE frequent, which is the failure the ticket is actually about: a gate that fires routinely teaches operators and agents to pass publication flags reflexively. The fix is fewer ways to unpublish, not another prompt, so dropping the rule is now the only one. For the same reason the gate still asks the anchor OBJECT's marker and not the anchor's .lootattributes: policy and marker disagree both when a marker was lost and when a rule has just begun publishing a path, nothing there can tell them apart, and trusting policy would turn the standing rule into a way to publish unasked. A future marker-losing path would make the gate nag again, and that is the failure worth having. Ask 4 -- the forge's published index cannot disagree with main: this repo pushes to relay.millerbyte.com and has never pushed to the forge, so the index holds no README row. Had the push happened inside the window, it would have received the unpublished object. ADR 0041 gains a 2026-08-04 amendment; CONTEXT.md's Published entry records the carry/originate split. ec50222e · dbf3dbe6…diff
  • a path publish tier rides the delta itself, so diff and status stop disagreeing about what is published (#786) c98b2dad · dbf3dbe6…diff
  • surface refuses over an edit no change records, and --force names what it overwrote (#782) 7efc0a3e · dbf3dbe6…diff
  • a sparse view scopes what the working tree materializes, and nothing about what syncs (#39) fa6c6749 · dbf3dbe6…diff
  • what a peer declares in a negotiation is a type now, so the head list that strands a pull cannot be passed (#799) #40's fault-injection harness found a peer holding 3 of 6 files whose head had advanced to the sender's tip, with the negotiation reporting up to date. The ticket read that as "an interrupted pull cannot resume." It does resume: `pull_via_interrupted_fetch_resumes` has been proving that since #217, and `pull_via` declares `negotiation_have()` at all three of its call sites. The harness resumed by declaring `bob.repo.heads()` instead. That is the defect — not the sync path, but the fact that `have` accepted any `Vec<Oid>` and the obvious thing to reach for was the wrong one. `Repo::heads`' own doc said "what a peer passes as have", so the trait was teaching it. `loot clone` was doing it too, correct only because a freshly inited repo has no heads. What a peer may declare is now `Have`, and `negotiation_have()` is the only thing that makes one — the #217 filter is the check the type certifies. `Have::nothing()` is the one other constructor, for transports probing a relay and clients with no repo to ask yet; declaring less can only over-send, and over-declaring is the strandable direction. Threaded through the receiver-declares half only: SyncTransport, loot_net::{offer,fetch,pull} and the forge's signed twins. The sender side keeps `&[Oid]` deliberately. `have` means one thing in both directions — what the recipient of the bundle holds — so there is no second concept to name; what differs is provenance, and a type cannot carry a guarantee across a network. Wrapping it would advertise a check that does not happen. That reasoning lives on `Have`'s doc, which is the only place it is written. Candidate 1 from the ticket — refuse to advance the head over an incomplete closure — is not built, because ADR 0024 already priced and rejected it under "cross-batch atomicity is deliberately not provided". A confirmation note says so there, so the next reader does not re-derive it. The new `tests/sync_resume.rs` asserts the head advance rather than treating it as the failure, and covers the fault the suite genuinely missed: a bundle that is short but valid, where every batch succeeds, the pull returns Ok, and the operator is told it worked while holding half the files. The next pull heals it, which is what is pinned. Acceptance criterion 3 ("am I up to date" must answer no while any object is absent) is deliberately not built: `closure_complete` is unsatisfiable on the forge path, where a reader legitimately never receives ciphertext it holds no grant for, so the warning would fire forever on every forge repo with restricted content. That is #803. `an_interrupted_transfer_resumes_to_convergence` runs with its ignore deleted. 271dd5cf · dbf3dbe6…diff
  • a lane carries its own hold, and custody crosses back whole at finalize (#811) 941cafb9 · dbf3dbe6…diff
  • a lane's embargoed key crosses into the shared escrow, and the reap cannot take the reveal with it (#828) e126b8e3 · dbf3dbe6…diff
  • adopt resolves its target in the shared graph, so a lane-landed change is refused by name, not reported missing (#829) 52dd2c3d · dbf3dbe6…diff
  • a forge advertises its retention window on /info, and burn prints one honest bound per host it disclosed to (#814) 652f4023 · dbf3dbe6…diff
  • settling onto a tip is one dance that always clears the tree-hash, and the finalize binding has one home (#862) The settle dance - bring the tree to show a finalized tip, move the position, clear the tree-hash, persist - was hand-assembled at eight sites, and only four remembered the clear. The drift was benign solely through an unstated proof (every no-clear site leaves the Draft clean, so the snapshot fast path cannot fire); a future caller settling with a live draft would have broken silently. Two private Workspace coordinators now own it: settle_advance / settle_seed (Position's advance-vs-seed vocabulary stays visible at the call site) share one tail - clear tree-hash, persist - and a closed SettleTree enum carries the tree half: Materialize { from }, OnePath (resolve's #233 single-path write, which must never widen), or InPlace. Draft moves and signing stay with callers; finalize_working and restart_on remain composite coordinators wrapping draft-handling around a settle. The finalize signature (version-id, change-id, predecessors, authored_at - ADR 0029/0032/0043) was typed verbatim at four sites; a fifth that forgot authored_at would mint a valid-looking, unverifiable signature. sign_version(&Oid) is now the one binding, keyless gate inside, no persist (the carry loop signs N and persists once); public sign_change keeps its interface as sign_version + persist. Decision: #850 (map #849). fast_forward_to, converge_heads's survivor-adopt and resolve_conflict now clear the tree-hash they skipped; the extraction is the fix, no separate tickets. CONTEXT.md gains the Settle glossary entry. d4ed0644 · dbf3dbe6…diff
  • loot archive exports only the paths its caller may read and says what it left out rather than looking complete, and loot shortlog rolls the changes loot log lists up by author (#416) 1b57778e · dbf3dbe6…diff
  • the adopt catch-up decides in the shared reconcile table, and keeps its own hands for the fold (#866) `adopt_harbor` is now the same three steps as the ferry reconcile: prologue (the two "cannot name the target" refusals, the #829 shared-graph ingest, the ADR 0047 hold refresh) then `adopt_view`, then `reconcile::decide`, then adopt's own executor. Ferry and adopt can no longer drift on WHAT to do. A sibling of `apply_plan`, not a mode flag on it: the same `Plan::Merge` means carry for the bridge (ADR 0039) and fold for adopt (ADR 0034), so that distinction stays a named seam rather than a runtime argument. Adopt's hands are `fast_forward_to` and `fold_line_in`, with `fold_line_in`'s internal short-circuits intact (`lane merge` is a live caller of them). The #418 seal-WIP override stays executor-side in both verbs as the typed `RepoError::SealWip`, deduped into one `seal_wip_gate` helper carrying the verb name. Routing it through `Plan::Refuse` would demote a matchable guard+override refusal to a stringly error, and it reads live state at the signing seam beside the mis-seal gate. The View is unchanged at six fields. Adopt becomes the first production caller that passes `covered: true` (its stale doc note is updated), and the first that captures BEFORE computing `covered`, which is legitimate because the table is order-indifferent: `covered` outranks every other field. The capture normalization stays verb-owned and is skipped once `covered` holds, so an already-current catch-up still leaves the operator's capture exactly where it was. One behavior change falls out of the routing: a catch-up with no local line at all (`pinned: None`) now takes the plan's Adopt arm and fast-forwards onto landed main, where it used to fall through to `fold_line_in` and refuse with "nothing to merge into yet, record a change first". Catching up is what the verb is for, and it is what `reconcile_onto` has always done in that state; `fast_forward_to` takes `Option<&Oid>` for the missing `from`. `loot adopt <version>` is untouched: taking a target wholesale makes no reconcile decision. Tests: the choices are the table's, so adopt's repo-materializing tests keep only what is still adopt's own, the prologue, the capture normalization, and each executor hand's effect. Added the second prologue refusal (a landed main the shared graph lost) and two executor smoke tests: the `pinned: None` adopt arm, and the no-op leaving a capture untouched. One duplicate drop-the-redundant-capture test folded into its twin. 46b3f587 · dbf3dbe6…diff
  • every persisted artifact crosses the store as its own type, and raw paths retire to the four recorded owners (#863) Land A of #863 (decision #851): RepoStore gains a typed read/write pair per artifact — identity, graph, keyring/escrow and their shared halves, manifest, purges, attestations, burn log, conflicts, resolved — each owning the file, the atomic replace, and the absent-file semantics, with the codec staying where it lives. The engine's save_to/load_from/refresh_hold/gc/verify raw sites, the op log's capture/restore and ops file, and the bisect session all migrate through; the ops file and view restore gain the temp+rename atomicity they lacked, and the op-log capture now waits out the Windows replace window instead of recording a live file as absent. Absent is now NotFound alone everywhere but `resolved` (best-effort by contract): a torn read fails loudly rather than reading as an empty artifact. The migrated getters drop to pub(crate); `objects/`, the keypair/peers, `git-mirror/`, and doctor keep raw access as the recorded second owners. 0edf769d · dbf3dbe6…diff
  • a ferry refusal reaches a machine consumer as itself, not as a generic error (#867) Every seam inside `ferry::run` returned `Result<_, String>`, so a pass — which is mostly other people's failures — flattened all of them before the CLI could emit them. A typed `RepoError` the engine had already coded, and the reconcile executor's `Plan::Refuse`, both reached a `--json` consumer as the generic `error` code. The prose was fine; the machine channel was the loss. The nine phase signatures now carry `CliError` end to end, which restores the engine's own slugs for free: engine calls propagate with a bare `?` instead of `.map_err(|e| e.to_string())`, which is precisely what was flattening them. Exactly three slugs join the frozen ADR 0023 contract (amendment recorded): `undescribed_parent` and `foreign_paths` from a new `reconcile::Refusal::code`, built alongside the unchanged wording by BOTH executors — `apply_plan` and #866's adopt catch-up — so two verbs sharing one table cannot become two taxonomies; and `git_mirror` where ferry lifts git2, deliberately coarse because the bit a consumer acts on is retry-the-environment versus obey. `git_mirror.rs` stays stringly inside: inventing a loot taxonomy for git2's errors would be fiction. `rollback_note` keeps the abort's code and grows only its message — a rollback is context about that failure, not a different one. Every refusal's wording is byte-identical; the human channel does not move. Ferry's own handful of refusals stay honestly generic, and so do the tier's remaining flatten sites, which graduate one at a time when a consumer needs them. The one loot-first line is the seam back out: its orchestrator keeps a String channel, so `From<CliError> for String` makes that a `?` rather than a hand-written `.to_string()` at every call. Found while pinning the codes: the uncaptured-WIP ferry test refuses with `foreign_paths`, not `undescribed_parent` as it reads. Both wordings offer `describe -m`, which is why prose was never a taxonomy. 4fc73479 · dbf3dbe6…diff
  • the change graph gets one read view, and the two forwarding bands over it retire (#868) 826c3339 · dbf3dbe6…diff
  • a key's provenance is stated at the one door, and a grant is refused at the two that bypass it (#864) 82e06601 · dbf3dbe6…diff
  • the change graph gets one read view, and the two forwarding bands over it retire (#868) (conflict resolution: crates/loot-cli/src/workspace.rs) 31a918e5 · dbf3dbe6…diff
  • one refusal, one slug: the un-described-parent code survives the workspace seam (#878) d72ef9ef · dbf3dbe6…diff
  • the ignore dialect refuses what it cannot mean: a live ! line stops the snapshot, recorded texts keep their old inertness (#843) 74d9e7be · dbf3dbe6…diff
  • the reader names its own holes: the completeness filter and Have retire, and pull reports what never arrived (#803) f15d576e · dbf3dbe6…diff
  • absence is not one boolean: surface writes what it can and names the rest, and capture asks whether the tree is a materialization (#891) ad28eb57 · dbf3dbe6…diff
  • the review's findings: one shape for an unstated build, the glossary carries the field, and the ignore rule gets the test that was its point (#920, #922) 8784832a · dbf3dbe6…diff
  • a rule you inherited is not a rule you typed: the ignore negation warns instead of wedging every older lane (#921) a64e595e · dbf3dbe6…diff
  • the gate proves the tree the position holds, and main receives the merge: a land refuses a lane behind landed main (#939) main went red at 3597c9d with two lands that each passed their own gate: #920 added a caller (`Ignore::parse`) in one file, #921 deleted the callee in another. Disjoint edits, no path in common, so loot per-path conflict detection had nothing to look at — and each land compiled the tree its LANE held while the ferry projected the MERGE, a tree nothing had ever compiled. The repair is one line: #920 test takes `parse_recorded`, the sole parse since #921, which is also what it means (it pins the shipped file behaviour, not a live-tree strictness that no longer exists). The guard is the rest. `Workspace::behind_landed_main` is the read-only half of the `covered` question `adopt` already asks, and `land_gate` refuses on it, naming what it is behind and pointing at `loot adopt` + a re-review. The land does not adopt for you: a catch-up merges, a merge can conflict, and burying that inside the verb whose promise is shipping the reviewed change is the wrong place for it. The same question is asked again after the gate and before the finalize, for the sibling land that completes inside a multi-minute cargo test — there rather than inside the harbor lock, which would be airtight and would serialize every concurrent land behind one full test suite (the ADR 0036 cost). The residual window is the git-quiet finalize plus the lock acquisition; workflow.md names it rather than papering over it. 510a5be7 · dbf3dbe6…diff
  • the store spells a path one way: tree keys go slash-separated at every boundary, and a windows store loads nested on posix (#988) d2889f60 · dbf3dbe6…diff
  • the second machine's bridge stops being frozen: ferry learns landed main from the checkout, and adopt says when its answer is stale (#1000) 93bce6cc · dbf3dbe6…diff
  • a ticket claims one lane: a repeat claim refuses instead of forking a silent sibling, and one search answers for both the id and the directory (#1002) dda6e6a6 · dbf3dbe6…diff
  • an empty holder list means unknown, not nobody: a pulled Restricted path stops reading as a permanent local edit, and one engine rule answers for every content comparison (#1005) 65b6946c · dbf3dbe6…diff
  • a store knows which machine materialized it: an arrived copy refuses to record until loot rehome, so a bootstrap cannot silently capture every sealed path as deleted (#986) 4c15a2ca · dbf3dbe6…diff
  • a push deposits this identity's own keys: a standing self-grant carries Restricted custody to the second machine, so a same-identity clone opens what it authored (#980) 684f37cb · dbf3dbe6…diff
  • the primary stops authoring: describe -m and new -m refuse on a shared floor and print the exact lane take that carries the work onward - ADR 0058's loot lane take built, --allow-primary now a triple (#985) 67df9345 · dbf3dbe6…diff
  • a forge push deposits the keys the forge itself drops: standing self-grants widen to Internal and embargoed-to-self at forge remotes, closing #502's second-machine cost (#1042) f8edab5d · dbf3dbe6…diff
  • a lane path prints as something you can paste: the \\?\ extended-length prefix comes off at every face, machine ones included, while the registry keeps the canonical form (#1049) 181fd4f6 · dbf3dbe6…diff
  • the one-place path spellings become reachable and reach: tree_path goes pub(crate) and covers the five walk-derived surfaces that could not call it, lane merge stops being the face #1049 missed, and two doc claims shrink to what is true (#1063) 94908511 · dbf3dbe6…diff
  • a relay's deposit stops standing in for the forge's: the push-time dedupe moves off the Manifest onto a per-remote deposit ledger, so relay-first Restricted custody reaches the forge inbox the browser reads (#1052) 499739af · dbf3dbe6…diff
  • the lane-hold drill tells the truth again: it was refused the primary authorship its own fixture needs, and read results at a tip that a carried-in line never moves (#1068) d392d0ba · dbf3dbe6…diff
  • a verb that could only publish to main unreviewed is retired: lane merge goes, its fold machinery stays where adopt needs it, and the mirror-free fold that genuinely leaves with it is named rather than glossed (#1048) 3f7635a9 · dbf3dbe6…diff
  • the mis-seal gate stops missing the suffix that names a secret: *.env joins .env*, and the tier it has always watched is called Internal in the four places that still said Public (#1108) 72176935 · dbf3dbe6…diff
  • every durable .loot artifact declares its format discipline in one inventory, and the fixed-width set files refuse malformed bytes instead of reading as empty (#1135) 58072a13 · dbf3dbe6…diff
  • custody at rest seals to your own key behind an optional passphrase, unlock is a session file or an env var, and no non-interactive caller starts prompting (#1138) c8ffee3b · dbf3dbe6…diff
  • the mis-seal gate covers keys, cloud credentials, password stores and token-shaped data files, measured to add no wall on this tree (#1139) 59df2a25 · dbf3dbe6…diff
  • a no-remote repo converges its diverged heads with loot converge, and the diverged hint names the verb the repo can actually run (#1183) 2d9a2d31 · dbf3dbe6…diff
  • a refused clone removes only what it created, so the 403 every stranger earns stops blocking its own retry, and the refusal pin dispatches every command both arms name (#1209) 7e399a76 · dbf3dbe6…diff
  • a passphrase-locked repo can pull from a relay, because filing keys needs only id.pub, and it says plainly that it wrote no tree and folded no heads instead of looking like a whole pull (#1172) cbc60097 · dbf3dbe6…diff
  • the unlock session holds the seed sealed under this repo's own ciphertext instead of the passphrase you typed, so a synced config directory leaks a window and not a secret you reuse elsewhere, and the passphrase is zeroized from the moment loot owns it (#1173) 1943d2c7 · dbf3dbe6…diff
  • the keyring and escrow share one codec so a custody decision is written once, the locked refusal has one home instead of five drifted ones, and the terminal predicates each say why they differ (#1174) 33aca9d9 · dbf3dbe6…diff
  • a wrong LOOT_PASSPHRASE stops shutting the locked-pull door it was never a key for, the one verb a locked repo runs names the session file it refused, and ADR 0068 stops contradicting itself about its own cryptography and its own amendments (#1231) ddcf499d · dbf3dbe6…diff
  • an ingest pins the primary line before a sibling head can land, so a capture stops forking from the union of both heads and silently dropping the un-surfaced side out of the recorded tree (#1197) fc6b0b7c · dbf3dbe6…diff
  • an ingest records which paths it did not write, because that is the last moment absence and deletion are different facts, so a capture after --no-surface stops recording the content it just fetched as removed (#1227) cbf4a65f · dbf3dbe6…diff
  • the custody sweep residuals are answered on the record: the sealed-path demo is re-run so the evidence page shows the binary it names, the disk scan learns base64 and UTF-16, the pull entry points drop their bool, and the mailbox keeps failing loud by decision (#1232) 333e9517 · dbf3dbe6…diff
  • a seal to a holder list that excludes its author refuses and names what nobody could ever open, --allow-lockout is the deliberate way through, and ADR 0038 records why the gate refuses rather than filing a key for the author (#1249) bbb858b0 · dbf3dbe6…diff
  • an ingest records the digest of the disk it did not overwrite, so a capture can tell a stale copy from a real edit: the stale copy stops reverting the peer and an edit over it becomes a conflict instead of a silent winner (#1256) 97fe45f0 · dbf3dbe6…diff
  • the sweep fix-up lands: a land translates a lockout refusal into the lane ceremony that actually works, the seal gate stops refusing over a stale copy the capture will never seal, and the surface prune stops deleting a copy of a path the line still holds (#1271) 68230ea5 · dbf3dbe6…diff
  • a pull declares its ancestor closure instead of bare heads, so a server behind the client stops re-sending the whole history to say nothing new: the live forge no-op drops from 15.6s to 1.85s with no redeploy, and the wire gains an opt-in timing instrument (#1258) c1971c5e · dbf3dbe6…diff
  • the consent slices become one Allowances struct a swap cannot compile against, the lockout gate calls the engine predicate it used to mirror, and the site gate steps destructure so a third step stops compiling (#1272) ac6e2096 · dbf3dbe6…diff
  • the pull declaration becomes an enum a subset cannot alias through, which also stops a fresh clone re-downloading the full delta every batch, the wire timing line gets named fields, and the reap window scalar stops sharing its name with the pair type (#1284) 3b657494 · dbf3dbe6…diff
  • the absence and presence records become workspace/disk_record.rs: one face over the two dotfiles, the seal gate asks stale_copy_on_disk instead of re-deriving the compare, and the record rule stops living in nine places (#1289) 943b1eb5 · dbf3dbe6…diff
  • the harbor read face becomes workspace/harbor.rs: eleven mirror predicates and the raw ledger parses leave the workspace namespace, and the write side stays inside the ferry pass where ADR 0055 pins it (#1290) be35c3f6 · dbf3dbe6…diff
  • the capture chokepoint becomes workspace/capture.rs: the snapshot pipeline, the three consent gates and the finalize seam share one module, and the next allow flag costs one Allowances field and one arm (#1291) 16c998d0 · dbf3dbe6…diff
  • the Network sync entry records what #1284 changed about a fresh clone, and pull_via_declaring asks its posture once through declared_have instead of twice (#1300) d436aea1 · dbf3dbe6…diff
  • lane gc takes the tree down marker-last so a reap that fails on a held file leaves the lane identifiable and reapable on retry, a held file is named as one with the re-run advice, and an emptied shell that will not go is re-stamped with its id (#1275) c1f6a485 · dbf3dbe6…diff
  • the Network sync entry says what a multi-batch pull actually declares, the measure rule records why its selectors stay direct-child, four restated stories become pointers to one telling each, and the last two hand-rolled repo walk-ups join tests/common (#1314) a906a965 · dbf3dbe6…diff
  • the refit executor joins the planner it was already documented as belonging to, so the ADR 0032 superseding rule stops being half in refit.rs and half in a 15,000-line namespace, and the five verb wrappers read the same from outside 77bae3cb · dbf3dbe6…diff
  • eleven hand-rolled relay doubles become one TestRelay configured by value, so a withheld address and a pre-803 posture are settings that carry their tickets rather than structs re-declared under three names, and eighteen tests ask the seam instead of the repo 306942dd · dbf3dbe6…diff
  • the two deposit plan-execute pairs become one plan_deposits over a RemoteKind that carries both halves of 1042's widening, so the three path accessors go private and the forge-vs-relay rule is a predicate a test can reach instead of a doc comment b3e21d6d · dbf3dbe6…diff
  • a verb run from a subdirectory finds the repo instead of advising an init that would nest a second one, path arguments rebase from the cwd the way a tester types them while view's globs stay root-relative, and the resolved root is named on stderr whenever cwd is not it eca43819 · dbf3dbe6…diff
  • three papercuts from the alpha sweep: a defaulted diff on a clean tree agrees with status instead of exiting 1, evolog's when column shows the authored clock instead of the git-bridge stamp while porcelain keeps the frozen one, and view stops claiming a deletion it never performs and counts what it left on disk 19473311 · dbf3dbe6…diff
  • the composition TestRelay newly allows gets a test that fails if a fault is applied before the postures instead of after, a holding constructor absorbs the three hand-built relay fixtures, and the two invariants a comment was carrying say why they are load-bearing 34a33d0c · dbf3dbe6…diff
  • discovery asks one structural question at every depth instead of two that disagreed, so the installer's own dot-loot is never a repo even standing in it and unlock can no longer be handed that directory, while a damaged repo refuses in its own name from a subdirectory rather than letting an ancestor answer 974715e8 · dbf3dbe6…diff
  • loot view --prune removes the paths a narrowed view stopped covering and refuses whole, naming every one, when any of them holds an edit no change records, taking nothing out of history so a clear writes them all back de9f395a · dbf3dbe6…diff
  • the ratchet records a build that exists again, ADR 0006 finally carries the verb that leans on its third case, and three documents stop asserting things the code beside them contradicts including a pin that held the number and not the strictness it was written for 62bf03ff · dbf3dbe6…diff
  • the four consents become one owned Consents parsed and rebased at a single argv door, so allow-demote stops travelling a route of its own and the transposition Allowances was built to prevent has one place left that could still make it instead of eight ffb85a76 · dbf3dbe6…diff
  • a signing seam reads the working tree once and walks it once, so describe and land stop buying two whole-tree byte reads to look at file names, and loot-perf grows the capture timing that can see it (#1383) 31995cb5 · dbf3dbe6…diff
  • the implicit capture in front of every bare mutating verb reads the working tree once, so the tree it judges clean is the tree it records, and the doors already committed to capturing ask their refusals before paying for a read (#1395) 7f1fb453 · dbf3dbe6…diff
  • loot converge stops buying a second whole-tree read for the note it prints, because the implicit capture one seam below has already read that tree, and the series added to catch exactly this learns to fail: its fixture had neither the co-located checkout nor the standing fork the branch needs, so it could not reach the cost it was watching (#1401) 12c1207a · dbf3dbe6…diff
  • the landed-content hazard names loot edit, the second route to the same signature, because reopening a finalized change carries its handle and the loot new that closes the amend signs under it, and the block three hints rebuilt by hand at an indent a comment asked for is now assembled once in landed.rs (#1404) 909e8b37 · dbf3dbe6…diff
  • deleting a named read on Workspace stops being free, because the 377 places that reached past it into the engine and the store now go through those faces instead — repo() no longer exists, store() is module-private, and the three extracted children share their fixtures by name rather than through the parent's private test module (#1411) c454db04 · dbf3dbe6…diff
  • the store stops threading a dock nothing can ever produce through 43 signatures, because a store instance is already the position ADR 0034 retired named docks in favor of, and the sixteen path getters no caller outside loot-core reaches stop being public while the one main reservation that guards a git ref name stays (#1413) 23ab3abd · dbf3dbe6…diff
  • re-pointing the working tree becomes one call that carries the #436 clobber guard with it, so the four bisect checkouts stop destroying an edit made while testing a midpoint, and no caller is left owing a guard that only a doc comment could name for it (#1414) 17abc3a0 · dbf3dbe6…diff
  • a test can no longer mutate the engine past every named face, because with_repo_mut is deleted and the 27 fixtures that reached through it each name the state they fabricate, while the one that was re-deriving load_shared_lineage by hand calls it (#1447) 9c3573f6 · dbf3dbe6…diff
  • adopt_harbor stops open-coding load_shared_lineage's pair, because the branch its bool drives already sat on the far side of the ADR 0047 hold refresh the seam performs in the same order, and the tip it loads is an owned Oid the mirror spine hands back rather than a borrow of self, so the one copy of tonight's drift whose bool is load-bearing collapses without moving the refusal (#1461) e07e49da · dbf3dbe6…diff
  • content whose embargo has already lifted stops reading as sealed, because promoting due keys out of escrow is now the construction of the reader every content read hangs off instead of a doc-comment obligation six callers hoisted by hand, and deleting four of those hoists left all 2438 tests green while loot surface told an author to request a grant from herself (#1464) e59e46b3 · dbf3dbe6…diff
  • delta_of's modified arm stops reporting a due-embargoed path nobody edited as an uncaptured edit that wedges every #436 clobber-guard verb, because the base read it decides on hangs off the promoting reader its sibling arm in the same match already used instead of the bake-off trait's frozen public door, and ADR 0007 stops claiming a compiler forbids that spelling while the shipped tree held one (#1471) fbd758fe · dbf3dbe6…diff
  • a verb that names a file in this repo stops carrying its own copy of the #1330 rebase, because every argument slot's kind is now a required field of the table that already declared the leaf's flags and arity, so the argv door rebases from the declaration and a leaf that states nothing does not compile (#1466) 889e5821 · dbf3dbe6…diff
  • a merge stops settling a tip whose tree the caller still owes on the next line, because the two sites that open-coded finalize plus materialize_tree now settle through SettleTree::Materialize, and converge's hand-written persist() is the settle tail its own write was spelled out beside, with the one of the three that no test in the crate ever entered now pinned (#1468) 15e33421 · dbf3dbe6…diff
  • a second renderer stops being able to disagree with the first about which paths changed, because "what changed between two points" is now one seam taking (from, to, pathspec) whose endpoints may be a recorded change or the live working tree, and the six-rung content ladder and the in-the-clear plaintext count moved into it — so a renderer formats a number it is handed instead of tallying its own, and status and diff stop computing the same delta twice (#1541) 289ddd4e · dbf3dbe6…diff
  • loot stops having no way to read one recorded file, because `loot cat <path> [<selector>]` resolves one path at one change through the same endpoint, key oracle and burn lookup the delta seam already resolves a whole side with — and it names which of the four absences it hit rather than printing an empty file, while binary content passes through verbatim because the third option nobody names, a lossy render, writes a corrupt file and exits 0 (#1490) cc1384f9 · dbf3dbe6…diff
  • loot diff and loot cat stop giving two answers about one embargoed path, because the four-rung absence ladder cat.rs already had moved into the delta seam and both renderers now match on it instead of each re-deriving burned-vs-sealed — and beside it an option group stops claiming the positionals of a leaf that never declared an arity, which is what would have turned `loot diff HEAD~1 HEAD` into a pathspec of its own selectors (#1551) 2248a1f4 · dbf3dbe6…diff
  • a verb stops reading every loose object in the store before it can open one, because the open now indexes the object directory and reads the file at the address it is asked for, so one complete index still answers membership, the persist's candidate set, gc's sweep and ADR 0038's burn while a burned address keeps no route back through the directory it came from, and the sixth opt-in half that can see any of this refuses a fixture whose object directory does not hold the objects it claims (#1545) 5c98eff6 · dbf3dbe6…diff
  • loot-revset stops being a finished parser wired to nothing, because `loot log <revset>` and `loot shortlog` now read one door that tries the historic selector shape FIRST and only parses a non-selector word as an expression — which is what keeps `loot log <change-id>` working, since the crate reads bare words as hex while a change id is the letters k-z, so the grammar was never the strict superset its own docs and this ticket both claimed (#1495) dd223bf0 · dbf3dbe6…diff
  • an unexpected head stops being something the operator can only fold on faith, because `loot heads` lists every live head with what introduced it and how many changes it holds of its own — and since a head is a tip no other head reaches, a count of zero can only mean the head is not live, so #1477s empty head is superseded and the converge it asks for would merge nothing (#1496) 955ec9ff · dbf3dbe6…diff
  • a push stops walking its whole object store to answer one yes-or-no, because the unsigned-tip refusal now asks whether anything is offered at all and stops at the first thing it finds, instead of building the entire unfiltered offer and then discarding it — on every push whose relay declared heads — to walk again scoped to them; and that walk stops being unmeasurable, because no cli sample can reach it at all: the tier gives every push repetition a fresh relay, so have is always empty there and the discarding branch is never entered, which a new --offer-guard half over a 128-change history reads at 34.3 ms against 17.6 us (#1559) dca66add · dbf3dbe6…diff
  • a verb that takes no positionals stops being able to say so by claiming the opposite, because `NO_ARGS` now sits beside `NO_PATHS` and a census reads every verb invocation shape out of USAGE to find the ones still spelling nothing as open-ended — which is how `loot heads junk` was accepted 88 minutes after the same hole was closed at `loot cat`, and beside it `HEAD~n` ends where the revset lexer already ended it so a range can be typed at all (#1562) fb140e48 · dbf3dbe6…diff
  • the everyday `loot diff` stops answering about a change instead of about your work, because the endpoint you did not type is now the disk — one rule replacing two defaults, so `status` and `diff` read one DeltaSpec rather than two spellings of it, and seeing your own uncaptured edits no longer costs you a subject line you have not thought of yet (#1491) f07d74e7 · dbf3dbe6…diff
  • reading history stops meaning reading every row, because `loot log` grows a when column, --oneline, -n and --reverse — and the date it prints is the one `authored_clock` and `when_human` already decided for heads and evolog, not a fourth spelling, while `log_selector` stops stepping past flags by hand and reads the declaration instead, which is what would have made `loot log -n 3` refuse about a change id nobody typed (#1494) b61d5bc4 · dbf3dbe6…diff
  • loot grant stops issuing an untimed key for an embargoed path whose ciphertext has rotted, because the verb asks for a reveal_at directly instead of reconstructing one from a visibility whose absence has meant held-but-unreadable as well as not-held since #1545, and a timing that cannot be read now refuses instead of defaulting to zero (#1578) d291d168 · dbf3dbe6…diff
  • the implicit-capture door stops faulting the whole object store in from disk before every bare mutating verb, because the closure walk it gates on asks the index the membership question it always meant, and that door reaches a skip only through false so a held-but-unreadable address can no longer switch it into the narrower policy #891 wrote for an incomplete closure, while the half that watches the walk stops timing a store with no files in it (#1576) 732d63fa · dbf3dbe6…diff
  • loot status stops reading the whole working tree twice, because the working row and the pending delta now come from one walk taken through the door #1383 built for exactly this verb, and that walk's plaintext moves into the live side rather than being copied out of a tree nobody reads again, while the read-only door finally carries a number: a cli-tier status sample whose control refuses the two shapes that read the tree fewer times, and a test-only walk counter that pins the count at one (#1549) 8a9411f2 · dbf3dbe6…diff
  • a publication marker that could not be read stops being carried as an unpublished one, because the three re-seals that carry @world across a rewrite now ask a read that refuses instead of a predicate whose swallow was argued for the publish guard alone, and the refusal is a typed variant rather than prose so a corrupt seal reaches the run-loot-verify remedy a vanished one already did, while ADR 0012 table rows name their consumer and state the grep the census is the output of (#1582) 6ee38984 · dbf3dbe6…diff
  • loot status stops re-reading .lootattributes once per listed path, because the publish column now comes from the one-load derivation #481 built for exactly this, so a 2000-path listing pays one policy read instead of 2001 and that count is pinned as a number that does not grow with the tree, while the two whole-tree decrypts this ticket named measure at 1.5 ms and 4 ms and are left alone with those numbers written at the seams (#1591) 24dd342b · dbf3dbe6…diff
  • a corrected census stops being replaced by a fresh one, because ADR 0012's table drops the completeness claim that had already missed a consumer on the day it landed and states its rule instead, the swallowing publication read that consumer used is deleted along with it so outside loot-core there is no route to one left to enumerate, and HUNT-PERF's list of which perf halves re-open their fixture per repetition, which refuted itself inside a single sentence, is deleted in favour of each half's own doc rather than pinned as prose, which is #1587's half of the same defect (#1593) 4995641a · dbf3dbe6…diff
  • a disclosure invariant stops being written down with a reason its own test disproves twelve lines earlier, because what keeps a live delta from printing withheld content is two gates and neither is about the disk — `content_side_of` seals a recorded path outside `visible_paths_at`, and `classify` refuses rung 1 unless both sides hold bytes — which matters because `DagRepo::get` never checks grant expiry at all, so an expired grant is one decrypt away and only the seam refuses it (#1597) bc388741 · dbf3dbe6…diff
  • losing one bad file stops costing you every other uncaptured edit, because `loot restore <path>...` discards per path against the same delta seam `status` reads — and it never decrypts to decide, so a path whose recorded side is sealed, embargoed, burned or merely past its grant expiry refuses the whole run rather than clobbering the readable half beside it (#1498) cd463799 · dbf3dbe6…diff
  • the harness stops measuring only what a run produces and starts measuring what it does, because the three work counters this run built and threw away are now one family in loot-count that a single during region reads whole, with logical object opens and disk reads kept as separate numbers since 2,000 of the first can cost zero of the second, and the family ships compiled out rather than as cfg(test) one-offs because cfg(test) reaches neither an integration test nor loot-perf and would foreclose the gate #1602 exists to add, at a cost measured at zero bytes in the released binary (#1600) 482a72c1 · dbf3dbe6…diff
  • a named landmark stops needing a format major to carry its message, because a landmark IS an attestation under a reserved role envelope the signature already covers — and that is also what keeps a retirement from eating the thing it retires, since the attestation log is keyed by (change, attester, role) and writing both under one role made the retirement silently REPLACE the attestation rather than record beside it (#1508) 2d06014f · dbf3dbe6…diff
  • a repo open stops materializing half a million manifest entries no verb asked for: ChangeNode.tree becomes a Manifest holding either the decoded map or a proved-decodable byte range into one shared Arc, so topology decodes eagerly because reachability needs it while a change's path manifest decodes only when something reads it - which is where the cost actually was, since framing the 37.9 MB graph without building the maps takes 6.8 ms against 507 ms to build them, and that 507 ms was the bulk of the 1050 ms every verb paid, including loot whoami whose whole job is to print 93 bytes out of id.pub. Deref carries the ~270 existing node.tree read sites unchanged and Deferred is module-private, so a deferred manifest cannot exist over bytes Manifest::walk has not already refused with the same framing the eager pass uses, which is what earns an infallible &TreeManifest return rather than a Result; and an unmaterialized manifest is pinned never to read as an empty one, the rule read_graph_required already states for an absent graph file. Verbs also declare what they read now: RepoNeed rides beside CustodyNeed through the already-pub(crate) open_at_clocked_needing, so no new public door is added and the sealing door custody_lock pins by string and call-site count is untouched, with whoami, conflicts and manifest narrowing to POSITION_ONLY, defaulting to EVERYTHING everywhere else, and a withheld graph or store aborting loudly rather than reading as empty. whoami 555 to 42 ms against a 40 ms control, conflicts 557 to 41, manifest 557 to 42, log -n 5 626 to 144, status 704 to 227, lanes 4069 to 717, peak RSS for whoami 148 to 7.1 MB, and twelve verbs byte-identical against main including 414 KB of loot log. Three of this ticket's premises were wrong and are corrected rather than worked around: the keyring it named as the second cost measures 1.1 ms and is left alone, read_graph_outline did not fit because it discards the parents reachability needs, and the object-store half it called the boolean that costs a gigabyte had already landed in #1545 - while the stat cache stays unbuilt, which is the whole of what status and lanes still spend (#1536) b923abb1 · dbf3dbe6…diff
  • loot log grows --graph, and the half this ticket is named for turns out to have landed already: #1494 built the per-head sectioned listing, so the 255 refusal on a diverged repo was gone before this started and three of the six criteria were already met - and --all is dropped rather than built, because loot's default listing is already the union of every head while git needs that flag only to widen from HEAD's ancestry, so porting the spelling would have shipped a flag that changes nothing. What is left is the drawing, and it goes in behind a seam rather than inside the formatter: topology.rs resolves each row's recorded parents into indices within the listing and flattens the fork's three sections into one children-first sequence, holding the two derived facts a JSON parents array and an ASCII gutter would otherwise compute separately and disagree about silently, so #1540 can emit machine topology off Topology.nodes without touching lay_out or draw. HistoryRow gains parents from graph_view, ids only, so no manifest materializes and the 507 ms d6be741 removed stays removed - and no DAG walk was needed at all, because the per-head and shared split is already a valid children-first order, which makes the whole feature one map lookup per recorded parent rather than a second traversal. The empty head is named above the drawing rather than drawn, since a head recording no changes of its own is the one fact a picture cannot show by drawing it and a two-head repo would otherwise look unforked, and the shared-history heading goes away under --graph because the join is visible as a merge line instead of a third section to correlate by eye. The fork pin is extended rather than replaced, --all is pinned as refused with its reason in both the unit gate and the smoke test, four independent breakages were each proved red before the tests were trusted, and a join drawn as a fork opening - which abandoned the leftmost column and indented a thousand rows under a lane that had already ended - was a layout defect only the real two-head repo showed (#1497) e963a2a9 · dbf3dbe6…diff
  • the pathspec option group gets its first production consumer, and being inert turns out to have been invisible by construction: all five .groups attachments sat inside cfg(test), so Pathspec::keep returned at its is_active guard on every production call and no test could see it, because a filter that never runs changes no output - which is why delta.rs claimed in the present tense that diff, status and show were all served by that line while none of them attached the group. diff and status now attach it, diff narrowing at #1541's seam so rows, --stat and --content inherit one filter rather than each applying its own, and a -- separator arrives with it because a pathspec needs a way to say the rest are paths. Two states the ticket did not name are where the real bugs were. status builds its delta lazily, gated on the human format, so --porcelain and --json never reached the seam at all and attaching the group alone would have left both machine formats printing an unfiltered listing at exit 0 - the format an agent reads, which is the caller this class is dangerous to; and with no working change status returns before reading the tree, so loot status zzz would still have exited 0 in the primary checkout while passing every test on a seeded temp repo, since a seeded repo is never empty. Both are now answered from the seam, the second gated on is_active so pathless callers pay nothing. status also stops silently dropping its argument, which removes it from OPEN_BUT_TAKES_NONE and leaves 23 - by the other route, taking paths properly rather than declaring NO_ARGS, and the list's prose implies the latter is the fix for the names still on it, which would foreclose the better answer for surface, converge and ferry. The false sentence is narrowed rather than deleted and carries why it was written a ticket early, in delta.rs, flags.rs, the census doc and CONTEXT.md. Eight breakages proved the tests non-vacuous, planned before they were written, and dropping both attachments - the exact pre-ticket inert state - turns six of six process tests red plus the census pin (#1493) 35ed624c · dbf3dbe6…diff
  • the mis-seal question gets a verb, and the reason it could not have one turns out to be that the decision existed three times: visibility_for, is_published and public_by_fallthrough were three near-identical loops over the attribute rules, so there was nowhere to hang an explanation off and the rule against writing a second matcher was impossible to honour until they collapsed into one Attributes::first_match. That collapse, not the line numbers, is this change - the line numbers were already tracked since #1329 for MalformedRule, including the convention of counting blanks and comments so a number matches what an editor shows. loot explain now names the .lootattributes line that decided a path, lists what it passed over so first-match-wins is visible rather than asserted, reports which .lootignore line covered it, and answers for a path with no file on disk, which is the mis-seal question asked before the mistake rather than after. Because explain and the real decision are projections of one traversal they cannot drift, and that is pinned twice rather than assumed: one test deletes the line explain named and asserts the real visibility moves, then deletes each line explain called skipped and asserts it does not, hardcoding nothing about which rule should win; the other runs a real capture and asserts that for every recorded path the sealed tier equals explain's token, with the ignored file absent and a positive control that the sealed set is non-empty. Eight breakages were each proved red before the tests were trusted, including giving explain its own matcher while the decision kept the old one. And loot explain . would have printed a fully furnished answer about a path that is not one, because ** matches the empty string in this glob dialect and the door rebases a bare dot at the repo root to the empty relative path, so in any repo carrying the catch-all ADR 0038 says every real repo wants it would have named a tier and a line for nothing at all - refused at the door now, with a control showing the rules really would have answered. Found by running it rather than reading it. Five censuses needed updating where the plan named two, and the two nobody would predict are the README verb block, which is a docs file failing in the bin suite, and the derived impl-width sentence in verbs/mod.rs, because adding a single inherent method to Workspace reddens a doc comment (#1521) ebd3285f · dbf3dbe6…diff
  • user-declared gates arrive as data rather than as hooks, and the decision the ticket owed splits rather than picking one of its three options, because the gate vocabulary is two halves with different properties. The mechanism moves down into loot-cli, since its value is concentrated in the fake - a FakeSpawner that panics on an unstubbed step is what turns no process spawned from a claim into a failure, and two fakes drift with a fake that has quietly stopped intercepting being indistinguishable from one that works; loot-first keeps a re-export shim so no orchestrator caller changed. The phase axis is duplicated deliberately because it is not the same axis: loot-first asks about two trees, one of which only a land produces, while the CLI asks about two commitments, and a shared enum would carry four variants of which each consumer must refuse two - a rule nothing checks, where two small enums are checked by the type system. A new crate was rejected on arithmetic rather than taste, since loot-first already depends on loot-cli and there is no pair needing a bridge; loot-core was rejected because only half of #1682's criterion transfers, and that half is now a census asserting the engine spawns nothing rather than a sentence. A gate runs after the capture and can never prevent it: it does not decide whether the tree is recorded, only whether it is signed or sent, so a refusal costs the signature and nothing else, which is the ordering #174 already chose. On the confidentiality boundary the absolute form of the criterion is unachievable by any mechanism that runs a command in the working tree, and saying so is better than a strong claim that will not hold - loot surface writes and never deletes, so a path whose grant lapsed stays on disk as stale plaintext readable by cat, grep, the editor and every process running as the operator. The guarantee is therefore the narrow checkable one, that a gate is granted no read the operator does not already have, held up by four facts each of which a regression would have to break first: the runner never sees a Workspace, nothing is materialized, a Step carries program args and cwd with no environment channel, and there is no shell. A fifth fact answers a different attack - the declaration is untracked, so a pull cannot write one, which is git's own reason for never transporting hooks and matters more here since ADR 0075 has the forge holding proposals. This ticket also understated the surface that already exists: loot resolve --tool hands a user command decrypted plaintext today via LOOT_BASE and its siblings, so gates are not the first such door but they are the one that opens none (#1538) 44d4869a · dbf3dbe6…diff
  • loot surface stops opening every object four times, and the instrument had to come first because the land's own gate is structurally blind to this: the gated fixture reaches the engine's Repo::surface, a different loop with no view filter, no report, no clobber guard and no CLI wrapper, and loot surface is in no cli tier sample, so a flat gate here says nothing either way and the named signal is a new loot_count::during region instead. The win is that the clobber guard has already resolved the exact Modified set and the materialize then rewrote every path anyway, so a SurfacePlan carrying at_risk, measured_at and already_on_disk hands that answer down: object_gets falls 4N to 3N at every one of the ticket's sizes (200 to 150, 800 to 600, 3,200 to 2,400) with object_disk_reads flat at N, which is the shape of an answer that got cheaper rather than one that changed. The dangerous part is not the speed but the authorization, because the third gate is inside the very step the set skips - sealed::open's embargo-then-visibility check is the one chokepoint and a trusted path is not re-authorized - so the trust is taken BELOW the view (#39) and grant-expiry (#20) gates rather than above them, and both orderings are pinned by tests that were proved red by moving the trust up: an expired grant materialized, and a path outside the view written. A plan measured against another change is refused by measured_at, and that breakage went red with disk holding the midpoint's bytes where the tip's were owed, which is content corruption rather than a slow path. Building the set by stat instead of a proven get went red twice, once on an embargo that has not lifted and once on --force restoring recorded content, so the set only ever names a path a Readable::get already returned Ok for - a precondition on the caller that the engine cannot re-derive, stated in the doc of the pub method rather than implied. The pin asserts the multiplier is identical at all three sizes rather than any literal, and its control refuses a store that cannot reach disk, which is #1576's --closure lesson made executable: the blind shape is the Workspace that authored the change, where put filed the plaintext and a surface decrypts from a map at object_disk_reads 0; the control is demonstrated refusing that, accepting a reopened one, and a separate non-vacuity guard is demonstrated firing on an empty fixture. One of this ticket's premises is wrong and is pinned rather than repeated: surface --json has always materialized once, not twice, because the machine arm returns before the human arm ever reaches surface_with_report - measured at 3N over a wiped tree - while rehome --json really did run two complete materialize passes and now reads the written it already had. The readable_at-per-path hoist is declined with the reason written into resolve_delta rather than left silent: on an empty escrow, which is every repo with no pending embargo including this ticket's own fixture, Escrow::flush is a map emptiness check collecting an empty iterator and contributes nothing to object_gets, so the hoist would measure flat in its own signal and wants an instrument that can see escrowed entries before it wants an edit. absent_paths_at keeps its N opens deliberately, because it walks the whole recorded tree including out-of-view paths the materialize loop never reaches and deriving it from that loop would silently narrow the listing. One TOCTOU window widens in kind and not in size: a file another process edits between the disk walk and the materialize is now left alone while still reported materialized, the same window the guard's answer already went stale in and in the operator-safe direction, so the type doc says surface's disk-mirrors-this-change promise is that much less absolute (#1703) 43a89f31 · dbf3dbe6…diff
  • loot ferry parses both policy files once per commit instead of once per changed path, and the instrument had to be built before the fix because PolicyLoads sat on Attributes::load while both ferry doors call parse, so the counter read a structural zero over this whole path and would have read zero after the fix too - a 0-valued counter being indistinguishable from one watching code that does not run. The tally moves to Attributes::parse and Ignore::parse_recorded rather than being routed through a counted door, because the fix itself removes those door calls: a counter on them would read zero afterwards, which is the same blindness relocated. That changes what the counter MEANS, from policy file reads to policy re-derivations, so all three existing pins were re-read rather than adjusted until green - status moves 2/3 to 3/4 with the +1 being its single Ignore::load now counted, and the point is that the GROWTH half passed untouched (narrow equals wide) while only the constant moved, so the test is re-pinned and not re-decided; the tier-exclusion test is still green because the in-process tier links loot-core and never loot-cli, so ADR 0073's exclusion needs no re-taking; and lane_new_sweep's note that policy_loads is one per read_tree_at is repointed to two, since read_tree_at calls both loads. The instrument was proved non-vacuous against the UNFIXED code first, at 18, 66 and 258 parses over four commits of two, eight and thirty-two changed paths, which is exactly 2 plus 2 times commits times width; after, it is 2 plus 2 times commits, so 258 falls to 10 at width 32 and stays 10 as width grows. The most useful thing learned here is a red proof that inverts an assumption: blinding the instrument by putting the tally back on load makes the counter read a constant 1 everywhere, so the constancy pin passes AND the parses-greater-than-zero guard passes, and only the GROWTH assertion catches it - a positive-value pin does not protect against a blinded instrument, which is what ADR 0072's controls bullet credited it with, and that bullet is corrected rather than left standing. This ticket's own wall clock does not reproduce and is corrected rather than repeated: the removed re-parse is 4.63 microseconds per path in release against this repo's real policy files, not 22.0, so a full-history ferry is about 1.9 seconds rather than 8,819 milliseconds - 22.0 is close to the debug reading of 31.53, so the hunt appears to have measured a debug build, and a figure taken under a different build is not a smaller version of the same number. ignored_under is deleted rather than kept as a pure forward once the parse is hoisted, and ADR 0028 is amended because it argued its delete-arm decision partly on an Ignore::parse per deleted path, a cost that no longer exists - the decision stands on the attribution argument, which was load-bearing anyway. seal_under takes a parsed Attributes and narrows pub to pub(crate) since Attributes is crate-private, and its doc said it keeps the bridge from re-parsing the policy twice per path, which was true about the wrong unit: it halved a cost that should never have been per-path. One honest regression is recorded rather than hidden: a deletions-only commit now costs 2 parses where it cost 0, because the hoist is unconditional (#1704) 1822132f · dbf3dbe6…diff
  • a proposal row gains an owner, because it had none: /propose asked whether the signer may propose here and never whether this ROW was theirs to write, so both stores upserted unconditionally with the state reset to open and only landed excluded - which made the ban-and-quota handle rewritable by any account holder who could present the same change id, and made the author's withdrawal reversible by a stranger. It was proved by running the hijack rather than by reading, with a throwaway witness that printed the takeover happening: state Withdrawn with the presenter as proposer before, state Open with the hijacker as proposer after. The rule is the party whose act would be overwritten and nobody else, and it is refusal FIVE because it needs the change id the byte refusals produce. Open lets the PROPOSER amend, which is the carrier re-presenting an updated stack and is the legitimate case the rule cannot simply forbid, so it carries a positive control. Withdrawn lets the AUTHOR alone reopen, because a withdrawal is the one-actor decision withdraw already makes and letting the carrier undo it reaches that decision from behind. Declined lets EITHER party, and that asymmetry is argued rather than assumed: the decline is the OWNER's act and an owner cannot propose, so a stricter rule would leave a declined change id with no exit at all, which is precisely ADR 0075's honourably re-proposed arm. Landed is untouched and stays the store's own invariant rather than being restated. Every party is read from the row ON FILE and never from the presented tip, which the caller chose. The refusal is kept from becoming an existence oracle twice over: concealed and visible-but-not-yours collapse into one sentence that names no term of the row, asserted byte-identical across two keys and both settings of metadata_public with a loop proving it names no proposer, author, tip or state - and the structural half is the stronger one, since both entitled parties are already standing readers, so the write gate is a PROPER SUBSET of the read gate, pinned with the owner as the control because the owner reads and is refused writing. One instruction was subtly wrong and is recorded rather than followed: withdraw pairs ABSENT with concealed, but /propose has no absent refusal at all since an unused change id is ADMITTED, so the assertable pair is concealed against not-yours and the residue - that being refused says the change id is taken - is stated in the amendment instead of being claimed away. The disclosure stops overclaiming: it said nothing has left this machine yet while a SIGNED posture read had already gone and a test welded that sentence, so it now names what went and what it told the forge. And burn stops calling a proposal a push, which is the rule disclosure.rs states two files away, with is_disclosure and the tier token both untouched and a test asserting the prose moved where the token did not (#1746) db0f3142 · dbf3dbe6…diff
  • converge retires a superseded head durably instead of only in memory, and the investigation had to come before the fix because the ticket named a symptom whose two halves have different answers. The stray head b0cffc66 is a superseded version under ADR 0032 of change txmplwto, #1042's forge-push work: same parent, 19 minutes earlier than the version that landed, so it is an AMEND SIBLING and not a child, which is why nothing that walks parents ever excluded it. It carries nothing a converge could merge, established three ways rather than asserted - the replacement was the primary's sole head at ops 975 to 979 and is on landed main; own reads 0 over the liveness-filtered walk, and since a head is always in its own exclusive line, own 0 can only mean the head itself is not live; and Liveness::partition puts it in stale, which converge abandons without merging. lane rm and lane gc structurally COULD NOT have taken it, which retires the ticket's own framing: both verbs are the same two steps, delete the lane directory and delete the lane entry, and neither opens the primary's .loot/heads - a lane's own head set lives in the lane's file, which rm removes with the directory, so #532's owner scoping never came into play. The SEATING is not reproduced and that is reported rather than papered over: today's lane new puts one head in and one head out against a copy of the real store, the derive_all_heads fallback produces 213 tips rather than 2, a local edit-and-new leaves one head, and an apply of a superseding bundle drops the predecessor and persists - four hypotheses refuted by running them. The PERSISTENCE is reproduced and is the live bug: converge abandons stale heads in memory, but the only persist on that arm is the settle that runs when the tip MOVES, so when the survivor is already the tip - which is exactly this ticket's orientation, a primary on landed main beside a superseded original - nothing is written and the drop dies with the Workspace. Measured on a metadata copy of the real store, converge printed already on one line, nothing to converge and left the head file at 64 bytes with the stray head intact, so loot log's own advice to run pull was a no-op for this whole class, and converge and heads reported the same head set while disagreeing about it. Removing the persist fails the new test on the REOPEN with both heads back while every assertion above the reopen still passes, which is precisely why this lasted 490 ops. A second and separate hazard was found on the way and is fixed with it: derive_all_heads, the fallback when the head file is absent or empty, excluded only nodes named as a parent, so every amended-then-landed original was a permanent derived tip - verbatim what the artifact's own doc says must not happen, and the case #1135 refused for a MALFORMED file while the empty one still fell through. Predecessors are covered now, and on a copy of the real store with the head file removed it derives 31 tips where it derived 213. The reap decision is that lane rm and lane gc should NOT grow head reaping: the head was never theirs, a lane's finalized head is a signed change already in the shared store, and stacking an irreversible head deletion behind an irreversible directory delete is two irreversibles - a head may be retired automatically only under a predicate proving it is not the sole reference to anything live, and that predicate's consumer is converge, which also holds the merge base, the conflict classifier and undo, none of which lane gc has. Neither change can destroy work: abandon retires a head ENTRY, the graph is written as a union with disk, and gc roots from that file - after the fixed converge the head file went 64 bytes to 32 and the graph stayed 41,598,284 bytes with the change still present. The ticket's claim that loot log exits 255 is stale and is recorded as such: it exits 0 today and renders both heads (#1477) 8448e586 · dbf3dbe6…diff
  • ADR 0079 settles what a visibility tier MEANS for a live delta row, and the answer is the shipped one for a reason narrower than the ticket argued: a live side is read at the tier the disk assigns it, because visibility describes SEALED content and loot gates on a key rather than on a tier - reading a live row at its recorded tier would make a declared tier withhold something all by itself for the first time anywhere in loot, and the first bytes it would withhold are the author's own from the author. The ticket's framing overstates what actually ships, and that was measured rather than repeated: with the embargoed path FINALIZED so the anchor holds it, a bare loot diff --content REFUSES at 0 paths rendered in the clear with the plaintext sitting unsealed on disk, so the real rule is that the record wins wherever it holds a version and the disk answers only for the rest - and the rest is a path added since the anchor. The two positions were far closer than the ticket says. The losing position is recorded in its own terms rather than strawmanned, because #536's discipline is about what leaves in a transcript and not about what the operator could otherwise reach, and an agent piping into a PR body does capture what the verb used to refuse; it lost because it has no well-formed answer for the row this ticket is actually about - a path with no recorded tier must fall back to .lootattributes, which is the disk's tier the long way round - and because the refusal is a BAD signal for that job, firing on paths the operator wrote thirty seconds ago while staying silent on the restricted paths #536 always meant to print. The residual is named with no mitigation claimed, since nothing stops the PR-body case and no mechanism at this seam could, the same bytes being one cat away. Its concern nevertheless landed as two real fixes that nothing covered. Unreadable::of_row asked the OLD side about prev_visibility, which is always absent over a live endpoint, so it was really asking about the disk: one path at one instant answered embargoed from cat, content not readable here from --content and sealed from --stat, which is verbatim the #1551 defect, and two of those three pointed the reader at loot grant for something ADR 0007 lets nobody grant. Reverting that term fails the new agreement pin while #1491's own pin stays GREEN, which is the demonstration that the old pin never covered it. And the in-the-clear tally under-reported: Tier::disclosed_by read one side, so dropping a restricted rule and editing the path - the ordinary declarative un-restrict, and the case the function's own doc names - printed recorded restricted plaintext under 2 paths rendered in the clear naming no tier. It now reads 1 restricted, filled through a REQUIRED constructor argument rather than by relaxing prev_visibility, which would print a was-to-now transition for a policy edit nothing recorded and reopen the #1005 phantom; over a both-recorded delta the tiers are equal and max is idempotent, so the tally can only gain a note and never lose one. A pin that closed on embargoed greater-or-equal to one is tightened to full set equality on the disclosure, because a nonzero check is not a guard. One label changed and no gate did: content not readable here becomes the embargo sentence, which AC2 cannot be satisfied without, and no byte of content moves in either direction. A row may now read internal in its header and embargoed in its body, which is correct under this ADR and asserted deliberately - the header is the disk's tier and the body is why the record's side would not open, and moving the header would be the losing position arriving one field at a time (#1574) daaf69d3 · dbf3dbe6…diff
  • a move becomes a recorded fact instead of a later guess, and the premise this ticket rested on was false: #98's object reuse is keyed by PATH, so a moved path is absent from the outgoing tree at its new key, falls through to put_sealed and gets a fresh address - the same object under a different key was not a fact waiting to be read off the tree, it had to be made true by extending the reuse across the move, and everything else follows from that. The rule is that a move is recorded only when the path's sealed object survives it, so a move whose content also changed in the same capture window shares no object, records nothing and is two rows, while a move and then an edit across two captures of one change composes and reads as one row saying the content also changed. Empty content and any ambiguity are refused for the same reason the whole design exists: zero bytes equal all zero bytes, and choosing between two vanished twins would make a signed fact depend on iteration order. On the boundary the render side compares nothing at all - resolve reads keys and never an address, a visibility or a byte, pinned by running it twice over trees that agree on every key and differ on every address at three unopenable tiers, with a positive control beside it so agreement is not evidence it answers nothing. The capture side compares once: it opens a vanished object with this identity's own keys, fail-closed so an unopenable object is never read or hashed, and pairs only against an addition at an identical visibility and publication tier, with the digest living for one call, never an address, never stored and never on the wire. What a relay newly learns is that two keys held equal ciphertext, which recording a move states in plaintext anyway since tree paths are plaintext at every tier, so it is inherent to the feature rather than the ADR 0004 oracle - written into that ADR rather than left in a message. Renames ride the label seam change_id and authored_at already use: covered by the finalize signature so a relay cannot rewrite a move, never folded into the version id so two peers reaching one tree by different routes still agree on its address, which is why every existing id and signature is unchanged and a v11 store reads with no move recorded. That was tested rather than argued, against a real store built by the shipped binary, where the old move still renders as a delete and an add in the same repo the new one renders as a rename, because inferring the old one would be the equality oracle arriving through the compatibility door. Three further things were wrong and are corrected rather than worked around: ADR 0019 says an additive change bumps the minor and has never described this project, since the minor is still zero and all five additive changes took the major; plan_moves own doc comment claimed the tier is checked before the digest, three lines above code that does the lookup first, when the property actually holds by the stronger route that nothing unopenable enters the index at all; and the empty-content refusal was written on both sides of the pairing, where each made the other unprovable and removing either left the test green - a duplicated guard is not belt and braces, it is two guards neither of which can be shown to be doing anything (#1539) 3c7e029b · dbf3dbe6…diff
  • a silent merge left one row reading its content at one key and its tier at another, and the fix is a function that cannot hold two keys: #1539 read the old side of a move at the key it had, ADR 0079 read the old side's tier at the row's path, the two changes rewrote the same three lines, and the converge took one line from each without conflicting - so content came from old_key while the tier came from delta.path, and for a renamed row the old side holds no entry at the new key. That made from_visibility None on every rename, and prev_visibility is None there too since rows fills it only under Modified, so both terms in the disclosure tally were empty and everything fell through to the destination's tier. Demonstrated rather than argued: before the fix, a path recorded restricted=connor and moved to a key the attributes call internal prints its recorded plaintext to stdout under a closing line naming no tier, which is ADR 0079 section 4's defect verbatim. The repair is not the one word it looks like. side_at takes a single key parameter and returns the side and its visibility together, and delta.path is not in scope inside it, so content at one key and tier at another stops being a mistake the function can make rather than one a comment asks it not to - reopening this would require deliberately widening the signature, which no textual merge can do by taking one line from each side because there are no longer two lines to take. The ticket's second half is not production-reachable and saying so is the more useful half of this report: an embargoed old side cannot become a move's source, because plan_moves fails closed on an object it cannot open and ADR 0007 means nobody can open an embargoed one, while the capture refuses to overwrite content it cannot see before that. The unreadable ladder was reached the only way it exists, by recording the move while the embargo has lapsed and reading the delta at a clock standing before the reveal, and a third test now asserts both guards so that if either is ever loosened the ladder shape becomes production and whoever loosened it is told. Two smaller framings were wrong too: a move that also changes tier is never recorded as a move at all, since plan_moves pairs only at an identical visibility, so the reachable shape is a move at one tier and then a dropped rule - which is how un-restricting actually works. And the existing tally test stayed green under a faithful revert, which is exactly why this landed silently in the first place (#1767) 796d7e87 · dbf3dbe6…diff
  • the dirty question stops reading every file to answer one boolean, and the two clocks it depends on are not the same clock: a stat cache lands at .loot/<position>/stat-cache as a PositionOwned artifact the store treats as opaque bytes, so nothing derived from it reaches the graph, a manifest, a bundle, the wire or any peer - its version header is a local self-check whose only failure mode is read as no cache, which is the opposite of a compatibility break since an older loot does the full read and gets the same answer. An entry is trusted only when size and mtime match AND mtime is older than the walk's own stamp, so a write inside the walk's tick is always re-read and no granularity is assumed on any platform. What that missed on the first attempt is that the stamp comes from the PROCESS clock and every mtime from the FILESYSTEM clock, which on Windows are the precise and coarse system times about 15.6 ms apart - warm scans hit nothing at all, because the cache file's own mtime read as older than the stamp it carried. A one-second margin fixes it in the safe direction and is self-healing, and gross skew is caught separately by discarding a table whose file is older than the stamp it declares. loot lanes reads a foreign lane's cache and deliberately never writes one: the write would have been correct, and it was refused on a different ground, that ticket-to-lane's runbook promises this verb is read-only and agents run it against live lanes to read the claim board. So TreeScan has two named doors, read for a position walking its own tree and peek for one walking someone else's, and which a seam takes is spelled at the call site rather than inferred from the store it was handed. The cost of that refusal is named rather than hidden: with lanes warm, which is the steady state since every lane's session runs status, it is 13.6 to 16.8 percent against the 16.1 the write bought, and with every lane cold it falls to 4.5, which is not the cache at all but the seam now hashing each plaintext once where it hashed twice. status --porcelain is 24.8 percent. And the pin that #1536 asked for cannot do the job it was asked for: delete the cache and get the same answer passes on every answer assertion even when the cache is never consulted, because an inert cache answers correctly by doing all the work - what distinguishes them is counting hits and reads, whose cold zero-and-N is its own positive control, and a poisoned row that keeps size and mtime, carries a wrong digest over a genuinely clean tree, and must answer dirty. Three of this ticket's premises were also wrong: the 46 ms status baseline was measured where status never walks the tree, since with no working change it returns at is_clean, so the gap over the control was never the tree read; the byte read is a quarter of what holds these verbs up rather than the whole of it; and human status on a dirty tree cannot benefit at all, because rendering a per-path delta needs the plaintext (#1669) 96d922e8 · dbf3dbe6…diff
  • loot merge <head> is converge restricted to one head rather than a second fold: merge_head and converge_heads both reach the graph through one private fold_onto, so the three-way content merge is the ADR 0044 seam unchanged, the conflict record is the one loot conflicts and loot resolve already read, the merge node carries a minted mechanical subject naming the head it folded, and loot undo steps it back through an op recorded off the merge node rather than off an outcome map that can be empty. What it deliberately does not do is retire anything: converge drops superseded heads on the way past, and a retirement is a change to a head the operator did not name, which is the whole of what separates this verb from the one it restricts. A flat head is refused rather than silently skipped, because naming a divergent co-version does not make divergence content-mergeable. Two premises of the ticket were wrong. The un-described-change refusal does not apply as it does to the other merges: that refusal belongs to adopt, raised inside fold_line_in, which captures and signs its own merge parent, while converge never reaches it at all because capture-first defers on any working change one branch earlier - so this verb inherits the deferral, and the refusal is unreachable by construction rather than reimplemented. And loot heads resolves no selector, it only lists, so the spelling merge takes comes from loot abandon --head, which routes through resolve_selector. A third premise is narrower: conflicts are recorded and nothing proceeds, but a single named head has no loop to stop, so that criterion is merge_tips behaviour unchanged rather than anything this change makes true. One inherited shape is named rather than altered - a deferred merge --porcelain prints nothing and exits 0, exactly as a deferred converge does, because a deferral rides the human field while the machine contract is verdict rows (#1505) 6107561a · dbf3dbe6…diff
  • loot count-objects lands, and the constraint it is built under is that its number has to be the number gc acts on rather than a second opinion about it: the unreachable count and the reclaim figure are gc OWN pruner and sweeper called with dry_run, over a keep set extracted out of gc as DagRepo::gc_keep_set the way 1505 pulled fold_onto out of converge, so a census that disagrees with a collection is not a thing that can be written, and the pin measures a store, really collects it, then asserts the two halves and the sum all match. One premise of the ticket was wrong in the direction that saved work: nothing reported how large a store is as a VERB, but the totals half was already factored, since RepoStore::objects_size has been the perf record load-bearing counter since 632, so this adds no third walk of the object directory at all, it adds a caller. The extraction surfaced something gc had been getting away with: its keep set was position-dependent, because the primary own working change is the one root in neither the shared graph file nor the lane registry and verify has been adding it by hand since 1309. gc could not notice, since it refuses from anywhere but the primary, where those oids ride in the loaded graph anyway; a read-only census can run from a lane, so the root moved into the shared function and the answer stopped depending on who asks. Machine output is this verb product rather than a garnish, which is why the porcelain is one row per metric keyed by a leading mark instead of one wide row: ADR 0023 makes a column unaddable, and a census is the shape most likely to be asked for one more number. The anti-vacuity pin is the same fixture reading 1 and then 0 with nothing changed but a change naming the orphan, and the read-only pin fingerprints every file under .loot rather than trusting the op log, which would never have shown a pruned object. On this lane own store it reads 7910 objects at 146.7 MiB with 2330 of them unreachable at 36.9 MiB, which is the 1524 baseline (#1523) f10ba1d9 · dbf3dbe6…diff
  • format-patch stops filing re-seals under a heading that promises the opposite, and its header starts saying in numbers what the body could not carry. the omission list headed itself so that a path left out is never read as a path that did not change, while rung 2 - content is byte-identical, only the seal moved - was filed under it, so the closing N path(s) omitted added withheld paths and re-seals together and neither reader could recover their own number. there are now two headed lists and two closing counts, and patch_omission returns which list a row belongs on rather than a bare reason string; the bytes-differ-but-lines-do-not row stays on the withheld side, because something really changed there and this patch cannot carry it. the header gains omitted, omitted-reasons, unchanged and lossy, always present and 0 or none when empty so that nothing was left out and this writer does not say stop being one observation, all read off a single PatchFidelity the body's own classifier produces rather than a second walk that could call a patch complete while the rendering withheld three paths. PATCH_FORMAT stays 1: the rows are additive and the format had zero consumers, so there was no reader a bump could protect, and that reasoning expires the day 1513 ships. included stanzas deliberately carry no tier - it would have to ride in git's extended-header region, the one place that can cost the git apply promise, and a row's visibility is today's standing .lootattributes policy rather than the version's recorded marker, so exporting it would ship the sending repo's policy as if it were a property of the change. lossy is measured from the included stanzas' own bytes rather than declared, and the end-to-end smoke shows it firing on a real crlf file whose rebuilt bytes are lf. the git apply promise is pinned at last by a test that shells out to real git and skips with a printed reason where git is absent, and the mutation proving it non-vacuous found something worth recording: git apply is lenient about line numbers - shifting every hunk start by one left it perfectly happy - so the strict in-tree applier sees a defect this test cannot, and neither is redundant. beside that: converge gains merge's N path(s) need resolution summary from a shared helper, since the fold a no-remote repo reaches was the one printing conflict rows and then converged onto one line with no next move; hunk_body's comment stops claiming to be the only place file bytes reach a terminal, which patch_hunks made false the day it landed; the fourth-versus-second rendering count is settled at fourth, which is what Rendering's own members count; and the census seam records that a census run from a lane roots the primary's working change only through the working-change blob, so a torn one over-reports unreachable while costing nothing, the run being dry and a real gc re-rooting those oids from the primary (#1786) e41bae33 · dbf3dbe6…diff
  • the branch this ticket names is reachable and already correct, and the finding is its sibling: a first-line probe on every arm of converge_heads_retiring, run over the whole loot-cli lib suite, enters the stale-plus-reflected arm 3 times from three named tests, so it is neither dead nor untested - and the abandonment the ticket observed is gone, because 1477 landed the persist there on 2026-09-09, five days after this was filed two minutes behind 1468. that staleness is the one thing the ticket got wrong, and it got it wrong by being overtaken rather than by misreading the code. what the same probe found is the OTHER retire-and-persist exit, the one taken when the drop leaves two heads and every survivor is flat, at 0 of 938: the divergent-flat tests all return through that if with retired EMPTY, stepping over the write without ever entering it, so the half of 1477s fix that lives there was held by nothing. it is pinned now by a test that seats a superseded original beside two divergent co-versions, which needs two things reading alone would not have supplied - the amend has to happen in a lane so the primarys head file still names the original, and the peers co-version has to be parented on the originals own parent so the original stays childless and is not pruned as an ancestor at load. the pin was proved live by running the mutation rather than argued: deleting that persist fails it on the REOPEN with every assertion above still passing, and fails nothing else in the suite, 938 passed and 1 failed. no behaviour changed - the source edits are the new test plus comments at both arms recording the probe counts, and every probe was removed and grepped to zero (#1480) aa6d3239 · dbf3dbe6…diff
  • loot restore grows --source and it is the same verb with a different base rather than a second reader or a second writer: Workspace::delta_against takes the base mirrored_delta used to hardcode, so the disclosure gate, the four-kind absence ladder and the refuse-whole rule are inherited rather than restated, and what the seam hands back is now the base TREE rather than its key set because a caller re-sealing restored content has to be able to say what the base recorded it as. AC3 is plan-then-apply: every action is materialized from verdicts that were all decided above the refusal gate, so an openable path named FIRST beside an unopenable one is not written, which is the assertion a write-as-you-go loop would fail and the pin names it in that order. Three things are decided rather than inherited. A path the SOURCE does not record is reported and its local copy kept unless --delete-absent is passed - refused without --source, where it could only be a silent no-op wearing a destructive word - but a path in neither the source nor the working tree still refuses, because that is a typo and #67 says a typo must not exit zero looking clean, and AC4s deletion half presupposes a local copy to delete. The loss sentence is a different sentence: what a --source write replaces is recorded content plus whatever uncaptured edit sat on top of it, so #1498s nothing ever recorded them would tell an operator they had lost something loot cat prints in one command. And AC6 is a sentence rather than a behaviour, which is the ticket premise that turned out wrong in the useful direction: restore writes plaintext and seals nothing, so the next capture already derives the tier from todays .lootattributes and there was no publication decision to get backwards - what was missing was the report saying so, per path and on the dry side too. Two further premises were wrong. The raw --dry-run scan #1612 replaced was correct only by a coincidence this ticket expired, since --source is the verbs first valued flag and loot restore --source --dry-run a.rs now reads --dry-run as the selectors value. And the shared recoverability sentence opened in the past tense, so a dry run printed running this without --dry-run REPLACES ... what those paths held on disk is gone - found by running the shipped binary, not by review, and the tense now lives in the real runs frame where it is true (#1499) c193cf35 · dbf3dbe6…diff
  • loot grep grows the options that make a search usable, and two of the ticket's acceptance criteria turned out to ask for something this verb already did unconditionally: -n wants line numbers, which loot grep has printed since #391 with no flag at all, so making them conditional would silently change the output of every invocation ever typed and -n ships instead as a pinned compatibility spelling of the default, exactly as -F now spells out the fixed-string matcher that stays the default with the regex behind -E. the same compatibility argument decides the row shape, so the change column is printed only when the selector was a REVSET, a thing this verb could not search before, while a point selector keeps the three-column row it always had. the pathspec is the declared option group and not a fourth hand-rolled one, which cost the leaf its NO_PATHS declaration first: a group's trailing list begins where the leaf's own arity ends, so grep declares two opaque positionals and attaches PATHSPEC, and because its selector is optional a bare trailing path still reads as the selector and the operator says which they meant with the separator. -E is a hand-written POSIX ERE over bytes rather than the regex crate, which is loot-cli's standing dependency-light no-clap stance applied rather than a new decision; that stance is argued in the ctrlc note in the workspace manifest, not by adr 0041, which governs loot-core and the server-side dependencies and does not reach this; and the cost is measured rather than guessed, by resolving regex alone and diffing its closure against this lock: three new entries, regex and regex-syntax and aho-corasick, since memchr and regex-automata are already in the graph through bstr from gix, though regex-automata is linked there with default features off and carries no dependencies at all in this lock, so regex would compile far more of it than anything does today, all to serve one optional flag on one read verb. AC7 is a real requirement and it is keyed on the content address rather than the path, since repo.put mints a fresh address per call and only a carried tree entry reuses one: measured, a two-change revset over three paths each opens four objects where the natural walk opens six and returns byte-identical rows, which is why the pin counts ObjectGets instead of reading the answer. AC6's aggregation needed a fixture the engine refuses to build the obvious way, because a path this identity cannot open cannot be re-captured while it sits on disk, and the count is per change and path, so one sealed path carried through two changes is two skips. the per-object read is loot-core's own readable_tree classification lifted out rather than copied, so grep and surface cannot come to disagree about what a burned path is (#1500) c1ad796e · dbf3dbe6…diff
  • every conflict-stopping verb gets a named way out, and building it corrected the ticket in both directions rather than just adding flags: merge does not stop on a conflict at all, since merge_tips records the triples and fold_onto then signs the merge node and materializes the merged tree, so an abort there is a view step off signed history rather than the forgetting of a stop; while cherry-pick, revert and squash do the opposite and return before writing anything of their own, so their stop leaves even the paths that merged cleanly unapplied. the five named verbs are also not the whole set: converge, pull, adopt and ferrys carry bounce reach the same fold and the same ingest, and the stale-disk settle files a conflict on the way past any capturing verb with no operation behind it at all, so the covered set is now a census rather than an assumption. the state is one position-owned artifact at .loot/position/in-progress carrying the verb, its operand, and the op-log index the view sat at before the verb ran, declared Refuse on malformed content because a torn record read as absent answers nothing to abort over a tree that really is mid-operation, which is the single failure the record exists to prevent. it is a real artifact rather than a derivation because conflicts-are-non-empty-and-the-newest-op-is-a-stopping-verb answers no the instant the last resolve lands, which is exactly when continue must still work. nothing here re-implements a rollback: the inventory row is Captured at ordinal 7, so abort is that one index handed to op restore, which already restores the view, re-materializes the tree, refuses over edits made after the stop, and appends the compensating operation the op-log criterion asked for; force is declared on the five verbs and refused without abort. continue closes the stop rather than replaying it, because the re-run was built first and measured to reproduce the identical conflict: these verbs are capture-first and a resolve on the pre-dock home position never writes the tree, so the closure states instead that the operation applied nothing. status and conflicts name the operation through one derivation, and the pin asserts the identical line in both (#1506) 21b9faf1 · dbf3dbe6…diff
  • loot config grows a second scope and the local one is untracked because of where it sits rather than because a rule says so: .loot/config.local is skipped by the tree walk at name == DOT, ahead of every ignore call, which is a stronger guarantee than the obvious alternative - a root-level file plus a .lootignore line could not have held, since .lootignore is itself tracked and a fresh loot init writes none, so the first status in every new repo would capture a per-operator default identity as internal, and the dialect has no negation to walk it back with. verified rather than argued: the same content under .loot/ is invisible to status while at the repo root it lands as internal on the very next command. precedence is one function and the merged listing is defined through it, so the file a row names and the value that --get prints cannot disagree; --get exits non-zero on an unset key, pinned through the shipped binary because a library Result proves nothing about what a shell branches on, and a flag-shaped --get value is refused rather than looked up, since the door hands a valued flag whatever follows it. bare set and unset write GLOBAL, inverting git deliberately: loot config set identity is the route init own refusal advertises as one name across every repo on this machine. the settings are read where they matter - remote is consulted by resolve_remote, so a bare push, pull, grants, pull-grants or propose now contacts what the repo names, with origin still the answer when nothing says otherwise. two of the ticket premises were wrong. identity cannot do what it asks: a repo identity is already repo-local in .loot/identity, a config key that overrode it would manufacture the exact name-to-key drift doctor checks for, and loot init can never read a local answer at all because the file lives inside the .loot it is about to create - so the live case is clone run from inside a repo, which is what the pin asserts against a repo configuring nothing as its control. and the local file is not a store artifact: ON_MALFORMED has nothing to choose between, because the key = value dialect cannot produce an undecodable value - the real hazard was an unreadable file reading as absent and falling through to the other scope, which Layer::load now refuses outright. one argv finding rode along: config is the first family whose flags may precede its subcommand word, and args[0] was wrong in both directions, so the leaf now resolves from the first token that is neither a flag nor a flag value (#1507) 2c12d0f2 · dbf3dbe6…diff
  • nearest-landmark naming lands, and the one real trap in this ticket is that distance is not well defined on a DAG: the number printed is the count of changes reachable from the selector and not from the landmark, which is what git rev-list counts for landmark..selector, and it is not a path length - pinned over a diamond where one input reads 5 as a count, 3 as the shortest path and 4 as the longest, because a straight-line fixture cannot tell the three apart and would have pinned nothing. the briefing had this backwards: git describe does not count the longest path, its number is the cardinality of the same set difference, so taking the count here agrees with git rather than departing from it. a shortest path would call you 2 changes past a landmark that fifty changes have landed on top of, and a count is the size of a set, so nothing in it depends on a traversal order or on a clock - which matters on a lane where authored_at is advisory and never an ordering input. the candidate set is deliberately not the one loot buoy resolves: buoy keeps only the maximal attested change per name, so running nearest over that answer would report no landmark for every change below the tip of a name that has moved, when an older v1.0 is exactly what a change under the newer one comes after - so attested applies the same three ADR 0025 filters across every non-reserved role and skips the maximality reduction, and that split is pinned by asserting both halves in one test. ties are refused rather than broken: two landmark NAMES at the minimum distance exit 3 and are named, the ground ADR 0025 already refuses an ambiguous buoy on, while one name on two tied changes is not ambiguous at all, since every tied change renders the identical string and there is nothing to pick between. three things were widened rather than copied - the ancestor walk now has one home in buoy::ancestors, reached by the maximality reduction, by the new distance and by Workspace::ancestors_of, where there were about to be three walks; the three attestation filters are one predicate; and --dirty asks the same reflected-tree question the converge chokepoint and bisect start already ask. --nearest is the first valued flag this verb has ever had, so the door hands it the token that follows and loot buoy --nearest --dirty would have resolved a selector spelled --dirty, silently dropping the marker: refused by name, the remedy config --get landed, with the door handing the flag over asserted as the control. no landmark anywhere in the ancestry falls back to the short version id and still exits 0, because an answer was produced rather than withheld. (#1509) b7b0b031 · dbf3dbe6…diff
  • loot apply-patch lands as the inbound half of patch interchange, and the limit 1512 deliberately left open is CLOSED BY REFUSING: CRLF is refused on both sides, because the two halves are not the same kind of problem. A patch declaring lossy: crlf-normalized cannot reconstruct the bytes the change it names recorded - that is fidelity, read off the exporters own measurement rather than re-derived, since by the time the content is in the file it is LF. The half that actually bites is the LOCAL one: a hunks line space is LF, so writing an applied result back over a CRLF working file rewrites EVERY line ending in it, a whole-file change no hunk describes, that no report would show as more than the lines the patch touched, and that the next capture would sign. Reconstruction was rejected as a guess - the line space carries no endings at all, so an inserted line in a mixed file has nothing to decide from - and documentation as too weak for a silent whole-file rewrite. The cost is named rather than hidden: a CRLF repo cannot use patch interchange in either direction, and the remedy is a bundle, which carries content verbatim because it carries objects. Refusing is also what makes the round-trip pin true rather than approximately true. This is a separate verb from loot apply and not a mode of it, since a bundle carries objects, keys and signed change nodes while a patch carries none of those; dispatch is an exact-name lookup so nothing can collide there, and the place that DOES match loosely is verb_usage, which compares the token after loot rather than a prefix - pinned, because a prefix match would have dragged the whole apply-patch usage line into loot apply --help. Hunks apply strictly at the lines they declare, and what does not falls back to the ADR 0044 three-way against the change the patch names in its own parent row, which is the git am -3 shape and the reason that row is full width; loot-core gains one narrow door, converge::compose, because the theirs side of a patch is text at NO address while three_way_merge exists to pick between two addresses. A contended path is recorded and the run stops with the tree untouched, which makes this the sixth member of the 1506 resume family, and the census is what said so. Three of the ticket premises moved under it. The AC3 re-seal under the local .lootattributes is a SENTENCE and not a step, exactly as 1499 found for restore: the verb writes plaintext and then captures, so the tier is todays local policy by construction, and the export side already carries no tier for it to have inherited - the work was pinning it and saying it in the report. The AC4 no-partial-application rule forced the capture-first snapshot to run AFTER the plan rather than before, found by a fixture rather than reasoned out: a repo holding a path it cannot open has that paths plaintext on disk, capture_uncaptured_edits refuses to re-seal content it cannot read, and with the capture first the verbs own refusal was never reached at all. And a hunk body must be consumed by the counts in its @@ header and never by scanning for the closing separator, because a deleted line whose content is exactly a dash renders as two dashes, byte-identical to it. ADR 0082 freezes the header grammar now that a reader exists, which is why the 1786 fidelity rows went in before this ticket rather than after; the machine shape 1512 expected this ticket to name is deferred, with the reason written into ADR 0066 and PROSE_ONLY_CEILING, because it is one shape shared by both verbs and freezing the import side alone is the same half-contract 1512 declined on its own side (#1513) d5fba81e · dbf3dbe6…diff
  • loot fetch lands as the first half of pull by call rather than by imitation: the negotiate-fetch-apply loop comes out of pull_via_declaring as Workspace::fetch_via, and pull now runs capture-first, that call, then the fold, so the two verbs cannot drift about what arriving means - the same extraction merge made of fold_onto. the custody routing comes out of pull_routed the same way, so a locked fetch takes the exact fork pull takes, relay unsigned and forge refused by name, and fetch becomes the second caller of the sealing door, which the custody gate now enumerates by name instead of counting to one. safe on a dirty tree is structural rather than guarded, because capture-first sits above the extracted half, and it is pinned by byte-comparing status porcelain across the call with a modified tracked file in the fixture - the live version id is what moves, and a capture-first mutation reddens it while the path list stays identical. no re-fetch is measured rather than asserted: the test relay now records the raw wants count per round, a later pull is required to ask for zero addresses, and the non-zero ask the fetch itself made is the positive control - an ingest-free mutation makes that number read 2. what integrating would involve is read off the head partition converge executes, through a shared converge_partition, so fast-forward means no merge node rather than one head. two premises of the ticket are wrong. a subsequent converge or pull completes the GRAPH integration but not the working tree when the repo was strictly behind, because converge_heads returns before materializing on a single head and the auto-surface of pull fires only on a non-empty apply, so the content lands in the graph and never on disk while the pull prints nothing new - the same un-materialized state a no-surface pull and a locked pull leave, and fetch now prints that and names loot rehome first. and --remote is the first valued flag of six verbs rather than of this one, so loot fetch --remote --json resolved a remote named --json; the refusal went into remote_sel, where push, pull, propose, grants and pull-grants all reach it (#1514) 577ae796 · dbf3dbe6…diff
  • loot clean lands, and the two nevers the ticket asks for are properties of how the candidate set is BUILT rather than filters over one that already exists: the protected set is the union over every change tree in the graph, consulted inside the keep predicate of the tree walk itself, so a recorded path never enters a list at all, and nothing under .loot can be reached because that same walk already skips it at name == DOT ahead of every rule, which is why this file adds no second check free to drift from the first. the walk grew a WalkFilter trait so that skip stayed one line with two implementations rather than a second walk beside it. AC6 is the one that eats work and it has its own fixture: a delta compares path-and-address pairs, so a file the graph records which currently holds an uncaptured edit reads as absent and gets deleted - which is why the set is keyed by PATH and drawn from ANY change rather than the current one. a second fixture pins the other half of that word: a path only the FIRST of two changes records, which a set read off the tip or off surface_target calls unrecorded. both were run red by named mutations before being left green, and deleting the DOT skip put twenty three .loot/objects entries in the candidate list, so the pin is not vacuous. the default is a listing and --force is the only thing that deletes; -n is accepted as the explicit spelling of that default rather than a fourth spelling of the preview idea 1809 is open about, and -n --force is refused because one of the two readings deletes files. -x and -X choose which side of .lootignore the run acts on, and the rules are read by the same parser a capture uses so first-match-wins and the absence of negation hold; -e is repeatable through a new FlagSpec::values that value now delegates to, is parsed by that same parser, holds under -x the way git keeps its own, refuses a leading bang because unlike an inherited ignore file the operator is authoring it right now, and refuses a value spelled like a flag because a valued flag swallowing the bare flag after it has been the defect five times this run. the verb is prose-only and the ceiling moved to 55 with its reason beside it: the shape worth freezing is shared with loot view --prune, which has none, and the column that matters is the per-path spared-reason neither remover computes yet (#1515) c0b301ed · dbf3dbe6…diff
  • blame grows six options and the one that mattered was -w, because ignore whitespace when attributing a line reads like a second comparison and this crate has exactly one line matcher: MatchOpts is a parameter of lcs_match_with, lcs_match is defined as that function at the default options rather than as a copy of its body, and the only place two lines are ever compared is match_key inside it, so textdiff, blame and hunks stay byte identical and the whitespace rule could reach diff tomorrow without anything gaining a compare of its own. -L slices a FINISHED annotation rather than narrowing the walk, which is why a range costs exactly the object reads a bare blame costs and the 98 content-address collapse still fires under it, and its rows carry the line numbers of the file rather than of the slice. --since bounds the walk by authored_at, which ADR 0043 makes advisory and never an ordering input, so two rules keep an advisory clock from dropping history silently: a change recording NO clock never cuts the walk, and the first version past the bound is KEPT as a boundary, marked with a caret, so a line older than the bound says the walk stopped instead of being re-attributed to the oldest change the bound happened to keep. --ignore-rev and --ignore-revs-file send a line to the next OLDER version of the path, marked with a tilde, which is a list filter over the versions and not the nearby-line guess git makes there; an unreadable ignore file is REFUSED rather than read as empty, because that failure is silent and inverted, the annotation that comes back blaming exactly the change the operator wrote the file to skip. the ticket got one criterion wrong and it is the porcelain one: it asked for the contract version on that channel like the other machine shapes, but the other machine shapes do not do that, ADR 0023 settled in its 1540 amendment that json carries contract and porcelain stays header-free, and the sentence claiming otherwise is one ADR 0066 records as false by design and guards off the shipped surfaces. so both channels ship and the version rides json, and the spelling is --porcelain rather than -p because sixty verbs answer to one pair of machine selectors. sealed is pinned under every flag rather than once, and --since was the hardest of them: a bound tight enough to stop short of the seal legitimately yields no sealed row at all, so what it must not do is NAME those lines, and it does not (#1516) b8cb568b · dbf3dbe6…diff
  • ahead and behind against a remote without pushing to find out, and the thesis constraint is met as a property of the REQUEST rather than as advice: the question carries an EMPTY PAYLOAD and every comparison is local. a relay is asked POST /haves with a zero length body, a forge POST /ref with a signed envelope over zero bytes, so a repo of one path emits BYTE IDENTICAL bytes to a repo of ten thousand and the question has no room to encode paths, object addresses, or even our own heads. the two endpoints that could are forbidden on this path and ADR 0021 now records why: /wants sends our object addresses, which are per-content identifiers, so a status in a loop hands the relay a per-path edit-frequency profile of ciphertext it cannot open - ADR 0083 refusal at a higher call rate - and /offer sends our head ids, which a push may do because a push is a CHOSEN act and status is not. the pin asserts the recorded path-and-body list SET EQUAL to exactly /info and /haves both empty, asserts the two recordings equal across two very different positions, and carries a positive control that a real push through the same stub records a NON-EMPTY body; mutated into the rejected design it goes red with /wants carrying a literal 32 byte address. offline is split POSITIVELY, the site gate SKIPPED-OFFLINE rule: only reqwest own is_connect and is_timeout may be called offline, and anything that ARRIVED - a refusal, a 404, a proxy page, a truncated body - is unusable, because those are different facts and collapsing them is how a guess gets reported as a measurement. the relay answered and holds nothing is a THIRD thing and reads as declared with a count. every unknown count renders dash or null and NEVER zero, so a machine that never reached the network cannot emit a number, and log --unpushed REFUSES rather than printing an empty listing, because a listing has no row meaning I could not ask and an empty one reads as everything is pushed. the asymmetry is stated rather than faked: unpushed is exact, being the same change lane a push would send through the same ancestor_closure, while unpulled is zero exactly when every declared head is held here and UNKNOWN otherwise, since a head declaration says what the tips are and not how deep they run. no FORMAT_MAJOR bump and the reason is recorded: ADR 0019 marker exists to prevent misparse of a durable or on-wire artifact, an unasked status emits byte identical porcelain, and the R row lives behind a flag that did not exist when the shape froze. no revset predicate either, because loot-revset is handed a GraphView and a KeyOracle and nothing else (#868), so a predicate answering over HTTP would put a network round trip inside revset::select and therefore inside grep and format-patch too (#1522) 2237a331 · dbf3dbe6…diff
  • loot gc --pack lands the write half, and the claim ADR 0083 deferred to this ticket is taken and comes back NEGATIVE: there is no size win and there cannot be one. a packed object is byte identical to the loose file it replaces, so a pack weighs its bodies plus 11 plus 44 per entry and the census total always RISES - measured through the CLI on a throwaway fixture, 12 objects in 11 buckets, 12094 to 12743 bytes. the groupings that WOULD compress are exactly the ones the ADR rejects as leaks, so the absence is the PRICE OF THE ANTI-LEAK RULE rather than an optimization nobody wrote, and the verb says so at the number instead of leaving a true pair that reads as a loss. the repack IS the collector: a rewritten bucket carries forward only the keep set, so an unreachable entry is simply not copied and there is no delete-a-span path to get wrong. what that leaves is a plain gc unable to collect packed garbage, and the hazard is the SILENCE rather than the retention, because nothing to prune - every stored object is referenced by a change is true of the files the pruner walked and reads as a claim about the store, printed at the operator; so a plain gc now OBSERVES it, one header and table read per pack, and prints what it could not reach beside the flag that reaches it. the census asymmetry is untouched: reclaimable_bytes stays a dry run of the plain collector, because a census agreeing with the collection is the whole reason it is trustworthy. the ranged read is implemented and it reintroduced a hazard the ticket never named - a table sized from a corruptible u32 is u32::MAX times 44, 188 GiB of prealloc - bounded now by the file, and the mutation removing that bound ABORTS the test process rather than failing it. burn still refuses a packed address and the remedy now exists and is pinned, since gc_keep_set already drops burned oids so the ordinary collector rewrites the bucket without one and no burn-shaped branch exists in the packer; it is deliberately NOT wired into destroy_objects, which runs from save taking a shared reference, because a bucket rewrite moves every OTHER member offset and the live index spans for the burned object innocent bucket-mates would go stale instantly - a destruction path that repairs itself by breaking unrelated reads is not the fix. crash safety is temp, read back through this module own reader, rename, caller unlinks, and the invariant at every instant is that every address being carried forward is readable from at least one complete file at a stable name, both of them in between; an interrupted repack is not a new failure mode but one of 817 torn writes, since the stage name is skipped by read_all and collected by the existing sweeper. two pins the mutation sweep caught as weak are fixed, one vacuous because a repo that put its own objects holds them in memory where the re-point deliberately does not reach, and one mis-named because its injected failure fires BEFORE the ordering it claimed to prove; the read-back-before-rename ordering has NO fault injection pin and that is said rather than invented, argued structurally from write_bucket having exactly one rename as its last act. and the verbs census attribution went stale a THIRD run running, caught in review here rather than after landing (#1525) bbbe984d · dbf3dbe6…diff
  • loot verify --unreachable lands the dangling report and closes the packing family, and the thing it does NOT do is walk: it answers from gc_keep_set, so the whole new computation is a SET DIFFERENCE over the scan the integrity check was already doing - no third reachability walk and no second directory pass either. 1523 refused to write the second one and this refuses to write the third, which matters because a report that could disagree with the collection it names as the remedy is worse than no report. the two directions deliberately take DIFFERENT root sets and that is the subtle half: verify own node list stays the roots for MISSING, because it carries the referencing change and path that a bare oid set cannot, while the keep set is the roots for UNREACHABLE, because it additionally holds the loaded graph and it DROPS burned oids under ADR 0038 - so using it for missing would hide deliberate destruction, and reporting a burned object as missing damage is exactly the inversion burn exists to avoid. the primary own working change is seeded once in each, by the code that already seeded it. the exit rule is structural rather than a renderer discipline: is_clean does not read unreachable at all, so garbage alone cannot fail a verify however the report is rendered, and the process pin deletes an object the listing did NOT name, chosen by subtraction, so its missing half is a positive control on its unreachable half rather than a second assertion of the same thing. size is the SPAN length for a packed object and the file length for a loose one, never plaintext, which loot cannot know without keys - and the two halves are reported APART rather than summed, because that is what keeps each one checkable against its own collector: the packed half is GcReport packed_orphans by construction and the loose half is the census unreachable figure, and the census must go on excluding packed garbage to keep the promise it is pinned to. ObjectScan now carries how each present object is stored, which costs ZERO extra syscalls because both arms already held both facts at the instant they graded an address. one residual is documented rather than hidden: an address stored both loose and packed, the window inside a repack, is attributed once to its loose copy. and the empty case prints that every stored object is referenced by a change - the exact sentence 1525 had to qualify at gc as scope drift - where here it is NOT drift, because the enumeration behind it is scan_objects and that sees packed spans too; the asymmetry is pinned as a test rather than asserted in prose. the verbs census moved to 345 and its sentence moved with it, the first run in four where that did not go stale (#1526) 061af7f3 · dbf3dbe6…diff
  • shallow clone lands with the cut on the RECEIVING side, and that is why no wire moves and no format major does either: the fetch body is a format-marked pair of oid runs, so a depth field would be a new wire shape and therefore a bump, where taking the whole change lane and KEEPING n generations needs no new field, no new endpoint and no server change - a shallow client works against every relay and forge already deployed, including older ones. the price is stated rather than glossed: the change lane metadata crosses once in full on the first round, and what is saved is the object bodies, which is where a history bytes are, exact from the second round onward. the no-false-absence guard lives in THREE places and none of them is a verb - assemble, which every CLI open lands in, measures the frontier; apply_bundle_reaching, the only thing that can move one, re-measures it; and the dispatcher states it on stderr after BOTH the success and the refusal arm, because a refusal is a false absence WORST shape. it cannot be bypassed by a verb that forgets to ask: there is no path from the CLI to history that skips assemble, and the one way to open without measuring is to declare RepoNeed without graph, which makes the first history read PANIC - so the declaration that would silence the notice is the same one that aborts the verb. it rides stderr rather than the shape, so json and porcelain stay byte identical under ADR 0023, and a complete position emits nothing at all. depth never reaches the remote AT ALL, pinned three ways: every recorded request re-encoded through the real codec is exactly header plus 32 bytes per id with no room for a depth or a path, the union of every have and wants is a SUBSET of what the relay itself named in a prior answer, and the aimed one is that two positions cloning the same history at the same depth and differing ONLY in their sparse view emit BYTE IDENTICAL requests. the test relay recorder had to start capturing IDS rather than counts, because a privacy claim about a request cannot be checked against a length. two findings came out of the sweep rather than the design. one mutation stayed GREEN and refuted a claim already written into four files - that shallowness is stable because the frontier id rides the declared closure - since a declared have IS a closure claim and the held tips therefore already subtract everything behind the cut; every occurrence is now the narrow true sentence with the refutation beside it. and a count assertion caught a silent no-op: the obvious deepen posture, the closure minus the frontier, comes back with an EMPTY change lane REPORTING SUCCESS, because the remaining ids are still descendants of the cut - a deepen must declare NOTHING, and the posture is now derived from the bound so the wrong pair cannot be spelled. the body-deferring filter is NOT attempted and is the one criterion left: it needs a lazy object read on every get, surface and diff path plus a policy for what happens offline, and half-building it would put a FIFTH kind of not-here into a store that already distinguishes four (#1527) 2cccbe27 · dbf3dbe6…diff
  • the no-false-absence guard 1527 landed was BYPASSABLE, and the bypass was neither of the two escape routes its doc ruled out: buoy and bisect return from main before the notice single call site, so assemble measured the frontier and threw it away unprinted, and a plain loot buoy on a shallow position exits 2 with no buoy - a REFUSAL-SHAPED false absence, which the comment four lines up calls the shape an operator most needs the line for. the repair is SHAPE rather than a third bullet: main is now a two statement wrapper and the whole former body is dispatch(), which is the only expression in main that produces an exit code, so every exit code the binary emits is that call return value and a return anywhere inside it - any depth, any verb, ahead of the table or behind it - returns INTO the notice. a new early return is not a new escape route, it is the SAME route, and because nothing is enumerated nothing has to be maintained. that matters because the old doc argued unbypassability by ENUMERATING escape routes and closing each, which reads as exhaustive, is not, and failed on the first route nobody listed - the same shape as a hand written count beside a derived number, which this run watched go stale three times. the residual is STATED rather than enumerated away: a process that never returns through main, process::exit or abort or panic, which is a property of process TERMINATION and not of the verb table, so nothing a verb does BY RETURNING can miss the notice. a census pinning exactly one raw exit was considered and refused, because that walker own consumer count is itself a derived-and-pasted census and adding a consumer churns it. the failed enumeration is kept as HISTORY, marked as the thing that failed, with the shape argument replacing it, and a THIRD site carrying the same claim was found in CONTEXT.md glossary which the ticket never named. FRONTIER_WIDTH stops being a process global read by verbs that hold the authority: both callers now ask their own workspace, and the public spelling no longer compiles from outside the module, which is the narrowing made structural rather than promised. three pins that asserted nothing are gone. the re-encoding privacy pin is DELETED because no byte seam is reachable - the transport is a TYPED seam, so the test re-encoded its own recorded id vectors and asserted the codec is linear in their length, true for every possible input - and its doc now records what it did against what it claimed, with the criterion standing on the other two pins, which carry positive controls. the partition pin is read out of the RENDERED sentence now rather than from literal triples the constructor never saw. two mutations make the case by experiment: moving the notice back to the end of dispatch reddens the new pin while log and show stay GREEN, and re-adding the old partition body under a scratch name PASSES, so vacuity is demonstrated rather than argued. an eighth pin was written and then deleted, because a mutation proved the existing assertion already sits on the live case (#1828) b5eb05df · dbf3dbe6…diff
  • bisect grows log, replay and terms, and the replay refuses WHOLE at a gap rather than replaying up to it, because a partial session is INDISTINGUISHABLE FROM A WHOLE ONE IN THE ARTIFACT: status renders both identically, the search then runs from a range missing a bound it was TOLD about, and the only evidence is a warning that has scrolled away. all-or-nothing has no partial result to mistake. the pre-flight resolves every id against the graph before writing anything and names EVERY missing change with the line that asked for it, and on a shallow position it says so and names --deepen rather than pull. SESSION_VERSION stays 1 and that is the shape of the preservation criterion rather than a promise about it: the terms pair is a TRAILING codec section written only when non-default, so a default session is byte identical to what the previous release wrote and a pre-1532 session still decodes. the transcript is an INPUT and what is frozen is its GRAMMAR rather than its emitted text - what the parser accepts today it accepts forever with the same meaning, and later work may add directives rather than re-read existing lines. an unparseable line refuses the whole file and points at the line, which is the rule 1516 set for an unreadable ignore file, and EMPTY or all-comment is refused TOO, because zero directives and a session with no marks are DIFFERENT FACTS and a truncated download is exactly how the first arrives looking like the second. ids are full 64 hex only, no prefixes and no HEAD, because a transcript is read on a machine where neither means what it meant there. the vocabulary TRAVELS with the transcript: a terms line rides ahead of the marks and replay resolves mark words against a RUNNING table, so it round trips, and a transcript written under old and new whose terms line is missing is REFUSED rather than re-read as good and bad - the one reading that would silently FLIP A VERDICT. the rename ADDS a spelling rather than retiring one, so bisect good keeps working afterwards, and that is safe ONLY because a term may not shadow a subcommand name, which is what makes the inversion terms bad good unspellable. two of its own assertions were not AIMED until they were fixed: the start assertion was vacuous because the fixture made start and the transcript bad the same id, repaired by finalizing a sixth change between the log and the replay, and a doc claimed its consequence one assertion earlier than the one that fires. and the tests produced a finding worth keeping: writing the transcript INSIDE the repo makes it an uncaptured edit, so replay refuses over work the operator did not knowingly make - the guard was deliberately NOT widened, since a replay checks a midpoint out and must refuse over the same work bisect start does, so instead the refusal names the likely culprit and the smoke test ASSERTS that refusal rather than arranging to avoid it (#1532) 05f9263e · dbf3dbe6…diff
  • the fourth review sweep finds an enumeration asserted as exhaustive for the fifth time in one run, and the repair puts the RULE where the list was: two questions rather than a set of verb names. what does the key HOLD - a key the graph recorded as a change AUTHOR is mailmapped, while an attester, grantor, grantee, custody holder, manifest signer or admitted pusher holds a capability AT THE MOMENT IT IS PRINTED, and an alias there is a display that reads like an authorization claim. and where do the BYTES LAND - a mailmap is local display state, so it may reach a RENDERING and never an artifact whose bytes are hashed or re-ingested as a record. the second clause exists because of a site NEITHER the ticket nor a grep over the helper would have reached: git_mirror author_identity resolves a key the graph recorded as a change author and does not call that helper at all, so question one alone says mailmap it - and that would make a git mirror commit SHAs depend on a LOCAL UNTRACKED DISPLAY FILE, so two machines reading one repo would build two histories. applying the ticket rule mechanically would have INTRODUCED that defect, which is why the rule ships with two clauses and that site is named as its proof. of the four sites the ticket listed, three are mailmapped and tag is NOT: every key handed to that closure is an ATTESTER, the same key attester_trust decides the listing with, and that is the direction where a wrong call is security-shaped, so it stays put with the reason at the site and a pinned positive control. the four were four hand copies of one function body, which is HOW they drifted without anyone choosing to, and the body is now spelled once so there is no ladder left to copy. the grouping table is DELETED rather than narrowed: the enum grows ALL and a name round trip, parse SEARCHES that list and the refusal joins it, so the accepted set and the advertised set are one list read twice and the eighth vacuous pin becomes UNREACHABLE rather than watched - its replacement is pinned against an INDEPENDENT oracle, the hand written usage line read from source in another compilation unit, so a variant the parser accepts and the usage never mentions now reddens. AGENTS.md gains the class and explicitly NO guard, and the no-guard half is EVIDENCED rather than preferred: the census one would naturally write, over that helper call sites, would have caught four of the five instances and MISSED the git mirror - a guard that reads complete and is not would be one more instance of the class it exists to catch (#1832) 0cd1d291 · dbf3dbe6…diff
  • loot move lands as the sixth member of the REFIT FAMILY rather than as machinery of its own, and the whole re-anchor is ONE DELTA over tree entries: the shift, the new parent tree against the old parent, applied to every node of the moved line - so node N tree becomes old_parent then delta N, which is why DESCENDANTS NEED NO SEPARATE RULE and why the clash set is decidable from trees BEFORE anything is minted. the naming decision is the ticket own proposal taken rather than a third option invented on taste: loot move <selector> --onto <selector>, with no collision to find - loot has no mv, and a rename is something a capture RECORDS since v12 - and it is recorded in CONTEXT.md as a new Move entry that states the decision against the word rebase, which the glossary deliberately avoids. AC6 follows an EXISTING mechanism rather than a new one: split moves an entry whole and duplicate reuses entries entry-for-entry under ADR 0004, so move reuses that shape, and the structural proof is that there is NO put_sealed and NO ObjectStore::get on the move path at all - a re-seal would require acquiring a key the code never asks for. its pin carries a POSITIVE CONTROL, open_object failing before AND after, because without one an entry comparing equal proves nothing. the clash arm is the squash clash copied in shape - record conflicts, persist, op, mint nothing - and move joins STOPPING so it carries the abort, continue and force trio. two things are reported rather than papered over: the ticket parent line calls this the start of the jj rebase -r item but AC2 asks for the OPPOSITE of -r, descendants travelling WITH the change rather than re-parenting onto the old parent, and the AC as WRITTEN is what shipped. and the working change is itself a descendant, so it travels, re-recorded as a superseding version but deliberately UNSIGNED, because signing it would mint permanent history from work nobody named - which also means a clean move reports two descendants where the operator sees one, and that is pinned as true rather than hidden. eleven pins, each reddened by a named mutation with the count read rather than the word ok (#1517) 7ff4ef4d · dbf3dbe6…diff
  • the branch and reorder forms land as FLAGS ON MOVE rather than as verbs of their own, and the branch form is a SELECTOR SUBSTITUTION rather than a second planner: branch_root walks the first-parent line and stops at the first node the destination already descends from, the change just below it becomes the source, and the ordinary plan_move decides everything else - so the branch form CANNOT DRIFT from the subtree form refusals, clash set or sealed-carry rule, because there is one planner under both. reorder is DELTA COMPOSITION: the moved manifest becomes anchor then lower-over-upper, the overtaken one anchor then lower then upper, which equals its old tree exactly when the two touch disjoint paths - which is WHY descendants are re-parented with their manifests UNTOUCHED - and the intersection of the two deltas is the clash set, decided from trees before anything is minted. a DEFECT IN WHAT LANDED AN HOUR AGO is fixed here rather than filed: loot move <finalized> --onto @ was NOT refused, because @ on a different fork passes every #1517 guard - not self, not a descendant, not the current parent, not a merge - so it would have minted SIGNED HISTORY PARENTED ON THE UNSIGNED WORKING CHANGE, a version no push, bundle or fetch carries. the repair is stated as a rule over the parent-to-child EDGES a restructure creates, asked of the DRAFT POINTER and never the signature, because a keyless repo signs nothing - so both forms and any seventh Refit inherit the refusal instead of each enumerating its own cases. AC1 was already satisfied by #1517 and is CITED rather than re-pinned, verified against move_onto live-descendant closure rather than taken on trust. non-adjacent insertion is REFUSED BY NAME rather than half-built, because inserting at an arbitrary point re-parents the line it lands in front of, which is the -r shape #1836 holds. fourteen pins, each reddened by a named mutation with the count read - including two draft pins that go red together, which is the census proving the rule sits on the edge rather than on the verb. and two prose censuses caught the USAGE text itself, where a -b inside a jj rebase citation tokenized as an undeclared flag (#1518) 066564a1 · dbf3dbe6…diff
  • the draft rule stops being a claim about planners and becomes a property of the ONE SEAM EVERY REFIT CROSSES: refuse_building_on_the_draft is asked unconditionally as the first statement of apply_refit, over a new minted_edges whose match is WILDCARD-FREE, one arm per variant, each declaring the pre-existing parents it would record on. the proof is a MUTATION THAT DOES NOT COMPILE - adding an eighth variant fails with non-exhaustive patterns AT minted_edges, so a seventh Refit cannot forget the rule the way a census or a call-site list would have let it. the two planner calls STAY and are documented as ORDERING ONLY, naming an illegal destination before a clash set is computed, so forgetting one costs a MESSAGE rather than the rule. and duplicate is ruled a DEFECT rather than a named exception, because the artifact it shipped was genuinely broken: the copy is SIGNED so it travels, and it named as parent a version resolve_live_version excludes BY CONSTRUCTION - loot show on that parent exits 1 with no live version matching - with latent harm besides, since any later describe, squash or abandon of the draft makes it permanent rather than repairable by a later loot new. #398 SURVIVES CONTACT and its hard constraint was in a COMMENT its resolution never restates: duplicate must not reproduce a change at its own parent, because a content-addressed version id would collide, which is why the jj default of the source own parents is unreachable here and why #398 reached for the draft at all - the anchor satisfies that constraint just as well and is a version a receiver can resolve, so nothing in #398 required the draft specifically. the planner is no longer TOLD which version the draft is, its view loses that field entirely, which is the narrowing made structural rather than promised. two user-visible changes are stated rather than slipped in: a duplicate with work in progress now lands BESIDE the draft on the finalized anchor rather than on top of it, and --after @ now REFUSES. seven mutations, six red with the counts read and one a compile error, and the two halves are separable - blanking the declaration reddens both pins while neutralising the seam call reddens only one (#1841) 56bc591f · dbf3dbe6…diff
  • the note rule moves to the LANE WRITE SEAM and stops being a property of one verb: Workspace::attest takes a Record rather than a role string, so there is NO OTHER WAY TO SPELL A ROLE IT ACCEPTS, and two wildcard-free matches answer for every variant - role mints the string and refuses an operator one in the reserved namespace, prose declares the free-text payload and Some IS the gate. adding a fifth variant produces TWO compile errors, one per match, which is the obligation put where a list cannot be forgotten. all three doors were REPRODUCED FIRST and a FOURTH was found that no ticket named - tag --retire -m writes the operator reason to the same lane - which is why the repair is a seam rather than three verbs remembering. door one is REACHABLE rather than theoretical: unwrap_or_default meant an unreadable tree read as NO SEALED PATHS and allowed the write, and a shallow position supplies one, because HEAD~n walks one parent edge past the oldest change HELD and resolves onto a FRONTIER ID - signed, named as a parent, not held - so the gate now fails CLOSED and names fetch --deepen. door two leaked real bytes end to end: notes add refused, then attest with a reserved-shape role exited 0 and show printed the secret on a Restricted change. and ADR 0025 had that door WRITTEN DOWN AS AN ACCEPTED COST, saying a user who reaches past the verb can still attest the sentinel and the signature binds it to their key - the hole was documented as the price of the encoding, and it was a DEFECT rather than a cost, because the role parses back as a note, renders as one and rides the bundle as one. tag -m is GATED rather than excepted, and the line is KEY VERSUS PAYLOAD: a landmark name is a namespace key a reader re-types and matches by equality, so gating it would take ADR 0018 sign-off and ADR 0025 resolver down on every change in a repo with one sealed path, while a message, a reason and a note body are prose ABOUT one change content. that refuses tag -m in THIS repo, where .lootattributes seals a path on every change, and the bare name, the bare retirement, buoy and attest all still work. eight mutations with counts read, and mutation three is the argument: neutering the seam reddens three pins while #1519 OWN PIN STAYS GREEN, which is why the first gate could not see any of this (#1847) 93cc4944 · dbf3dbe6…diff
  • hunk selection lands on split and restore and AC2 is STRUCTURAL RATHER THAN CHECKED: everything the picker shows comes off the ONE DELTA SEAM, whose sides resolve through visible_paths_at - the key check plus embargo escrow plus grant expiry, the same door grep and archive take since #1500 - and the hunk grain has EXACTLY ONE CONSTRUCTION SITE, reachable only where both sides hold bytes, so an unopenable path is an Unreadable row that HAS NO HUNKS IN IT and there is no second visibility test to keep in step. such a path is still OFFERED AS AN OPAQUE WHOLE rather than dropped, because dropping it silently is #67 at the verb where it costs content, and its entry then moves BY ADDRESS with nothing decrypted. the keys are git add -p keys derived from ONE TABLE so parser, prompt and help cannot drift, with two forced divergences stated as such: q ABANDONS THE WHOLE SELECTION, because git keeps what it staged and loot has no index to keep it in, and there is no edit or navigate arm because each would mint a second grouping rule or a screen model. the terminal is a DOOR rather than a check - demand is the only constructor of the only Answers the binary has, so a verb cannot obtain a keyboard without crossing the refusal - and -p with --dry-run refuses as a PAIR, because -p IS the preview and a dry run would ask every question then discard the answers. content that does not survive a line split is offered whole, MEASURED BY ROUND-TRIPPING rather than by enumerating hazards. eighteen mutations, all red with counts read, and AC2 carries a POSITIVE CONTROL that moves the other way: emitting no hunk grain at all leaves the never-a-sealed-hunk half GREEN and reddens nine, which is exactly what that half alone cannot see. ONE ACCEPTANCE CRITERION IS DELIBERATELY NOT BUILT and that is the finding: AC1 names shelve -p, and loot shelve does NOT EXIST - #1502 was closed NOT PLANNED as premature scaffolding, so building a shelf to hang a flag on would reverse a recorded decision sideways from inside a ticket about something else (#1529) 1eb98dc8 · dbf3dbe6…diff
  • the note rule is a refusal on a LOCAL WRITE and the documents now say so in the first sentence a reader meets, because the claim that it is a property of the LANE was false in the direction nobody had checked: ingest inserts on att.verify alone, so a peer prose about a sealed change is stored and printed while the IDENTICAL TEXT FROM THE LOCAL OPERATOR ON THE IDENTICAL CHANGE IS REFUSED. that was built end to end rather than argued - a peer DagRepo not running the seam, a real sync bundle, apply_bundle, and the secret read back in the clear. gating ingest is REJECTED on four counts and the first two decide it: a drop destroys the local holder ONLY EVIDENCE OF A DISCLOSURE THAT ALREADY HAPPENED, at the one party who can act on it with burn, re-seal or rotate, and the remote author already had the plaintext so refusing un-tells nobody; and it would make the lane contents READER-DEPENDENT, where membership today is a property of the BYTES - a signature verifies or it does not, the same answer at every reader - so one peer would keep what its neighbour drops, the dropper would stop forwarding, and the lane would PARTITION. the third count is that it is not even COMPUTABLE where the ingest happens: the attestation loop runs BEFORE change nodes enter the graph, so a first-delivery record has no tree, and a gate that fails closed would refuse the ORDINARY case. the note codec header carried the sharpest error, since that module is the DECODER and a claim that such records cannot exist is exactly backwards there. and tag partial write is made UNREPRESENTABLE rather than documented: attest_all judges every record - role minted, prose gated - and only THEN records them all and persists once, so an annotated create is TWO RECORDS AND ONE ACT and the pre-check is DELETED rather than marked load-bearing. it had been copied from the notes verb along with the sentence saying deleting it costs a message and never the rule, which is true of a verb that writes ONCE and false of one that writes twice - the refusal now leaves NOTHING behind, not even the landmark, verified end to end at 0 landmarks. four mutations with counts read, and the ingest pin fails on its LOCAL-WRITE CONTROL half when the local gate is neutered, so both directions are live in one test (#1855) 2bc8c51a · dbf3dbe6…diff
  • HEAD denotes TWO things and that is now a recorded decision rather than a latent trap: the selector HEAD is the dock finalized tip, the revset HEAD is the graph live heads which INCLUDES the unsigned working change, so with WIP present HEAD~1 typed as a selector and HEAD~1 typed inside a revset are off by exactly one - measured on a built 4-change repo, where loot log HEAD~1 answers add f2 and add f1 while the revset arm answers add f3, and the NEGATIVE state built the same way with no WIP has both grammars answer the SAME change, which is what gives the fix its condition instead of a guess. unification is refused in BOTH directions with the callers each break would cost, recorded in ADR 0085 along with four concrete facts that would make the decision wrong later. the legibility lands at revset::select, the ONE argv-to-set door, so all five revset verbs inherit it, and it fires on three conditions rather than on every invocation: the word took the revset arm, so a bare HEAD or HEAD~n stays SILENT because that is the commonest loot log and it already gets the reading every other verb gives; the parsed AST stands on a head, asked of the AST through a new wildcard-free Expr::names_head so a new primary cannot compile without declaring a side and a description pattern spelling HEAD stays silent; and the two grammars ASKED disagree, comparing evaluate against resolve_selector rather than re-deriving is there WIP anywhere, so the note cannot drift from either grammar and the diverged-dock case works without being enumerated. stdout is untouched and the json and porcelain channels are pinned clean. site is NOT involved and that is a MEASUREMENT rather than an assumption - no page under site documents either grammar - with the rule recorded that a page gaining one owes a row. and CONTEXT caught a false claim en route: the selector-vs-revset entry still carried the #1562 reason that HEAD~x is unmistakably the HEAD~n shape with a bad number, refuted by #1562 itself and load-bearing for the shape-first order, so it is corrected in place with the history. seven mutations with counts read, two of them reddening BOTH the unit and the smoke pin, and one control asserting SILENCE when the readings agree (#1568) 68521a66 · dbf3dbe6…diff
  • the first review sweep of the night audits the night OWN three lands, and four of the seven findings were MINTED BY THEM - each fixed at the level that makes the claim true rather than hedged. the wildcard-free match doc said a FOURTEENTH variant would not compile, beside an enum that already had FIFTEEN, so the number is gone and the sentence keeps its point: a hand-written count beside code that can grow is the class AGENTS.md names, and this one was wrong the day it was written. the NO_PATHS sentence and the dispatcher minority sentence disagreed because #1569 narrowed one of them and left the other standing, so the surviving sentence states the SHAPE rather than a frequency - zero declared slots plus an open own list - which is what the #1551 PATHSPEC argument actually rests on, and counting the table shows nearly every was false either way you resolve it: twenty-four literal NO_PATHS and twenty-two NO_ARGS against twenty rows whose spec lives elsewhere. the ADR 0085 note fired TWICE on range-diff, which takes two positionals through the one shared door, and the fix is AT THE DOOR with a latch that catches on HAVING SPOKEN rather than on having been called, because latching on entry would let a first side with nothing to say SILENCE a diverging second one - pinned with a one-positional control, since an equals-one assertion passes against an over-eager latch. the fourth finding is the one with a real choice in it: the note measured the bare pair only, while CONTEXT and two doc surfaces read as though the whole HEAD and HEAD~n pair was covered. so the divergence was BUILT before it was believed - two identities, a converge, one merge tip - and there HEAD~1 as a selector refuses naming both parents while the revset walks the first parent through it, with both bare HEADs equal, so the shipped note was SILENT through it. the MEASURE branch was taken over the narrow one: the note asks the bare pair first and then every depth the AST stands on, with both-walks-exhausted counted as AGREEMENT rather than as a note, and the arm that cannot fire today is ASKED rather than asserted, because that is a fact about two functions and not a thing to write down. prose is narrowed at every surface that read wider than the measurement, the log USAGE line included. the three copies of one census derivation become one function, the ordering contract the shared constant carries for exactly ONE of its three readers is now stated, and the const-only condition under the groups assert is named as what would make it wrong later. five mutations with counts read - and ONE CAME BACK VACUOUS FIRST at zero passed against 1175 filtered out, because the census lives in the bin target and the lib filter reached nothing, caught by reading the COUNT rather than the word (#1882) 5fd6a12a · dbf3dbe6…diff
  • two readers stop answering a visibility question out of a book the enforcement path never opens - and the ticket OWN worry about which ROW is REFUTED before anything is built on it: path_in_history consults current_tree FIRST and returns outright, with the reverse-topo history walk only a fallback for a path the live heads no longer carry. the keeps-every-change-whose-tree-contains-the-path shape belongs to change_has_path and filter_history_to_path, which is what log --path filters on and which embargo-status never touches. so the verb was already answering about the CURRENT recording, and its defect was purely WHICH BOOK rather than which row - which makes the fix cleaner than the ticket allowed for, since the tree entry is still read, being what LOCATES the object, and only the visibility moved. embargo-status now asks a new seal_visibility, the refusing twin of visibility_of, sharing held_but_unreadable with embargo_reveal_at, which is re-expressed over it so there is ONE read door. the tree entry answers ONLY when the seal cannot be produced, and then the output SAYS SO, naming the source it read, the guarantee it does not carry, and the verb that tells a missing object from a corrupt one. seal-only was rejected with its reason recorded: this verb exists for why is this file not visible after a pull, which is exactly the case where the object legitimately is not here - a withheld forge object, a mid-pull absence - so refusing there would delete the verb purpose. healthy output is byte-identical to before. conflict_side loses its fallback rather than gaining a guard: one object read returns both the seal visibility and the open verdict, and sealed_to_us is extracted so the two doors cannot drift about which errors mean sealed-to-you. the sizing is stated rather than flattered - NO observable answer changes on any state reachable today, because old and new both propagate the rot, and what changed is REACHABILITY: the mutation restoring the pre-fix shape reproduces the defect exactly, a truncated object rendering as internal. ADR 0012 gains a row for each site, and its visibility_of row is CORRECTED, having claimed conflict_side as a live caller. the wire-format option is neither taken nor touched: folding visibility into the change id is a FORMAT_MAJOR decision, the ticket ruled it outside an AFK warrant, and the enforcement question is written up to be filed rather than begun. seven mutations, every pin asserting a VALUE - a reveal instant, an oid, an error code - and never merely that something failed, which is the trap #1578 pin fell into. and the vacuous-filter trap fired once and was caught by reading the NAMES rather than the word: a filter on embargo printed 22 passed, and none of those were the three new render tests, whose names do not contain it (#1581) 74526297 · dbf3dbe6…diff
  • the removal wait stops reading a FAILED SCAN as an absent file, and the hole was that ONE fallible answer served two callers needing opposite failure behaviour: the precondition, where false-on-failure makes the assert FIRE and is safe, and the exit, where it makes the wait STOP and is not. the scan now answers three ways rather than two - named, not named, or the scan did not run. NotFound stays not-named, because an absent directory naming nothing is a statement rather than a failure; every other error is an Err; and each caller decides explicitly, the precondition panicking with its own message about failing to establish its own precondition, and the exit leaving ONLY through a scan that RAN and did not name the address, waiting a transient error out on the same store budget the removal already followed one level up. flatten is GONE, and it matters at the exit for the same reason, one entry wide: the entry whose read failed may be the very address being waited on, so flatten reports not-named for a name the scan never reached. the proof is a REAL failing scan rather than a simulated one - a regular file standing where the objects directory goes is a genuine OS refusal, error 267, reachable with no second process - and the two arrangements are DISJOINT on one fixture: with the fixed exit it is 0 passed 4 failed naming that error, and with the pre-fix exit restored it is 4 passed 0 failed, which IS the quiet success, reproduced rather than argued. #1596 is otherwise untouched, same helper and same budget. the projection neither surface derived is settled by naming WHICH QUANTITY SCALES: the honest half, being the only arm a design satisfying the never-authoritative rule can reach - so 22.3 becomes about 223 at ten times the paths, on BOTH surfaces, with measured now separated from extrapolated, since the read COUNT is linear and pinned at three sizes while the TIME was measured at one. 223 is therefore the order of magnitude at which to re-open the question rather than a reading, and the other arm about 439 is named as explicitly not the number to quote. the pin the ADR claimed is now the pin the test asserts, strengthened rather than narrowed because the numbers had already been observed: the two-per-path-plus-one relation holds EXACTLY at all three sizes, run rather than trusted, 101 against 50, 401 against 200 and 1601 against 800 - with the per-path multiplier and the fixed overhead kept as SEPARATE constants, since two-N-plus-one and three-N agree only at one, and with the old greater-than line deliberately NOT kept beside it, because over the constants this file writes it is green whatever the code does. four prose corrections ride along: a step that stated the conclusion its own section refuses, a caveat a commit message claimed and no file carried, two runbook short forms stronger than the long form they point at, and a count of three defects that lists two - which STOPS COUNTING rather than inventing a third (#1899) a9018dad · dbf3dbe6…diff
  • the catch-up stops leaving its own consequence to a runbook the reader may never have loaded: adopt now prints the git step IT JUST CREATED, and only when it actually moved the tree and the checkout is measurably behind - naming the paths it wrote, which is exactly the set needing the proof. the ancestry is judged on the LOOT GRAPH through the mark map rather than by asking git, because git cannot answer it: the checkout has not fetched the landed commit. there are two arms by position, since which git command is correct depends on where HEAD is, and neither arm ever recommends git before a catch-up, because git-first WEDGES the primary and loot-side-first is and stays the order. and the land closing note is narrowed so its final word about the primary cannot read as being about the checkout: it now says which thing the fast-forward moves, says HEAD does not move with the dock, and names the verb that says the rest. the finding that changed the procedure is bigger than the ticket: the skill had the FETCH AFTER THE PROOFS, and a land pushes main FROM THE MIRROR, while the only fetches a land runs in the checkout are its pre-land drift and ADR guards - so at the exact moment an operator needs this procedure their origin/main is stale by that very land, every landed path reads as differing, and the freshly landed perf point does not resolve at all. followed literally the skill produced the precise wrong conclusion it exists to prevent, so the fetch is hoisted with the reason recorded, and the printed block fetches first by construction. twelve mutations with counts read, of which two are worth the reading. the tenth is a CONTROL: the skill quote wraps across two indented lines, so with the whitespace flattening removed the citation pin reddens over LAYOUT rather than over words, which is what makes the flattening load-bearing rather than decorative. and the fifth is the one to learn from - the lane guard test PASSED WITH THE GUARD DELETED, because the lane was silent for an unrelated reason, landed main having been recorded on the primary so the lane lineage-filtered graph had never seen it and the ancestry answered false. a pass that survives deleting the thing under test is not a pass, so the arrangement was rebuilt with landed main recorded THROUGH the lane and two explicit preconditions proving the lane reaches the checkout and can answer the ancestry at all - after which the same mutation reddens. what is NOT done is said rather than implied: a bare ferry still says nothing about git, its catch-up being a different path, so the note claims only the dock for it; the block deliberately stays silent on a repeat adopt, because its claim is that THIS run wrote over your tree and a no-op cannot make that claim; and none of this was reproduced live (#1679) c8ebe232 · dbf3dbe6…diff
  • the condition that could not be false is gone, and so is the pair that made it possible: merged stops being a FIELD set by hand at three construction sites and becomes a DERIVED method over the one bit that decides it, so the two cannot disagree - unrepresentable rather than guarded. eighteen reads follow it, and eight of those were conjunctions that are now TAUTOLOGIES, reduced to one term rather than left standing to read as two conditions. the test was rebuilt by moving a DIFFERENT AXIS, because a second catch-up can never reach the block at all: the fixture now puts a real second commit on the checkout main, so the DOCK is still behind and the catch-up genuinely runs, writes the tree and reaches the block, while the CHECKOUT is level and the block declines - and deleting the predicate reddens two tests. the DISCRIMINATION is the part worth reading: under that same mutation the OLD test stays GREEN, which is precisely the defect this ticket describes, demonstrated rather than asserted. the printed recipe stops handing the operator a command that ERRORS. the tree delta now returns what was WRITTEN and what was REMOVED apart, and a removal gets a tree read for its proof, because there is no file to hash, and a checkout for its repair - placed BEFORE the merge, since restoring the file is what lets the merge run at all. the plumbing needed a pin of its own, and that is proved rather than assumed: folding removals back into the written set reddens the workspace pin while the rendering pin stays GREEN. the off-main arm gains its PROOF and withholds only its REMEDY, and says that it withholds it and why, because the byte comparison does not turn on where HEAD is while the repair does - with that limit stated in the same list the module other limits live in, rather than left for a reader to discover. the section this run added about careful sentences gets its own grammar fixed, a stray fragment from an earlier draft removed and a clause given its missing object - and NO count added, with a number that was being carried forward replaced by the property, consecutive sweeps each turning one up. the only-shorthand claim is narrowed at all three sites to the width of the check that backs it, which is a claim about ONE declaration and nothing wider. and the tenth copy of the short-hash helper becomes one, with the unguarded universal above it replaced by what it actually is and an explicit note of what it does not cover (#1911) 53ede622 · dbf3dbe6…diff
  • a revset is accepted wherever a selector is, through the door that already existed - and the verb population is DERIVED from the shipped help text rather than from the dispatch table, which closes the two-spellings trap that had already cost two tickets UPSTREAM of this change, since help is pinned set-equal to the table in both directions. a placeholder-name list could undercount a DIFFERENT way, so the whole placeholder vocabulary of the help text is a SECOND set-equality: a verb spelling its change argument some new way now reddens the build instead of dropping silently out of the population. twenty-five selector-taking verbs, TWO of which a hand list would have missed and the derivation found - diff spells from and to, merge spells head, and neither says selector. four are adopted here, exactly the four #1563 named, all through the existing door via one shared consumer: no second parser and no per-verb precedence, because two verbs disagreeing about what a bare word means is worse than neither having revsets. the point-versus-set question is decided PER VERB with its reason rather than made uniform. cat is a point because its product is one path bytes written verbatim with no frame around them, so two versions arrive concatenated and indistinguishable from one file containing both. archive is a point because N trees are either N files, which is a different verb, or one file whose members collide path-for-path. blame is a point because the lineage walk STARTS there and there is no column for a second start. and ignore-rev is a SET USED WHOLE, because the only use of its result is a membership test, so every member is taken. diff is the interesting refusal: a two-member set does NOT collapse into the pair, because the pair is already spelled as the two arguments, so collapsing would give one argument an arity decided by data the operator cannot see - the same expression a comparison on one repo and a refusal on another - and a set has no SIDES, so a topological order would silently come to mean oldest-versus-newest. that is rejected explicitly and recorded rather than overlooked. the shared rule is exactly one, else REFUSE naming the count, and the door never takes a member. sixteen verbs are left, in two groups, both NAMED in the census rather than merely absent: nine where a set is a fan-out over a REWRITE, needing an order, a per-change failure rule, an undo story and a confirmation surface that do not exist - guessing on a destructive verb being the worst place to guess - and seven that are points by nature and left on scope, two of which have a coherent set arm and so are decisions rather than transcriptions. every pin RUNS THE VERB against a real three-change repo, each row in its own fresh copy with declared setup, so a refusal that fires BEFORE the selector resolves cannot pass vacuously - which is the exact failure this ticket exists downstream of, a criterion once pinned only at the flag gate. ten mutations with counts read. the control that matters is the one dropping a placeholder: the VERB census stayed GREEN, because diff line also spells another, while only the vocabulary census fired - which is what makes that second guard load-bearing rather than decorative. and the ignore-rev pin carries its own control, because without it the equality would hold just as well between two no-ops. three prose claims this change made FALSE are fixed, and one existing pin was MOVED rather than deleted, onto a verb the census names as selector-only so the choice cannot go stale silently (#1689) 5e9b9cb6 · dbf3dbe6…diff
  • the precondition that was prose becomes a TYPE AT THE SEAM, and both halves take that answer rather than only the cheap one. the vouching type moves into the crate that holds the door, with four private fields, and the door takes IT instead of a bare path set - so the only thing that can widen the set in a shipped build sits DOWNSTREAM OF ITS OWN GET. and that is not a second read, it IS the read the producer was already doing: the live arm was already exactly that expression, so the engine now RECORDS the open rather than the CLI asserting afterwards that one happened. one get either way. the same-change half, which already had a mechanism but one crate away from the door it protected, moves across the seam and gains two clauses NOTHING checked anywhere before - the reader and the clock - and it compares rather than records, returning the empty set on mismatch rather than an error, because a bisect midpoint must still pay the walk in full. the plan can no longer hold a set and a measurement that disagree, for the simple reason that it no longer has two fields. a THIRD precondition falls out structurally rather than being written down: only the live arm can vouch, so a plan built from a both-recorded spec now arrives EMPTY rather than full and unearned. the door stays public and that is now a DECISION with its reasoning in three places rather than an omission - making it private, as its neighbour three lines below already is, would close the hole for one crate while the public trait keeps surfacing content, so an embedder would lose the skip and nothing else. and the cross-key door is split out to take NO vouching parameter at all, because a move labels and never vouches: the hazard stops being expressible instead of being filtered by a dead guard. the mutations bought a REAL HOLE rather than confirming the work. two of them survived the first round, because every test built its vouch THROUGH the door, so the insert-side reader and clock checks were SHADOWED by the door-side ones - one identity open could fill another identity set, and an open at one clock could back a set labelled another. an isolating test was added, and both redden now. the counters are MEASURED rather than argued: an identical probe over surface and status at three sizes, run against a byte-identical copy of the unmodified tree, reads the same on every gated counter - with the honest caveat that the allocations column is VACUOUS, since that probe installs no counting allocator, so it is not claimed. and the single claim resting on the type system alone is NAMED as such: that no other crate can widen the set is held by the compiler privacy rules and not by any test, because this workspace has no compile-fail harness to assert it with (#1710) aca3a188 · dbf3dbe6…diff
  • the commit that derived a list precisely so it could not go stale had typed its cardinality into the doc above it, and both counts are gone: the doc now says the refusal names the whole flag set - every verb this binary dispatches and nothing else, pinned in both directions - and that what the one-line shape rests on is READABILITY rather than a size. the obvious fix, pinning the count as well, is refused on purpose, because it would fail a land for the offence of adding a sixth verb, and that refusal is PROVED by a mutation adding one to both sides that stays green. the distance-crate check stops presenting a denylist as a property: it is widened with the five crates the review named, loses its own typed length so the constant carries no count either, and says plainly that it is a named list of common spellings and not a test for the category - naming BOTH holes in the voice its other stated limit already uses, a crate under a name nobody wrote down and a renamed dependency, the second confirmed by a mutation that stays green on purpose. matching on any dependency that merely looks like a distance crate was rejected as a guess dressed as a check. the derived method width is decided to be a TRIPWIRE and not a ledger, so its gap prose is dropped rather than extended - it was wrong both ways at once, excusing a gap that another clause already attributed while a new one had opened unnamed - and the doc now says outright that nobody keeps the per-ticket clauses summing to the figure. and the flag argument left at the vouching seam is examined rather than refactored for its own sake: the triple riding three doors IS the resolved delta still being assembled, so a new struct would only be a second name for it, and a bare bool would be the same flag one level down. the one real narrowing is taken instead - a caller that always passed Some now takes the set directly, so no door a caller can reach lets it choose to skip recording the open (#1922) 0ac07831 · dbf3dbe6…diff
  • one delta derivation had THREE spellings rather than the two the review named, and now has one producer: the surface plan, the uncaptured-paths filter and restore each built the same since-the-surface-target, walk-the-disk delta on its own - and restore doc claimed to share its base with the overwrite guard while nothing enforced it. the single place that picks the delta spec and the walk policy is now one function, and one producer picks the base and hands back base and delta together, with all three callers going through it. the vouching #1710 made into a type is UNTOUCHED: the trusted set still comes only from the live arm after its own successful read, and the producer merely passes it along, which its doc says. the store-memo cost argument is made to SURVIVE the store changing its mind rather than pinned. the bundle memo now holds only a yes-or-no answer per address and never the object, so a future bound or eviction on the store own memo cannot quietly turn it into whole-history ciphertext held for the length of a bundle. pinning the retention instead was refused, because it would fail a perfectly reasonable memory-pressure change for the wrong reason. the one path that would need bytes again now re-fetches them, and no caller reaches it today, since both passes decide to send bytes from the address alone - so if that ever changes, the cost appears as a get the counters can SEE rather than as memory nobody measures, and a new test drives that path directly. the gate reads no move on every gated counter, the probe pinning exactly one get per distinct address stays green, and the method-width tripwire moves by exactly one - where a first draft that added two was caught by the suite and a helper inlined. and the shared base is shown load-bearing rather than asserted: moving it off the surface target reddens ten tests, across the clobber guards, the due-embargo cases, a process-level bisect and the plan test (#1711) 53c0e176 · dbf3dbe6…diff
  • visibility_under is deleted: it was pub with no caller in the workspace, sdk, tools or site and no consumer among the sibling repos, parsed the policy on each call and returned the visibility tag without the publication marker, and the 1553 rule says a dead pub function is deleted rather than narrowed; an out-of-crate probe reached it before the delete and fails to compile on that name alone after, and seal_under now records why its sibling is gone (#1714) 19198ed2 · dbf3dbe6…diff
  • lane gc and lane rm name what holds a lane they cannot reap and give a way back for a lane that lost its lane-id. on Windows a held path that is a directory is now reported as held by a file open inside it or by a process whose current directory is inside it, which holds the directory with no file open, a held file is told a process has it open, and both say loot cannot name the holder, point at Resource Monitor, and say to leave the lane alone and re-run; POSIX, where a current directory does not block removal, is told the path is busy. the refusal for a directory without a matching lane-id now names loot lane rm <id> --deregister, a new flag on rm that drops the registry entry and leaves the directory untouched, keeps the ownership guard, and refuses while the directory still carries the matching lane-id, because there the entry is what lets a retried reap finish, which also answers the ticket question about deregistering a held landed lane: no. a new test spawns a real sleeper process whose current directory is a temp lane and went red with the old advice and with the sleeper removed; its POSIX branch asserts the sweep reaps and was not run here. a deregister test went red with the recovery hint, the still-a-lane refusal or the ownership guard removed and with the directory deleted, and a cli test went red with the flag not passed through. the verb tier width moves to the derived count, and CONTEXT.md and the concurrent and issue-tracker runbooks say to leave a held lane alone and name the recovery. the workspace suite is green (#1760) 16968085 · dbf3dbe6…diff
  • loot resolve on the untracked home position now writes the resolved path to disk, and only that path, through the one-path settle a tip-tracking position already used, so the next capture no longer reads the pre-resolution bytes as an edit and loot new no longer signs a change that reverts the resolution. measured first in scratch repos on the home position, on a primary with a pinned tip and in a spawned lane: only the home position left the tree untouched, and on all three a re-run of the stopped revert over a tree showing the resolution still stops on the same path with the resolution as ours, because change_delta_merge runs its three-way without the settled ledger on purpose (#744). so --continue still closes rather than replays, ADR 0080 gains a #1798 amendment giving that reason, the resume module doc no longer names the tip-tracking arm as the one that skipped the write, and CONTEXT.md gives the reason too and names the stopping verbs by STOPPING rather than by a list that lacked apply-patch and move. the two new tests went red on the unchanged code (0 passed, 2 failed), the home-position test went red at its loot cat assertion with the disk assertion removed and at its sibling-edit assertion with the write widened to a whole-tree materialize (0 passed, 1 failed each), and the replay pin went red with the full ledger handed to the three-way (11 passed, 1 failed). the workspace suite is green (#1798) 31ffdf95 · dbf3dbe6…diff
  • loot resolve now refuses, naming the path, when the bytes on disk at the path it resolves differ both from the bytes recorded for it, in the change the disk mirrors or as the conflicts ours side, and from the resolution, so the one-path write #1798 gave every position no longer destroys an uncaptured edit to that file, while an in-place resolve still proceeds. the ours side is there because a home position with more than one head can mirror a head the disk does not show, which a ferry bounce test hit. spawned-binary tests on the home position and in a lane went red with the guard call removed (2 passed, 2 failed), the in-place pair went red with the resolution check removed (2 passed, 2 failed), and dropping the recorded check turned six resume tests red (10 passed, 6 failed). the line-ending land refusal now also refuses a NUL byte in the files it walks, which held none: a planted NUL went red before the change and with the check emptied (13 passed, 1 failed each). ADR 0080 scopes its ledger reason to revert and cherry-pick and records the guard, workflow.md and two skills say a format skew owes a relay and a forge redeploy and that the relay run needs the ALLOW_FORMAT_MAJOR_CHANGE override, and the sweep 9 citation, rationale, doc, long-line and short-line leftovers are fixed. the workspace suite is green (#1988) a1bfdaa2 · dbf3dbe6…diff
  • loot resolve no longer refuses a delete/modify conflict that ours deleted: the #1988 guard reads a conflict record whose ours is its base as a deletion, so a disk with no file there matches it and a file written back there is still refused over, and on a shared primary its refusal names loot describe --allow-primary, the capture that works there. spawned-binary tests on a ferry-bounced two-head home position went red before the change for the ours-deleted resolve and the restored file (1 passed, 3 failed with the primary hint test), and with the deleted-ours arm removed (3 passed, 1 failed), with ours read as bytes whatever the record says (2 passed, 2 failed) and with a position-blind hint (3 passed, 1 failed). the implement brief now takes the job cap as a placeholder, keeps deploys inside the warrant and names SKILL.md, cites only what AGENTS.md says, says which commands need the lane position and gives the CR count and describe in PowerShell too; AGENTS.md gains the cargo fmt ban, ADR 0062 names the per-ticket safeguard that replaced the implement review, and the redeploy commands in workflow.md and diagnose-push-rejected name the new tag, scope the relay override to a forward bump and say the forge takes none, as the scripts repo reads them. ADR 0082 and apply-patch say the text deletion arm checks no preimage either (#1992) and pin that --check refuses a binary deletion the same way, the line-ending docs say CRLF and NUL, nul_offenders is private, and the sweep 10 wrap leftovers are fixed. the workspace suite is green (#1993) 7ecaa467 · dbf3dbe6…diff
  • loot apply-patch no longer records a conflict base equal to its ours: the header-disagreement stop took the parent content as the base, which is the ours object when the path is unchanged since the parent, and the loot resolve guard from #1993 reads a record whose ours is its base as a deletion, so a file on disk holding those bytes could be refused over. the stop now leaves such a base out. a new verb-tier test went red before the change (0 passed, 1 failed, the recorded base equal to ours) and with the filter disabled (12 passed, 1 failed), and the resolve guard doc and ADR 0080 now say the deletion reading is only as true as the records it is handed. the sweep 11 prose fixes ride along: hygiene.rs states that tests already check docs and skills files instead of naming them, workflow.md uses <jobs> for the build job cap and gives the relay and forge redeploy commands a Set-Location <scripts> prefix with the placeholder defined, afk-loop points at the brief for its placeholders instead of listing them, the rewrap leftovers in afk-loop and diagnose-push-rejected are fixed, and the README apply-patch line no longer says CRLF on either side is refused. runbook_portability and the workspace suite are green (#1999) f221956f · dbf3dbe6…diff
  • the tree writers no longer remove the file they just wrote on a recorded case-only rename: materialize_target and materialize_promoted wrote the target tree and then removed each old path the new tree no longer holds by name, so where the filesystem folds case, as this windows machine and a default macos volume do, the removal took off disk what the write had just put there, and loot undo over such a rename reported pruned 1 path off disk, left the directory empty and made the next status read the recorded path as deleted, which loot move and converge did too, each reproduced by hand with the pre-change binary. both writers now prune through loot_core::disk_entry::prune_unwritten, the one home for the file and directory-entry rule apply-patch has taken since #2007 and #2014: an old path that is the directory entry of a path just written is renamed onto the written spelling, which keeps the bytes and gives the last name component the recorded case, the old name of a hard link is still removed by name, NotFound is a path that was not on disk rather than a path pruned, and any other removal failure is an error naming the path, where materialize_promoted discarded the error and materialize_target counted a failed removal as pruned either way. the identity helpers move out of apply_patch.rs into the new module, drop_empty_parents is shared with the view prune, and the prune makes no filesystem call beyond its removals unless an old path and a written path fold alike. the new pins went red with the entry rule removed (cli 1 passed, 3 failed; core 4 passed, 1 failed), with the old path kept and not renamed (cli 1 passed, 3 failed; core 4 passed, 1 failed) and with the removal error discarded and counted anyway (core 3 passed, 2 failed; the cli pins stay green, since none of them fails a removal). the perf gate dry run reads every counter the same before and after and says no move, ADR 0022 carries the amendment, ADR 0082 section 4 says where the rule lives, CONTEXT.md names it beside the apply-patch rule and in the re-pointing entry, and the workspace suite is green (#2011) de9fb5b0 · dbf3dbe6…diff
  • cherry-pick, revert and a surface that settles a stale-disk debt no longer destroy a case-only rename where the filesystem folds case, and the first no longer records the loss: #2011 routed two tree writers through loot_core::disk_entry and wrote that every writer which removes a path it may just have written asks that module, while two that did not were sitting in the tree as the sentence was written. Workspace::apply_change_delta, the one core cherry-pick and revert share, walked the merged actions as a BTreeMap and so wrote A.txt and then removed a.txt by name, which where case folds is the file it had just written, and it snapshots, so loot revert over a recorded case-only rename reported both paths converged over an empty directory and minted a change recording 1 deletion. Workspace::stale_debt_the_surface_left, the one scoped deletion a surface is allowed, compared the written set by name, and a case-only rename preserves the bytes, so the digest guard that exists to protect the operator matched the file the surface had just written: loot surface printed the path it had surfaced, left the directory empty, and the next loot status read it as deleted, which is the reproduction #2011 declined on the false reading that a surface prunes nothing. Each was reproduced by hand through the spawned binary first and then as a new spawned pin, and the pair went red over an empty listing before the change (4 passed, 2 failed) and is green after (6 passed); with the delta writer put back to removing by name they go red again (5 passed, 1 failed) and so does the new census (0 passed, 1 failed), and the stale-debt prune put back reddens the same two with the same counts. Both writers prune through prune_unwritten now. Which working-tree removals are weighed stops being a sentence: workspace.rs carries a census that reads every removal joined onto a working-tree root out of loot-cli/src and loot-core/src, names what each one removes and what the walk cannot see, and it named the offending function under each of those two mutations, went red at its own classifier control with ROOTS emptied and went red with a name dropped from its expected set (0 passed, 1 failed each). one_entry refuses a path that is or passes through a symbolic link now, since file_id follows links and on Windows two paths with one id were otherwise one entry, so fs::rename would have moved a link over the file it points at; the check is the symlink_on apply-patch already made, moved into the module, pinned with a junction here and red without it (5 passed, 1 failed). The hard-link pin names its links A.txt and a.txt so the fold map reaches the entry rule at all, where old.txt and new.txt never called it, and it skips a filesystem that folds case with a note; it stays unix-only and its red-first has not run here, though the body type-checks and runs on windows with the cfg lifted (7 passed). The four case probes become one loot_core::disk_entry::folds_case, and with it answering no the entry pin and two apply-patch case pins go red (5 passed, 1 failed; 18 passed, 2 failed). enclosing_fn moves into source_walk beside the #1929 census that asked it first and strips any visibility, after pub(super) made the new census name the wrong function. ADR 0022 carries the correction and its prunes-nothing claim is gone, ADR 0082 section 4 states where the probe and the link refusal live, CONTEXT.md names both writers and the census, the surface.rs and materialize_target notes stop claiming the two prunes answer the same set of failures, and the rewrap leftovers in ADR 0022, CONTEXT.md and the account page are fixed. The perf gate dry run reads every counter the same before and after and says no move both times, the site gate is green, and the workspace suite is green (3824 passed) (#2018) baec4112 · dbf3dbe6…diff
  • review sweep 15 fix-up: the evolog dash is on an empty FIRST LINE, the subject rule now has one home, and the surface prune says plainly that it discards. loot evolog --porcelain dashes its last column when the message first line is empty, which an empty message and a message whose body starts under a blank one both are, and three places said an empty message: the doc above the encoder, the ADR 0023 amendment of 2026-09-17 and CONTEXT.md. The wording moved rather than the bytes, because dashing on an empty message instead is a porcelain byte change for a message that opens with a newline, and a new spawned pin, a_message_whose_first_line_is_empty_is_the_dash_too, puts a run under the sentence: the file reads 5 passed, and with the whole message put back in the column it is 2 passed, 3 failed, the single-line control among the two that stay green. render::subject_line was billed as the one home while six call sites derived that first line inline, the four the ticket named plus the shortlog author rollup and the missing-object listing, so log --oneline, heads, show, range-diff and those two all ask it now, and the near-cousins that each do something more, the note ellipsis, the revert subject and the loot-first PR title, are named beside it rather than routed. The control is the spawned binary: 28 outputs over two repos, one carrying a multi-line message and one a blank first line, are byte-identical before and after, and with subject_line returning the whole message 10 of 11 of them move and the loot-cli lib suite reads 1219 passed, 4 failed, naming the shortlog rollup and the oneline row among them. The surface stale-debt prune keeps the removal error it discards, which #2018 declined and its closing note then reported as fixed, and the doc above it now says the let _ is deliberate, that apply_change_delta propagates where this one does not, and why: the delta writer snapshots, so a refused removal would be recorded as a deletion the disk never took. The removal census in workspace.rs states what its matcher is blind to, a type-annotated binding, a call split over lines and a path derived from a root-joined one, and names disk_entry::drop_empty_parents, whose remove_dir climbs from a dest its caller joined and which the fold cannot reach, because remove_dir takes an empty directory only and a directory holding a file the writer just wrote is not empty; widening the matcher would not have reached that call, so the blindness is stated instead. CONTEXT.md gives that census its crate and spelling scope. migration_serial records that hold blocks and is not re-entrant, and that it serialises migration runs rather than pg_authid, naming the fixed-name CREATE ROLE in tests/rls.rs that writes the shared catalog outside it; Held loses its Option and its Drop impl, since dropping the field is what the compiler already does. The migration census compares a BTreeSet the way the removal census does: with the const reordered it stays green where the Vec compare it replaced goes red (0 passed, 1 failed), and a name dropped from the const reddens it (0 passed, 1 failed). pg/tests.rs cross-references its own source walk and fn finder to source_walk in loot-cli and states where the pair differs. The folds_case helpers in two test files become folds_case_under, so neither shadows the disk_entry rule it asks. The rewrap leftover in account.tsx that #2018 moved rather than fixed is rewrapped with its neighbours, and so is a second one mid-paragraph in CONTEXT.md that the same rewrap reached. The forge suite is green against a throwaway 17.0 cluster (469 passed over nine binaries, no skip note) and its lock case goes red with the session dropped rather than held (0 passed, 1 failed), the site gate is green (643 passed, every surface under its ceiling), and the workspace suite is green (3833 passed) (#2023) c38e3299 · dbf3dbe6…diff
  • the Refit family reports which version each re-anchored change became, and move, squash and absorb emit that mapping through one machine shape instead of counting it. the executor already built the old-version to rebuilt-version remap to thread each line, and each arm reported its length as re-anchored N descendant(s) or intervening change(s): the column the #1517 census named as missing from three verbs at once and declined to freeze for one of them. the reports carry the list now as Reanchored rows (from, to, the durable change id both carry, and carried for a change rewritten only because it sat above one the verb was aimed at), and the human lines print the same bytes derived from that list rather than a count kept beside it - measured through the spawned v0.4.20 primary binary against the lane build over nine fixtures across move --onto, move --before, squash and absorb (the clean arms, the move and squash stops with their aborts, the absorb stay and a mixed absorb) with minted ids masked: the transcripts are identical. the shape is spelled once in loot_cli::refit_shape over the executor seam, by the rule that filed delta_shape beside its seam: R rows for the pairs in the order minted, C rows for the paths a stop recorded, and the A and S rows only absorb fills for where each hunk went and which stayed with its reason, following ADR 0088 at every rule, with a path the frame cannot carry withheld under unprintable through the predicate delta_shape already decides with, and change null rather than a dash string on a keyless repo. the handle is read off the rebuilt version because rebuild_node drops a superseded draft from the graph, which the first cut of the pins found by reading None off the original id. squash and move declare the machine pair beside the resume trio, absorb takes OUT, the usage lines advertise it, PROSE_ONLY loses the three and the ceiling comes down 56 to 53 as the #1517 entry paid, ADR 0066 tiers move 25 to 28 and 55 to 52 with the #1517 amendment marked paid rather than left stale, ADR 0023 gains the amendment recording the value, the rows, the flag members and the JSON fields, and CONTEXT.md the Refit entry. the pins resolve both ends against the graph in-process for every report and through a separate loot evolog process for every R row off the spawned binary, where a carried working draft is asked about as @ because the hex door excludes the working change by design, and the superseded end refusing by name is asserted as the reason the mapping exists. red first: the move arm marking every row aimed-at reddens the two workspace pins and the two spawned move pins (42 passed, 2 failed; 5 passed, 2 failed), the squash arm marking its intervening rows aimed-at reddens the squash workspace pin and both spawned squash pins (43 passed, 1 failed; 5 passed, 2 failed), and the porcelain R row printing from in the to column reddens the encoder pin and the two spawned resolve pins (43 passed, 1 failed; 5 passed, 2 failed). no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green over the final tree (loot-cli 1789 passed over 52 binaries, 3 ignored; the remaining crates 2094 passed over 69 binaries, 4 ignored) (#1837) 1be80495 · dbf3dbe6…diff
  • a superseded head is listed, not counted: loot log and loot heads stop counting a version superseded under ADR 0032 as a head, so a clean loot move --before no longer prints 2 heads - diverged; finalize then converge for a divergence that does not exist. the rule is spelled once as Liveness::is_superseded, which the head partition drops stale by, loot heads prints its superseded annotation by, and the counts now read: history() routes, counts and sections over the heads that predicate rejects, through a new LogGraph::retain_heads that re-indexes reachability into the kept heads, and the heads header counts the rows whose superseded flag is false and says how many superseded versions follow, still listed under the annotation. the head set itself is untouched - the stale head stays in .loot/heads until a converge retires it. reproduced first through the spawned binary on the lane tree at #1837 exactly as the ticket describes, and measured on move --onto too, where the stale head is not empty (B stays reachable only through it), which is why the rule is superseded and not own == 0. status never counted heads and never marks this with !, pinned rather than assumed. every machine shape is byte-identical modulo ids except the rows that were wrong: log --porcelain loses its two H rows and the working-change * row the fork view printed, log --json goes heads null, and heads --porcelain, heads --json and all three status shapes do not move. red first, four ways: the count rule undone reddens the workspace pin and the spawned reorder pin (0 passed, 1 failed; 1 passed, 1 failed), the header count undone reddens the heads pin and the spawned pin (6 passed, 1 failed; 1 passed, 1 failed), every head uncounted reddens the positive controls while the negative pin stays green (5 passed, 3 failed; 1 passed, 1 failed), and the predicate blinded reddens the partition, the annotation and the count together (6 passed, 1 failed; 0 passed, 2 failed). the #1477 production-shape pin moves to the new header, and ADR 0032 and CONTEXT.md record the amendment. no migration, no wire or format byte moves, and no forge or relay behaviour moves, so this owes no deploy. the workspace suite is green (3887 passed over 122 binaries, 7 ignored) (#1839) 8e6c0841 · dbf3dbe6…diff
  • sweep 1 fix-up over #1826, #1837 and #1839: a live change only a superseded head reaches is listed in the fork view, in a third section of its own, rather than in no section at all. LogGraph::retain_heads leaves such a change with an empty reachable_from, and history() partitioned the fork on exactly one head and more than one, so after a move --onto the ancestor the old tip alone reaches was printed nowhere - reproduced first through the spawned binary with two live lines (0 passed, 1 failed), the fork and the non-empty superseded head both holding as controls. GraphHistory gains unreached, the renderer prints it under behind a superseded tip after the shared ancestry, drawn_rows leads with it because such a change can only be a child of a lane or shared row, the path filter, retain_versions and all_rows walk it, and both machine shapes carry the row with no new column. HEAD on an untracked position with no working change now counts the heads Liveness::is_superseded rejects and answers the sole counted head by name, because Position::anchor answers the first graph head, which can be the superseded one; measured at the other two sites the ticket named, propose already reads is_live and IntegrationPreview deliberately counts the whole partition with retire for the superseded ones, so the liveness doc now points at the callers of the predicate rather than listing two readers. the move --onto usage line gets its space back; refit_shape spells its path columns through treepath::store_key with a backslash pin, and the #2033 census records the tree-path axis it is blind to by design; the shallow clause has one spelling, workspace::parents_not_held, read by the notice and the push refusal, with the pin the suite lacked; carried is carried_count, Reanchored.change is change_id, the squash and absorb op-log lines say re-anchored, frontier_beyond_the_host is private, the one-flag-member sentences drop one, ADR 0089 names both readers of .loot/shallow and both frontier spellings, and the one raw exit ADR 0089 rests on is held to one by a census in tutorial.rs. own > 0 for the move --onto stale head is pinned off the loot heads row. red first, six mutations in one round over disjoint pins: the unreached filter blinded, drawn_rows reordered, the HEAD filter blinded, store_key undone and a second raw exit added redden five pins with the sync shallow control green (1 passed, 5 failed) and the spawned fork pin (2 passed, 1 failed); the noun-verb swap survived tests/shallow (10 passed, 0 failed), which is why the clause has a pin now (0 passed, 1 failed under the swap). ADR 0066 keeps its gated 28 and points at the gate instead of at tickets. no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3895 passed over 122 binaries, 7 ignored) (#2046) 0b55a20f · dbf3dbe6…diff
  • a stop that will not be performed says so at the stop, and the promise #1518 AC4 made is restated as what holds. move, squash and the pick pair printed then loot <verb> --continue under their conflict lists and left the warning that the verb applied nothing to the closure, after the operator had resolved every path - reproduced through the spawned binary on a move --onto clash, where resolve then --continue closed the stop, C kept its version and its parent, and the signed resolution was the only new change. the advice is now resume::stop_advice, one sentence worded by applies_its_own_effect and reached from history.rs and pick.rs, so the stop and the closure read one table; whether a move or squash stop could resume stays the unmeasured question #1986 holds and is not decided here. the capture a restructure writes before its refusals is load-bearing, not merely early - @ names the draft the capture records and the working change travels with the line, read off the graph - so refused before anything is written is corrected to refused before anything of the restructure is minted in the AC4 pin, move_onto, reorder, CONTEXT.md and ADR 0080, and a new pin measures the cost: an uncaptured edit is the working change after a refused move, reorder or squash, and the signed graph is the graph the refusal found. measured too and recorded rather than changed: on a clean tree straight after loot new that same capture mints an empty working change that loot edit then refuses over, the #682 shape the snapshotted door gates and these wrappers bypass, which the machine-shape pins #1837 landed hold as the shipped carry. red first, three ways: the old sentence restored in stop_advice reddens the unit pin and both spawned pins (2 passed, 1 failed; 0 passed, 2 failed), the reorder stop keeping its own copy reddens only the reorder-squash-revert pin (3 passed, 0 failed; 1 passed, 1 failed), and the capture removed from move_onto reddens the cost pin on the assertion that measures it while the AC4 pin stays green (2 passed, 1 failed; 2 passed, 0 failed). no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3899 passed over 122 binaries, 7 ignored) (#1842) 7fd74d7b · dbf3dbe6…diff
  • a prose pass narrows claims that read wider than the code, and LineDelta::rendered goes crate-private. ADR 0007, CONTEXT.md and the object_and_key_at doc say the reveal gate is asked at every read through the grant key door, not at every read, since grant_sealed keeps its escrow fallback by design; the Refit enum count, the family list in CONTEXT.md and the refit.rs header, and a stale claim that every planner runs the draft check, now point at the enum and the wildcard-free minted_edges match instead of a count; role_display gets back the blank doc line rustdoc folded into the last bullet, verified in the built HTML, and a record type added to ReservedRecord replaces a third record type; the Landmark entry records the one-line rendering from #1519; the revset named door and the hunkpick PATCH constant state the exceptions a reader finds (grep writing its own refusal, restore spelling -p out under the main.rs census); the ChangeGraph insert doc stops saying the callers do not insert parents-first, names DagRepo::apply_sync for an apply_bundle DagRepo does not have, and says why the ordered ingest_shared_lineage splice stays on insert; ADR 0023 now says what #1870 wrote in the delta_shape header, that the reveal_at split is about the field and not the number, which embargoed@ can carry. nineteen assertion messages lose the run of spaces a rewrap left inside the literal. LineDelta::rendered and Rendered become pub(crate), with a compile_fail probe and a positive control: the probe was red before the narrowing (2 passed, 1 failed), and making the method pub again reddens it (3 passed, 1 failed). items 2, 8 and 14 need no fix here: the #1515 raise is real because #1516 lowered the ceiling between the two raises, #1968 dropped the width narrative, and #1860 withdrew the figures. cargo doc warnings are unchanged for loot-core, loot-codec and loot-cli. no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3913 passed over 123 binaries, 7 ignored) (#1848) eb966bdb · dbf3dbe6…diff
  • the child slot of Refit::minted_edges is read against the executor, and the range-diff Handle becomes a newtype. each declared edge now names its child as MintedChild::Supersedes or MintedChild::Copies, and every_minting_refit_declares_what_its_executor_records drives edit, split, squash, absorb (at the root of history and above it), duplicate, move --onto and move --before through the real verbs and reads the graph back: every declared edge was recorded, child and parents, and every recorded edge was declared unless each of its parents is a version the same refit recorded and none is the working change it leaves, which is the reason the minted_edges doc gave for leaving edges out, now held by a test. the premise was half right: split was never wrong, since its lower change records the draft as its predecessor (workspace.rs already pinned that), but duplicate named the source as the child, absorb at the root declared nothing, and edit declared only the first parent of a merge target while DagRepo::reopen_change records all of them, so the plan now carries every parent. duplicate --after @ now refuses naming a copy of the source. the doc and CONTEXT.md sentence calling edges between versions one refit mints out of scope by construction is corrected, since move and reorder keep the rebuilt draft the working change. Handle is a struct with private fields, so outside range_diff the handles function is the one way to build one, with a compile_fail probe and a positive control. red with the fixes undone: duplicate naming a superseded source (0 passed, 2 failed), absorb declaring nothing at the root (1 passed, 2 failed), the reopen declaring its first parent only (9 passed, 1 failed); red under mutation: reorder declaring one step fewer (0 passed, 2 failed), squash naming its source as the child (0 passed, 2 failed), the undeclared-edge rule blinded in the checker (1 passed, 1 failed), and Handle fields made public (1 passed, 1 failed). no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3923 passed over 123 binaries, 7 ignored) (#1849) 35d9a3e3 · dbf3dbe6…diff
  • sweep 3 fix-up over #1849, #1850 and #1853. the afk-loop sweep states one rule for a declined blocker, admitting the ticket for the scope that survives and naming the declined part, or excluding it with the declined blocker as the failing condition when nothing survives, and names the 100-comment cap of gh issue list, with gh issue view as the full read. the executor pin now edits a merge target through the real verb and reads both parents back, and it fails when a minting Refit variant goes undriven: executor_pin records each variant it sees through a wildcard-free match, and the test destructures that record without a rest pattern. plan_move and plan_reorder ask the draft rule of move_edges and reorder_edges, the derivations minted_edges declares, instead of their own copies; MintedEdge names the edge tuple; the Reopen comment says first parent; counts beside growing sets in refit.rs, landmark.rs, CONTEXT.md and the skill point at the set instead; the range-diff Handle control names the type the compile_fail probe imports. declined: a MintedEdge struct, since readers destructure the tuple and a struct adds no invariant, and a shallower reserved-role oracle, measured at about half a second, since four fields deep builds no landmark and fails its own class count (0 passed, 1 failed). red with the every-parent fix undone, the merge-target case (0 passed, 1 failed); red under mutation: a dropped duplicate case (0 passed, 1 failed), move_edges keeping the source on its old parent (0 passed, 2 failed), an undestructured Seen field (E0027), and the Handle type renamed (1 passed, 1 failed). no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3925 passed over 123 binaries, 7 ignored) (#2057) 9634e655 · dbf3dbe6…diff
  • attest_all no longer has a fallible step that can fail part-way through its write loop. the keypair lookup and the signing move into the judging phase, and the write is one call to a new DagRepo::add_attestations, which verifies the whole batch before storing any of it, so a refusal leaves memory as it was found and not only disk. the old loop fetched the keypair and verified record by record, and only the unreached persist kept an earlier record off disk. the sentence saying nothing can reach the lane one record at a time is restated in workspace.rs, verbs/attest.rs and CONTEXT.md, since attest is public and is that slice with one record. the tag seam pin now also reads the store back after the refused act. red before the fix, the engine batch pin with record-as-you-verify (0 passed, 1 failed); red under mutation: the batch verifying only its first record (0 passed, 1 failed), and attest_all recording each record as it is judged (0 passed, 1 failed, on the in-memory assertion while the disk read-back stayed green). no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3926 passed over 123 binaries, 7 ignored) (#1861) 4dbc7c83 · dbf3dbe6…diff
  • a locked pull stops leaving a stale copy for the next verb to capture: the ingest parks the claim it cannot check, and the first open holding the key finishes it. #1256 recorded nothing when it could not open the pre-ingest content, on the ground that loot rehome materializes and moots the question, which it does not do when it refuses, and in that state it refuses: the untouched pre-pull copy read as an edit no change had recorded, so rehome declined it and loot converge, the next step the pull note names, captured it silently, one change below the version that had just arrived. the claim, being the path, the pre-ingest oid and the digest of the bytes on disk, now goes to .loot/stale-disk-unverified, a position-owned artifact of the same shape and codec that nothing in force ever reads, and Workspace::settle_unverified_stale_disk makes the same comparison at the first unlocked open that was given a graph, beside heal_hold and under its gate: bytes that match move into the record in force, bytes that do not are the operator own work and the claim dies, an open that cannot answer leaves it parked, and a write that disposes of the bytes a claim is about retires it, whether it wrote over them or pruned them. rehome refusal stops asserting the arrival guard would refuse a capture where no guard stands: it asks Workspace::arrived_unmaterialized, keeps the circle where the guard does stand, and elsewhere says what recording those bytes actually does and that a verb capturing on entry records them whatever rehome refused. the locked pull note says to stop if rehome refuses. review fixes: the pin for the printed order now runs the three printed commands and nothing else, since any extra verb opens the repo and an open is what settles the claim, with the record own story moved to a second test; and the three malformed-record refusals stop offering a first remedy the refusal itself prevents, because they refuse at the open, so the file has to go before any verb can run. the Known Issues entry stays, wearing FIXED_IN_MAIN, because v0.4.20 is the binary a reader can install, and pin 5 comes back over it as its own retirement note instructed. red first: the note recipe, run in order, left the arrived version on disk only after the fix (0 passed, 1 failed). red under mutation, each 0 passed and 1 failed unless noted: the claim dropped rather than parked, the open never settling, the settle promoting without the check, dropping rather than keeping what it could not check, leaving a checked claim parked, and running on an open given no history; the surface, the whole-tree write and the one-path write each keeping a refuted claim (that last two 6 passed and 1 or 2 failed); the disposed set naming only what a surface wrote and not what it pruned; rehome claiming the guard everywhere and rehome never claiming it at all; the note without its stop clause; the page deleting the held entry, the page dropping the marker, and the held row witnessing a fix the tree does not carry. no migration, no wire or format byte moves, since the new file is local to a position and never bundled, and no forge or relay byte moves, so this owes no deploy; the CLI change owes a release, and the Known Issues entry leaves with it. the workspace suite is green (3962 passed over 122 binaries, 7 ignored) (#1963) f7b7e533 · dbf3dbe6…diff
  • loot seek answers for a git repository at a URL without a working tree and without its blobs: a scheme or an scp-like word names a remote, a git+ prefix skips the loot-host probe and a loot+ prefix or an http URL whose info endpoint answers as a loot host refuses naming ticket 5, and everything else is read through a blobless shallow bare cache under LOOT_SEEK_CACHE, else the XDG cache home, that a spawned git clones once and refreshes on every invocation by fetching the remote HEAD and pointing the cache HEAD at it, because a bare clone configures no fetch refspec and a plain fetch moves nothing, which the first draft of the refresh proved by answering the old tip. the rows are the same P, T and S rows through the same git2 backend, kind git-remote, resolved the URL. the blobs a search or a read needs are fetched in one batch before the scan, only those the object database does not hold, chunked at a named constant sized for a Windows command line, and a listing fetches none: the unit test holds every blob absent after the clone and after a listing, present for the searched subtree alone after a scoped search, present for one file after a read, and untouched by a refresh. a server that ignores the filter is refused in git's words and the half-made cache removed; git absent from PATH refuses naming the requirement; a depth-one cache refuses HEAD~1 by name. the spawned-binary pin drives a file remote through LOOT_SEEK_CACHE, reads the cache object database after each question, counts one cache directory per remote, and runs with an empty PATH for the missing-git refusal; a spawned relay pins the loot-host probe. the two-axis review before landing made the home rule one function the config base and the cache share, made one scp reader serve both the classification and the cache path, passed the git verb once, named the probe timeout, skipped the probe once a cache exists, and replaced a counted refusal list and a membership claim with what defines each set. red under mutation: a search fetching every blob (0 passed, 1 failed), a listing fetching blobs (0 passed, 1 failed), the refresh skipped (0 passed, 1 failed), held blobs fetched again (0 passed, 1 failed), a refused clone leaving its cache (0 passed, 1 failed), the filter warning made non-fatal (0 passed, 1 failed), an scp-like word not a remote (0 passed, 1 failed), and one cache per host (0 passed, 1 failed). no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3961 passed over 123 binaries, 7 ignored) (#2082) f38f0915 · dbf3dbe6…diff
  • loot seek answers for a loot repository on a relay or a forge without a working tree and without its bodies, and does so beside #2043 rather than behind it: a loot host, named by a loot+ URL, by an http URL whose info endpoint answers, or by a bare word the ambient repo config resolves as a remote name, is read through a body-less cache position, an ordinary loot store under the cache home that copies the ambient repo keypair, or the one --identity-from names, and never mints one, because a fresh key is 403 at a forge and reads only Published on a relay. each invocation refreshes it with the metadata half of a pull only, the closure-declared fetch every bounded pull starts with, ingested at depth one and with no wants round, through the routed transport a pull already uses, and collects a forge standing self-grants as clone does. a key rides beside its ciphertext, so a body-less change carries no key: a listing gates on the tier the tree records, Internal listed and Restricted or a live Embargo counted sealed, and the real key gate decides after a search or a read has fetched the listable bodies in one batched round, only those the store does not hold. the store already tolerates withheld bodies and this verb never reads one it has not fetched, so the fifth kind of absence #2043 would name is never classified here; what #2043 still owes stays its own. the workspace gains the two thin seams, pull_metadata_via and fetch_objects_via, and a holds check the engine now exports. ADR 0090 records the decision the map left to #2043, in docs/adr where a decision lives: the cache directory is this verb alone, the private key copy is refused for a sealed source and a failed make leaves none behind, offline is a refusal never a stale answer, and grants are collected without acking so a peer grant is never consumed into a cache. the two-axis review before landing put the pull doc back on its function, made the listing gate Internal-only because an embargo key has no bundle lane even after its instant, refused a multi-head remote without --at, excluded burned objects from a prefetch, probed the host once per invocation, made --identity-from a plain word read from the process directory, split collect_grants out of pull-grants with the ack a choice, folded the loot loop into loot_answer beside git_answer with one row push, and pinned the refresh closure declaration, the cache object store through the binary, and a remote name resolving to the same cache. the unit tests pin the seams over the in-memory relay, the tier gate, the object fetch that brings only what was asked and declares the closure, a refresh that sees a later change at depth one, and the cache over a spawned relay carrying the source identity; the spawned-binary pin pushes the fixture to a relay spawned in the test, holds the remote listing to the local one by path and visibility with the sealed path withheld and counted, checks the cache identity is the repo's, reads a body verbatim, refuses from outside any repo naming --identity-from, and answers with it. red under mutation: the metadata pull asking for every object (0 passed, 1 failed), held bodies fetched again (0 passed, 1 failed), the object fetch declaring nothing (0 passed, 1 failed), an embargoed path listable without its body (0 passed, 1 failed), the cache minting an identity (0 passed, 1 failed), a second open making a second position (0 passed, 1 failed), and the refresh skipped (0 passed, 1 failed). no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3972 passed over 123 binaries, 7 ignored) (#2092) 669baef4 · dbf3dbe6…diff
  • loot seek reads what a question keeps and nothing else: the loot backend read every change through readable_tree_at, which decrypts the whole tree and only then narrows to the pathspec, so a scoped search cost the repo and a listing decrypted every file to print its name. a search now opens each kept path once through readable_object_at, keyed on the content address so an object carried across the changes of a revset is read once, the rule grep has followed since #1500, and a listing decrypts nothing: a local target asks the visibility gate per kept path through a new per-object form of the same predicate, Readable::visible_object and Workspace::may_open, together with the store index and the burn log, which names the set surface prints; a remote target keeps the tier gate. the counted read pins it in tests/seek_object_reads.rs with its control: a whole-tree search opens one object per path, a listing reads one sealed header per kept path and none outside the pathspec, a scoped search opens the subtree and not the tree, and a revset over two changes opens a carried object once, seven where the old walk opened twelve. a listing still reads each kept header once because the key gate reads the sealed header to ask the keyring, which the counter counts as a get; it decrypts nothing, and that is stated where the pin is rather than claimed as zero. measured on this repo: a scoped search over crates/loot-cli 189 ms before and 123 ms after, a bare listing 167 ms before and 124 ms after, a whole-tree search 241 ms before and 223 ms after, best of three each; this repo holds small objects, so the win here is the fixed per-invocation cost and the pins are what carry the claim at scale. the two-axis review before landing made the engine gate one function the tree walk and the per-object view both call rather than two copies under a parity sentence, reduced the listing to the oracle alone since can_open is already false for an unheld or burned object, named the scan cache Scanned with unreadable, silent and hits, said the count is not surface count because surface labels a burn and this verb counts it sealed as before, pinned a burned path leaving the listing and never being opened, and recorded that the ticket premise of a returning visible_paths_at was false. red under mutation: the address cache dropped (0 passed, 1 failed), a search reading the whole tree (0 passed, 1 failed), a listing gating the whole tree (0 passed, 1 failed), a listing skipping the key gate (0 passed, 1 failed), and the engine gate no longer asking the keyring (0 passed, 1 failed). no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3981 passed over 124 binaries, 7 ignored) (#2102) d7606a23 · dbf3dbe6…diff
  • loot seek carries less when asked and its refusals carry a code: under --text, -l keeps the P rows of the paths that hit and nothing under them, --count puts one N row under each P row with how many lines hit, and --max-line <bytes> cuts a hit line at a character boundary into a C row of T arity, clipped true under --json, each emitted only under a flag that did not exist before, the rule the R row set in #1522, so a consumer that never passes them reads the bytes it always read. the three are one scan rendered three ways, so skipped_sealed, truncated and the row order never move, and an uncut hit carries no clipped key so every hit shipped before renders byte for byte as it did. measured on this repo, a search for fn over crates/loot-cli/src is 259,793 bytes in 3,888 rows whole, 9,923 bytes in 92 rows under -l and 10,369 bytes in 183 rows under --count. the refusals join the #430 coded channel: a refusal under --json is one object with contract first and a code the CliError was built with, never a parse of its sentence, declared beside the verb in seek::code as conflicting_flags, read_is_bytes, no_identity, multi_head, bad_revision and no_such_remote, while a directory that is neither repo keeps no_repo and an unusable --limit or --max-line keeps bad_flag_value because a second spelling of one fault is the #1597 defect; there is no unreadable slug because --read refuses every machine format before it reads and a listing or a search counts a sealed path rather than refusing. the multi-head refusal names each head with its id prefix and subject so the next call pins one with --at in one round trip; the ticket first asked for the newest head by default and that cannot be honest from a depth-one cache, where every head reads generation 0, and authored_at is never an ordering input (ADR 0043), so the ticket was corrected before the lane opened and ADR 0023 records why. error.rs stops listing the three CLI slugs as the whole set and names what defines it, and loses a two-raiser census that was stale before this change. the pin on that one object found a defect #2092 shipped: every remote loot seek printed the dispatcher shallow note after its answer, because the body-less cache is refreshed at depth one by design and the width it records is the process-wide one main reads, so the note was wrong twice, about a cut nobody can close and naming a loot fetch --deepen that runs where the operator stands; the cache open and its object rounds now run outside_shallow_notice and a remote answer leaves stderr empty. the two-axis review before landing also refused --max-line beside -l or --count where it was silently inert, mapped the selector door stringified refusals to bad_revision on a loot target so a typo in --at carries the same code on both backends, pinned the multi-head refusal through the verb and the prefix it prints as a selector the verb accepts, pinned the cut through the scan on a multi-byte line and on an invalid byte the lossy decode widens, read the three remaining driven refusals back under --json, and narrowed the code claim to the refusals the verb decides, a stringified failure staying error as #430 says. pinned in the shape tests on every format, on a multi-byte line for the cut, on the code of each parse refusal, and on two signed heads over a spawned relay, and through the spawned binary on the seek fixture with every refusal it drives under --json read back as one object with its code. red under mutation, counts read each time: -l still writing the T rows (0 passed, 1 failed), --count off by one (0 passed, 1 failed), the JSON hit_count dropped (0 passed, 1 failed), the clip ignoring character boundaries (0 passed, 1 failed), the clip marking nothing in JSON (0 passed, 1 failed), the clipped row keeping the T mark (0 passed, 1 failed), --max-line 0 accepted (0 passed, 1 failed), the shaping flags riding a listing (0 passed, 1 failed), -l with --count accepted (0 passed, 1 failed), a parse refusal on the generic code (0 passed, 1 failed), the read refusal losing its code on the wire (0 passed, 1 failed), the multi-head refusal naming no head (0 passed, 1 failed), the multi-head refusal on the generic code (0 passed, 1 failed), the git bad-revision code generic (0 passed, 1 failed), --max-line beside -l or --count accepted and inert (0 passed, 1 failed), a bad --at on a loot target left on the generic code (0 passed, 1 failed), the multi-head guard unwired from the verb (0 passed, 1 failed), the clip through the scan measuring raw bytes (0 passed, 1 failed), and the cache refresh recording its width for the shallow note (0 passed, 1 failed). no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3984 passed over 124 binaries, 7 ignored) (#2105) 25d946e7 · dbf3dbe6…diff
  • ADR 0076 amended by #2101: .loot/gates gains a land phase and two hand-offs, and git hook names are mapped or refused by name. Phase::ALL has three members and every sentence that spells the set derives from it (Phase::words, Phase::usage_alternatives), so the parser refusal, the loot gates usage and the --phase errors moved without a second edit. A land gate is the user-gates row of orchestrator::GATES, between the text gates and the expensive ones, PreLandAndMerged and Outside: it runs on the lane tree and again on the merged tree when the converge moved the position, --skip-tests never silences it, and loot-first land gains --skip-gates which honors honored and never required per declared posture and records the skip in the op log after each pass, because a gate holds no Workspace. A finalize or land gate may write {message}, which expands to the path of a temp file holding the message about to be signed, newline-terminated and unlinked the moment the child returns, and {paths}, the touched paths root-relative with forward slashes, sorted, deduplicated, one argument each, deletions included; a push gate that names either is malformed, since a push has no single change, by Phase::commits_one_change rather than a list. The hand-off is built by Workspace::gate_handoff only when a gate in the phase names a placeholder, so a declaration that never asks never pays the working-delta walk, and gates::run still takes no Workspace; that accessor is the 384th Workspace method and the verb tier module doc now says so. A {message} with no message is unstartable, never a pass. The spelling was decided by running the tools, not reading them: pre-commit 4.6.2 files stage hands a hook the paths as separate arguments and its commit-msg stage hands exactly one filename, and a bare script reads that file, so a file path and argv tokens are the compatible spellings; pre-commit itself needs a .git and so cannot run inside a lane, recorded in the amendment. git pre-commit and pre-push are refused naming finalize and push; any other word is refused by name with the one sentence that holds for every unmapped hook, no index, no checkout, no rebase, no merge commit, and GIT_HOOK_PHASES is the map, never a claim about git set; the optional init --from-git was not built. Measured through the real binary in a throwaway repo, since .loot/gates is shared over the store and a land line there would make every other session binary refuse its finalize phase as malformed, which the amendment and workflow.md now warn about: two Python gates over 13 paths and the message cost 77 ms and 64 ms wall for the whole phase against 23 ms for a listing, so a contended land pays about 140 ms; loot new through the binary handed the message file and 13 paths, and --skip-gates recorded skipped p in the op log. Five pins each broken once by a named mutation and confirmed red at 0 passed 1 failed, then green at 1 passed: the row demoted to PreLand, the message file cut to its first line, the sort dropped, pre-commit unmapped, and required skipped under the flag. CONTEXT.md Phase entry and workflow.md step 7 say the same. loot-cli gates 25 passed, change and sync 65 passed, loot-first lib 225 passed after the two table assertions learned the ninth row; two lands refused on censuses the targeted runs never selected, the module-doc method count and the help placeholder classification in revset_everywhere, and each fix is one line saying the new spelling. No wire, format or store byte moves, no migration (#2101) 5c4e3996 · dbf3dbe6…diff
  • loot seek pays fewer round trips: a remote target is answered from its cache inside --fresh <secs> with every round trip the open makes past the record check skipped, today the info probe, the metadata pull and the grant round, and a bare http URL a loot cache already stands for is routed to it before the probe, from a record the cache writes on every refresh, the first included, holding the clock the workspace reads and never a file mtime, and a record that is missing, will not parse or is ahead of the clock refreshes rather than trusts; the answer says so, target.refreshed under --json only under the flag, the key that did not exist before riding the flag that did not exist before as the R row rule has it, and one prose line when the refresh was skipped, on stdout beside the other summary lines and on stderr for a read whose stdout is the bytes; --fresh over a local target is conflicting_flags since there is no refresh to skip and a window that is not a number is bad_flag_value; the window skips the refresh rounds and never the fetch a question needs, and since a host answers an object round with whatever the declared closure lacks, a change pushed since the refresh rides back beside the bodies a search or a read fetched, which ADR 0090 records and the smoke pins rather than hides. the bodies a question needs travel in rounds of seek::OBJECTS_PER_FETCH, 256, through fetch_objects_via whose batch is now the caller's while the pull keeps its own 32, chosen by measurement over a relay spawned in-process holding 400 small bodies, best of three on loopback: 32 per round is 13 rounds and 243 ms, 64 is 7 and 193 ms, 128 is 4 and 158 ms, 256 is 2 and 136 ms, 512 is 1 and 126 ms, the table flattening past 256 while the memory a round holds keeps doubling; the 413 halving the ticket first asked for is not built and the ticket was corrected before the first test, because a fetch request carries ids alone and the response is unbounded, so there is no 413 to fall back from. several --text patterns are one read of each kept object and the union, a line carried once however many matched it so --count counts it once and --max-line cuts it once, with a pattern that will not parse naming itself as it did alone; a repeated --text was accepted before and silently narrowed to its first value, the one shape that moved, and ADR 0023 says so. ADR 0023 records the key and the union, ADR 0090 the window and where the record lives, and CONTEXT the three. pinned on the record round trip and the inclusive window arithmetic, the union over a fixture line two patterns hit, a git cache and a loot cache each answering stale inside a window and fresh outside it, the batch read off the test relay round count with the verb own prefetch one round, and through the spawned binary on a head pushed between two calls, unseen at the recording clock inside the window and seen at a clock outside it, with the refreshed key present under the flag and absent without. red under mutation, counts read each time: a line two patterns hit carried twice (0 passed, 1 failed), only the first --text scanned (0 passed, 1 failed), the window never holding (0 passed, 1 failed), the window exclusive (0 passed, 1 failed), the git cache ignoring the window (0 passed, 1 failed), the loot cache not recorded on the first make (0 passed, 1 failed), the refreshed key written without --fresh on a loot remote (0 passed, 1 failed) and on a git remote (0 passed, 1 failed), a window over a local target accepted and inert (0 passed, 1 failed), the refreshed key dropped from the JSON target (0 passed, 1 failed), the batch argument ignored (0 passed, 1 failed), the verb prefetching at the pull batch (0 passed, 1 failed), a record ahead of the clock trusted (0 passed, 1 failed), the git answer dropping the refresh (0 passed, 1 failed), a stale read saying nothing (0 passed, 1 failed), and a stale read fetching no body (0 passed, 1 failed); the loot-remote key mutation first ran green against a pin that read only the git remote, and the pin was widened before it was believed. no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3987 passed over 124 binaries, 7 ignored) (#2108) 83c96c6a · dbf3dbe6…diff
  • loot seek is easier to drive: --name <glob> matches a basename at any depth, spelled into the same pathspec as --path three ways, the glob for the root, **/glob for everything below it because the dialect **/ crosses one directory or more and never none, and **/glob/** for what a directory of that name holds because the bare-directory affordance reaches a root-level pattern only, joined by the group any-match rule, because the dialect stops * at a slash and every Rust file was a --path pair agents get wrong, with a glob carrying a separator refused as a path by code; the ambient target resolved is the root lexical absolute path rather than a dot, the same root --in <dir> prints, with no verbatim prefix on Windows, a value that moves where the key does not because a dot was never a location a later call could use, which ADR 0023 says; and loot seek --schema prints the verb machine contract as one JSON document from anywhere, contract first, derived from the code rather than typed: the flags off SPEC and the attached group, the marks and their columns off MARK_COLUMNS, the codes off code::ALL, and three examples the verb renders itself, an answer carrying every optional key, a counted answer and a coded refusal, with --json accepted and any question beside it refused. the codes come in two sets that say which is which, verb_codes off code::ALL and shared_codes off CliError::SHARED defined in error.rs where those slugs are minted, and the tables that are lists are pinned against what the code does: every mark constant read off the file is in MARK_COLUMNS with the arity a rendering under every Detail emits, code::ALL is every pub const in the module read off its own source, SHARED is every slug error.rs mints, and every key Answer::json can write is in the examples, the refusal example rendered by the multi-head raiser itself. the two-axis review before landing renamed the codes key before it froze under a name that read as the whole set, moved the --name refusal ahead of the target open so a bad glob costs a remote no round trip, said that --name is not rebased by the argv door because a basename is not a path and pinned it from a subdirectory, pinned the git-local and relative --in resolved, and corrected the ADR to the spelling the code makes. pinned in the unit tests on a nested fixture and the refusal, on the schema derivation, and through the spawned binary on --name equal to the pair it spells and not to **/ alone, unioned with --path, the ambient resolved absolute and equal to --in <root>, and --schema from a directory that is no repo. red under mutation, counts read each time: --name not spelled at any depth (0 passed, 1 failed), a --name with a separator accepted (0 passed, 1 failed), the ambient resolved staying where the verb ran (0 passed, 1 failed), --in <dir> resolved staying as typed (0 passed, 1 failed), --schema taking a question beside it (0 passed, 1 failed), MARK_COLUMNS forgetting the count row (0 passed, 1 failed), MARK_COLUMNS with an arity wrong (0 passed, 1 failed), code::ALL forgetting a slug (0 passed, 1 failed), the schema example carrying no refresh (0 passed, 1 failed), the schema omitting the bare flags (0 passed, 1 failed), SHARED forgetting a slug (0 passed, 1 failed), the schema omitting the shared codes (0 passed, 1 failed), the schema refusal example not the raiser (0 passed, 1 failed), a --name refusal waiting for the target to open (0 passed, 1 failed), and --schema answering a question instead through the binary (0 passed, 1 failed). no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3999 passed over 124 binaries, 7 ignored) (#2115) ca25f385 · dbf3dbe6…diff
  • a fetch gains a depth the host honours: the request carries a trailing depth after its wants, written only above zero so a request at zero is byte for byte what every client sent before, and an old host decoder returns after the wants and never sees it, which is what makes it safe to send to any host; the relay and the forge confine the change lane to the nodes within that many generations of their live heads, one being the heads and nothing older, intersected with the delta past have and, on the forge, after the entitlement gate, so a caller refused metadata in a full bundle is refused it at any depth and a node the caller holds is never re-sent, while a want is answered by address whatever the depth, the lane walking the cut changes as it walks the held ones; the seeds are live heads and not childless ids, the forge reading the ref declared set a client computed with its retire applied and the engine excluding any version a node names as a predecessor, since on a host that ingests amends a superseded sibling stays childless and would otherwise seed a generation of its own, 213 such tips against 31 real on this repo; /info advertises it as fetch_depth, false when absent, so a caller that needs the bound refuses a host without it and a caller that can afford the fallback proceeds. the receiver still cuts and that stays the rule, ADR 0089 amended rather than overturned: pull_metadata_via and the depth round of a bounded pull ask the host for the depth they will keep, FromTips as its n and a deepen as zero since the frontier is this position own, and applies IngestDepth to whatever arrives, so a host that predates the field sends the history and the position is the same one generation deep, which the test relay pins both ways by playing a host that honours the depth and one that does not. the WASM core encodes the same bytes, frozen in the parity suite against the native vector at depth one. decided by a grill of six questions on 2026-09-20 and recorded on the ticket and in the ADR: bounded per invocation regardless of persistence, a depth on the existing fetch rather than a listing endpoint, counted from the host heads, search kept on the receiving side over bodies fetched by address, the host cut an optimisation the client never depends on, and the SDK and the cache cap to follow. this is the wire half of the browser SDK stopping its 53 MB stateless read and of the serverless runner per-job pull; measured against the live relay in the landing comment once the hosts are deployed. pinned on the same shapes at each host so the two walks are held to one answer, a chain at depths one, two and zero and past a have, a fork whose two heads are both seeds, a superseded sibling that never is, and depth zero byte-identical to the one-pass walk this walk replaced, the forge with a reader the gate refuses getting nothing at any depth; in loot-net on the field decoded both ways with an old body as zero, an odd remainder refused, a pre-field info as false and one frozen vector both encoders pin; over the wire on a spawned relay and a spawned forge each answering one node of two at depth one and advertising the cut, with a want riding across the cut; and in the CLI cache refresh asking depth one. a clone at a depth asks the host too, so its declined count is 0 under a host that cut, since nothing behind the cut arrives to be declined, and the frontier width is what says the position is bounded, which the shallow suite now pins against a host that honours the depth and one that predates it; the count stays exact on a deepen, which asks the host for no cut because the frontier it deepens from is this position own. red under mutation, counts read each time: the engine ignoring the depth (0 passed and 1 failed), the engine cut counting one generation too many (0 passed and 1 failed), the depth never written (1 passed and 1 failed), the depth never read (1 passed and 1 failed), the relay not advertising the cut (1 passed and 1 failed), the relay handler dropping the depth (0 passed and 1 failed), the forge ignoring the depth (0 passed and 1 failed), the forge cutting before the gate (0 passed and 1 failed), the forge not advertising the cut (0 passed and 1 failed), the cache refresh asking no depth (0 passed and 1 failed), the receiver stopping its own cut when the host cuts (0 passed and 1 failed), the wasm framing never writing the depth (0 passed and 1 failed), the forge handler dropping the depth (0 passed and 1 failed), the engine seeding from a superseded version (0 passed and 1 failed), the forge seeding from every childless id declared or not (0 passed and 1 failed), an odd remainder read as no depth (1 passed and 1 failed), and the wants lane skipping the cut changes (0 passed and 1 failed). no migration and no format major move: a trailing field the old side never reads is a minor move, and /info default false is the whole compatibility story; the relay and the forge owe a deploy, which the release cut carries. the workspace suite is green (4017 passed over 126 binaries, 7 ignored) (#2123) e3ddfdce · dbf3dbe6…diff
  • loot pipeline (#2127, ADR 0091, map #2099 ticket 3): the tracked .lootpipeline at the repo root, in the .lootattributes grain with no dependency column, one step per line as name, trigger, kind and what, where the trigger names the tree a step judges, change or main, never the moment, and the kind is run, argv through the spawn seam with {message} and {paths} as gates reads them through one shared builder gates::spawnable, or check, a loot-native gate deferred naming #2128 until the checks live where the verb can reach them, and a deferred step is a refusal exiting 1, never a pass. loot pipeline lists and spawns nothing; run judges the working tree by default or, under --version, a scratch tree of a held version written by Workspace::scratch_tree from readable_tree_at so the readability decision precedes any file, holding the version own .lootpipeline, and removed on drop, with the hand-off from Workspace::version_handoff naming the paths whose readable content differs from the first parent; --only narrows to one step and an unknown name is refused naming the declared set; status reads the pipeline/TRIGGER/KIND passes and pipeline/approve admissions trusted keys signed on a version through attester_trust, passes only. A tracked file never runs implicitly: loot new with a run step declared and no .loot/gates line reaches no spawner, pinned. Frozen shapes in ADR 0023 amendment, exit code on the shape. Measured on the release binary: a two-step run 74 ms, run --version 47 ms and a listing 18 ms on a three-file repo; on this repo of 1452 tracked files, run --version HEAD materializes the whole readable tree in 1.2 s against a 0.15 s listing. Found by that measurement, not by thought: under load a Windows scanner held a freshly written scratch file, the one-shot remove_dir_all failed and its swallowed error left plaintext in the temp dir, so ScratchTree drop now retries over half a second and names the path on stderr if it still cannot remove it. Seven censuses moved, each read off its own refusal: the verb tier method count to 387, MACHINE_OUTPUT gains pipeline in sorted position, the usage spells the list leaf as an invocation line, README block and its all 84 verbs sentence, 133 verb lines in three places, the revset placeholder lists take <change|main>, <step> and <id> and SELECTOR_ONLY takes pipeline status, and the site CLI page names the verb; ADR 0066 counts 84 dispatched, 82 in the table, 32 emit machine output with an amendment. CONTEXT.md Pipeline entry says what is built. Nine unit pins plus two in change.rs, five broken once by a named mutation and confirmed red at 0 passed 1 failed then green at 1 passed: a check made a pass, the trigger filter widened, the scratch removal removed, the approval role made a job, and the tracked file run at a finalize. loot-cli and loot-first green across 64 binaries, 2162 passed; the site verb census green. No wire, format or store byte moves, no migration (#2127) fe693773 · dbf3dbe6…diff
  • buoy joins the dispatch table and bisect is left alone on the early path with the only reason that is still its own: #1764 made an output shape able to carry its own exit code, which was the whole of why buoy was dispatched ahead of COMMANDS, so the arm is gone and cmd_buoy returns a shape like every other verb. emit::Buoy reads ADR 0025 exit codes off the BuoyVerdict it already holds, one arm per row of that table, so the rendered answer and the code are one decision and the empty porcelain of the none row cannot come apart from its 2; buoy --nearest carries its code as a value on the message instead, having collapsed to one token with no structure to read an outcome back off; and buoy_write is deleted rather than moved, because the dispatcher outcome was already byte-for-byte the same #870 rule about a reader that left early. everything that walked the table and the buoy spec separately now walks the table alone — the telemetry note_dispatched call, the machine-output census, the verb census, the documented-flag census, the template-flag census, help_for and the completion list — and nothing is counted twice: the telemetry report is asserted to say buoy once, the table is asserted to declare no name twice, the offered completion names are asserted to hold each name once, and the census sum is now an equality with the table rather than the table plus one. going through the argv door means declaring an arity, so exactly one invocation moved: loot buoy reviewed junk refuses by name and exits 1 where an open claim used to drop the word and resolve for reviewed, which is #1562 reaching a verb that had been standing outside the gate enforcing it. every code is pinned through the spawned binary in tests/buoy_exit.rs, each outcome in every rendering it accepts. red under mutation, counts read each time: the verdict codes flattened to zero (buoy_exit 4 passed and 2 failed, emit_snapshot 12 passed and 1 failed), Message::coded discarding the code it is handed (loot-cli lib 1334 passed and 1 failed, buoy_exit 5 passed and 1 failed), the early telemetry call put back beside the one in the table (telemetry 2 passed and 1 failed, reporting buoy three times for two runs), buoy pushed onto the completion list beside its own row (loot-cli bin 125 passed and 1 failed, buoy_exit 5 passed and 1 failed), the row declared twice (loot-cli bin 121 passed and 5 failed), the row declining its arity again (loot-cli bin 123 passed and 3 failed, buoy_exit 5 passed and 1 failed), and the census sum left at the table plus one (loot-cli bin 125 passed and 1 failed). ADR 0025 records where each code now comes from and the one that moved, ADR 0076 closes the open work it had recorded and narrows the early-dispatch path to bisect, ADR 0066 moves the split inside its dispatched total, and CONTEXT.md stops naming buoy beside bisect. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4058 passed over 129 binaries, 8 ignored) (#1976) fa68a7d3 · dbf3dbe6…diff
  • the timed deposit lane asks the seal whether a path belongs in it and not only when its key is released: #2185 took the reveal instant off the seal at embargoed_paths and left the tree entry deciding whether that lane was reached at all, so an entry claiming an embargo over a seal that records none was answered 0 by embargo_reveal_at, the number for content under no embargo, and a Restricted seal content key was fanned out to every registered peer as a timed grant the relay releases on arrival - the same disclosure direction widened by the repair that narrowed the other one. reproduced at the plan before deciding, running the disagreement cases through embargoed_paths, restricted_paths and internal_paths and through plan_timed and plan_standing, where the row read c.txt to bob and to carol at 0. membership of the lane comes out of seal_visibility now rather than embargo_reveal_at, because the tier and the instant come out of one read and the number alone cannot tell an agreeing entry from one whose seal records no embargo; the unreadable seal fallback stays, the deposit it feeds refusing on the same read. which lane a path is offered to is still matched off the entry and stays with #2187, deferred on the store read per finalized tree path it would cost rather than on impossibility, and both missing directions are pinned: the claimed embargo the seal denies, and the internal claiming entry over an embargoed seal, where what keeps the untimed lane off the path is the key and not the tier, a live embargo key being staged in the escrow that lane does not read. the sentence naming the internal lane as the one a lying entry moves to now names what selects a lane, the reason that argued a seal knows nothing about paths says cost and #2187 instead, the negotiation stop rule comment cites its pin under the name #2185 gave it, the key lane fixture says what defines the set of builders rather than counting them, the census helper doc says compiled into rather than asking, the spike crdt key lane records why the census cannot see it, and BTreeSet stops being spelled in full beside an imported BTreeMap. red under mutation, counts read each time: the dropped arm made to plan the row again (custody 67 passed and 1 failed, reading Some(0) where None belongs), internal_paths widened to the escrow (67 passed and 1 failed, the live embargo reaching the untimed lane), and the lane selection made to ask the seal, which is the #2187 repair (67 passed and 1 failed, the timed lane reading cve.txt and other.txt where other.txt alone belongs), each restored to 68 passed and 0 failed. no migration, no wire or format byte moves and no host behaviour moves, but what a push deposits moves on the client, so this rides the next release and owes no deploy. the workspace suite is green (4103 passed over 132 binaries, 8 ignored) (#2188) efb3d8ed · dbf3dbe6…diff
  • the permissive deposit arm gets the pin its defence was resting on, the standing-lane assertion that could not fail gets a comparison, and the declined cross-shape census weighs the instrument it never considered: #2194 carries the sweep 14 fix-ups plus the five a focused review of #2188 found, each premise re-verified on the tree first. item 8 is judged kept rather than closed to match its siblings, because where the seal cannot be produced embargoed_paths keeps the tree entry claim and the tree entry instant, which an unsigned tree can spell 0, and the recorded defence is real, grant_sealed reading the same object through the same door before it seals anything; so the arm stays and both halves are asserted now, the row surviving with the number the entry chose and the grant that row plans refusing at the same address, under a control that the same call succeeds while the seal reads. item 9 is made to bite: a second path over the same Restricted seal is entered honestly, so the standing-lane arm is a comparison rather than an absence, and the comment names the mutation that reddens it. item 10 narrows a pin message that reached past what it asserted, onto the deposit-plan pins that do assert the fan-out. item 1 corrects the ADR 0023 cost paragraph, which named the spool leaf where the endpoint moved too, off an unescaped format string onto the shared escaping, so an endpoint holding a quote or a backslash had been emitting JSON a parser rejects; the matching commit correction is already a comment on #1971. item 3 weighs the text census and records a measurement instead of an assumption: it needs no visibility widening and spawns nothing, so no census can was the wrong shape of decline, but what a text census reads is a spelling and section 3 is a rule about rendered bytes, and measured, the comma-join needle finds its own home and sites that are not columns while the contract field is not spelled one way, so a needle on either spelling reads a subset and says nothing about a shape carrying no such field at all, which is the defect #1971 came here to fix. item 2 replaces a false inference in the land-change skill: those three headings belong to the cargo test gate, and every other gate arrives as a finding under prose of its own, the line-ending gate among them, so an unfamiliar heading is another gate before it is a new kind; workflow.md scopes the same sentence. item 11 takes the completeness claim off the deposit accessor doc, where privacy is what bounds the callers, and off the census block that asserted a key guard over a membership rule which does not check it. item 12 names the two readers still deciding off the entry, the mirror projection and the forge manifest fold, with what bounds each, and records that neither belongs to #2187. items 4 to 7 are the smells: the forwarding closure, the clone taken and then borrowed, the exemption pin that read the real machine config and now builds its report from values, and the reversed import. red under mutation, counts read each time: the unreadable-seal arm dropped (68 passed and 1 failed, the row reading empty), that arm handing on a substituted instant (68 passed and 1 failed, 9999 where 0 belongs), restricted_paths made to ask the seal (68 passed and 1 failed, secret.txt joining owned.txt in the standing lane), a contract field added to the report the sender posts (6 passed and 2 failed) and joined_col joining with a semicolon (loot-cli 1335 passed and 8 failed), each restored to 69 passed and 0 failed. no migration, no wire or format byte moves and no host behaviour moves, and no output byte moves either, every code edit here being a refactor or a test, so this owes no deploy. the workspace suite is green (4110 passed over 132 binaries, 8 ignored) (#2194) 609afd9f · dbf3dbe6…diff
  • the git bridge projection asks the seal for the tier it publishes on, so a restricted seal entered as internal stops reaching a pushed branch: #2196's premise was re-verified on the tree first and it holds, ferry's public_delta_tree chose from the change tree entry's visibility, a field sealed::seal's address does not cover and compute_change_id_raw discards before the finalize signature is taken over it, and the bound was never the mirror being local but the open, which refuses an unauthorized or embargoed seal, so a live embargo never rode a lying entry out while a restricted seal this position holds the key for did, as did an embargo whose reveal had passed. the cost was measured rather than assumed: workspace::readable_object_and_class, the door #1581 built, hands back the seal's tier and sealed::open's bytes from the one DagRepo::object the projection was already paying for, and the same projection moves object_gets by 3 before and after with every other counter identical, so the store-read objection that defers #2187's lane selection does not arise at this site; it also retires ferry's hand-rolled copy of the sealed-to-you rule. the entry is still read and can now only withhold, which is what keeps a pass over every unrepresented change from opening a store object per sealed path, and on a disagreement the path is dropped into the omitted-sealed-paths report rather than refusing, because project walks every change no mark stands for and a refusal there would stop every land and every review push over an entry the refusing operator did not write. both directions are pinned on one hand-recorded tree, under a control asserting each lying seal opens from this position so an omission cannot pass for the open's doing. red under mutation, counts read each time: the seal's tier ignored again (55 passed and 2 failed), the match tightened so nothing projects (37 passed and 20 failed), the entry's cheap withholding half dropped (56 passed and 1 failed, object_gets reading 4 where 3 belongs), the disagreement dropped off the report (55 passed and 2 failed) and the fixture's seal made unopenable (56 passed and 1 failed, the vacuity control firing), each restored to 57 passed and 0 failed. the census row moves with the decision and stops asserting which sites have moved, and ADR 0012 takes a ninth amendment recording the direction, the measurement and the git-side ingest demotion guard this does not reach. no migration, no wire or format byte moves and no host behaviour moves, but what a ferry projects moves on the client, so this rides the next release and owes no deploy. the workspace suite is green (4123 passed over 132 binaries, 8 ignored) (#2196) 4c0c6c9e · dbf3dbe6…diff
  • the git-side ingest asks the seal for the tier it guards on and for the one it reuses under, so an entry overclaiming Internal can no longer buy a silent demotion nor ride into the next tree as this position own recording: #2203 asked for every premise to be re-verified on the tree first and the body named one reader where there are two decisions a few lines apart over one field, the demotion refusal taking demotes(&old_entry.1, &vis) and the Act::Reuse decision comparing old_entry.1 with the derived tier and then carrying that entry oid AND its tier forward, beside a third use that decides nothing, parent_tree going over whole to ingest_change as the base every untouched path is carried on. the two decisions get two answers because they are two questions and proximity is not an argument, that being what produced the bound #2187 had to refute: the guard is about the tier a path is recorded at, a path in disagreement is recorded two ways, so both are weighed and the narrower stands, the seal supplying the half that was missing while the entry keeps the withholding half it has at public_delta_tree, because asking the seal alone would have deleted a refusal rather than repaired one — an entry narrower than its seal is exactly what withholds that path from the projection, so ingesting over it widens the recording that was doing the withholding. the reuse gets the object alone, its question being whether a fresh put_sealed would produce what is already on disk, which the entry is not a recording of, and a disagreement there needs no refusal because withholding a reuse only means doing the work; it refuses where the projection drops, because dropping a path here drops a break-glass commit edit out of the very change that stands for that commit. the comparison is == and not the deposit lanes discriminant (#2187), the holder list here being exactly who could open the object a reuse declines to re-seal. the cost is measured at this site rather than inherited from #2196 already-open projection object: the arm pays 2 object_gets for a path the parent tree records and 0 for one it does not, read as a difference over a real ingest_commit, and the entry spelling cost the same pair. red under mutation, counts read each time: the seal term dropped from the guard (62 passed and 1 failed), the entry term dropped (62 passed and 1 failed), the reuse comparison reading the entry (61 passed and 2 failed), Act::Reuse carrying the entry tier (62 passed and 1 failed), the tier taken by a second store read (62 passed and 1 failed, object_gets reading 4 where 3 belongs), the overclaiming fixture seal made unopenable (62 passed and 1 failed, the vacuity control firing) and the underclaiming fixture entry made honest (62 passed and 1 failed, the refusal correctly stopping), each restored to 63 passed and 0 failed. the site is a census row for the first time, the field having been reached through .1 which that census header names as outside its needle, so its silence was never evidence, and ADR 0012 takes a thirteenth amendment recording the two answers, the measurement and the carry this does not repair. no migration, no wire or format byte moves and no host behaviour moves, but which git commits ingest moves on the client, so this rides the next release and owes no deploy. the workspace suite is green (4137 passed over 132 binaries, 8 ignored) (#2203) 231f38e6 · dbf3dbe6…diff
  • adopt and ferry stop re-reading the whole shared graph once per absent tip, and the correctness question came before the perf one: ingest_shared_lineage is the one seam that re-reads that file mid-process (#265) because another session may have written it, so nothing may be memoised across a verb, and what lands is a LineagePool the verb owns rather than a cache the repo holds. a pool is one read of the file, handed to the tips of a single pass and dropped with it, and it is never trusted to say no: a tip it cannot name refills it from disk, which is the authoritative answer the per-tip loop used to take every time, so a change another session lands mid-pass is still found. WHAT IS GIVEN UP IS NAMED RATHER THAN LEFT TO BE FOUND: a node the pool holds and the file no longer does is spliced where a fresh read would have called it absent, which needs a prune inside the window between a pass reading the graph and that same pass reaching a tip. objects and custody catch up once per pool at its first arrival instead of once per tip, on the ordering the per-tip version already rested on, a node the pool names having been in the graph file before that scan ran and its objects written before its graph entry. measured in store_file_reads, a count that is bit-exact under load, and not in wall clock, because another session had the desktop. a ferry pass over trailered commits whose changes landed outside the lineage-filtered load read 42 store files over 2 such commits and 48 over 5 before, and reads 40 at both after; resolving an adopt prefix over the same two shapes read 6 and 12 before and reads 3 at both after, the prefix search having been a whole read of the same file on its own. red under mutation, counts read each time: ferry put back to a read per commit failed its pin (0 passed and 1 failed, 43 against 49), the adopt resolution put back to a read per tip failed its own (0 passed and 1 failed, 6 against 12), a pool believed on a miss failed the later-arrival pin (1 passed and 1 failed) and a pool that never catches up failed the every-tip-brings-its-bytes pin (1 passed and 1 failed), each restored. the hand-written workspace width in verbs/mod.rs moves to 389, which its own derived census demanded. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4162 passed over 132 binaries, 8 ignored) (#2224) 96a35669 · dbf3dbe6…diff
  • a deposit plan builds the tree it ships once instead of once per custody lane, and the first act had to be an instrument that could see the difference: no counter a land can read moved over a repeated whole-graph pass, so Work::GraphSorts now tallies ChangeGraph::in_order and is gated at 0% beside the object pair and store_file_reads, reading 8 on the gated fixture and one string across the drift pin repetitions. THE TICKET ASKED FOR THE TALLY TO REUSE Work::TreeWalks INSIDE finalized_tree and for ADR 0073 to re-decide that exclusion, and that was refused rather than followed: tree_walks counts a whole-tree FILESYSTEM walk of a working tree and this is a graph pass, and the in-process tier links loot-core and never loot-cli while nothing in loot-core outside its own tests calls the deposit lanes, so a tally there would have read zero on that tier anyway and the pin would have stayed green while its stated reason went false. the exclusion therefore stands unmoved on its own measurement, and the ADR records the trigger that did not fire rather than a re-decision it did not force. measured in the counter and not in wall clock: a forge plan over a fixture carrying an embargoed path, a Restricted path and the Internal default read 3 sorts before and 1 after, a relay plan 2 before and 1 after, the three lanes now taking the finalized tree as an argument plan_deposits builds once. the tips membership test inside that build became a set lookup rather than a Vec scan, which no counter can see and which is named as such rather than claimed. red under mutation, counts read each time: the shared build removed so each lane derives its own again failed the new pin (0 passed and 1 failed, graph_sorts 3 against 1, and with the forge arm relaxed the relay arm failed at 2 against 1), the tally dropped from in_order failed the anti-vacuity pin (10 passed and 1 failed) and the new pin (0 passed and 1 failed), and graph_sorts dropped from gate::COUNTERS failed four at once (7 passed and 4 failed), each restored to 11 and 1 passed. the two generated membership sentences in CONTEXT.md and HUNT-PERF.md are pasted by hand as their pins demanded, the hand-written workspace width in verbs/mod.rs moves to 390, the visibility census gains the two argument bindings the by-reference lanes create, and a count in loot-count that was wrong in the commit that wrote it is replaced by the property. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy, though the gate records a new metric from the next land. the workspace suite is green (4174 passed over 132 binaries, 8 ignored) (#2225) fe6a089f · dbf3dbe6…diff
  • the sentence written to replace a deleted list names the definer instead of counting, and the fix-up is read back under the rule it enforces: ADR 0073 said the gated work counters were the object pair and a further one at each amendment since, which is false because the #1903 amendment added none, so the clause now names measure::WORK_COUNTERS and stops. every other count beside a set that can grow is replaced by what decides membership rather than by a corrected number - the first nine and the six artifact counters and the other three Work variants in gate::COUNTERS become every row WORK_COUNTERS does not name, the Tally Display doc shows one label=value per Work::ALL entry and an ellipsis instead of hand-listing the labels, the deposit lane counts point at kind.widens, and the landed proposal state points at store::ProposalState. THE SWEEP MISSED THREE COPIES OF ITS OWN ITEMS and they are corrected here too, because a corrected claim left standing in a second copy is how #1903 shipped one stale: the three lanes sentence in loot-core custody and again in its test, and the ADR 0075 three terminal states sentence duplicated in a forge server test. two code fixes ride along: #2174 landed the wire_state doc inside the propose withdraw doc comment, so the may_propose rationale documented the wrong function and the route documented nothing, and the stale-tip refusal was broken across source lines with no continuation, so an operator read the indentation of the source in the middle of the message. ONE ITEM IS CORRECTED RATHER THAN FOLLOWED: the ticket reads that literal as carrying a newline, and on the tree it is a single line carrying two runs of collapsed indentation, so the rendering defect is real and its shape is runs of spaces rather than a break. red under mutation, counts read each time: the continuations removed so the break rides in the string again failed the strengthened pin (0 passed and 1 failed, the panic printing the refusal across three lines), restored to 1 passed. the CONTEXT rewrap is a pure reflow, identical byte count and identical word stream, so the generated membership sentence and its pin are untouched. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4174 passed, 0 failed, 8 ignored) (#2234) f7721acb · dbf3dbe6…diff
  • the visibility gate goes on reading the whole object and the ticket is refused on a ceiling, because a binary with the read deleted outright still reads one loose object file per recorded path: can_open consults facts that sit beside the ciphertext rather than inside it, so reading less looked free, and the measurement says the read does not belong to this seam at all. a knowingly wrong body answering from the store index and the keyring alone, reading no object bytes, is strictly less work than any header only spelling could do, and over 200 paths of incompressible bytes at 1 KB and 16 KB and 128 KB it moves loot status from 14.13 and 20.57 and 67.91 ms to 14.21 and 20.84 and 66.73 ms and loot surface from 16.64 and 23.11 and 59.09 ms to 16.58 and 23.18 and 56.16 ms, a spread that runs in both directions and peaks at 5 percent on the largest fixture. THE COUNTER IS WHAT MAKES THAT A REFUTATION RATHER THAN A QUIET READING: object_disk_reads reads 200 on every one of those twelve readings, before and after alike, while object_gets falls from 401 to 201 on status and from 600 to 400 on surface, so the span really was deleted and the file reads moved rather than went away. they move to the pass that wants the bytes - same_content behind status and the clobber guard behind surface - which the read memo makes free today and which would pay for the whole tree if this call stopped filling it, and a header only body would add its own opens on top of that. the signal the ticket opened with is therefore not added either: the read counter that exists already settles the question, and a byte counter would have read the same total under the ceiling and a larger one under any header only body. so the code is UNCHANGED and what lands is the reading, recorded on can_open where the next hunt reads it before refiling. one list class defect is corrected beside it: the sentence naming two further sites that could move to this predicate named a merge site that cannot, since the readability question there sits on the theirs side of a cherry pick delta restriction and its own comment says the content must be read to re-seal it, and the sentence now states what decides membership instead of naming members. no mutation proof, because nothing was fixed and no pin was added, and the measurement controls stand in its place: the get counter moves, so the instrument is not blind to the span the ceiling deletes, and restoring the pristine body returned both counters and both timings to the landing band. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4174 passed over 132 binaries, 8 ignored) (#2223) 4ab1afee · 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
  • shared_version_ids is deleted from DagRepo and Workspace, since LineagePool::ids answers the same whole-store id set out of the pool the adopt resolver already holds and the door had no caller left outside a test; the #829 reason it was added now sits on LineagePool::ids, and the adopt test that used it as a precondition asks the pool instead. the two #2224 counter pins stay two tests, and the adopt pin now says why at the code: what they share is the relation and its non-vacuity check, while what gives each its meaning is a fixture building a different position, which is where #2247 differs, its guards being one question put to one ingest timed and counted. the verb tier module doc now states 390 methods, the width its derived pin reads after the deletion. riding along, the can_open block no longer calls the discarded 1,091.9 ms reading neighbourless in the sentence that says its neighbours did not support it. red under mutation, counts read: the pool precondition inverted (0 passed and 1 failed), restored to 1 passed. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4254 passed, 10 ignored) (#2236) 160e9a33 · dbf3dbe6…diff
  • loot blame and loot log --path read the one path they ask of each change through the new GraphView::address_at, which answers off the manifest frames through the new Manifest::address_of where the frames provably are the map (no backslash key, keys strictly ascending in Path order, checked by the keys_ascend step Manifest::addresses uses) and from the decoded map otherwise, so a history walk no longer decodes every manifest on the lineage to read one entry of each, and blame no longer clones each tree; has_path keeps decoding, since its remaining production caller asks many paths of each of a few changes. the new --path-history opt-in half times address_at over every change of a re-opened chain at 200 paths, asking the path that orders last; paired, one loot-perf-gate --features count built per arm with the frame arm never taken as the before, interleaved, the gate reading load idle at 1 to 4 percent (peak 7): 34.87 to 34.97 ms down to 4.46 to 4.53 ms at depth 256, 138.8 to 139.3 down to 18.01 to 18.03 at 1024, and 279.2 to 280.6 down to 35.9 to 36.2 at 2048; counted by the new path_history_allocations test at width 200, the walk allocates 484,352 times at depth 1024 against 0. through the binary, against the landing position binary over a repo 1,025 changes deep and 203 paths wide, min of 15 over two rounds: loot blame 160.4 to 160.6 ms down to 33.7 to 33.8, loot log --path 164.8 to 165.8 down to 43.7 to 44.0, plain loot log 28.9 to 29.2 on both, and every output compared was byte identical with its exit code and stderr: blame plain, --porcelain, --json, with a selector and on an absent path, log --path on flat, nested, dotted and absent paths, and shortlog --path. new pins: over every golden, a file whose Path order is not its byte order, the legacy separator file and the three #2273 shapes, address_of answers what the eager map answers for every key and for probes that are not keys, decoding nothing exactly where the frames are the map, and it answers off the frames past a holder name that is not UTF-8; a spawned blame and log --path find a/b.txt beside a-b.txt and a.txt. red under mutation, counts read each time: the frame arm never taken (0 passed and 1 failed in the codec pin, 1 and 1 in path_history_allocations), the ascent check dropped (0 and 1, on the answer), the counts check dropped (0 and 1, on the control, since Windows reads a backslash as a separator), byte order in the stop (0 and 1 in the codec pin, 0 and 1 in the spawned pin), each restored to green. riding along: HUNT-PERF names the new half. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4289 passed over 137 binaries, 12 ignored) (#2283) 045c7e42 · dbf3dbe6…diff
  • loot revert --continue and loot cherry-pick --continue now replay the stopped pick instead of closing the stop, on the answers loot resolve gave to that stop and on nothing else: the in-progress record gains the version the verb applies, the conflicts its stop recorded and the answers, resolve writes an answer there only when the conflict it settles is the one the stop recorded at that path, and change_delta_merge takes only the answers its caller hands in, none from an ordinary run, so the repo settled ledger is still never read by a pick (#744). a path whose content moved after it was answered is asked again rather than merged, a path holding the answer bytes at another address keeps it, a replay that stops again keeps the record restore point, and a record written before this closes as it always did. ADR 0080 gains the #1986 amendment deciding the three questions and recording, measured through the binary, that move, squash and apply-patch stop again after a resolve for reasons that are not a ledger and that apply and merge have nothing to replay; CONTEXT.md, the resume module and the usage note say the same. the resume runs to completion through the binary on the home position, on a pinned primary and in a lane. red under mutation, counts read each time: the repo ledger handed to the resume (0 passed, 1 failed in the stale-entry pin), the repo ledger handed to the three-way (0 and 1 in the loot-core pin, 0 and 1 in the re-run pin), a moved path left to the three-way (0 and 1), the address test alone (0 and 1 in loot-core, 0 and 1 through the binary), an answer keyed on the path alone (0 and 1), the record sentence worded by verb (0 and 1), the replay branch emptied (0 and 4), the re-stop taking a new restore point (0 and 1) and the usage note dropping revert (0 and 1), each restored to green. the record gains a trailing section, no format byte moves and there is no migration, so this owes no deploy. the workspace suite is green (4302 passed over 138 test binaries, 12 ignored) (#1986) 8bd8e53c · dbf3dbe6…diff
  • the review-sweep fix-up over #2287, #2283 and #1986. a resumed pick now asks again about a path its stop asked about when the record holds no answer for it, rather than handing it to the three-way, which merged over the resolution and put back the edit it rejected: change_delta_merge takes the stopped record whole and takes every question on it out of the three-way, keeping an answer only where the line still holds it, so a resolve that recorded no answer (a loot from before #1986, or a failed answer write, which the workspace comment now truthfully says costs a re-ask) re-stops with nothing applied, keeps the restore point, and lands once the question is answered again; refusing --continue was weighed and rejected in the ADR 0080 #2291 amendment, since nothing would be left to resolve. the replay reads its direction from pick::SPECS, which now pairs each spec with it, and a record naming a version under a verb SPECS does not hold refuses instead of replaying as a cherry-pick. the usage note names no verb, so each stopping verb line is true of its own verb, and its test now fails if the note names a verb that replays. a cherry-pick replay to completion is pinned through the binary on the home position, a pinned primary and a lane. false sentences fixed: the in-progress operand reader, the pick stop comment, the resume test doc link, the continue_clause list, the CONTEXT.md in-progress entry and HUNT-PERF on graph file reads; rewrap leftovers in fixture.rs and engine.rs are fixed, and the replaying tests keep their resolution file outside the working tree. red under mutation, counts read each time: questions taken from the answers alone (16 passed and 1 failed in the loot-core pin, 31 and 2 through the binary), an unknown verb replayed as not inverted (20 and 1), the note naming cherry-pick and revert again (19 and 2, the second the census the usage lines no longer matched), each restored to green. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4309 passed over 138 binaries, 12 ignored) (#2291) 812c48c7 · dbf3dbe6…diff
  • loot doctor names the deferred absence and every ordinary open refuses a store in it, the first stage of the body-deferring filter: a store whose bodies are fetched on demand now carries .loot/deferred-bodies (the remote and what asked, read fail-closed so a record that will not parse still counts), the seek cache position writes it whenever it opens a position lacking it, doctor reports it as an info bodies line, and Workspace::assemble refuses such a store naming the state for every open except the new Workspace::open_fetching_bodies_at, which the cache opens through, so a verb that would read an unfetched body as absent or sealed is refused rather than answered; a body fetch that fails in the cache now refuses naming the state and the host. ADR 0093 records the state, the refusal at the open and the offline policy (a refusal, never an absence and never a stale answer, reachability never recorded), ADR 0090 gains a #2043 amendment and CONTEXT.md gains the deferred kind under Absence. what #2043 still owes is the filter on a working position, whose flag name is left open, and verbs that fault a body in instead of refusing. red under mutation, counts read each time: the open refusal dropped (0 passed and 2 failed in the seek pins, 0 and 1 through the binary), the record read fail-open (0 and 1), doctor never naming the state (0 and 2), the fetch refusal unwrapped (0 and 1), the record written only when made (0 and 1), each restored to green. a new plain-text store file, no format byte moves and no migration, so this owes no deploy. the workspace suite is green (4313 passed over 138 binaries, 12 ignored) (#2043) ee57a2ef · dbf3dbe6…diff
  • the review-sweep fix-up over #2291, #2043 and #2080. loot verify no longer walks past the deferred-bodies refusal: it finds its store by layout through resolve_store_dot and never met the record, so in a store carrying .loot/deferred-bodies it reported a body it had never fetched as missing, and verify --accept-loss wrote that body into .loot/lost as unrecoverable while the remote still held it. the refusal is now decided once, in BodyNeed::admit, which the open (Workspace::assemble) and the layout door both call, so the doors it guards are that function callers: resolve_store_dot takes a BodyNeed, verify declares Held and is refused naming the state, and doctor and config declare the new Unread, since they read no body. the relay doors take none, and loot_net::is_relay records why: each refuses a store without the relay role marker before reading an object, no code that writes the record makes a relay, and gc there removes only objects the store holds; gc --dir, count-objects --dir, relay reap-grants, relay abandon-head and serve --dir were each driven through the binary at a deferred store and each refused it. the refusal now says the verb did not declare it fetches first rather than that it reads a body, since whoami, status and log read none, and doctor, ADR 0093 section 3 and CONTEXT.md say the same; section 3 is restated as the property and records the #2295 correction. false sentences fixed: the counts beside GATES in spawn.rs and orchestrator.rs now point at the rows, the sdk gate doc and SDK_GATE_STEPS say build:wasm can reach the network and refuses rather than degrading there, the in_progress verb and operand docs state the property instead of listing verbs, HUNT-PERF stops counting the reads around the graph load, and the rewrap leftovers in the land-change skill and ADR 0071 are fixed; resolve_store_dot also gets back its own doc, which sat on registered_peer_keys. red first through the binary: verify answered 1 missing (0 passed, 1 failed). red under mutation, counts read each time: verify declaring Unread (1 passed and 1 failed), verify refused but --accept-loss let through, which accepted 1 missing object as lost (1 and 1), the refusal in admit keyed on the wrong need (0 and 2), each restored to green. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4323 passed over 137 binaries, 12 ignored) (#2295) ded7dfa3 · dbf3dbe6…diff
  • the review-sweep fix-up over #2295, #2096 and #2137. the discovering layout door takes its caller body need: workspace::ambient_store_dot, which fixed BodyNeed::Unread and so would have admitted a body-reading verb that found its store through it to a body-deferred store, is now discovered_store_dot(start, bodies), passing the need through to resolve_store_dot, and loot doctor declares Unread at its call. ambient_local_config keeps a fixed Unread, since it hands back the config path and lane id rather than the store, and its doc says so. pinned in workspace.rs: a Held caller is refused naming the state from the root and from a subdirectory, an Unread caller still finds the store, and the same repo before the record is the control; red first with the need ignored (0 passed and 1 failed), red under mutation with the door fixed at Held (0 and 1), restored to green. the refusal text, the doctor bodies line and the ADR 0093 section 3 heading now say a verb that declares neither that it fetches first nor that it reads none is refused, since Unread passes. set sentences replaced by the property in workspace.rs, CONTEXT.md and ADR 0093: a door that finds a store without reaching BodyNeed::admit is unguarded and whether that is safe is a question for that door, is_relay is recorded as why a relay store needs no guard, and a verb behind an admit door meets the refusal unless a need other than Held is named there. the site marker pin: the hash now covers every module the page reaches by a relative import, so RETENTION_DAYS on /privacy moves it, and the privacy pin is re-pinned with its date unchanged; the scan reads markers with comments stripped, so a comment saying last updated no longer makes a file unreadable; the header defines a dated marker as the words last updated and says why the known-issues Last reviewed date is left to its own pins. red first against the old test, counts read: RETENTION_DAYS moved to 31 stayed green (9 passed), a terms comment saying last updated went red (2 failed and 6 passed); with the change the first is red (1 failed and 10 passed) and the second green (11 passed). red under mutation: the import reach dropped from the pin (1 failed and 10 passed), comment stripping dropped from the marker read (1 and 10), the import walk reading nothing (2 and 9), each restored to green. stale sentences fixed: HUNT-PERF names every other read the open makes rather than the reads in the body of assemble, the in_progress verb is the verb that wrote the record rather than a STOPPING member, the SDK read test no longer calls its shape the one where the head derivation is load-bearing, and repo.ts and the ChangeView doc name the superseded-head half of within_depth_of_heads and say its working-change branch is not mirrored. no migration, no format byte and no published wording moves, so this owes no deploy. the site gate is green (680 passed and 62 skipped over 62 files, 62 surfaces) with no ceiling moved, the sdk build:wasm and type-check are green, and the workspace suite is green (4324 passed over 137 binaries, 12 ignored) (#2299) d7d5ef94 · dbf3dbe6…diff
  • the review-sweep fix-up over #2299, #2142 and #2154. a seek cache round now claims the busy marker before it decides anything: LootCache::claim_and_make creates loot-seek-refreshing with create_new before it asks whether the position stands or makes one, so a position another invocation is making carries a marker from before its store exists, and a round that finds a live one neither makes the position over it (refused, naming the state, when no store stands yet) nor removes it when its own refresh fails; a marker past the hour is taken over. before, the marker was written after the make, and a second round inside that window read the half-made store as a leftover and removed it. a first refresh now means no record file at all (Refresh::absent): the record is written through atomic_write rather than truncated in place and read through read_replaced, so an empty or unreadable record is a refresh that finished and a failed refresh leaves that position standing. pinned in seek.rs by a_make_in_progress_is_not_removed_by_another_invocation, which drives a make through claim_and_make and fails a second round inside it, and by a_record_that_does_not_read_is_not_a_first_refresh; red first against the unchanged rule (1 passed and 2 failed, the make extracted into claim_and_make with the marker still after it). red under mutation, counts read each time: the claim moved after the make (1 passed and 3 failed), begin in place of claim (1 and 3), a round that finds a live marker deciding first on the record (2 and 2), absent read as unreadable (3 and 1), the record written with fs::write (3 and 1), no refusal over a claimed unmade position (3 and 1), a stale marker read as live (3 and 1), each restored to green. the seek unit tests now default to a cache home under the temp directory, pinned by the_unit_tests_cache_home_is_never_the_runners (red with the default dropped, 4 passed and 1 failed). stale sentences fixed: the ADR 0090 #2142 amendment is corrected, the CONTEXT.md seek entry names the marker exception and the gc exit, ADR 0023 scopes the refusal-only exit to a question, root_notice and discovered_store_dot say why the function is free rather than counting callers, the in_progress verb is the verb whose stop created the record and another verb is refused, the last-updated header states its import reach by reachedModules, the seek module doc states the exit property, and a CONTEXT.md rewrap leftover is rewrapped. through the lane binary under a scratch LOOT_SEEK_CACHE, a position with an empty record stands after a failed refresh and a marker-only position is refused with its marker left. no migration, no format byte and no published wording moves, so this owes no deploy. the site gate is green (680 passed and 62 skipped over 62 files, 62 surfaces) and the workspace suite is green (4328 passed over 137 binaries, 12 ignored) (#2303) 4c489c97 · dbf3dbe6…diff
  • a push now carries an attestation recorded over a change the remote already holds, so loot tag after loot push reaches a relay and a forge instead of being left behind under a success line: a local attestation ledger (.loot/attestation-ledger) records per remote what each push delivered and is read by a push and by no open or save, written by RepoStore::record_attestations_sent as a read-merge-write under the shared-store lock; the push sends the attestations over the held changes of the remote that the ledger has not recorded beside the send set and prints how many, the bundle builder keeps a late attestation only over a change inside the have closure of the recipient whoever handed it in, the forge /ingest keeps one over any change its repo holds through a new changes_held store read that costs no query when every attestation rides its change, and /info gains an additive late_attestations field so a forge that does not advertise it is sent none, has nothing recorded as sent, and the push warns how many it left behind. the land gate store_file_reads is 24 on its workload with no move, where a first cut that read the ledger on every open measured 26 and was refused; an open reads 20 store files, 21 with that cut. the #48 bound holds on the wire: a push carrying one late tag sent 256 B at a relay and 306 B at a forge over both 2 and 24 held tags. the ticket recipe, whose fresh clone lacked late-tag2 through the 0.4.24 binary, shows it through a lane build. red under mutation, counts read each time, each restored green: the open reading the ledger again (0 passed and 1 failed), the ledger write overwriting instead of merging (0 and 1), the ledger ignored (0 and 2), the late lane dropped (0 and 2), the privacy filter removed (1 and 1), the forge back to in-this-bundle (2 and 1, and end to end 1 and 1), the forge keeping any change (2 and 1), a relay push recording nothing (0 and 2), a push recording to a forge that does not keep them (1 and 1), the /info flag ignored (1 and 1). no format constant, codec byte or migration moves; the forge change is live once the forge is redeployed. the pull half is not built: a pull still carries an attestation only with a change it sends. the workspace suite is green under bash ci/local.sh against Postgres 18 (4425 passed over 139 binaries, 13 ignored; site live suites 7 files passed) (#2251) 414ba6b2 · dbf3dbe6…diff
  • the attestation ledger records what a remote is known to keep rather than what a push put on the wire, the review-sweep fix-up over #2251: a push records rows only for a host whose /info answered, as a relay or as a forge advertising late_attestations, and a host whose /info did not answer is still sent them but has nothing recorded; against a forge without the flag it records what rode with its change, so its left-behind warning counts the late ones and no longer grows; the ledger keeps per forge the tip generation the last recorded push committed at, and a push that reads a lower one sends every attestation over the changes that forge holds again and says it may have been restored, with deleting .loot/attestation-ledger documented as the recovery for a relay and for a restore hidden by later pushes; a ledger that will not read or write warns and never fails a push or skips its deposits, and a record over an unreadable one starts it afresh. the forge misread as a relay the sweep named fails at its unsigned /wants before sending, pinned; the rule stands without it. red first with each fix undone, counts read each time, each restored green: an unanswered /info read as keeping them (0 passed and 2 failed), a forge without the flag recording nothing (0 and 1, left behind 2 where 1), a restored forge unnoticed (0 and 1), an unreadable ledger fatal (0 and 1), an unwritable ledger fatal (0 and 1), the first-seal summary defaulting an unknown change to no rows (0 and 1). also stated: the privacy filter rests on the declared have, a forge holding a head without its ancestry drops a late attestation it is recorded as keeping, a ledger is one clone, late_attestations states the property rather than naming verbs, RemoteSync bound records nothing and its docs are current, the first-seal summary refuses an unknown change and names its tree clone, and the Route doc states where hosting is decided. the ledger stays push-only: store_file_reads is 24 on the gate workload with no move. no format constant, codec byte or migration moves. the workspace suite is green under bash ci/local.sh against Postgres 18 (4430 passed over 139 binaries, 13 ignored; site live suites 7 files passed) (#2355) 9511111e · dbf3dbe6…diff
  • the review-sweep fix-up over #2355 and #2258: the forge restore note says what the push does for the host it is talking to, so a forge without late_attestations, the live forge until redeployed, is told none is sent and that its left-behind count includes attestations it may still hold, where it was told they are sent again; and the restore mark is lowered only by a push that sends what it owes to a forge known to keep it, so a push that cannot deliver leaves the restore visible to the next one that can. the unknown-host rule of #2355 is pinned through push itself, against a front that refuses /info before a real relay. red with each fix undone, counts read each time, each restored green: the relay arm recording what it carried (2 passed and 1 failed), the note saying sent again to a forge without the flag (2 and 1), a push that does not deliver lowering the mark (2 and 1). record_attestations_sent is renamed record_attestations_kept for what it records. stated rather than overstated: the #2258 scratch naming reproduces the recorded message and whether it caused that sighting is not established, and its pin is red only where the clock is coarse; CONTEXT.md names the restore limit as pushes bringing the generation back up to the mark; RemoteSync bound states the property of its callers and that it records no row but a forge push still records the mark; ADR 0018 counts no bullets; a comment in store.rs is indented to its block. no format constant, codec byte or migration moves, and the ledger stays push-only: store_file_reads is 24 on the gate workload with no move. the workspace suite is green under bash ci/local.sh against Postgres 18 (4434 passed over 139 binaries, 13 ignored; site live suites 7 files passed) (#2360) 3248da71 · dbf3dbe6…diff
  • a plain loot ferry in a lane refuses to project anything no land has carried, where it projected the lane unlanded line onto the mirror main every position shares so the next land from any of them pushed it unreviewed: the pass asks what it would project before the ingest and again at the gate seam after the reconcile, since --seal-wip can seal into the line, and loot-first land marks its own pass with RunOpts landing; a lane catch-up that projects nothing runs as before, and the refusal names the land of the PR that carries the lane tip or else the fresh-lane route of #962. the review reap says landed only from a land pass, on main for a projected version otherwise, and signed, no land has carried it for an unprojected one, where it said landed for any signed version. the seal-wip recovery recipe is chosen by position: the primary keeps its follow-up round and a lane gets the fresh-lane route, one const shared with the #2314 refusal. reproduced through the 0.4.24 binaries in a scratch repo with its own lanes (the advised ferry projected 2 lane changes and printed landed) and checked through the lane-built binary (refused, mirror main unmoved, lane catch-up still runs, reap and recipe reworded). the lane-simulated lands in the ferry tests now run as land passes, and 2 primary reap pins read on main. pinned by six new tests; red with each piece undone, counts read each time, each restored green over the ferry tests: the guard off (69 passed and 3 failed), a land pass refused too (61 and 11), every lane pass refused (70 and 2), the seam ask dropped (71 and 1), the PR route dropped (71 and 1), the old reap word (69 and 3), the recipe always the primary one (71 and 1) or always the lane one (71 and 1). ADR 0039 amended, CONTEXT.md, concurrent.md and the land-change skill updated. no format constant, codec byte or migration moves. the workspace suite is green (4442 passed over 139 binaries, 13 ignored) (#2366) dac3385d · dbf3dbe6…diff
  • review sweep 12 over the lane projection guard: items 1 and 2 did not reproduce through the binaries in a scratch repo with its own lanes, since a position graph is its own lineage-filtered load (ADR 0022), so after a lane signed K a primary plain ferry projected nothing and minted no mark for K and the lane ferry still refused, and a lane with nothing of its own caught up while a sibling seal sat unmarked; the guard question stands and both scenarios are pinned. a lane ferry --seal-wip over described work refuses before the reconcile, where it sealed into the catch-up and then refused, leaving a sealed line no PR carries (reproduced on the landed binary). the reap never says landed, since the push that follows a land pass can still be refused, and says projected for a marked version, which a mark is and on main is not. the ferry verb landing false is pinned through the spawned binary. a refused pass carries its notes. the primary recovery recipe names --allow-primary for the verbs that need it there. the #2314 and #2366 lane refusals print one route rule, lane_landing_route, which sends a lane to its PR land only when the PR carries the tip with nothing captured on top. ADR 0039 amendment, ADR 0092 (--skip-gates), CONTEXT.md and concurrent.md corrected. red with each piece undone, counts read each time, each restored green: the verb flag flipped (0 passed and 1 failed), the seal-wip refusal dropped (75 and 1), the reap saying landed (73 and 3), the route ignoring captured work (75 and 1), the route ignoring the PR (75 and 1), notes dropped (75 and 1), the lane recipe on the primary (75 and 1), the guard off (71 and 5), every lane pass refused (73 and 3). the scripts MAINTAIN ask is its own scripts commit. no format constant, codec byte or migration moves. the workspace suite is green (4450 passed over 140 binaries, 13 ignored) (#2370) 18ea6db5 · dbf3dbe6…diff
  • review sweep 13 over the proposal conversation: loot propose --show verifies a comment against the repo and change id of the proposal it reads, since one change proposed to two repos shares its tip and a forge could serve one repo comment as said in another, and cages the body, the path and a refusal quoting the envelope through render sanitize, so ESC or a bare CR from a stranger prints as its control picture. an author is an account: a live key of the account whose key wrote a comment edits and withdraws it after a rotation, the key that signs an edit becomes its author so readers still verify it, and the author withdraws without the visibility check, so a lost view does not strand their words. the door arm calls may_propose_in and a party is asked live_account_in, its second half, where a copy stood. the comment routes read at most COMMENT_REQUEST_MAX, refusing a larger request before its signature is checked. /propose/read serves a thread a page of THREAD_PAGE comments at a time with a cursor, a trailer on both payloads, and --show follows it. an anchor must be spelled as a tree spells a path and is asked of tree_entry by its key through manifest_holds; Manifest::address_of reads manifest bytes the forge does not hold. a runner key under no account is refused moderation, and ADR 0095 now says any live key on the owner account moderates. finalized_before_review sends a lane to lane_landing_route rather than a land with no PR to name. ADR 0095 records the account rule, the paged read, the unbuilt withdrawal warning of its section 6 and the rewritten #2332 pin; ferry.rs says the #2370 pins run in-process; a pin holds the 0022 CHECK equal to COMMENT_BODY_MAX. red with each piece undone, counts read each time, each restored green: the envelope not bound to its proposal (0 passed and 1 failed in loot-net and in the CLI), the body or the path printed uncaged (0 and 1 each), authorship by key, and an edit keeping the old key as author (0 and 1 each), the author asked to see before withdrawing (0 and 1), the door arm a copy (0 and 1), no per-route limit (0 and 1), the forge thread unbounded and the CLI stopping at one page (0 and 1 each), anchors matched by Path over the whole tree (0 and 1), a runner key moderating (0 and 1), the lane land its own (0 and 1), the cap moved on one side (0 and 1). bash ci/local.sh is green against Postgres 18 (4498 passed over 139 binaries, 13 ignored). no format constant or migration moves; the proposal read gains trailers and the forge behaviour changes, so this owes a forge deploy (#2374) bb95f4fb · dbf3dbe6…diff
  • loot show, the deletion rows of loot status and a surface listing name the published tier off the marker the object carries rather than off the .lootattributes on disk, so a published object whose rule was deleted no longer prints internal and a plain one given a rule since no longer prints published: the delta seam reads each row marker in Workspace::rows_of, off the side that carries the row, and keeps the rule for a live side (ADR 0079 section 1) and for a seal it cannot read, while the clobber guards take ResolvedDelta::classes and pay no read; surface, rehome and the pull auto-surface list through the new Workspace::with_sealed_publish_status. the status machine listing is the working tree on disk and keeps the rule. measured over a 400-path tree with the counters, every added read is a memo hit: object_gets moves by one per row with a recorded side (show of a half-edited change 1602 to 1802, status with a third deleted 804 to 938, surface --json 802 to 1203) and object_disk_reads is flat on each (601, 401, 401), a clean status is flat on both, and a hyperfine A/B over 2000 paths reads within noise (show 98.2 vs 97.3 ms, status 93.1 vs 92.6, surface --porcelain 165.4 vs 168.3, status with deletions 130.8 vs 129.8, load 0 to 6 percent). surface_object_reads counts the listing marker read apart from the opens, and the restore pin reads the seals its capture wrote. both directions pinned red first (0 passed and 2 failed with the fix undone), each through the verb: show and status deletion rows, and surface over trees ferried from git commits that delete and add a published rule. red with each piece undone, counts read each time, each restored green: the delta seam on the rule (1 passed and 1 failed), marker or rule, the overstating repair (1 and 1), the new side asked for a deletion (1 and 1), the listing on the rule (1 and 1), the listing as marker or rule (1 and 1), the guard paying the reads (0 and 2 in surface_object_reads). the published values in diff and surface machine output change for such rows and no key or column moves (ADR 0023). no format, wire or migration byte moves, so this owes no deploy. the workspace suite is green (4527 passed over 140 binaries, 13 ignored) (#2352) 0c27bd77 · dbf3dbe6…diff
  • review sweep 15 over the proposal conversation and the published tier: acting on one proposal decides its parties by account as seeing it does, so the withdrawal and decline gates ask propose::sees, the withdrawal author check and the re-proposal parties in admit ask speaks_for, and a rotated tip author whose original key is retired withdraws their proposal with the account live key on a metadata-private repo, where by key it was told the proposal did not exist, and on a public one, where it was told it did not author the tip; a rotated proposer offers a declined proposal again. the #2378 rotation pin now retires both original keys on a Rotated fixture that files the offer before the rotation. the comment-page EXPLAIN pin asks for Sort anywhere in the plan, where its line-start form could never see a nested Sort node. an unread seal answers not published in rows_of and with_sealed_publish_status, the direction that never overstates, where it fell back to the rule; show renders an unread object by address as sealed, so the listing carries the pin. propose.rs, ADR 0095 and CONTEXT.md state which gates are by account as a property, a read of or act on one proposal, and cite #2006 for list, CONTEXT.md scopes the #2352 tier sentence and points at #2381, and append_page refuses a comment repeated within one page. red with each piece undone, counts read each time, each restored green: the withdrawal seeing by key, the withdrawal author by key, admit parties by key (0 passed and 1 failed each), a retired key speaking for nobody (0 and 1 on the rotation pin), a misshapen page index on a throwaway Postgres 18, comment_id before created_at and created_at descending (0 and 1 each, 1 passed restored), the listing falling back to the rule (0 and 1), a repeat within one page taken (0 and 1); the rows_of fallback mutation stays green, since no verb reaches it. bash ci/local.sh is green against Postgres 18 (4530 passed over 139 binaries, 13 ignored). no format constant, codec byte or migration moves; the forge rules change, so this owes a forge deploy (#2382) eceef7eb · dbf3dbe6…diff
  • loot apply-patch names each applied path by the tier its seal records, and loot new --no-snapshot lists the paths the signed change first seals: the apply-patch report printed the .lootattributes resolution, a Visibility, which cannot spell published, so a path this repo publishes read internal while loot show said published, and it now reads each row through Workspace::tiers_as_sealed, the #2219 read generalized from first_seals_as_sealed to any paths of a recorded change, rendered through render::tier_label. the first-seal summary took its set of paths from the disk walk of seal_gate, so under --no-snapshot a .lootattributes the earlier capture first sealed and the disk had since lost was left off; finalize_capturing_over now returns the paths the signed change holds and the anchor the gates judged does not (Workspace::first_seals_of), paths alone, since the tier is the seal. both pinned red first (0 passed and 1 failed each): published, internal and restricted rows through the apply-patch verb, and a --no-snapshot summary listing the lost rules file and neither a path the disk gained since nor one the anchor holds. red with each piece undone, counts read each time, each restored green: the marker ignored, published claimed for every row, the rule resolution printed (0 and 1 each on the apply-patch pin), the set taken from the disk gate, the anchor filter dropped, the anchor read after the finalize (0 and 1 each on the summary pin). reproduced through the 0.4.24 binary before and a lane build after. CONTEXT.md and the Workspace width move with it. no format, wire or migration byte moves, so this owes no deploy. the workspace suite is green (4532 passed over 139 binaries, 13 ignored) (#2353) 22faa0d6 · 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.