Changes touching this path

  • Workspace accepts its clock; lane flag-gate goes pure so cmd_* tests never touch a real .loot (#322) 20ac82e1 · dbf3dbe6…
  • loot embargo-status <path>: report embargoed/revealed/not-embargoed (#15) cb471c93 · dbf3dbe6…diff
  • Refuse --expires on the tag-1 file-grant path; only --relay enforces expiry (#352) 0dfb8d46 · 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
  • loot diff --content: unified content diffs from inside a lane (#536) 7b01b038 · dbf3dbe6…diff
  • Keyless burn and hard-maroon: name the verb that exists (#556) 67bf5ad7 · dbf3dbe6…diff
  • describe/new: refuse a stray positional instead of silently truncating the subject (#545); last hardcoded relay port (#569) e1ba880d · dbf3dbe6…diff
  • burn::Burn: one home for the burn/purge concept (#662) 8e7834a7 · dbf3dbe6…diff
  • loot diff --stat counts the lines each path moved, and a path it cannot read says why rather than reporting zero (#416) 08d586c7 · dbf3dbe6…diff
  • a --remote name resolves through the remote registry, and a verb's positional is scanned past the flag values it consumes (#860) 798707da · 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
  • a --remote name resolves through the remote registry, and a verb's positional is scanned past the flag values it consumes (#860) (conflict resolution: crates/loot-cli/tests/cli_smoke.rs) 93255f97 · dbf3dbe6…diff
  • the conflict resolution keeps both sides whole, appending the --remote test after the archive and shortlog tests (#860) d1ad475b · 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
  • loot init's refusal leads with the command to run instead of describing two mechanisms, because it is the first thing a stranger types after installing (#1158) 19cdb435 · 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
  • loot id phrase cuts the key for the recovery door loot id recover already opened, issuing the 24 words for any identity ever minted with no derivation change, and refusing outright when stdout is not a terminal because those words are the private key 8bd665a5 · 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
  • a flag stops arriving where a subcommand reads its positional, because loot id rotate --permanent resolved the flag as the directory a rotation writes its re-grant wave into, and all eight subcommand families now gate and read positionals through the one table completions are derived from (#1407) e880cd19 · 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 migrate and loot maroon stop refusing an author's own revealed content as unauthorized, because the two re-seals promote due embargoed keys at their own first line instead of borrowing one from a capture that --no-snapshot skips outright and that an unchanged working tree short-circuits past before the engine is ever called (#1485) 34b5bb79 · 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
  • 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 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
  • 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
  • a flags value stops being read as a flag, because `FlagSpec::given` asks in flag position the way `positionals` already does and every verb-local scan in both binaries is deleted behind a census that refuses the shape — which is what had `loot tag -m "-d"` signing a retirement, `loot describe -m "--no-snapshot"` silently skipping the capture, and `loot-first land --allow-publish "--skip-tests"` skipping the gate that is its whole job (#1612) b40e1843 · 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
  • ADR 0074's destination guard was catching one planted shape in three, and the ordinal that a one-word fix left behind stops being a number at all: the census read only a scheme, so a bare relay.millerbyte.com in shipped source and a host assembled at runtime both passed silently while the ADR claimed no hardcoded destination could enter without someone editing the list - the predicate now takes a scheme or two dot-separated labels ending in a known web TLD, with underscore a token character rather than a separator so config.app_name does not read as a host hitting the TLD app, and that widening finds exactly one production line across the whole crates tree, the disclosure address in USAGE, which becomes the seventh allowlisted entry with its reason. every needle in that list is now asserted to still exempt a live line, so a dead entry fails instead of sitting there, and the ADR's own seven-entry sentence is read out of the ADR and compared rather than typed twice, with a stated-exactly-once guard so a vanished sentence fails rather than passing green. the limits that survive the widening are pinned as controls and written into section 9 rather than implied away: a suffix outside the known list, a bare IP, a host built at runtime, and any shipped file that is not rust. and the fifth thing this amendment said ADR 0074 adds becomes one more thing, because the enumeration it counted was deleted by the very change it describes and no referent survives to derive from - a position nobody types cannot fall out of step with a list that moved, which is the second wrong count in that one bullet and the reason for deleting rather than re-numbering. the twice-paid claim beside it gains its predicate too, since under any wrong count in this file the number is at least four and only under a typed count of one of its own lists is it two; and the revset composition #1497 promised turns out to work and is pinned behaviourally at last, the load-bearing half being that a two-change revset draws two nodes and a tilde rather than an off-screen parent (#1680) 37d450f1 · dbf3dbe6…diff
  • loot log gets a machine shape, and the seam it needed turns out to have existed all along: every verb already renders through Emit, so nothing had to be plumbed - what was missing is that the prose-only verbs return Message, which collapses their structure into a String at the cmd_ site, before the format is known. So adding machine output is not teaching the renderer a second format, it is stopping a verb destroying its own data on the way out, which is why it cannot be done to forty-seven verbs mechanically and why this ticket does one. Log, LogChange and LogWorking hold the structure and render all three formats from it, consuming topology.rs by making the same two calls --graph makes and reading row, edges, elided and merge straight off the nodes - no second walk, no ancestry query, and the module never sees a Workspace, so the manifests d6be741 stopped materializing stay unmaterialized. The load-bearing subtlety is that edges are indices into the shipped array, so --reverse has to happen before Topology::of rather than after, and the pin reads edges back as version-byte pairs so a wrong index cannot pass as a right one. The empty head is why head rows exist at all: b0cffc66 records no changes of its own, so it has an H row and no change row anywhere, and without the head rows a two-head repo would reach a machine reader looking unforked. What stops the other verbs being forgotten is a census that names rather than counts, two set-equal lists over derived vectors plus a sum, reusing the existing machine_output_verbs so it cannot disagree with the USAGE census - and the debt list may only shrink. Two ADRs are amended rather than edited past: 0023 quotes its own sentence that nothing needs machine output from log or manifest and says log graduates while manifest does not, and narrows its porcelain-first framing as written for a homogeneous per-path verdict rather than a shape with nested arrays; 0066's three counts had to move in place because a test asserts them against the roster. And both numbers in this ticket's scoping were wrong the same way source_walk was wrong in 9559f0a, a grep for one spelling of a thing with two: the table holds 66 verbs and not 54 because twelve rows are verb_of, sixteen took a machine selector and not seven, and ADR 0066 had recorded the right figures three weeks earlier. Criterion four had nothing to satisfy, because the SDK never calls loot log at all (#1540) 915db92c · 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
  • 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
  • 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
  • loot archive grows prefix, format, -o and a pathspec, and the pathspec is the DECLARED group rather than a fourth matcher: no Glob, no matches and no string compare against a pattern is written anywhere in this change, so the empty-match refusal and the 1330 cwd rebase arrive free with the attachment. the attachment forced one thing the ticket did not name - archive was declared NO_PATHS, so under 1551 the group trailing list would have begun at position ZERO and swallowed the output file - and it now states its own arity of two opaque slots first, both opaque because a file name, an -o value and a prefix are not repo paths and must not be rebased. the honesty criterion is the one that carried risk, since this adds a SECOND reason an archive can be partial beside the reason 416 built the summary for, and the two must not collapse into one number that reads as either: three populations are kept apart, written, then unreadable as selected minus written which KEEPS 416 word partial, then outside as total minus selected which gets its own clause, its own denominator and the explicit words by request not unreadable. the first denominator switches to selected and SAYS selected exactly when a pathspec is in force, so 1 of 2 cannot be read against a five path change, and the pathspec clause prints EVEN AT ZERO because silence there is indistinguishable from no pathspec at all. written is derived from selected by MEMBERSHIP, so written under selected under total holds by construction rather than by a subtraction that could underflow, and the engine skipped count is deliberately NOT used because it is taken over the whole tree and would therefore count paths the operator never asked about - that IS the collapse. the format is tar.zst and it adds an EDGE rather than a crate: zstd is already default-on in loot-codec and loot-core takes it with default features, so it already compiles into the shipped binary and the lock gained exactly one line with no new package. gzip was rejected on MEASUREMENT rather than taste - flate2 is in the lock but its only dependent is automerge, which only spike-crdt links, so it reaches NO shipped binary and choosing it would put a second compressor in the released loot; the wider extractor reach is the real cost and the format table says so. it does not reuse the codec own compressor either, because sharing it would weld archive bytes, a thing people compare hashes of, to a level chosen for objects in a store. the byte-reproducibility promise is NARROWED where an operator reads it: the ustar bytes are host independent, the zstd framing is reproducible only for a given zstd build. two pins were aimed rather than merely live - the -o ambiguity mutation is invoked with @ rather than HEAD precisely so the mutated build SUCCEEDS, which makes refusal the assertion, and tar.zst is checked by DECOMPRESSING rather than by length, since a wrap that compressed the wrong buffer stays green under a size assertion. and a stale doc claim was found on the way: readable_tree was described as what loot grep searches, false since 1500 moved grep to readable_object, narrowed here and in the copy of it that archive carried (#1530) 95a1cfeb · dbf3dbe6…diff
  • shortlog grows -n, -e and --group, and the mailmap lands as a DISPLAY grouping whose boundary is three structural facts rather than a comment. it lives under .loot, which walk_stat skips at name == DOT BEFORE any ignore rule, so it cannot enter a tree, a change, a bundle or the git mirror and nothing arriving over a wire can write it. the module is pub(crate) in the loot-cli LIBRARY, while verification lives in loot-codec and loot-core and AccessClass and pusher admission live in loot-forge and loot-relayd - every one of those is a crate loot-cli DEPENDS ON, or one that does not depend on it, so the use does not compile there and making it compile means a DEPENDENCY CYCLE. and the type answers key to STRING and never key to key: there is no canonical-key operation and no iterator, so even an in-crate caller, where privacy does not help, cannot obtain the key behind a key. inside the CLI the scope is judged rather than maximised - grant-status, attest, custody and manifest deliberately KEEP the un-mailmapped resolver, because those columns name a key that HOLDS A CAPABILITY and an alias there would be a display that reads like an authorization claim. the rotation criterion takes the DOCS arm and the reason is sharper than the ticket knew: the keypair is ALREADY SWAPPED by the time the report renders, so a rotation that failed because a display file could not be written would report FAILURE OVER A COMPLETED ROTATION. the ritual prints the exact two-line entry at the moment the operator needs it, and a pin feeds that printed text back through the parser so it provably pastes. the file is untracked BY LAYOUT because display names in loot are LOCAL: .loot/peers is local, the ambient identity is local, and blame porcelain author column was frozen as the KEY precisely because two machines reading one repo render different words for one author. a TRACKED mailmap would make some labels repo-wide and leave the rest local, two authorities for one column, and would hand anyone who can land a change the power to relabel an author in everybody else log. one mutation could not be WRITTEN and that is the finding rather than a gap: routing the mailmap into the key column is not EXPRESSIBLE, since the resolver hands back str and String and exposes no key at all while the key columns take a 32 byte array, so a collapse mutation was substituted to prove those assertions are live and the inexpressibility is reported as the structural claim instead of as a green test. one behaviour change is deliberate: the default order flips to NAME order so that -n selects something, because an -n that named the only existing order would assert nothing. and two pre-existing guards caught real drift on the way - the documented-flag census tokenizes on whitespace and brackets only, so a dash-flag followed by a comma or a backtick in PROSE reads as a documented flag, and the verb-grep census needs its row on ONE line, so a wrapped row silently loses the count (#1531) cfaaad9f · 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
  • 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
  • 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
  • the open-ended range is typeable, and its meaning is WRITTEN BEFORE IT IS BUILT: x.. is exactly x..HEAD, the ancestors of the revset HEAD minus the ancestors of x, which in words is everything on the current lines of work that x does not already reach - stated in the grammar and mirrored at Expr::Range, at parse_range, in the log USAGE line, in CONTEXT and in the parse-error hint. three things are said explicitly because each is where another tool habit would mislead: it is NOT descendants(x) minus x, since a sibling line reaching a live head is in the range and is not a descendant; it is PLURAL on a diverged dock; and it inherits the ADR 0085 note. it is a DESUGAR rather than a new AST node, so parse of x.. equals parse of x..HEAD and the evaluator, head_depths and the CLI door grow no case and cannot drift from the definition. ..y is REFUSED and the reason is an ASYMMETRY rather than a preference: the two conventions an operator arrives with DISAGREE, git reading ..y as HEAD..y and jj reading it as root()..y which in loot, with no root sentinel, is exactly ancestors(y) - different sets, so either default silently hands half the users the other half answer. the RIGHT endpoint has no such split, git x.. being x..HEAD, jj x.. being x..visible_heads(), and the loot revset HEAD already BEING the live heads, and that asymmetry is the whole argument. secondarily the jj reading is already spelled ancestors(y), so accepting it would mint a second spelling of an existing primary. precedence is PINNED AS TREES rather than as sets, nine shapes including the one that decides the rule - a1..~@ parses as a1.. ~ @, because ~y is not a primary and so the .. before it is open - and open-versus-closed is decided by a wildcard-free starts_primary, so a new token cannot compile until it declares its side. the note behaviour changes DELIBERATELY and is recorded in both places: x.. parses to Range(x, Head), so an expression containing no HEAD token can now print the ADR 0085 note, which is the note doing its job, because nothing in abc123.. hints that its answer includes the working change. two stale claims are corrected where they live, the HEAD~5.. and abc123.. are-errors assertions and the this-ticket-does-not-close-it block, since HEAD~5.. now ANSWERS. seven mutations with counts read, and ONE EXPOSED A BLIND SPOT IN THE NEW PIN ITSELF, left stated rather than implied: the smoke repo has one live head and it IS the working change, so x..@ and x..HEAD denote the same set there and a wrong supplied endpoint leaves the smoke GREEN - which endpoint the grammar fills in is measured in the revset fixture instead, whose graph forks (#1570) ee081e23 · 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
  • 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
  • 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
  • loot-first land now asks the loot binary it pushes with which FORMAT_MAJOR it writes, through a new loot --version --json that prints the contract and the crate version, and reads the major the landed commit declares out of the mirror. the relay push runs only when both were read and are equal; otherwise the verdict says relay=REFUSED, the land stands, and a final block gives the reason. the landed line gains a trailing pusher= field naming that binary as version/v<major>@path, with UNKNOWN in place of the version and major when it reported neither, and the bare loot --version line is unchanged. stub pushers staged as real child processes pin the refusal, the matched push, a pre-1776 binary and an unreadable tree: removing the equality went red (207 passed, 1 failed), pushing past the refusal went red (205 passed, 3 failed), trusting the linked constant instead of asking went red (206 passed, 2 failed) and dropping the field went red (202 passed, 6 failed), and the spawned --version --json pin was red before the shape existed (0 passed, 1 failed). the land-change and afk-loop skills, workflow.md, CONTEXT.md, ADR 0023 and ADR 0088 follow. the workspace suite is green (#1776) d3e2f994 · dbf3dbe6…diff
  • a .lootattributes rule whose pattern ends in a separator refuses the capture instead of sealing its subtree internal, and the decision is recorded with the widening it declines. the trailing-separator subtree affordance is .lootignore dialect - build/ prunes there, and #1859 repaired it there - while an attributes pattern is compared against the whole relative path, which never ends in a separator, so docs/ restricted=connor matched NO path and every path under docs/ fell through to Internal, the ANYONE-granted tier a relay reads by design. reproduced through the spawned binary first, which is what the ticket asked: loot new sealed docs/plan.md internal at exit 0 with one first-seal-summary line as the only signal, the #1108 shape exactly, and loot explain said the rule was tried and none matched. the ticket offered two answers and they are not equally safe, so the rejected one is measured rather than argued: reading the separator as .lootignore does MOVES what a path resolves to, and first-match-wins means not always narrower - with it honoured, docs/ internal above docs/secret/** restricted=connor resolves docs/secret/keys.md internal, and a_recorded_inert_pattern_moves_no_paths_tier goes red (27 passed, 4 failed over the file). silent exactly where the mis-seal gate is blind, a first seal of a basename that is not secret-shaped; an already-recorded path would trip the demotion guard instead, loud and just as unasked-for; either way the operator file would mean something new because the binary changed. so the line is RECORDED, not re-read: Attributes::parse keeps every rule exactly as it always parsed it, so no path changes tier and ferry ingest, which reads the rules and never the faults, is untouched, and the line becomes a MalformedRule that Workspace::malformed_attrs_gate already raises RepoError::MalformedAttrs over - one variant and one slug for both faults, because the taxonomy grows where a driver must act differently (#1582) and the tree one consumer of the slug does not, so each fault renders its own remedy beside its own line rather than one sentence teaching a reader the mistake they did not make. read off the compiled glob, so the docs-backslash spelling a Windows shell completes is the same fault and is quoted in the spelling it is compared in, the one loot explain prints for that line (#1859). scoped to the trailing separator and not to a pattern that matches nothing, since a bare docs internal is a fine rule for a file named docs. VISIBILITY blast radius measured rather than accepted: every .lootattributes in the estate - this repo and its lanes, scripts, millerbyte, the test and perf-scratch repos - plus every recorded revision of this repo own, holds no trailing-separator pattern, so no position next capture refuses and no repo changes tier, and the_shipped_attributes_holds_no_rule_the_gate_refuses keeps the shipped file that way. mutations: the recording dropped reddens the unit pins, the seam pin and the spawned pin (28 passed, 3 failed; 4 passed, 1 failed; 0 passed, 1 failed, the last naming the capture that succeeded); the separator honoured instead reddens four including the no-widening pin; a docs/ line added to the shipped .lootattributes reddens its census (0 passed, 1 failed). the path compared never ending in a separator is measured too, since the argv door rebase strips one and loot explain docs/ asks about docs. ADR 0038 carries the #2030 amendment and CONTEXT.md both halves, the attributes refusal and the .lootignore entry stating its affordance does not reach the other file. no migration, and no forge, relay or site byte moves, so this owes no deploy. the workspace suite is green (3859 passed over 119 binaries, 7 ignored) (#2030) 3e849712 · dbf3dbe6…diff
  • review sweep 17 fix-up: the separator-rule census checks what its name claims now and reads every caller of a pattern door, two more callers ask policy::unix_separators instead of answering the separator by hand, and the one class of path whose protection depends on that answer is pinned rather than lucky. clean::Sieve::keep spelled the replacement itself and compared the key against the recorded store keys and against an Ignore built from operator-typed -e values, the #1859 class in a third file at a verb that DELETES what it declines to protect; ferry::ingest_commit did the same for the .lootignore and .lootattributes it reads out of a git commit, a fourth home the ticket did not name and one that is inert because git hands over /-separated paths. both ask the normalizer now, whose body is the expression each replaced, so no path moves - measured through the spawned binary before and after on a scratch repo over ten invocations, -x, -X, the default, three backslash-spelled -e values with their slash twins, and a --force run with the tree it left, whose transcripts are byte-identical. what was NOT pinned is the half that matters: the protected set holds /-separated store keys while the walk hands keep a native rel, so only a recorded path with a separator in it can catch a key nobody normalized, and the clean fixture held none - sub/recorded-deep.txt is in it now, and let key = rel.to_string() reddens four cases over that file, the removal matrix among them, having taken a recorded path as a candidate (10 passed, 4 failed), where the whole file was green under that same mutation before. the census is renamed and derived on both halves rather than trusted: the files are every .rs under this crate src, production halves only, and the doors are read off policy.rs, a function there taking rel or path as a &str, so a new door joins by existing; an offence is the pair per function rather than the spelling alone, because a backslash pass on the way to a display line or a map key is the other axis and a spelling-only census would have to name its exceptions. mutations: the hand-spelling put back reddens it naming src/clean.rs fn keep, and put back in ferry naming src/ferry.rs fn ingest_commit (0 passed, 1 failed each); the door reader blinded reddens its anchors; the needle blinded reddens the fixture (left empty, right the one site); and the floor call dropped reddens source_walk own consumer census, whose derived count line this land repastes with the new consumer. #1859 AC4 is measured rather than argued, as #2028 did: from another module of the crate, Patterns and Pattern literals are E0451 on their fields and reaching for .list is E0616, throwaway probes, and the doc says privacy is what holds the pair and that a literal inside the module is not closed by it. a_recorded_inert_pattern_moves_no_paths_tier runs its claim over the paths its own rule list mints, with the inert line and without it, over the tier and over publish-ness, and the separator honoured reddens it naming a derived probe (27 passed, 4 failed); ADR 0038 and CONTEXT.md stop citing a three-path sample for no path changes tier and rest that on the parse. ADR 0038 and view.rs stop rating the .loot/view gap oppositely: closed on the tier axis, and the cost is materialization, which view.rs holds and the ADR now points at. conformance.rs stops resting its counted pair on a comment and points at ClassReads, where every other MetadataStore method is unreachable and the trait defaults none, so a third read in AccessClass::of panics there instead of undercounting here - measured, it does (0 passed, 1 failed). propose::visible_to and Viewer are pub(crate), with the field privacy named as what holds the pair and the in-module literal named as what it does not. seven string literals in cli_smoke.rs that carried a raw newline where the file spells the escape are fixed, five more than the ticket named, from a scan of the whole file. no migration, and no forge behaviour moves - a visibility narrowing, doc prose and one test fixture - so this owes no deploy. the live loot-forge suite is green against a throwaway 17.0 cluster with the CI env block and LOOT_FORGE_TEST_REQUIRE_DATABASE (476 passed over nine binaries, no skip note) and the workspace suite is green (3859 passed over 120 binaries, 7 ignored) (#2033) 4720219d · 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
  • loot seek asks a repo one of three questions without materializing it: list the paths a pathspec admits, search them with --text, or read one file verbatim with --read, in the ambient repo or in another loot repo on disk named by --in and opened with its own identity and keyring; a remote name, a URL or a git directory refuse naming the map ticket that builds them (#2063 tickets 3 to 5). the question is explicit, never inferred, so a glob matching one file does not turn a listing into a read; --read refuses a machine format rather than render bytes lossily. the verb computes no content of its own: the listing and the search are Workspace::readable_tree_at, the read is path_content_at, a hit is grep::scan_file, and the spawned-binary pin holds the listing to surface --porcelain, the hits to grep -n and the bytes to cat on one fixture. the frozen shape is P path rows, T hit rows under the nearest P, one S summary row carrying the sealed-skip count and a truncated flag, and --json with contract first and target.kind drawn from the Target enum; zero rows exits 0 and a refusal alone is non-zero. the usage line, the README verb block, ADR 0066 four counts and its amendment log, the MACHINE_OUTPUT and ATTACHED censuses and the revset_everywhere lists all learned the verb, and ADR 0023 gains the #2068 amendment; PROSE_ONLY_CEILING does not move. red under mutation: the sealed count dropped (0 passed, 1 failed), truncated reported for every bound (0 passed, 1 failed), --read under --json allowed (0 passed, 1 failed), the S row omitted (0 passed, 1 failed), and a sealed path name pushed into a row (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 (3942 passed over 123 binaries, 7 ignored) (#2068) a733d45c · dbf3dbe6…diff
  • loot seek answers for a local git repository: when --in names a directory that is not a loot repo but is or is inside a git checkout or a bare repo, the git backend lists the blobs of the tree at a git revision, searches them through the same grep scanner, or reads one blob verbatim, through the git2 crate the mirror already links, with no spawned git. the rows are the same P, T and S rows and the same --json: change is the commit id, visibility is the machine format one dash in porcelain and null in json as ADR 0088 V3 says, nothing is sealed so the count is zero, and --at is a git revision peeled to one commit, whose refusal says it is not a loot revset. a path the commit does not hold refuses in cat's words rather than printing an empty file, and a tree or submodule entry is skipped because it has no bytes to list or read. the change column became full hex text rather than a loot object id so a twenty-byte commit id fits the row unchanged, the resolved target now carries a backend, a boxed workspace or a git repository, so the shape names four target kinds while two answer, and the two backends build a row through one rule, Question::hits_in. the two-axis review before landing turned a minted dash constant into the shared one, made the Target doc point at Backend instead of listing which kinds lack one, and added the pins it found missing: a bare repo holding a commit is listed, --first and --last run on a git target, a nested read goes through the binary, and the resolved git root carries no trailing separator. the unit tests build a two-commit checkout in-process with git2 and pin the listing, the search, the read, the revision, the pathspec, the bound and the resolution of a checkout, a subdirectory of one and a bare repo; the spawned-binary pin covers the same through the binary. red under mutation: a tree entry listed whatever its kind (0 passed, 1 failed), the revision ignored (0 passed, 1 failed), a search row kept with no hits (0 passed, 1 failed), an absent path read as an empty file (0 passed, 1 failed), the pathspec unapplied (0 passed, 1 failed), and a git directory refused as not a repo (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 (3952 passed over 123 binaries, 7 ignored) (#2075) 32edebda · 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
  • 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 flag that puts a verb in a shorter shape declares the arity that shape takes, and the door refuses past it: #1928 gave every bounded verb one number, the widest shape its usage shows, so a flag selecting a narrower shape left the surplus word inside that number, admitted by the door and read around by the arm, which is the #1419 class one level down, since loot grant --relay origin a.txt bob junk sealed the grant for bob and never looked at junk. the declaration is Args::narrowed, a flag beside the arity its shape takes, and FlagSpec::arity_for reads a given one ahead of max_positionals rather than through it, which is what lets a shape carry an arity on a leaf that declares none: loot diff attaches the pathspec and counts nothing, while loot diff --conflict <path> reads no selector and no pathspec at all and its surplus word had no number to be past. the refusal is the door own sentence with the shape that was typed named in it, loot grant --relay takes 2 positional arguments, because loot grant really does take three and a refusal saying so would send the operator to check a count that is right; the narrowest declared narrowing wins when several are typed, since refusing against the wider would admit a token neither shape reads, and each is read through given rather than off raw argv, so a flag value spelled like another narrowing does not select it. the ticket offered a check inside the two arms it had found and made the declaration conditional on a third verb turning up in the census, so the census was written first and ran the decision: verbs did turn up beyond the two, one with two narrowed shapes of its own, one already carrying the check by hand, and one whose arity is None and for which a per-arm check would have had to invent a number, so the per-arm answer was a hand-written copy of the door sentence per shape and the declaration won. which shapes those are is the census answer and is in no list: forward, a documented shape typing a flag outside brackets and showing fewer positionals than the widest shape its own spec shows must declare a narrowing on one of those flags at its own count, and the door is then driven at an argv built from that shape and at one word past it; reverse, every declaration must name a flag the spec declares and carry a count some documented shape of that spec shows. a usage line is read against the spec whose arity the door actually enforces for it, the longest-named table row or family leaf its shape opens with, so loot id unlock --permanent is a statement about the arity of id unlock, which is zero and already refused, rather than about id. what the census cannot see is said where it lives: a mode flag documented as a bracketed option on the wide line rather than as a line of its own, which is loot apply --abort junk and loot archive --list junk, both measured still dropping the word, and closing those means splitting usage lines that README and the published CLI page are pinned to, so it is a ticket and not a clause. loot tag --retire gives up the surplus check it had written by hand and declares the narrowing at each spelling instead, and its pin loops the declared narrowings rather than the aliases typed out. the three readings of a usage line shape the censuses want, the positionals, the flags a shape requires and an argv in that shape, are one walk with three projections, because a copy of the bracket rule per reader is a copy that can come to disagree about which tokens a shape claims. red under mutation, counts read each time: the grant narrowing dropped (0 passed and 1 failed in the census, 0 passed and 1 failed at the process), the diff --conflict narrowing dropped (0 passed and 1 failed, 0 passed and 1 failed), both grants narrowings dropped (0 passed and 1 failed, 0 passed and 1 failed), resolve --tool declared to take a count no usage line shows (0 passed and 1 failed), a bracketed flag read as required by its shape (0 passed and 1 failed), no shape reading as requiring a flag at all (0 passed and 1 failed), the -d alias losing its narrowing (1 passed and 1 failed), arity_for ignoring the narrowings (1 passed and 2 failed in loot-core, 0 passed and 1 failed in the census), the refusal no longer naming the shape (1 passed and 2 failed, 0 passed and 1 failed), the narrowing read through max_positionals rather than ahead of it (2 passed and 1 failed, 0 passed and 1 failed), the widest narrowing winning over the narrowest (2 passed and 1 failed), and the check relaxed so a narrowing need not narrow (2 passed and 1 failed in the loot-core doctests). no migration, no wire or format byte moves and no host behaviour moves, since every byte of this is argv on the client side, so this owes no deploy. the workspace suite is green (4027 passed over 125 binaries, 7 ignored) (#1934) c62387ee · dbf3dbe6…diff
  • the seek cache home is bounded, and loot seek --gc tends it: every remote an agent touches leaves a position under the cache home and nothing bounded it, so the rule is now one function, seek::gc::plan, that the verb and the cap share and cannot disagree on, a position being a directory the two caches make, a .git holding a HEAD or a .loot holding a .loot, with its bytes on disk and the clock of its last refresh read off the #2108 record and never an mtime; a position whose record is older than the age asked for goes, then oldest first by that record positions go until the home is within the cap, and a position with no readable record is never removed by the cap because its age is not known and a cap is not a reason to guess, listed with a dash and removed only on the explicit ask, --older-than 0, which removes every position; a position another invocation is refreshing right now is busy and removed by nothing, since both caches write their record last and a position in use would otherwise be exactly the oldest candidate, so a refresh writes loot-seek-refreshing when it begins and removes it on every exit, a marker older than an hour reading as a crashed refresh; 0 is no cap. on every invocation that refreshed a position the home is measured against LOOT_SEEK_CACHE_CAP, 2 GiB when unset, and the oldest other positions go until it fits, said on stderr in one line, never the position just refreshed whatever its age, and a skipped refresh inside --fresh measures nothing, nor does the ambient repo; the environment cap is read before any target opens so a value that will not parse is bad_flag_value at no round trip, the just-refreshed position is matched on canonical paths so a URL spelled in another case still names its own, a removal that fails is said in the same line and never refuses the answer, and the measure is a walk of sizes and records only, targets read by --gc alone, paid once per refreshing invocation, 2 ms over this desktop cache of three positions and 217 files against the 30 ms process floor, which is the cost of having no size record to trust. loot seek --gc does the same on demand with --older-than <days> defaulting to 30, --cap <bytes> defaulting to the environment, and --dry-run deciding and removing nothing, printing one row per position with kind, bytes, refresh clock, what was done and why, and target, and the home bytes before and after, a failed removal a failed row with the error on the notice channel; beside any question it is refused by code as --schema is, its flags without it the same way, and a value that will not parse is bad_flag_value. its porcelain is two new marks, G and H, under a flag that did not exist before, in their own GC_COLUMNS table the mark census now reads beside MARK_COLUMNS, and --schema files the four flags under maintenance rather than among a question flags, since the SDK reads that set as the fields of a question, which its schema pin holds green unchanged; the shapes are frozen where every seek shape is, in an ADR 0023 amendment, and the one stderr line is recorded against its remote-answer sentence there. pinned pure on the rule, by age, no age, oldest first under the cap, the unrecorded position standing though the home is still over, the explicit ask, the kept position under both, the busy one under all three, a record ahead of the clock, and a tie on the clock going in path order; on the walk over a hand-made home with a bare git repo, a loot position, an unrecorded one, two directories that are not positions, a fresh marker and a stale one, canonical paths and the sizes-only walk; on a failed removal rendered failed with its bytes standing and the removals around it still done; on the schema census over both tables and the maintenance key; and through the spawned binary in the smoke suite over the relay and the git caches it already makes, the cap on an invocation removing the older git position and never the loot one just refreshed, a malformed cap variable refused by code before an unreachable host costs a round trip, the ambient repo measuring nothing, a skipped refresh measuring nothing, --dry-run in porcelain and JSON removing nothing with the H totals the sums of the G rows, a cap the two standing positions just fit removing the older git one in JSON, a cap of one byte unable to reach the unrecorded position, thirty-one days removing by age, a busy marker holding the unrecorded position against the explicit ask until it ages out, and the seven refusals by code. ADR 0090 and ADR 0023 amended, CONTEXT Seek, the usage line and the synopsis. red under mutation, counts read each time: age never removing (0 passed and 1 failed), the cap removing newest first (0 passed and 1 failed), the cap guessing at an unrecorded position (0 passed and 1 failed), the just-refreshed position going by cap (0 passed and 1 failed), the just-refreshed position going by age (0 passed and 1 failed), --older-than 0 not the explicit ask (0 passed and 1 failed), the walk skipping the loot kind (0 passed and 1 failed), the walk reading no record (0 passed and 1 failed), a loot target never read (0 passed and 1 failed), the gc flags landing among a question flags in the schema (0 passed and 1 failed), the H row losing its removed count (0 passed and 1 failed), --dry-run removing (0 passed and 1 failed), the cap on an invocation never running (0 passed and 1 failed), the cap on an invocation removing the just-refreshed position (0 passed and 1 failed), a skipped refresh measuring the home (0 passed and 1 failed), a gc flag beside a question admitted (0 passed and 1 failed), a question beside --gc admitted (0 passed and 1 failed), a busy position going (0 passed and 1 failed), a stale marker busy forever (0 passed and 1 failed), the walk reporting the spelled path rather than the canonical one (0 passed and 1 failed), a failed removal reported as removed (0 passed and 1 failed), the loot refresh writing no busy marker (0 passed and 1 failed), the git refresh writing no busy marker (0 passed and 1 failed), the guard never dropping the marker (0 passed and 1 failed), the busy marker surviving the refresh (0 passed and 1 failed), and the environment cap read after the target opens (0 passed and 1 failed). no migration, no wire or format byte moves, and no host behaviour moves, so this owes no deploy beyond the one #2123 already owes the release. the workspace suite is green (4023 passed over 126 binaries, 7 ignored) and the SDK seek suite is green in the lane (11 passed) (#2125) 70ea581b · dbf3dbe6…diff
  • a network budget refusal now says how long the wait lasted beside the limit, and a relay loot-net serves writes a received line and an answered line per request under LOOT_NET_TIMING=1 on the client clock, because the #2079 stall is unreproduced and nothing on record could say where its time went. the wait is timed around the whole send while the reqwest limit starts inside it, so time well past the limit was spent in this process rather than waiting for the host. the relay lines landed in the failing test captured output beside the client lines when a panic was put after a push and removed, and the land-shell use of the flag is written into workflow.md. no retry is built and no cause is claimed. a workspace run went red once on both format_skew_gate tests under load, one printing the wait lasted 5.5 s on its 5 s probe, and both passed alone: that file now prints what its stub took. the seek smoke test drops the flag from the loot it spawns, since it pins their stderr and failed with the flag set. red under mutation, counts read each time: the wait printed as the limit (1 passed and 1 failed), the wait stored as the limit (0 passed and 1 failed), the relay layer not attached (0 passed and 1 failed), no line on arrival (0 passed and 1 failed), each restored to 2, 1, 1 and 1 passed. no migration and no wire or format byte moves; a relay prints lines only with the flag set, so this owes no deploy. the workspace suite is green (4196 passed over 133 binaries, 9 ignored) (#2079) ac8afc4b · 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 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 seek tests no longer write into the cache home of whoever runs the suite, and a cache position with no refresh record is removed when its refresh fails. every loot the seek smoke test spawns is now built by one closure that sets LOOT_SEEK_CACHE to the test home; the unreachable-host refusal ran under the operator home and was the one call in the suite that could make a 127.0.0.1:1 position there, and the --name refusal made none, since it is read off argv before the target opens, so the premise that it did is corrected. the unit test that resolved git+file:///no/such/dir through Target::resolve now opens GitCache under a home of its own. the verb: a fresh make whose refresh fails was already removed, measured through the binary, but a position standing when a round began was not that round make, so a failed refresh removed nothing and the cap never removes an unrecorded position; LootCache::open_in now treats a position with no readable record and no live busy marker as on its first refresh and removes it whole when that fails, recorded as an ADR 0090 amendment and in CONTEXT.md, and gc::busy_at is the busy rule the open and the gc walk both read. pinned in seek.rs by a_position_whose_first_refresh_fails_is_removed_whole and in the smoke test, which seeds such a position in its own home. red first against the unchanged rule (1 passed and 1 failed, the passing one the edited resolver test). red under mutation, counts read each time: the removal keyed on the make alone (unit 0 passed and 1 failed, smoke 0 and 1), the busy check dropped (0 and 1), the record check dropped (0 and 1), no removal at all (unit 0 and 1, smoke 0 and 1), the smoke closure pointed at another home (0 and 1), each restored to green. the listing of the real cache home is unchanged, mtimes and sizes, across the full suite. no migration, no format byte and no published wording moves, so this owes no deploy. the workspace suite is green (4325 passed over 137 binaries, 12 ignored) (#2142) fc5a0816 · dbf3dbe6…diff
  • test scratch directories are named by one shared helper, common::scratch, from the process id and a per-process counter rather than the clock, so two tests asking for one tag at once are no longer handed one directory that the second caller empties and refills (the #2258 mechanism): 19 test files under crates/*/tests named a temp_dir path from SystemTime nanoseconds in 21 statements and now call fresh_dir or fresh_path, loot-cli files reaching it through mod common and loot-first exit_codes compiling the same std-only file by path, and revset_everywhere and plaintext_equality_census, which already counted, call it too. scratch_dirs.rs holds the #2258 pin, moved from revset_everywhere and aimed at the shared helper, red under the clock naming in 13 runs of 13 (0 passed and 1 failed, 1 to 16 of 400 directories handed twice), and a guard that walks every .rs file under crates/*/tests and refuses a temp_dir statement that reads the clock, red over the unconverted tree naming the 19 files (1 passed and 1 failed), red with buoy_exit put back (1 and 1) and red when the walk finds nothing (0 and 1), each restored green. no other test changes what it asserts. the workspace suite is green (4533 passed over 140 binaries, 13 ignored, from 4532 over 139: the guard is new and the pin moved into its own binary). test-only, so it owes no deploy (#2358) 6dd1e05b · dbf3dbe6…diff
  • every compiled-in and current-tense loot host moves to loot.build (map #2412, ADR 0099): loot --help names security@loot.build and https://loot.build/trust, ALPHA_PROMISE_URL is https://loot.build/install and the telemetry DEFAULT_ENDPOINT https://loot.build/api/telemetry, so the next release carries them; the site install one-liners, R2_BASE and HOP_BASE (https://dl.loot.build, https://loot.build/dl), the metrics SITE_SCOPE and own-host referrer, the disclosure links on trust, terms, privacy and known-issues, and the install, quickstart and guides pages follow, with the privacy and terms markers moved to 25 September 2026 and re-pinned since readers see the new address; README, CONTEXT.md (the forge door now forge.loot.build), the release checklist, site CI and test fixtures follow, and the two specs carry a note instead of a partial rewrite. the destination census gains build in WEB_TLDS, without which security@loot.build would be invisible to its bare-host clause, and exempts the two shipped lines that tld makes read as hosts (state.build, r.build()) by their exact text; ADR 0074 section 9 says twelve-entry to match. 37 lines still name millerbyte.com outside ADR bodies, evidence, research and scratch, each kept on purpose: dated history, millerbyte.com the site and its @millerbyte/ui tokens, the two noted specs, and relay-era tooling with no successor host, filed as #2438. census red with build dropped from WEB_TLDS, restored; site gate 819 passed and within budget; workspace suite 4559 passed over 142 binaries, 13 ignored (#2418) c5663115 · 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.