Changes touching this path

  • 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…
  • 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
  • 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
  • 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
  • Give a lane a session owner, so gc and rm refuse someone else's (#532) 47af2987 · dbf3dbe6…diff
  • GitMirror: the bridge owns its own layout (#665) b0461c4c · dbf3dbe6…diff
  • a lane carries its own hold, and custody crosses back whole at finalize (#811) 941cafb9 · dbf3dbe6…diff
  • the change graph gets one read view, and the two forwarding bands over it retire (#868) 826c3339 · dbf3dbe6…diff
  • not a lane is not the same as shared: the finalize notice stops telling clone users their private tree is crowded (#967) The #436 warning fired on `lane_id().is_none()`, which is true of the dev primary AND of every clone (ADR 0026) — and then asserted that other sessions share the tree and pointed at `loot lane new`. In a clone every clause is false: separate repo, own store, own keypair, nobody else in it, and the clone already IS the isolation. Its audience is the reader least able to catch that, because a clone exists for a different AUTHOR (ADR 0047). The position now asks the store how many other positions exist over it (`registered_lane_count`, the registry read without the per-lane peek — a finalize is not the place to pay a `loot lanes` sweep) instead of inferring it from its own name. That is a fact rather than a guess and it is right in both directions the moment it changes. Two texts, because the hazard has two sizes and saying the larger one everywhere costs the smaller one its credibility. With siblings: named, counted, and the lane remedy. Without: the half that is always true — this signed the tree in front of you, `loot undo` steps it back. ⚠ The old #436 test asserted the sharing clause over a freshly init-ed repo with no lanes at all — byte-for-byte the state of a clone — so it was pinning the bug. It now spawns a lane first, to MAKE the claim true rather than assume it, and #967 gets its own test for the negative. Proven by running it: a lane-less repo says `directly on the working tree (no lane)`, and the same repo one `lane new` later says `1 lane live over this store — they share it`. 2f805d57 · 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
  • 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 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
  • 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
  • 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
  • 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
  • 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
  • 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
  • loot lane new stops paying the whole lane board to print the one row whose id it already holds: lane_views filtered after lane_statuses had finished sweeping, and that sweep does a full DagRepo::load_from, a read_tree_at over a FOREIGN working root and an owner read per registered lane, so a spawn cost N graph loads and N whole-tree walks and then discarded all but one. It now resolves the spawned id directly through a new Workspace::lane_status, which reaches read_lane_entry - literally the per-id call list_lane_entries loops - and shares one row derivation with the board through two extracted helpers, so the two answers cannot drift. tree_walks goes N to 1 at every size measured (3 to 1, 7 to 1, 13 to 1), and policy_loads tracks it identically because both are one per read_tree_at; the spawn path itself contributes zero walks, which is why the before was exactly N rather than N plus something. The pin asserts CONSTANCY across three registry sizes rather than any numeric target, because a literal is the wrong shape for a claim about a slope, and it carries two controls rather than one for a reason that was found by experiment and not by taste: blinding the fixture so it registers nothing fires the first control, but deleting that control then fails on the second - the registry did not grow across the sizes, so the slope is unmeasured - and a REGISTRY edited to equal sizes passes the first control honestly because each such fixture is truthful about itself. lane_views is deleted rather than left with a dead keep parameter, since lanes was its only remaining caller and that verb's question genuinely is the whole registry, so it keeps the full sweep deliberately. The comment above the call said this was a cheap side-effect-free registry read with no format-gated cost to avoid, which was half true and wholly misleading: side-effect-free it was, cheap it was not, and heads.rs has recorded the same sweep at 4.2 seconds for some time. The replacement quotes the old sentence and refutes it, and the cost argument is hung on Workspace::lane_statuses itself rather than on this one call site, because that is the symbol the next caller with this defect will reach for. find_lane and ensure_lane_name_free do filter after list_lane_entries and are left alone: that is a registry-only read with no graph load and no tree walk - it is what the old comment described, just not what it sat beside - and both are whole-set questions anyway. The land's gate reads flat here and is structurally incapable of anything else, since loot-perf links loot-core and never loot-cli, WORK_COUNTERS is ObjectGets and ObjectDiskReads only, and the_excluded_counters_are_zero_on_this_tier pins TreeWalks at a structural zero - so the pin lives in loot-cli's own tests and a second gate reading would have been theatre against an unchanged binary (#1705) 49b0799a · 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
  • 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
  • sweep 5 fix-up: the lane usage refusal no longer prints a run of spaces, because the rewrapped literal gets its line continuation back, and the subcommand gate test now asserts that no line of that refusal holds two spaces past its leading indent, which went red on the old text (0 passed, 1 failed). the cli smoke deregister case writes its seed with an escaped newline again and spawns on one line. rm and rm --deregister now share one owner-scoped lookup and one entry removal, and deregister drops a redundant exists check; disabling the shared ownership guard turned both the rm and the deregister test red (1 passed, 2 failed), and the verb tier width moves to the derived count. the census lexer reads a C-string raw literal, cr with or without hashes, as raw, with a fixture that failed first on a brace count that did not close (3 passed, 1 failed). ADR 0035 gains a dated amendment for --deregister and for keeping the entry of a held lane, the verb tier doc stops narrating per-ticket widenings as a decision beside the #1922 tripwire one, and the proposal loop argument in propose.rs and ADR 0075 is stated as a rule about any route that calls record_proposal or close_proposal instead of naming the routes. doc fixes: the LANE_BARE doc points at the union test, remove_lane says --reap-others also authorizes --deregister, the Resource Monitor advice is hedged, store.rs loses a method count and corrects its upsert heading, ADR 0075 says the old read would have been refused, and rewrap leftovers are reflowed. the workspace suite is green, and the loot-forge suite is green against a throwaway cluster with the CI env block (#1968) 15bcde31 · dbf3dbe6…diff
  • review sweep 6 fix-up: ADR 0088 section 4 gains the divergences the sweep found, each checked against the code - the evolog E row printing a raw whole message (#1973), the status and buoy R rows and the tag name column holding free text ahead of the last column, tag and heads naming people by display name with no key in heads json, a sealed diff row printing a bare tier label, telemetry status and off printing a spool dash where V3 says null, and telemetry on beside show as report json with no contract - and says it records what was found rather than proving nothing else diverges. V7 now means no version line or preamble, and the visibility token and Delta renderer member lists point at the code that defines them. the template-flag census gains a twin in loot-first over its FLAGS table, both reading one spelling list now in loot_core flags; the twin went red with --template and with --pretty planted in loot-first tag, and the loot census with -T planted in lane gc (0 passed, 1 failed each). ADR 0023 credits the open flag set to ADR 0047, the OPEN_BUT_TAKES_NONE census is called an ordered comparison, its assertion points at the list instead of naming describe and new, a ceiling raise has one place for its reason, a stale family count and a stale entry position are dropped, rewrap leftovers in ADR 0088 and ADR 0035 are reflowed, CONTEXT.md follows, and owned_lane takes a Removal with named verb and refusing fields instead of two swappable strings. the workspace suite is green (#1974) ecd53283 · dbf3dbe6…diff
  • loot lanes now reads the shared graph and scans the object directory once per board draw rather than once per registered lane: a new DagRepo::load_positions_from reads the position files of every lane first and the shared half once after them, so each row keeps the read order load_from_needing documents while every row reports against one moment of the shared store. that is the decision the ticket asked for, recorded on peek_lanes with its reasons, among them that option c cannot help because a row needs both the graph and the objects. a single load now reads the working change before the graph, the order that doc states. on a scratch repo with twelve lanes and a 4.9 MB graph the board went from 237 to 190 ms, paired in both orders with loot status flat as the control, and its porcelain and json output is unchanged apart from heartbeat age. a new board test relating the store file reads of the board to those of its rows built one at a time went red on the unchanged code and with per-row loads put back (0 passed, 1 failed each), and an engine test comparing a batch load with solo loads went red with the mixed-store refusal removed and with one position half reused (0 passed, 1 failed each). the perf gate reads no move, CONTEXT.md follows, and the workspace suite is green (#1782) caed7d82 · dbf3dbe6…diff
  • review sweep 8 fix-up: the owed relay push recovery now lives once, in a new workflow.md section, paying an owed relay push, which covers relay=FAILED and relay=REFUSED in both shells with the position on every command, the build job cap and the in-flight guard, and states one done-when: the push succeeded, or its format skew is recorded on the issue as owed to the release relay redeploy. land-change and afk-loop point at it instead of carrying their drifted copies, afk-loop refers to that item and to its sections by name rather than by step number, as AGENTS.md now does too, and its when-it-goes-wrong list gains relay=FAILED. probe_pusher now says why a report was refused, with the parser message or the version check, and keeps the pre-1776 explanation for output that does not open a JSON object, and the pusher= docs say UNKNOWN means the report was not read. the new pin and the tightened pre-1776 pin went red before the change (0 passed, 2 failed), and red again with the pre-1776 arm removed (9 passed, 1 failed) and with the pre-1776 explanation given for the new pin reports (9 passed, 1 failed). LanePeek becomes a struct and a dead unwrap_or_default becomes an expect, the 4.2 s lanes figures in heads.rs and lanes.rs are labelled as history, the PositionHalf doc credits the reorder with matching the documented load order, the ObjectIndexScan doc no longer names its CLI consumer, ADR 0028 says merge_tips reads tree_at itself, and rewrap leftovers in land-change, format.rs and CONTEXT.md are joined. the workspace suite is green (#1983) 2ee76c18 · 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

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.