Changes touching this path
- day 0: loot hosts loot
f4c30e75 · dbf3dbe6… - evidence: crew minted and verified (#86)
1fada823 · dbf3dbe6…diff - hard embargo engine/wire lands (#14, format v5)
7784bcac · dbf3dbe6…diff - embargo CLI (#88) + attack demo (#89) + section-B evidence + maroon propagation fix + drive setup
c670cc2b · dbf3dbe6…diff - catch up: GB1 loot ferry lands (PR #114)
6816f50d · dbf3dbe6…diff - normalize working tree to LF: byte-stable co-located bridge (.gitattributes -text)
e58fdda6 · dbf3dbe6…diff - catch up: sign-resolutions fix lands (PR #118)
8c37c7c7 · dbf3dbe6…diff - dock merge: emit --porcelain/--json via the CA3 verdict serializer (#126) (#127)
`loot dock merge` dropped its typed per-path outcomes at the `println!`
boundary — the exact anti-pattern CA3 (ADR 0023) removed from apply/conflicts/
status/ferry. Route it through the same seam: `cmd_dock_merge` now reads
`out_fmt(args)` and branches to `verdict::porcelain`/`verdict::json` over
`verdicts_of(&outcomes)`, with the Human path unchanged. `merge_dock` already
returns `BTreeMap<PathBuf, MergeOutcome>`, so the lift is direct; no engine
change. Now an agent driving a dock merge reads the same frozen `=/M/C/R`
contract as every other reconciliation verb.
Verified: cargo test -p loot-cli (45) + -p loot-core (223) green; driven-binary
smoke shows =/M porcelain rows and JSON, human output unchanged.
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
88b97955diff - Fix #128: pull/apply collapses a concurrent two-writer fork (#129)
Engine `apply_sync` ingests a peer's divergent tip as a *sibling head* and
classifies per-path outcomes, but never merges tips — so a keyholder that had
also advanced its own line was left on two heads with a working tree showing
only its own side (the peer's content in the graph but never materialized).
No CLI verb converged it (`dock merge` is docks-only). This is the gap the
concurrent-agents proof surfaced under a real run.
Add `Workspace::converge_heads(base)` — the peer-side analogue of `merge_dock`
(ADR 0011: keyholders collapse forks on pull+apply). It folds every other head
into our line via `merge_tips`, signs each merge so it travels, then
materializes the merged tree (visibility-aware: a sealed path the identity
can't open stays relayed, not dropped). `cmd_pull` calls it after the batch
loop, passing the pre-pull head as our side, and now also honors
`--porcelain`/`--json` like the other reconciliation verbs.
Proven end to end by docs/evidence/scripts/concurrent-agents-demo.ps1 — both
acts pass: Act 1 (local docks → harbor, real conflict + resolve + buoy),
Act 2 (two identities push concurrently, relay DAG forks, agent's pull
collapses it; public converges with no side dropped, restricted path relays R).
Regression test: converge_heads_collapses_a_two_writer_fork_no_side_dropped.
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
bb4c849fdiff - S1: implicit auto-snapshot on mutating verbs + demotion guard (#144) (#156)
Flip the snapshot trigger from explicit to implicit (ADR 0030): every mutating
verb captures the working tree first, so edits are never lost between commands
and no manual `loot status` is needed. Read-only verbs still never snapshot.
- Add `implicit_snapshot` + `SnapshotOpts` (--allow-demote repeatable,
--no-snapshot/--ignore-working-copy) and wire it into `new`, `describe`,
`grant` (both forms), `maroon`, `migrate`. A `positionals` helper strips the
`--allow-demote <path>` value so it is never read as a verb positional.
- `loot new` now captures pending edits before finalizing via
`Workspace::finalize_capturing`, dropping an empty/tip-duplicate capture so a
bare `new` mints no empty signed change. `working_message` preserves a
described name across an implicit capture.
- Demotion guard (#62) rides the implicit snapshot and is now a typed
`RepoError::Demotion { paths }` (matchable, not a Backend string); the message
is verb-agnostic ("re-run with --allow-demote").
- Docs: CONTEXT.md working-change + .lootattributes notes; USAGE.
Verified end-to-end on the built binary (new/grant capture without status;
demotion aborts + --allow-demote overrides; --no-snapshot skips) plus 6 new
tests. Full workspace test suite + clippy green (no new warnings).
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
8dc00304diff - loot-first tooling: ferry --with-wip review lane + loot-first.ps1 (#155) (#160)
The build half of map #148. `loot ferry --with-wip` projects the ambient
dock's unfinalized working change to a sealed-free review/<dock> branch:
provisional commits carry Loot-Change-Id + Loot-Provisional and NO
Loot-Signature (the missing trailer is the machine-checkable "not
finalized" marker) while staying SSHSIG-signed for integrity. Lanes are
keyed by the durable change_id in .loot/git-mirror/wip - deliberately
outside the mark map - and reaped lazily on every ferry pass once the
change id turns up signed (landed) or gone (abandoned). Rounds append
per revision (#150); ingest refuses provisional commits and mark
rebuilds skip them, so nothing throwaway can enter the round-trip spine.
tools/loot-first.ps1 is the orchestrator that owns every GitHub call
(loot core stays git-agnostic): `review` publishes the branch by
single-ref inline-URL push and opens the PR (pr-map ledger, #153 seam
2); `land -Pr` verifies approval, finalizes on the PR's dock (seam 3),
ferries, fast-forwards GitHub main and points the PR head at the landed
sha so GitHub marks it Merged by reachability, then pushes the relay;
`init-hook` installs the warn-only direct-commit guard (#151).
Shipped git-first, deliberately: this commit is the last one that has
to be - the tooling it lands is what flips the workflow.
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
533d94d5diff - S2: reconcile the verb surface — read-only status, columnar log, eager change ids (#145)
Reconciles the daily verb surface to ADR 0030, consuming S0's durable
change_id (#143) and S1's implicit snapshot (#144).
- status is now READ-ONLY: it recomputes the pending delta live and never
persists a snapshot; -m is dropped (naming is describe's job). It shows the
working change's durable change_id alongside a live, non-durable version id
(a plaintext content fingerprint, distinct by construction from the sealed
snapshot id — Seam #1).
- new is the finalize/sign boundary and eagerly mints + prints the *next*
change's durable handle, so a fresh change has a name from birth. Adds the
convenience `new -m`. init mints the first change's handle too.
- log/status go columnar: change · version · message · vis · author, with the
change id as reverse-hex LETTERS and the version id as hex DIGITS (ADR 0029)
so the two ids disambiguate at a glance. The working change is rendered once,
as a live row shared with status (log agrees with status).
- status porcelain/JSON gain an `@` change-identity header carrying change_id
+ version id (hex); the ~ path rows and status chars stay a frozen contract.
Engine: snapshot_assigning carries an eagerly-minted handle onto a fresh
change's first version; working_preview computes the live, non-durable version
id + emptiness without sealing or recording; mint_next_change_id gates on
authorship. Store persists a per-dock next-change handle.
bd5d6a8f · dbf3dbe6…diff - Merge pull request #159 from Connor-Miller/s2-verb-surface
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
42d3411bdiff - S4: operation log + undo (#146)
Implement ADR 0031: an append-only, repo-wide, local-only operation log
(`.loot/ops`) backing `loot undo` / `loot op log` / `loot op restore`, the
safety net that makes ADR 0030's implicit auto-snapshot safe to trust.
Core (`loot_core::oplog`):
- `Operation` captures the resulting **view** — change-graph heads, each dock's
working/tip pointers, the conflicts set, and the ambient-dock pointer — as raw
pointer-file bytes, so restore is a pure pointer reset that never touches the
object store or the append-only graph (nothing is ever deleted).
- `record` appends one op per view-changing command; `undo` steps the view back
one op and appends a compensating op (the log grows on undo, so redo lands);
`op restore <n>` jumps to any op. A 1-based ordinal is the durable ref; a
`pos` field walks the history back one step at a time.
- Barriers: `push`/`grant`/`maroon`/`pull-grants` are recorded non-undoable;
undo refuses to cross one and names the real remedy (keyring/manifest are
one-way state a view reset cannot retract).
CLI:
- `loot undo`, `loot op log`, `loot op restore <n>` verbs + USAGE.
- One `record_op` per view-changing command; read-only verbs record nothing.
loot has no standalone snapshot op — S2 made `status` read-only, so every
capture rides a mutating verb and that verb is the one op.
- undo/restore reload from the restored files and re-materialize the ambient
dock (writing the restored tree, pruning what the step removed).
The oplog never enters a bundle (bundle serializes changes/objects/keys, never
reads `.loot/ops`) — asserted by test. Tests cover append-on-undo, walk-back,
barrier refusal, absent-pointer round-trip, and full CLI undo/redo. clippy clean.
ce2f96a7 · dbf3dbe6…diff - Merge pull request #162 from Connor-Miller/s4-oplog-undo
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
4204625fdiff - S3: divergent change — marker + loot abandon (#147)
Surface and collapse a divergent change per ADR 0029/0030 — the last slice of
the jj-ergonomics trio (map #142). A divergent change is one durable change id
carrying more than one live version id (two writers rewriting one change id); it
is data, not an error, and is detected per change id, not by head-counting.
- Engine: `divergent_change_ids(abandoned)` scans every node (a divergence can
sit under a single graph head, e.g. as merge parents, with identical trees) and
returns change ids with >1 live version; `versions_of_change`; `abandon_head`
drops a version from the live heads. `record_carrying` is now public — the
amend primitive that makes divergence exist (and constructs it in tests).
- `log`/`status` render a trailing `!` on a divergent change id and list each
version (shared `change_col` helper). A log whose only multi-head reason is one
divergent change stays the flat listing — routed by distinct change *lines*,
not head count, so the "run `loot apply` to converge" branch never mis-claims a
divergence apply cannot collapse (Act 3 reproduces flat, as in the prototype).
- `loot abandon <version-id>` drops a version, leaving the other live version(s)
under the change id. Nothing is deleted — the version stops being a live head
and joins a local-only `.loot/abandoned` set the live view filters. It refuses
a non-divergent change (never hides a change's sole version), and is one
undoable operation: the oplog View now also captures `.loot/abandoned`, so
`loot undo` brings the version back. `resolve`/`dock merge` are untouched.
Tests: engine detection + abandon; workspace abandon→collapse→undo and the
non-divergent refusal; store abandoned round-trip; `change_col` `!` rendering.
Live-verified Act 3 (flat `!` listing → abandon → undo restores). clippy clean.
43d5cd72 · dbf3dbe6…diff - Merge pull request #168 from Connor-Miller/s3-divergent-abandon
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
dd573561diff - R4: buoy's frozen machine contract gets a tested encoder home (ADR 0025, #180)
13bba560 · dbf3dbe6…diff - R6: structural snapshot-before-mutate - the Snapshotted handle (ADR 0030, #182)
6f0c24b7 · dbf3dbe6…diff - R1: Workspace becomes the CLI's only face of the engine (#177)
06340e98 · dbf3dbe6…diff - R5: render-returns-String seam for verb output (#181)
7b2e2579 · dbf3dbe6…diff - review fixes: retire with_repo, collapse forwarders, reconcile_* renames, fork-view naming (#177/#178/#182 + code-review)
b071df66 · dbf3dbe6…diff - loot edit: amend a finalized change; supersession travels as signed predecessors (ADR 0032, #171)
Implement the amend model: `loot edit <change-id>` reopens a finalized
tip change as the working change - a sibling (parent = its parent, tree
carried address-for-address, durable handle kept) whose `predecessors`
names the reopened version - so once `loot new` signs the amend, the
claim that X-prime replaces X is signed data that travels, not a
local-only abandon.
- Format: FORMAT_MAJOR 6 -> 7 (ADR 0019). ChangeNode.predecessors:
Vec<Oid> rides the bundle + durable graph after the change id,
canonically sorted, empty = ordinary; folded into the version-id
computation (a no-op amend still mints a distinct version) AND into
the finalize signature (version_id || change_id || predecessors) -
ingest trusts received ids, so stripping/forging a supersession claim
on the wire must break the signature directly. v7 reads v<=6 as
predecessors-empty; goldens updated, v5/v6 kept decode-compat.
- Liveness (amends ADR 0029's definition): superseded - named as a
predecessor by any in-graph same-cid version, regardless of that
supersessor's own abandoned/superseded state - joins abandoned as a
live-view filter in divergence detection, versions_of_change, and
log/status rows. Abandon means kill, never revert.
- Converge: converge_heads drops superseded heads before collapsing
forks (a solo amend lands at peers as a clean replacement, never
content-merged with the version it replaced); dock merge adopts an
amend of our tip as a fast-forward and treats the mirror case as a
no-op (supersedes() requires the claim to sit ON the other line).
- The verb: a named Workspace mutation; refuses on an in-progress or
uncaptured working tree (the documented ADR 0030 exception - edit
replaces the working change and never implicit-captures), on a
divergent handle (abandon first), and on descendants (tip-only v1).
One undoable op (ADR 0031); output through the render String seam.
- dock switch: an idle dock no longer parks a tip-duplicate working
child on its tip (the finalize_capturing duplicate-drop now runs
there too) - the stray polluted the tip descendants and, post-0032,
would have content-merged against amends.
Tests: engine liveness + canonical hashing + signature strip/forge;
codec round-trips + v7 goldens; workspace edit e2e / guards / undo /
dock-merge FF / converge drop. Live-verified on the built binary
(edit -> amend -> new; guards; undo). 366 tests + clippy clean.
8176f2e0 · dbf3dbe6…diff - (working change)
64d767d4 · dbf3dbe6…diff - loot dock rm: remove a dock, dropping parked unsigned WIP; undoable (ADR 0022 amendment, #212)
c14695b4 · dbf3dbe6…diff - Liveness: one loot-core home for live/superseded/divergent/parked + the head partition (#216); CONTEXT.md vocabulary; rides along: cargo-dist release config (dist-workspace.toml, release workflow, 0.1.0 manifests) from the install-prototype session (loot-site map, #206)
3630469b · dbf3dbe6…diff - pull_via over a SyncTransport seam: the pull pipeline gets an interface and tests; negotiation uses complete heads (#217)
68836d58 · dbf3dbe6…diff - loot-first in Rust: land policy behind a Forge seam (#218) — new loot-first bin crate (ledger/forge/policy/orchestrator), in-process Workspace reads via loot-cli lib split, typed pr-map owner + shared ferry::WipState; every policy decide-tested against a fake forge. Build+test only; shadow-run, real land, and ps1 deletion remain operator-gated.
58c28888 · dbf3dbe6…diff - resolve conflict at crates/loot-cli/src/main.rs
7488b55d · dbf3dbe6…diff - One materialize chokepoint: pull/apply capture-first, converge waits (#219)
pull/apply now capture uncaptured disk edits into the working change before
they touch the tree, like every other mutating verb (ADR 0030 amendment). A
dirty pull ingests (graph append is always safe) but DEFERS convergence — the
working-change guard makes converge a no-op for that pass — and emits a note
("captured working change <id>; heads left unconverged — finalize then
re-run"). A clean pull converges as before.
The seam: one internal tree-write chokepoint. converge_heads evaluates
dirtiness ONCE at entry (before any head is dropped, so the reference stays
queryable) and the adopt/merge materialize paths refuse over uncaptured dirt
rather than clobber it; undo/abandon resurface is exempt by intent. capture
skips only a mid-flight transfer (anchor closure incomplete, via new engine
closure_complete) — a genuine delete-all still captures, never refuses.
- workspace: capture_uncaptured_edits, tree_is_dirty_over chokepoint, PullReport
{ outcomes, deferred }; converge_heads gates both writes on disk_dirty.
- cli: cmd_pull renders the defer note + records the op; cmd_apply captures.
- core: extract closure_complete (negotiation_have reuses it).
- ADR 0030 dated amendment + CONTEXT.md glossary; #169 pull-over-dirty gotcha
becomes a guarantee.
- tests: dirty divergent pull, dirty independent-head pull (defer then
converge-after-finalize), chokepoint invariant, delete-all-captures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
d5dd28e4 · dbf3dbe6…diff - Spawn DevX (#232): loot lanes observability, --ticket lane spawn, wayfinder claim-to-lane
f0a885c2 · dbf3dbe6…diff - #243: loot abandon --head <version> — drop a fork tip
The mirror re-baseline needs to walk a drifted dock off a stale fork so a
re-ferry fast-forwards onto landed git main instead of *merging* the stale
tree (which resurrects files deleted upstream). `loot abandon` only
collapses a divergent co-version; it refuses an independent fork head
("keeps its single version"). This adds the non-divergent counterpart.
`Workspace::abandon_fork` reuses `abandon`'s undoable machinery — the node
is union-preserved on disk (the shared graph is an immutable node store),
the abandoned set keeps it out of the live view, `loot undo` restores it —
minus the divergence gate, plus two guards: refuse a version that is not a
live head, and refuse dropping the dock's last live head (never empty it).
Wired as `loot abandon --head <version>`; two seeded-fork tests.
abc2c58e · dbf3dbe6…diff - #244: loot adopt <version> — settle a dock onto a landed change
The re-baseline primitive #243 needs and could not do safely. A dock can
end up on a divergent local line that must be *discarded* in favour of a
landed change (the #243 state: the primary main dock on a stale fork while
origin/main moved on). apply/converge MERGES the lines — against a stale
fork that resurrects files deleted upstream; abandon --head drops a signed
fork tip but not the working-change head. adopt <version> fills the gap:
take the target wholesale, abandon every competing head, no merge.
Composition of shipped parts (no new engine machinery), per the committed
spec docs/specs/loot-adopt-target.md:
- Workspace::adopt: resolve the target among live finalized changes, fence
it to the harbor/main lineage (reachable from the change the mirror's
main projects, read from the local ferry spine — no network, no git
process), WIP-gate (refuse a dirty dock unless --discard-wip), then
abandon every competing head to a fixpoint (dropping a transient ferry
merge resurfaces its parents, so the whole divergent line is walked into
the abandoned set), settle tip on the target, and materialize its tree
via the existing resurface checkout — one undoable op (ADR 0031).
- loot adopt <version-id> [--discard-wip] CLI wiring (COMMANDS + USAGE).
- Amend ADR 0034 (the <version> take-wholesale arm + discard-vs-merge
table) and the CONTEXT.md Adopt entry.
Seven tests: settle+abandon+undo, dirty-refuse, discard-wip drop, off-
lineage refuse, unsigned-target refuse, a merge-over-stale-fork fixpoint
walk, and an end-to-end adopt-then-ferry asserting nothing projects (§6.5).
Verified live through the CLI: rolled a drifted dock back onto a landed
change, undo restored it, and the lineage guard refused an ahead-of-main
target.
Deliverable 3 (the §6 live reconcile that finishes #243) is an operator-run
repair of live .loot state and stays a follow-up per execution order §9
(it runs after this branch lands).
01bae608 · dbf3dbe6…diff - feat(loot-cli): no-arg `loot adopt` — harbor catch-up merge (#250, ADR 0034)
22524137 · dbf3dbe6…diff - refactor(loot-cli): retire in-place dock switching from the CLI (#3b layer 1; layer 2 = #253)
c62ec238 · dbf3dbe6…diff - feat(loot-cli): wire `loot --version` / `-V` to the crate version (#237)
36472343 · dbf3dbe6…diff - finalize refuses to sign an un-described change, and status's hint names describe -m (#174)
`status`'s hint on a dirty tree pointed at `loot new`, but `new` is
capture-*then*-finalize: following it signed the dirty tree in one stroke under
the `(working change)` placeholder, skipped the review lane, and rode to git
main as a permanent commit subject. The hint was the trigger; `new`'s
willingness to sign a nameless change was the foot-gun.
- The hint names `describe -m` — capture *without* finalize, the first verb on
dirty work.
- `finalize_capturing` refuses an un-described change (no message, or the
stored placeholder). It sits *after* the capture, so edits are held and only
the signature is withheld, and *below* the empty/duplicate drop, so a bare
`new` on a clean tree stays a no-op. Both callers inherit it — `loot new` and
`loot-first land` — so 'describe before landing' is enforced, not remembered.
- Deriving a subject from changed paths was rejected: it mints plausible history
nobody wrote, and loot has no changed-path concept to derive from.
- The placeholder now has one mint (`working_message_or_placeholder`) and one
test (`is_undescribed`); 10 stray literals collapsed onto them, so the guard
cannot silently stop firing on a drifted copy.
ADR 0030 amendment + CONTEXT/workflow/evidence updated. Residual (dock merge and
ferry sign the operator's authored work in passing) named in the ADR and filed
as #275.
a8fb4684 · dbf3dbe6…diff - reject unknown flags on every verb instead of silently ignoring them (#67)
e4583cb7 · dbf3dbe6…diff - merges refuse to sign an un-described change too, closing the #174 residual (#275)
#174 stopped the *deliberate* finalize from signing a nameless change, but three
verbs still sealed the operator's own work in passing, to make a signed merge
parent: `dock merge` and the `adopt` catch-up (via `fold_line_in`), and `ferry`
over a git main that moved (via `reconcile_capture`). Only the *trigger* was
mechanical — the content was authored — so the placeholder still reached main.
Reproduced against the pre-fix binary: a ferry over unnamed disk work landed the
literal subject `(working change)` on git main in one silent pass.
Both paths now refuse an un-described merge parent, below their capture (edits
held, only the signature waits) and below a redundant-capture drop (a pass with
no real work to sign stays a no-op, never a nag). Two reasons the ADR gave for
NOT doing this were checked and dropped: a land never reaches these paths dirty
(it finalizes before it ferries), and #219's refuse-on-dirt rejection was about
being forced to capture, not about being asked for a name.
The merge *nodes* are untouched — they are machine-authored and already carry an
honest mechanical subject. That is the line: mechanical content may be named
mechanically, authored content may not.
- `refuse_if_undescribed` (one rule, two messages) + `drop_capture_if_redundant`
(the empty/duplicate drop, previously copied at two sites and missing at a
third — which is what would have made `dock merge` nag).
- Cost, documented: uncaptured dirt takes two passes, since naming *is*
capturing. `loot-first review` asks for a name only when it must merge.
ADR 0030 amendment (correcting the two wrong claims), CONTEXT, workflow, and
concurrent.md updated. Closes #275.
aced27c1 · dbf3dbe6…diff - review refs carry the position, not the dock (#281)
Every lane's home dock is main, so dock-named review refs made N
concurrent lanes share one review/main branch: the second lane's
ferry --with-wip force-pushed over the first's in-flight PR head,
and either position's reap pass could misjudge (and retire) the
other's live entry, since liveness reads the positional working
pointer a foreign position cannot see.
The review lane is now keyed by its owner position end to end:
- ferry projects review/<lane-id> from a lane, review/<dock> on the
primary; the wip and pr-map ledgers gain an owner column (- =
primary; pre-#281 short rows parse as primary-owned) and the
review line carries owner=.
- Reap is owner-scoped: only the owner judges liveness; a foreign
pass reaps exactly the entries whose owner lane is gone from the
registry, so an abandoned lane's review ref dies with it instead
of leaking.
- land derives the collapse branch from the pr-map lane's owner and
refuses to run from any other position: it finalizes the current
position's working change, and the dock guard cannot catch that
mismatch when every lane's dock is main.
Docs: ADR 0033 amendment, concurrent.md (review projection is now
genuinely parallel-safe), workflow.md, CONTEXT.md review-lane entry.
aa68437a · dbf3dbe6…diff - Gate flags per subcommand: a sibling subcommand's flag refuses instead of riding ignored (#278)
The #67 gate declared flag specs per verb, so `loot lane`'s spec was the
union over new/list/name/rm/gc and a flag real on a sibling subcommand
(`lane new --stale-hours`, `dock rm --at x`) passed the gate silently.
Each branching verb (`lane`, `dock` -- the others declare no flags) now
re-checks the resolved subcommand's own spec via FlagSpec::check_sub
before the workspace opens; a test pins the table's union to the
subcommand specs so the two gates cannot drift. Ride-along: bare
`loot lane` no longer panics slicing an empty argv.
1ada4da4 · dbf3dbe6…diff - resolve conflict at crates\loot-cli\src\main.rs
a609fc96 · dbf3dbe6…diff - Remove the two files the #284 reconcile-merge resurrected
tools/loot-first.ps1 (retired in the #218 tail, 69fd3cd) and
crates/loot-first/src/ledger.rs (moved to loot-cli in #232) came back
from the dead in merge d3ca4b8: the ferry reconcile of the #281 lane
line against the ingested #278 land re-materialized files both lines
had long deleted. Neither is referenced -- the ps1 is inert and the
orphan ledger.rs is not declared in lib.rs -- so this is a pure
deletion restoring the pre-#284 tree shape. The resurrection is a
real ferry-merge bug, filed separately; so is the finalize gap this
land tripped next (a deletion-only change is dropped as a
tip-duplicate -- same_tree_content is blind to deletions), which is
why an unused-mut warning fix in main.rs rides along: the change must
carry a content edit to survive finalize at all.
fc99da1a · dbf3dbe6…diff - Catch up to git main 809ddfe: #260 smoke default, #271 arm64 build, loot diff (#1), ADR 0034 dock retirement (#253)
587684dd · dbf3dbe6…diff - Relay: explicit 64 MiB body limit; push batches byte-capped (#309)
0ac2fa23 · dbf3dbe6…diff - loot diff --conflict <path>: inspect both sides of a conflict (#13)
5f50590f · dbf3dbe6…diff - loot pull: auto-surface after a non-empty apply (#3)
9ac8510b · dbf3dbe6…diff - loot status: show new/modified/deleted vs previous change (#7)
eee3a1d5 · dbf3dbe6…diff - loot init: print .lootattributes quickstart template (#21)
2d7c081a · dbf3dbe6…diff - resolve conflict at crates\loot-cli\src\main.rs
b59ddd37 · dbf3dbe6…diff - resolve conflict at crates\loot-cli\src\main.rs
4df84fe2 · dbf3dbe6…diff - #315: route log/attest/abandon through resolve_selector (one #305 selector grammar)
- abandon: swap the hand-rolled resolve_live_version call for
resolve_selector, so @ / HEAD / HEAD~n / a version-or-change-id prefix
all name an abandon target, same as loot diff already accepts.
- attest: retire resolve_change (its bespoke version-hex-prefix match) in
favor of resolve_selector. The returned Oid is a version id, exactly
what Attestation records under change_id throughout the engine
(attestations_for/buoy_resolution are keyed by version id) -- no
version->change-id remap is needed or type-correct here. Keep the
ADR 0018 guard that refuses the still-mutable working change, now
checked post-resolve (@ legitimately resolves, since diff wants that
too; attest alone must still refuse it).
- log: add Workspace::ancestors_of, a full multi-parent ancestor walk
(unlike walk_single_parent's HEAD~n rule, it never refuses at a merge).
`loot log <selector>` resolves the selector and filters the rendered
view down to that version's ancestry; the live working row only
survives when the selector names it directly (`loot log @`). No
selector -> unchanged full-history behavior.
New Workspace-level tests prove resolve_selector composes with attest,
abandon_fork, and the new ancestors_of walk; cmd_* stay untested directly
per the existing loot-cli idiom (a full invocation would walk cwd up to
a real .loot).
3f7ca995 · dbf3dbe6…diff - #315: align abandon usage strings with the selector grammar (review nit)
The help block advertised `<selector>` but the arg-missing usage errors still
said `<version-id>`; rename the local `prefix` → `selector` to match. Cosmetic.
a543eb56 · dbf3dbe6…diff - loot whoami --pubkey + peer add - (stdin) for scripting (#4)
54034e40 · dbf3dbe6…diff - Workspace accepts its clock; lane flag-gate goes pure so cmd_* tests never touch a real .loot (#322)
20ac82e1 · dbf3dbe6…diff - One rendering seam for the Verdict contract (#321)
bea1221c · dbf3dbe6…diff - loot log --path <file>: filter history to a path (#6)
f3de23a6 · dbf3dbe6…diff - loot completions <shell>: zsh/bash/fish completion scripts (#23)
aeee8145 · dbf3dbe6…diff - loot grant-status <path>: list current grantees (#5)
0547ce91 · dbf3dbe6…diff - Add loot verify: object-store integrity check (#19) - rehash every loose object against its address, report corrupt and missing by address, exit 1 on problems; load-free so a corrupt store can still be diagnosed
aeb147b3 · dbf3dbe6…diff - resolve conflict at crates\loot-cli\src\main.rs
5ed8c2a2 · dbf3dbe6…diff - resolve conflict at crates\loot-cli\src\main.rs
917ac7c7 · dbf3dbe6…diff - resolve conflict at crates\loot-cli\src\main.rs
07799c1d · dbf3dbe6…diff - loot verify: name each missing object's referencing change+path, and add --accept-loss lost-ledger (#335) - missing report carries provenance (MissingObject/MissingRef); .loot/lost records operator-acknowledged unrecoverable losses so verify gates CI again while new damage still fails; primary-only, store-locked
57350aa4 · dbf3dbe6…diff - loot embargo-status <path>: report embargoed/revealed/not-embargoed (#15)
cb471c93 · dbf3dbe6…diff - Bounced-land resolutions inherit the described ours-line subject (#337)
fbcb501d · dbf3dbe6…diff - loot embargo-status <path>: report embargoed/revealed/not-embargoed (#15) (conflict resolution: crates/loot-cli/src/main.rs)
18bc16f3 · dbf3dbe6…diff - Build the mis-seal gate: secret-name refusal + first-seal summary (#343)
12d2ea42 · dbf3dbe6…diff - Grant expiry: optional expires_at on GrantEntry and tag-3 wire (#20)
a01c3d53 · dbf3dbe6…diff - loot grants --quarantined / --trust: review and trust quarantined senders (#12)
86c46a0d · dbf3dbe6…diff - Build loot burn: destroy + tombstone, no resurrection (#344)
0d1cc5af · dbf3dbe6…diff - Mis-seal gate covers loot edit and audited signing chokepoints (#353)
e4e3a804 · dbf3dbe6…diff - loot id rotate: new keypair, expiry-preserving re-grant wave, archived old key (#16)
2b16256f · dbf3dbe6…diff - Refuse --expires on the tag-1 file-grant path; only --relay enforces expiry (#352)
0dfb8d46 · dbf3dbe6…diff - Rotate ritual warns per expiring re-grant: expiry does not travel (#368)
Decision on #368: accept + document loudly (option 3). Routing re-grants
through the tag-3 sealed path conflicts with the wave-runs-first abort
safety of the rotation ceremony (the new key does not exist yet when the
wave is minted, and rotation is deliberately offline/file-based); the
tag-1 wire extension was already rejected in #352.
The ritual output now names each re-grant bundle that carries an expiry
and warns that the expiry is recorded only in the rotating machine's
manifest - the applying machine records none (tag-1 limitation, #352),
so the applied copy is effectively unexpiring - and points at the one
wire that enforces expiry: the original grantor re-issuing via
loot grant --relay (#20). Docstrings on RotateRegrant, rotate_regrants,
and cmd_id_rotate no longer overclaim that the expiry travels.
593f77c4 · dbf3dbe6…diff - ADR 0039 build: pure-projection review + carry-at-land (#362)
Review mode (`ferry --with-wip` / `loot-first review`) is now a pure
projection: no ingest, no dock reconcile, no mirror-main advance, no
spine rewrite - it mints the provisional commit from the lane's own
anchor marks and pushes only review/<position>. A lane behind git main
reviews normally; REFUSE_REVIEW_STALE_ANCHOR is deleted with the fold
it guarded (#292/#302), and the #349 review-mode trigger is
structurally gone.
Reconcile lives only at the signing verbs, and its diverged-line shape
is now the carry (DagRepo::carry_line): a self-authored suffix replays
onto landed main as superseding versions - same change id, same
subject, single parent, stale original kept as predecessor - so landed
history stays exactly one commit per change with no ferry:
1412f811 · dbf3dbe6…diff - Candidate 1a: verbs return an Emit shape, not stdout (in-process seam)
Every cmd_* verb now returns Result<Box<dyn Emit>, String> instead of
printing to stdout itself; main.rs's dispatcher renders the shape once with
the resolved OutFmt and prints it. This is the deepening the architecture
review's top recommendation called for: the verb interface becomes an
in-process test surface, so a test can assert on a verb's output as a value
without spawning CARGO_BIN_EXE_loot (the friction tests/emit_snapshot.rs
documents: "there is no in-process seam to call").
- emit.rs: add a Message shape (prose/silent verbs — one rendering across
every OutFmt) and make Status own its data so a shape can outlive the
verb's Workspace.
- main.rs: Verb.run signature + dispatcher; ~50 verbs converted to build and
return their shape. Machine verbs return their existing emit shape; prose
verbs accumulate their former stdout into a Message. print_step /
print_surface_listing now return String so verbs can compose them. Streaming
verbs (push, serve) keep direct progress prints and return an empty Message.
- clone folds init_repo's now-returned summary into its own output; pull folds
its post-reconcile surface listing into the shape's Human field.
- New test exercises the seam directly; the emit_snapshot characterization
tests confirm byte-identical output.
Keystone for candidates 1b (CliError) and 5 (prose-into-Shape).
1301bafb · dbf3dbe6…diff - Build the seal-WIP guard (#418): refuse a bare sync verb that seals live described WIP
Graduated from #356's "Prevent + hint" resolution (map #354). After ADR 0039
(#362) made review a pure projection, the only way to strand a described
working change as a PR-less signed line is a *deliberate* bare sync verb.
Guard that path at its source.
- New typed refusal RepoError::SealWip { subject, verb } in loot-core, a sibling
of MisSeal/Demotion, on the ADR 0030/0038 guard+override pattern. Overridable
with --seal-wip on both verbs.
- ferry seal path: thread seal_wip through ferry::run -> reconcile_onto; the
Merge-with-wip arm (the ONLY place a bare ferry seals — land pre-finalizes,
adopt folds through fold_line_in) refuses unless overridden. reconcile_onto
now returns the sealed subject so the caller reports the seal from the seam
that decided it, not a post-state heuristic.
- adopt seal path: thread seal_wip through adopt_harbor; refuse before
fold_line_in when a described line diverged. The clean fast-forward and
redundant-capture drop return earlier, so a no-op catch-up never trips.
- Fires ONLY on a described line the sync would actually fold: an un-described
one stays the #275 refusal; a break-glass ferry / no-op sync (no live
described WIP) is untouched.
- On override the verb prints the follow-up-round recovery recipe
(SEAL_WIP_RECOVERY, one shared seam); the review "nothing to review" and land
"not in the pr-map" paths print it too when sealed_unlanded_anchor() detects a
signed line ahead of mirror main with no PR.
- tag's ferry passes seal_wip=false — it is not a finalizer, so it refuses
rather than silently seal live WIP while cutting a release.
- docs/agents/concurrent.md §"One seal path remains" rewritten to describe the
guard; CONTEXT.md gains the "Seal-WIP guard" vocabulary entry.
Tests: guard at both seams (refuse without / seal with --seal-wip), un-described
stays #275, no-wip catch-up untripped, full ferry::run break-glass + no-op pass
untripped, sealed_unlanded_anchor signal. Full suite green (loot-core 337,
loot-cli 243, loot-first 69); the describe_contention CPU-load flake passes
in isolation.
42409371 · dbf3dbe6…diff - TS SDK slice 6: physical mode openRepo over the shared LootRepo interface (#428)
The second backend: openRepo(path) drives an on-disk .loot/ checkout by
shelling out to the installed loot binary, returning the IDENTICAL LootRepo
the in-memory mode defines — so calling code is backend-agnostic. The binary
owns all crypto/codec; physical mode adds none (no WASM).
- CLI (sanctioned tiny machine-output addition, #428): `loot surface
--porcelain`/`--json` emit the current readable tree as path+visibility
(loot-core `verdict::surface_{porcelain,json}`), so `list()` never scrapes
human text. An empty repo is an empty tree, not an error.
- sdk/src/physical.ts: openRepo → PhysicalRepo via child_process. list()
parses `surface --json`; read() streams the materialized file (a real byte
stream) → NotFound on ENOENT; edit/remove write the working copy
(capture-first) and record a client-side overlay so status/diff report kinds
(added/modified/removed) against a committed baseline captured at open +
refreshed on push — loot folds a described change into the current tree, so
surface alone can't tell add from modify. describe/push shell out
(`describe -m`, `new`); guards map to --allow-demote. Errors map to the
shared taxonomy (missing binary → setup error; parent-moved → ConflictError;
non-repo → NotFoundError).
- Seam: `runReadContract` extracted to sdk/test/read-contract.ts and now runs
VERBATIM against BOTH backends (connectRelay and openRepo) — the proof they
are interchangeable behind one interface. physical.behavior.test.ts adds a
write round-trip + error surface. 32 SDK tests; loot-core/loot-cli green.
Deferred (documented): physical private-visibility authoring (a .lootattributes
rule) and pull-with-remote behavior — public content covers the AC and the
in-memory backend covers private.
Refs #428, #422.
9d12a388 · dbf3dbe6…diff - Slice 6 review fixes: stream pull, distinct setup error, clean empties (#428)
Addresses the code-review findings on 4ca54a2:
- pull() now STREAMS the child's stdout via spawn (was buffered via
execFile then yielded once) — satisfies the read/pull streaming AC.
- Missing/incompatible binary gets its own SetupError (code "setup"),
distinct from generic failures and the deferred-private path; an old
binary lacking `surface --json` / reading an older format maps to it too.
- allowReveal is rejected with a clear error rather than silently dropped
(physical slice 6 authors public content; reveal isn't mappable); guard
mapping centralized in guardArgs.
- Empty-repo machine output no longer string-matches the "nothing to
surface" error: new Workspace::surface_tree() returns None on a headless
repo, so cmd_surface emits an empty tree cleanly (no prose-scraping).
- read()'s ENOENT→NotFound handling deduped into a shared streamFile helper
(was repeated in the collector and the iterator).
Acknowledged, kept (documented in code): the client-side overlay/baseline for
status kinds (loot has no kinded-delta machine output, so capture-first can't
"map directly" for status); read streams the materialized file rather than a
`loot` stdout (no cat verb); the faithful status/overlay mirror of RelayRepo.
Green: loot-cli 243, 32 SDK tests, tsc clean.
Refs #428.
3ece1bad · dbf3dbe6…diff - CLI: machine error channel — coded CliError + RepoError::code() under --json (#430)
Give the binary a machine error channel so its error taxonomy travels as data
instead of the SDK regex-matching stderr prose. RepoError::code() (in loot-codec)
maps each of the 10 variants to a stable slug and is the source of truth. A new
CliError { code, message } (loot-cli/src/error.rs) carries it: From<RepoError>
(code from the variant), From<String>/&str (code "error"), and to_json() emitting
{"contract":N,"error":{"code","message"}}. Verbs now return Result<_, CliError>;
the dispatcher's fail() emits the JSON error object to stderr under --json and the
byte-for-byte-unchanged `loot: <message>` otherwise. CLI-level failures carry
codes: no_repo (open_repo) and unknown_flag (the flag gate).
Scope note: Workspace already stringifies RepoError internally (its methods return
Result<_, String>), so no live verb path yet propagates a typed RepoError to the
dispatcher — engine failures emit code "error" (message intact) while no_repo and
unknown_flag flow as real codes. From<RepoError> is in place and unit-tested;
threading typed engine errors through Workspace is a larger follow-up.
cargo test --workspace green.
97b9becb · dbf3dbe6…diff - CLI: thread typed engine errors through Workspace so slugs travel (#430 follow-up)
#430 laid the CliError plumbing but the taxonomy stayed inert on live verb
paths: workspace.rs methods returned Result<_, String>, stringifying RepoError
before the verb's `?`, so every engine variant collapsed to code "error" at the
--json boundary. Convert that boundary: impl Workspace / Snapshotted / lanes
methods return Result<_, CliError>; engine (self.repo.*) sites use
.map_err(CliError::from) so code() is preserved; the MisSeal/SealWip guards
propagate the typed RepoError instead of .to_string(). Non-RepoError sources
(io, format!, bare strings) stay code "error", messages byte-for-byte intact.
Workspace::open/init stay Result<_, String> (open_repo() stamps the CLI-level
no_repo slug), keeping the loot-first ripple to two boundary conversions. New
emit_snapshot test proves a real RepoError::UnsupportedFormat reaches --json
stderr as {"code":"unsupported_format"} on `loot apply <garbage>`, with the
non-json `loot: <message>` line unchanged.
Also: share loot_core::verdict::json_string (drop error.rs's byte-identical
twin) and collapse the emit_snapshot run_with_code/run_stderr_with_code helpers
into one run_streams. Known residual: the ferry.rs bridge path still flattens
engine errors to "error" (deliberate scope boundary — git2 errors ride its
String contract). cargo test --workspace green.
627897cb · dbf3dbe6…diff - SDK: inject transport/runner seams + map binary error codes (#432, #433, #434)
Three architecture-review deepenings that make the two LootRepo adapters'
decision logic testable without a live relay or the real binary, and replace
stderr regex-scraping with the binary's coded error channel.
#432 — RelayTransport seam. A narrow dumb-pipe (`post`) + default
HttpRelayTransport, injected via `connectRelay(url, id, { transport })`. All
interpretation stays adapter-side: response classification (401→AuthError+pubkey,
non-2xx/connection-fail→TransportError) is a pure helper, and the push
visibility-resolution + GuardError enforcement is extracted to a pure
`resolvePushVisibilities`. New relay.unit.test.ts proves error classification,
path-scoping, decode, and compose/guard against a fake transport — the WASM core
exposes no bundle encoder, so decode/path-scoping replay golden `/fetch` bytes
captured from a real relay (test/fixtures, regenerate with gen-relay-fixtures.mjs
after a format bump). Relay integration trimmed to the read + write round-trip
smokes (read.behavior/write.behavior); push-errors.behavior deleted.
#433 — LootRunner seam. `run` (buffered, never throws on non-zero) + `spawn`
(streaming) + default SubprocessRunner, injected via `openRepo(path, { runner })`.
Physical error-mapping, arg composition, and pull streaming are unit-tested
against a fake runner (physical.unit.test.ts); physical integration trimmed to
the read + write round-trip smoke.
#434 — map binary error codes → LootErrorCode. physical.ts reads `error.code`
from the binary's `{"error":{"code","message"}}` under --json and maps it in one
place (demotion/mis_seal/seal_wip→guard; unsupported_format/no_repo/unknown_flag
→setup; not_found→not-found; else generic); all stderr prose regexes dropped.
`run`/`pull` append --json, so the CLI's `new`/`describe` verbs now accept --json
(not --porcelain — no consumer) to emit coded failures. loot's engine has no
conflict-family slug (it accumulates forks rather than rejecting), so conflict
stays deferred/generic — ConflictError remains exported for when a slug lands.
e75ba364 · dbf3dbe6…diff - loot bisect: binary-search history to find a regression (#390)
bec6bfc5 · dbf3dbe6…diff - loot grep: content search across history via the key oracle (#391)
e47d1412 · dbf3dbe6…diff - loot grep: content search across history via the key oracle (#391) (conflict resolution: crates/loot-cli/src/main.rs)
fd448b56 · dbf3dbe6…diff - loot blame: line-level authorship annotation (#389)
e0eb02e0 · dbf3dbe6…diff - loot grep: content search across history via the key oracle (#391) (conflict resolution: crates/loot-cli/src/main.rs)
873af425 · dbf3dbe6…diff - loot bisect: binary-search history to find a regression (#390) (conflict resolution: crates/loot-cli/src/main.rs)
8a7745fa · dbf3dbe6…diff - loot cherry-pick + revert: apply/invert a change's delta to the current line (#392, #393)
0db6666c · dbf3dbe6…diff - loot duplicate: copy a change with a new change-id (#398)
36c1463b · dbf3dbe6…diff - loot evolog: per-change-id evolution log (#397)
2c197bec · dbf3dbe6…diff - loot duplicate: copy a change with a new change-id (#398) (conflict resolution: crates/loot-cli/src/main.rs)
dfa8752c · dbf3dbe6…diff - loot split + squash: move/fold change content via ADR-0032 supersede (#395, #396)
9f3cbe33 · dbf3dbe6…diff - loot absorb: auto-distribute hunks to the nearest relevant ancestor (#399)
42de6420 · dbf3dbe6…diff - conflicts map stores base OID + loot resolve --tool for external 3-way merge (#400, #401)
a0711731 · dbf3dbe6…diff - loot doctor: diagnose common setup problems (#22)
2ea3fa43 · dbf3dbe6…diff - loot remote info + relay GET /info endpoint (#10)
0d05d373 · dbf3dbe6…diff - loot push --dry-run: report bundle size and counts without posting (#11)
51614854 · dbf3dbe6…diff - loot remote info + relay GET /info endpoint (#10) (conflict resolution: crates/loot-cli/src/main.rs)
11c60df7 · dbf3dbe6…diff - loot remote info + relay GET /info endpoint (#10)
Also fix a release-build regression from #11: push_dry_run called the test-only Workspace::repo() accessor, so 'cargo build --release' (used by the relay redeploy) failed on main. Switched it to the public bundle_wanted_batched path (the live push path); dry-run counts unchanged.
6c02fd76 · dbf3dbe6…diff - loot doctor: diagnose common setup problems (#22) (conflict resolution: crates/loot-cli/src/main.rs)
4aa36c98 · dbf3dbe6…diff - Rename the Public visibility tier to Internal (ADR 0041 §2, #480)
6969626d · dbf3dbe6…diff - Publish mechanism: the `published` keyword + @world grantee (ADR 0041, #481)
a6758352 · dbf3dbe6…diff - Publish: surface the `published` visibility token across CLI/porcelain/JSON/WASM/SDK (#481 refinement 1)
082ad335 · dbf3dbe6…diff - Authenticate the purge lane: a purge is a signed request (#503)
A wire purge carried only an oid and yielded an unauthored tombstone, and
stow honored it before storing objects -- so any authenticated pusher could
destroy any oid across every tenant, needing no key, only the address.
ADR 0038 already called a purge event a request asking cooperating relays
and peers to destroy their copy. Cooperating meant nothing: loot honored
anyone. The signature now says who is asking; a per-receiver policy decides
whether to honor. Peers honor registered senders and quarantine strangers;
relays honor their push allowlist. Local burn is untouched and needs no key
-- only propagation requires a signature, so a keyless repo burns locally
and refuses to propagate, explicitly.
No global entitlement predicate exists: ChangeNode.tree is a full manifest
and loot duplicate copies a tree address-for-address, so authored-a-change-
referencing-this-oid is satisfiable by any cloner. Both withdrawn rules
have regression tests.
Format 9 to 10. A v10 reader parses a legacy purge lane and drops it, so no
unsigned request is honored while the rest of a v9 bundle still decodes; an
older client hard-fails on v10.
Destruction is structurally unreachable without verification: HonoredBurn
has a private field and authorize_burn is its only constructor.
Both halves are signed, with domain-separated schemes -- without tags a
maroon signature re-encodes byte-for-byte as a burn tombstone at path
length 31. Maroon entitlement is an exact Manifest grantor join, and a
grant only records a grantor when it actually installed a key, so a crafted
grant cannot plant one for content you already hold.
Closes #503.
913c5cc6 · dbf3dbe6…diff - 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. (conflict resolution: crates\loot-cli\src\main.rs)
da5b4de8 · dbf3dbe6…diff - loot tutorial: guided nine-step walkthrough of the core model in a temp dir (#24)
9bae4337 · 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. (conflict resolution: crates\loot-cli\src\main.rs)
8452b07d · dbf3dbe6…diff - Forge service skeleton: /ingest, the sync surface, and CLI forge remotes (#502)
A new `loot-forge` crate and binary, sibling to the relay and never a mutation
of it: `loot-net` is a dependency of `loot-cli`, so a storage driver in the
relay would ship inside the released CLI binary (ADR 0041:53-57). The ban runs
one way -- the forge may use the other crates; none of them may gain a driver.
Every endpoint is envelope-authenticated, where a relay authenticates only
/stow. The one declared exception is GET /info: a client reads the capability
probe before it knows how to authenticate.
POST /ingest = envelope_sign(bundle || head_declaration), one signature over
both, so a genuine bundle can never be replayed under someone else's head
claim. CAS first; nothing is written before the generation compare. A lost CAS
answers with a 409 BODY carrying the current generation and head set -- behind,
not bounced -- so the client re-carries with no extra round trip.
/stow keeps relay parity and moves no ref, and consumes no generation either,
since a client reads the generation once and then stows N batches. It DEFERS a
change whose objects have not all arrived; /ingest REFUSES one, because a
declared head whose objects are missing is a repo no pull can complete. That
asymmetry is what keeping /stow is for.
Bundles are filtered PER OBJECT by the caller's access class, closing the leak
the ticket names: bundle_impl ships a key for every ANYONE-granted object, i.e.
every Internal object. Change metadata rides the second #494 axis and gates the
whole walk -- a ChangeNode carries the full tree, so shipping one while
withholding its bytes still discloses every path, address, message and author.
Validation before any write: author signatures, and a tree whose visibility
contradicts its objects is rejected. vis_tag is derived from the SealedObject
and never trusted from the tree, which is what makes content-addressed upserts
safe.
Forge purge policy: burn iff burner == object.introduced_by, recorded at first
arrival as introducing.author else the envelope pubkey; maroon read off
grant_log, global and exact. Both are judged before any object is stored and
outside the CAS transaction.
Repos are pubkey-addressed at /k/<pubkey>/<repo>, auto-created on first push,
and may_push holds iff the signer owns the namespace. resolve_remote is
untouched -- still no default host. CORS from day one.
Storage sits behind three narrow traits with in-process reference
implementations the whole suite runs against; the Postgres DDL they mirror is
checked in at docs/sql/forge-schema.sql and its driver is #516.
CLI: /info capability detection, the live head declaration off the Liveness
view, and a refusal to push a lane -- v1 ingests the primary position only.
verify_authored_change moves to loot-codec so the forge runs the identical gate
without linking the fs-hardwired engine.
Recorded in ADR 0041, the spec and CONTEXT.md: the forge stores no content key
but a published one, because filing the ANYONE key lane would let the server
read every Internal object, and the keystone is that only Published is
server-readable. The cost is stated rather than hidden -- a fresh clone of your
own Internal content from a forge is ciphertext you cannot open until #488
lands.
Closes #502.
300e06ff · dbf3dbe6…diff - Stop shipping grantee nicknames on the wire (ADR 0015, #521)
55ba71eb · dbf3dbe6…diff - Say what a relay can actually read (#520)
11055d72 · dbf3dbe6…diff - Stop shipping grantee nicknames on the wire (ADR 0015, #521) (conflict resolution: crates/loot-cli/src/main.rs)
cc53df9f · dbf3dbe6…diff - loot diff --content: unified content diffs from inside a lane (#536)
7b01b038 · dbf3dbe6…diff - surface: hedge inferred holders instead of stating them as policy (#521 follow-up)
8bc37308 · dbf3dbe6…diff - loot diff --content: unified content diffs from inside a lane (#536) (conflict resolution: crates\loot-cli\src\main.rs)
6070f90c · dbf3dbe6…diff - Deliver a relay grant inside the mutation, so a refused delivery records nothing (#546)
2aa65397 · dbf3dbe6…diff - Give a lane a session owner, so gc and rm refuse someone else's (#532)
47af2987 · dbf3dbe6…diff - 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 - relay: count the bytes a push received and report them in the push response (#633)
2dc9f961 · dbf3dbe6…diff - verbs/: seam scaffold + the repo family (init/clone/config) (#661)
28849eaa · dbf3dbe6…diff - verbs/change: the working-change family (status/diff/describe/new/edit/abandon/adopt) (#661)
cfae20f3 · dbf3dbe6…diff - verbs/oplog + verbs/history: undo/op and log move in-process (#661)
8caa506f · dbf3dbe6…diff - verbs/lane: the lane family (new/list/merge/name/rm/gc, lanes) (#661)
b4504930 · dbf3dbe6…diff - verbs/store: gc + verify move in-process (#661)
f47d0ee2 · dbf3dbe6…diff - burn::Burn: one home for the burn/purge concept (#662)
8e7834a7 · dbf3dbe6…diff - verbs/store: gc + verify move in-process (#661) (conflict resolution: crates\loot-cli\src\main.rs)
305a5978 · dbf3dbe6…diff - verbs/custody: surface/grant/grant-status/embargo-status/maroon/migrate/burn (#661)
e3e7937f · dbf3dbe6…diff - verbs/grants: the mailbox family (grants/pull-grants/purges) (#661)
faaacc8f · dbf3dbe6…diff - verbs/identity: keygen/whoami/id/peer (#661)
25ea4f86 · dbf3dbe6…diff - verbs/attest: manifest/attest/buoy (#661)
d1122e4b · dbf3dbe6…diff - verbs/conflicts: conflicts + resolve (#661)
91726d3d · dbf3dbe6…diff - verbs/sync: push/pull/bundle/apply/ferry/serve/remote (#661)
be33453b · dbf3dbe6…diff - emit_snapshot calls verbs in-process; test-support feature retired (#661)
8033f451 · dbf3dbe6…diff - Sweep the rust-1.96 clippy debt; document the land-holds-the-binary hazard (#667, #681)
26cfbaa9 · dbf3dbe6…diff - a view step refuses over work no change records, and a primary finalize says so on stderr (#436)
a3248ede · dbf3dbe6…diff - loot <verb> --help prints that verb's usage block plus its accepted flags, not all ~80 verbs (#585)
b87eae53 · dbf3dbe6…diff - a view step refuses over work no change records, and a primary finalize says so on stderr (#436) (conflict resolution: crates\loot-cli\src\main.rs)
da9f4e0f · dbf3dbe6…diff - surface refuses over an edit no change records, and --force names what it overwrote (#782)
7efc0a3e · dbf3dbe6…diff - a sparse view scopes what the working tree materializes, and nothing about what syncs (#39)
fa6c6749 · dbf3dbe6…diff - the relay's grant mailbox authorizes before it parses, and a read addresses only the caller's own (#621)
4b349b72 · dbf3dbe6…diff - a lane carries its own hold, and custody crosses back whole at finalize (#811)
941cafb9 · dbf3dbe6…diff - a relay is measured, not capped, and the only bytes it may forget are torn writes (#817)
f52d53e4 · 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 forge advertises its retention window on /info, and burn prints one honest bound per host it disclosed to (#814)
652f4023 · 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 - adopt: catch up to landed main 1b57778e
67478c73 · dbf3dbe6…diff - the reader leaving mid-pipe becomes a quiet end: one stdout boundary tells EPIPE from a failed write (#870)
08caedf2 · dbf3dbe6…diff - the argv scan asks the spec everywhere: tag, clone and burn stop reading a flag value as their positional (#869)
95b41e53 · dbf3dbe6…diff - the reader names its own holes: the completeness filter and Have retire, and pull reports what never arrived (#803)
f15d576e · dbf3dbe6…diff - the relay stops re-serving a dead line: an operator retires a stale head from the live view, and nothing is forgotten (#996)
fabfaddc · dbf3dbe6…diff - a ticket claims one lane: a repeat claim refuses instead of forking a silent sibling, and one search answers for both the id and the directory (#1002)
dda6e6a6 · dbf3dbe6…diff - ferry stops calling an unpulled change gc'd: where a relay could still produce it, the ingest refuses and names loot pull (#1007)
e896ee3b · dbf3dbe6…diff - a store knows which machine materialized it: an arrived copy refuses to record until loot rehome, so a bootstrap cannot silently capture every sealed path as deleted (#986)
4c15a2ca · dbf3dbe6…diff - a browser-minted identity reaches the CLI: loot id recover decodes the 24-word phrase to the exact seed, and a shared vector holds both implementations to the same bytes (#926)
fd153402 · dbf3dbe6…diff - the primary stops authoring: describe -m and new -m refuse on a shared floor and print the exact lane take that carries the work onward - ADR 0058's loot lane take built, --allow-primary now a triple (#985)
67df9345 · dbf3dbe6…diff - a verb that could only publish to main unreviewed is retired: lane merge goes, its fold machinery stays where adopt needs it, and the mirror-free fold that genuinely leaves with it is named rather than glossed (#1048)
3f7635a9 · dbf3dbe6…diff - the site stops understating what the forge enforces, and the host path it documents becomes runnable: --allow learns the only pubkey form the CLI actually prints (#1086)
c83e98f5 · dbf3dbe6…diff - the relay half of the honest-limits pass stops overstating itself: rotation no longer claims burn authority it never took, and the push allowlist is named as the one gate it is (#1086)
23c8d4b2 · dbf3dbe6…diff - the README stops describing a third of the CLI: What works today carries the full 61-verb list regenerated from loot --help, dock/docks are gone with ADR 0034, and a guard test pins the block to the dispatch table (#1107)
0d284658 · dbf3dbe6…diff - the seal comments name the tier the code actually gates on (Internal, not Public), and the README count guard pins the sentence it claimed to pin (#1116)
f7bce81c · dbf3dbe6…diff - custody at rest seals to your own key behind an optional passphrase, unlock is a session file or an env var, and no non-interactive caller starts prompting (#1138)
c8ffee3b · dbf3dbe6…diff - loot --help names the vulnerability disclosure route, and /docs cites the trust matrix instead of restating it wrong twice (#1142)
8e79e7e8 · dbf3dbe6…diff - loot doctor reads a solo no-remote repo as healthy and keeps failing partial sync setups (#1150)
961d7c8a · dbf3dbe6…diff - a no-remote repo converges its diverged heads with loot converge, and the diverged hint names the verb the repo can actually run (#1183)
2d9a2d31 · dbf3dbe6…diff - the alpha promise's line 2 tells the truth about machine output: 13 of 63 verbs, the contract number only on --json, and a guard that keeps loot --help's annotation honest (#1151)
95ace495 · dbf3dbe6…diff - loot clone probes the host and speaks the signed forge client, so a forge URL reaches the door instead of dying at the transport, and the sealed grants arrive with it (#1162)
98f5b662 · 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 - the alpha promise ships in the product: /install carries ADR 0066's canonical block, docs and loot --help carry the headline verbatim, and one test pins all three surfaces plus the disclosure route (#1137)
9d724d85 · dbf3dbe6…diff - the Known Issues page publishes what costs a tester their work, nine findings re-run against v0.4.16, and it is updated on report against a dated last-review rather than a cadence nobody keeps (#1143)
48f5788f · dbf3dbe6…diff - the verb counts in prose are read off USAGE instead of hand-copied, the /install pin fails on the superseded line 2 it used to pass, and ADR 0066 stops asserting the number it calls a mismeasurement (#1216)
75768f1f · dbf3dbe6…diff - the allowlist parsers get one home in loot-net, which un-forks six copies and restores the OpenSSH form loot-relayd had silently narrowed back to hex a day after #1086 widened it (#1167)
a702c5dd · dbf3dbe6…diff - a passphrase-locked repo can pull from a relay, because filing keys needs only id.pub, and it says plainly that it wrote no tree and folded no heads instead of looking like a whole pull (#1172)
cbc60097 · dbf3dbe6…diff - the unlock session holds the seed sealed under this repo's own ciphertext instead of the passphrase you typed, so a synced config directory leaks a window and not a secret you reuse elsewhere, and the passphrase is zeroized from the moment loot owns it (#1173)
1943d2c7 · dbf3dbe6…diff - the site stops offering public as a tier name in the three places a reader copies it from, and a grammar-precise pin reads the vocabulary off USAGE so the page cannot drift from the CLI again (#1185)
fd5bb36f · dbf3dbe6…diff - a wrong LOOT_PASSPHRASE stops shutting the locked-pull door it was never a key for, the one verb a locked repo runs names the session file it refused, and ADR 0068 stops contradicting itself about its own cryptography and its own amendments (#1231)
ddcf499d · dbf3dbe6…diff - a land builds the site and weighs the eager bytes of every published surface, so a redesign cannot spend the byte lead in silence, and the four places that called site/ ungated now say what is true (#1241)
674361ad · dbf3dbe6…diff - loot 0.4.17: the Known Issues page is re-reviewed against the binary it now names, four entries move because their fixes shipped, and the land runbook learns that a skip drops three gates and not one
44de9890 · dbf3dbe6…diff - the known-issues pin becomes four tests that each say what broke, the phrase-issuer guard asserts the whole public surface of the mnemonic module instead of guessing a function name, and the trust versus known-issues split has one canonical copy the other sites point at (#1217)
f751adc3 · dbf3dbe6…diff - the anchor refresh and the deposit race become shared contract rules every relay backend must answer, the reap report has one renderer instead of a drifted hand-copy, and a rule that is written but never asked is now a test failure (#1223)
d31bf81e · dbf3dbe6…diff - the custody sweep residuals are answered on the record: the sealed-path demo is re-run so the evidence page shows the binary it names, the disk scan learns base64 and UTF-16, the pull entry points drop their bool, and the mailbox keeps failing loud by decision (#1232)
333e9517 · dbf3dbe6…diff - a seal to a holder list that excludes its author refuses and names what nobody could ever open, --allow-lockout is the deliberate way through, and ADR 0038 records why the gate refuses rather than filing a key for the author (#1249)
bbb858b0 · dbf3dbe6…diff - loot 0.4.18: the Known Issues page is re-reviewed against the binary it now names, the seal-away-from-yourself entry leaves because the lockout gate shipped, and the reverted-peer-edit entry narrows to the locked pull that can still do it
29f571e7 · dbf3dbe6…diff - four publication pins leave the binary for tests/published_surfaces.rs, and the five that read USAGE and COMMANDS stay in main.rs with the reason recorded beside the pointer (#1292)
47881eb5 · dbf3dbe6…diff - the working-change artifact declares the fall-through its readers always had and the walk reader is the one recorded refuse-shaped exemption, and the test-side repo walk-up has one home in tests/common with main.rs named as its only twin (#1309)
51361264 · 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 - a verb run from a subdirectory finds the repo instead of advising an init that would nest a second one, path arguments rebase from the cwd the way a tester types them while view's globs stay root-relative, and the resolved root is named on stderr whenever cwd is not it
eca43819 · dbf3dbe6…diff - the completion table stops disagreeing with the CLI it completes and the test over it stops passing either way, deriving its expectation from USAGE so drift fails in both directions, and PowerShell joins the three shells on a shipped Windows triple
20e4e5c6 · 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 - six entries this run deleted for fixes only unreleased main carries come back marked as such, because the page says it describes v0.4.18 and that is the binary a stranger can install, and a fifth pin holds every one of them until the release that fixes it moves REVIEWED_AGAINST
7adab105 · dbf3dbe6…diff - discovery asks one structural question at every depth instead of two that disagreed, so the installer's own dot-loot is never a repo even standing in it and unlock can no longer be handed that directory, while a damaged repo refuses in its own name from a subdirectory rather than letting an ancestor answer
974715e8 · dbf3dbe6…diff - loot view --prune removes the paths a narrowed view stopped covering and refuses whole, naming every one, when any of them holds an edit no change records, taking nothing out of history so a clear writes them all back
de9f395a · dbf3dbe6…diff - the four consents become one owned Consents parsed and rebased at a single argv door, so allow-demote stops travelling a route of its own and the transposition Allowances was built to prevent has one place left that could still make it instead of eight
ffb85a76 · dbf3dbe6…diff - the docs become fifty-six routed entries derived from one registry that the nav, the reading order, the search and the prerenderer all read, so those four can no longer drift apart the way ten hand-listed surfaces already had to be watched; the thirty-eight pages nobody has written yet are real bookmarkable URLs that say what they will cover and carry noindex until they earn a place in an index, rather than dead rows in a sidebar; and the byte gate learns to walk nested surfaces, which is the only reason the fifty-two new pages weigh anything at all instead of shipping past a gate that read one directory level (#1378)
424be083 · dbf3dbe6…diff - the docs demo runs between two markers that cannot silently widen, and the six surfaces still sending readers to a dead /docs anchor name the route that exists, with a test that fails the next time either class rots (#1394)
3493a354 · 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 - the store stops threading a dock nothing can ever produce through 43 signatures, because a store instance is already the position ADR 0034 retired named docks in favor of, and the sixteen path getters no caller outside loot-core reaches stop being public while the one main reservation that guards a git ref name stays (#1413)
23ab3abd · dbf3dbe6…diff - an extra positional stops being dropped where a subcommand reads its arguments, because every leaf in the eight families now declares its arity beside its flags and check_sub refuses the token nobody expected by name, which caught a lane new t502 in the suite that had never carried its handle (#1419)
90472de4 · dbf3dbe6…diff - a verb that names a file in this repo stops carrying its own copy of the #1330 rebase, because every argument slot's kind is now a required field of the table that already declared the leaf's flags and arity, so the argv door rebases from the declaration and a leaf that states nothing does not compile (#1466)
889e5821 · dbf3dbe6…diff - three claims stop outrunning the code they describe, because ADR 0007's escrow census is now the output of a grep the ADR states rather than a list someone re-derived and left two files short, `Args::any_path`'s short circuit now runs above the ancestor walk it always claimed to skip and is pinned as a walk that does not happen, and CONTEXT's settle count now names the unit it counts — which is what made a number that was right read as wrong (#1482)
bd385596 · dbf3dbe6…diff - a shared CLI option stops being declared once per verb, because the argument table now carries option groups a leaf attaches in one line — so the pathspec four separate tickets were each going to add is stated once, and a leaf that attaches it inherits the PATH slot kinds, the cwd rebase and the empty-match refusal without restating any of them, while every flag traversal reads the derived list so help and the refusal cannot drift from what the gate accepts (#1537)
24404922 · 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 - losing one bad file stops costing you every other uncaptured edit, because `loot restore <path>...` discards per path against the same delta seam `status` reads — and it never decrypts to decide, so a path whose recorded side is sealed, embargoed, burned or merely past its grant expiry refuses the whole run rather than clobbering the readable half beside it (#1498)
cd463799 · dbf3dbe6…diff - a named landmark stops needing a format major to carry its message, because a landmark IS an attestation under a reserved role envelope the signature already covers — and that is also what keeps a retirement from eating the thing it retires, since the attestation log is keyed by (change, attester, role) and writing both under one role made the retirement silently REPLACE the attestation rather than record beside it (#1508)
2d06014f · dbf3dbe6…diff - a 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 0.4.19: the Known Issues page is re-read against the binary it now names by running it rather than re-reading its source, the six entries pin 5 held leave on the release that finally carries their fixes, and the pin retires with its emptied table on its own instruction — the re-run is what caught the evolog entry rotting without an edit, because loot log grew a when column somewhere in this range and the entry had claimed for two releases that evolog was the only time the CLI prints, and the recovery-door note is rewritten rather than deleted since loot id phrase shipping does not change that both it and recover open the repo first; and the restore no-op test stops calling its own instrument blind when the clock is the coarse thing, because it stamped the setup write and the restore microseconds apart against a file clock that ticks every 15.6 ms — it was failing four runs in five on main, standalone, while passing inside a full suite slow enough to straddle a tick, so it now probes for a distinguishable mtime rather than sleeping a constant that would bake this filesystem granularity into the assertion
4dcc9ff6 · dbf3dbe6…diff - the valued half of the argv door gets a floor: FlagSpec grows value() and message() so a valued flag's payload is stepped over by the spec that declared it rather than found by a bare scan of raw argv, and all 29 readers move onto it along with six more the ticket's list had missed, which is what left loot ferry --dock --git-dir yielding a --git-dir nobody typed while its --json twin was the single case anyone had pinned; and the census meant to stop this class recurring widens from the previous bug's exact spelling to the class itself and gains a positive control, which caught two further hand-rolled scans in help_sub and resolve_tool_path and took the crate from eight offences to zero (#1652)
f86ffb25 · dbf3dbe6…diff - telemetry gets a contract before it gets a sender: ADR 0074 settles what an opt-in loot binary may report, and the alpha promise stops enumerating what leaves your machine because that list could not have absorbed a fifth item anyway - a telemetry report is not experimental in the sense line 3 means, it is us. A report is a fact about the tool and never about the repo: every verb is counted with burn and maroon collapsed to other because each records an incident rather than a feature, timings are refused as repo scale laundered except one bucketed Workspace::open, and the rule that decides future fields is enforceable rather than prose because the report carries no runtime-constructed string except the telemetry id, which forecloses paths, messages, search terms and repo names by construction. machine-id is not reused because the arrival guard and an unlock session compare it, so exporting it would ship a security binding value to a server we operate. The spool is the outbox so show cannot drift from what is sent, the opt-in is a verb and never a first-run prompt because the dominant user of this CLI is a non-interactive agent and an opt-in a machine can trip is not an opt-in, and the relay half splits because loot serve runs from this same binary on a machine somebody owns while relayd and forge would be reporting on third parties who never opted in. What ships is the contract plus the guard behind the rewritten line 3, which sweeps every rs file under a crate src, blanks cfg(test) items by brace depth, and allows six occurrences each carrying a written reason - and it was checked against a planted destination before being trusted (#1626)
f2998fea · dbf3dbe6…diff - the two censuses that each carried the other's blind spot stop being two walks and become one, and the defect turns out to be four walks rather than two: flags.rs cut production at the first line starting with cfg(test) and never restored it, so 55.5 percent of loot-cli src sat exempt from its second clause at 36164 of 65168 lines - and the 60.0 percent this ticket claimed was measured with a looser predicate than the walk actually used, which is the ticket committing the very class it was filed about - while ADR 0074's guard scanned from any marker forward to the next brace and lost 635 production lines to semicolon-terminated items and to markers sitting inside doc comments; a third walk in main.rs split on the marker string, and a fourth in custody_lock broke at the first marker line and was right only by luck, because all three write_session callers happen to sit above their own files' first marker. the shared walker classifies every char as code, comment or literal before it looks for a marker at all, and ends an item at the first semicolon at depth zero while counting parens and brackets alike, because endpoint.rs's const ALL takes an array of thirteen routes whose semicolon lives inside the type and ended the item early in the first draft, keeping fifteen lines of test table as production. the floor is a discrimination rather than a count, since a count passes on both original bugs: it asserts that a known production line below a marker is kept and a known test line is cut, and it was broken in both directions to prove it can fail. every blindness was demonstrated by planting a live violation in production source and watching both censuses stay green before the change and exit 101 after it; and Clock::time now refuses a second call on one repetition rather than silently reporting only the last region measured (#1665)
12e18beb · dbf3dbe6…diff - a repo open stops materializing half a million manifest entries no verb asked for: ChangeNode.tree becomes a Manifest holding either the decoded map or a proved-decodable byte range into one shared Arc, so topology decodes eagerly because reachability needs it while a change's path manifest decodes only when something reads it - which is where the cost actually was, since framing the 37.9 MB graph without building the maps takes 6.8 ms against 507 ms to build them, and that 507 ms was the bulk of the 1050 ms every verb paid, including loot whoami whose whole job is to print 93 bytes out of id.pub. Deref carries the ~270 existing node.tree read sites unchanged and Deferred is module-private, so a deferred manifest cannot exist over bytes Manifest::walk has not already refused with the same framing the eager pass uses, which is what earns an infallible &TreeManifest return rather than a Result; and an unmaterialized manifest is pinned never to read as an empty one, the rule read_graph_required already states for an absent graph file. Verbs also declare what they read now: RepoNeed rides beside CustodyNeed through the already-pub(crate) open_at_clocked_needing, so no new public door is added and the sealing door custody_lock pins by string and call-site count is untouched, with whoami, conflicts and manifest narrowing to POSITION_ONLY, defaulting to EVERYTHING everywhere else, and a withheld graph or store aborting loudly rather than reading as empty. whoami 555 to 42 ms against a 40 ms control, conflicts 557 to 41, manifest 557 to 42, log -n 5 626 to 144, status 704 to 227, lanes 4069 to 717, peak RSS for whoami 148 to 7.1 MB, and twelve verbs byte-identical against main including 414 KB of loot log. Three of this ticket's premises were wrong and are corrected rather than worked around: the keyring it named as the second cost measures 1.1 ms and is left alone, read_graph_outline did not fit because it discards the parents reachability needs, and the object-store half it called the boolean that costs a gigabyte had already landed in #1545 - while the stat cache stays unbuilt, which is the whole of what status and lanes still spend (#1536)
b923abb1 · dbf3dbe6…diff - loot log grows --graph, and the half this ticket is named for turns out to have landed already: #1494 built the per-head sectioned listing, so the 255 refusal on a diverged repo was gone before this started and three of the six criteria were already met - and --all is dropped rather than built, because loot's default listing is already the union of every head while git needs that flag only to widen from HEAD's ancestry, so porting the spelling would have shipped a flag that changes nothing. What is left is the drawing, and it goes in behind a seam rather than inside the formatter: topology.rs resolves each row's recorded parents into indices within the listing and flattens the fork's three sections into one children-first sequence, holding the two derived facts a JSON parents array and an ASCII gutter would otherwise compute separately and disagree about silently, so #1540 can emit machine topology off Topology.nodes without touching lay_out or draw. HistoryRow gains parents from graph_view, ids only, so no manifest materializes and the 507 ms d6be741 removed stays removed - and no DAG walk was needed at all, because the per-head and shared split is already a valid children-first order, which makes the whole feature one map lookup per recorded parent rather than a second traversal. The empty head is named above the drawing rather than drawn, since a head recording no changes of its own is the one fact a picture cannot show by drawing it and a two-head repo would otherwise look unforked, and the shared-history heading goes away under --graph because the join is visible as a merge line instead of a third section to correlate by eye. The fork pin is extended rather than replaced, --all is pinned as refused with its reason in both the unit gate and the smoke test, four independent breakages were each proved red before the tests were trusted, and a join drawn as a fork opening - which abandoned the leftmost column and indented a thousand rows under a lane that had already ended - was a layout defect only the real two-head repo showed (#1497)
e963a2a9 · dbf3dbe6…diff - the run's own signature defect, found inside the file it built to fix it: source_walk said four consumers while the very next land added a fifth, and the sentence is now generated rather than corrected - it walks src and tests through the module's own code mask so a mention in a comment or a string is not a call, groups by enclosing function because the destination census reaches the walk through a use import and a call-site count would miss it entirely, and pins the shape as well as the number by requiring every consumer's file be named, the declaring units equal the consuming units, and every consumer call the discrimination floor first. that last requirement is where this ticket was wrong: it said one consumer skipped the floor and that two others called it, but the crate held exactly two call sites, so three of five skipped it - all five call it now, and the reason is recorded at the verbs site, since its three anchors check that this walk found Workspace's methods while the shared floor checks the walker on the two cases it was written for and this tier has none of. ADR 0074's section 9 stops describing a walk that no longer exists: only the mechanism sentence moves, with a dated amendment quoting the old text verbatim and saying why it was worse than stale, because it named a bug the walk did not have while carrying two it did, while the purpose paragraph and the six-entry allowlist claim are untouched. the trust page's twelve-item enumeration goes, and the durable half is a fourth pinned surface beside install, the ADR and CONTEXT: the positive arm pulls line three out of ADR 0066 itself and requires both pronoun-free halves verbatim rather than typing them, the negative arm forbids a code element inside the alpha section because that is the regrowth shape twice observed, and both were proved red before being trusted. two stale copies this ticket did not name are repointed at the derived header instead of restating a number, and ADR 0066's own amendment turns out to name four things while listing five, which git show against the pre-amendment line settles as five (#1676)
c1a195b9 · dbf3dbe6…diff - the pathspec option group gets its first production consumer, and being inert turns out to have been invisible by construction: all five .groups attachments sat inside cfg(test), so Pathspec::keep returned at its is_active guard on every production call and no test could see it, because a filter that never runs changes no output - which is why delta.rs claimed in the present tense that diff, status and show were all served by that line while none of them attached the group. diff and status now attach it, diff narrowing at #1541's seam so rows, --stat and --content inherit one filter rather than each applying its own, and a -- separator arrives with it because a pathspec needs a way to say the rest are paths. Two states the ticket did not name are where the real bugs were. status builds its delta lazily, gated on the human format, so --porcelain and --json never reached the seam at all and attaching the group alone would have left both machine formats printing an unfiltered listing at exit 0 - the format an agent reads, which is the caller this class is dangerous to; and with no working change status returns before reading the tree, so loot status zzz would still have exited 0 in the primary checkout while passing every test on a seeded temp repo, since a seeded repo is never empty. Both are now answered from the seam, the second gated on is_active so pathless callers pay nothing. status also stops silently dropping its argument, which removes it from OPEN_BUT_TAKES_NONE and leaves 23 - by the other route, taking paths properly rather than declaring NO_ARGS, and the list's prose implies the latter is the fix for the names still on it, which would foreclose the better answer for surface, converge and ferry. The false sentence is narrowed rather than deleted and carries why it was written a ticket early, in delta.rs, flags.rs, the census doc and CONTEXT.md. Eight breakages proved the tests non-vacuous, planned before they were written, and dropping both attachments - the exact pre-ticket inert state - turns six of six process tests red plus the census pin (#1493)
35ed624c · dbf3dbe6…diff - 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 - tag and buoy stop disagreeing about whether a landmark is live, and the disagreement turns out to have been built by a sentence that was true of the resolver and read as true of the verb: both module docs said buoy is untouched by construction, which holds for loot_core buoy resolve and is false of the verb the operator runs, so the fix is entirely in what buoy consults and resolve is not changed by one line. a retirement annotates and never withdraws - that was already decided rather than open, since ADR 0025's #1508 amendment calls a retirement advisory, and four recorded facts each independently forbid withdrawal: there is no trustworthy order to withdraw by because newest is topological precisely since authored_at is advisory and never an ordering input; the attestation log is keyed by change, key and name so re-attesting after a retirement is a no-op, which would make retire the one permanently destructive verb on an append-only lane by a route that deletes no byte; buoy trusts peer-registry union self, so under withdrawal any registered peer could silently un-name somebody else's landmark; and the result would be the mutable ref that ADRs 0022 and 0025 exist to reject. so buoy now reports the retirement it already resolved through, in all three formats, additively - the B row and the human answer line are byte-identical to their pre-fix selves and the porcelain contract pin stayed green through a revert that reddened three others, which is the additive claim proving itself. an un-retired control runs in the same test so both-mention-retirement cannot pass on a verb that always mentions it, and four independent reverts were each proved red including one dropping the trust gate, which surfaced a stranger's validly signed retirement. no record is deleted or rewritten, so #1508's AC3 stands and its pin passes unchanged; the false sentence is narrowed at both copies rather than deleted, naming resolve as its subject (#1611)
ac8ee01f · 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 - the mis-seal question gets a verb, and the reason it could not have one turns out to be that the decision existed three times: visibility_for, is_published and public_by_fallthrough were three near-identical loops over the attribute rules, so there was nowhere to hang an explanation off and the rule against writing a second matcher was impossible to honour until they collapsed into one Attributes::first_match. That collapse, not the line numbers, is this change - the line numbers were already tracked since #1329 for MalformedRule, including the convention of counting blanks and comments so a number matches what an editor shows. loot explain now names the .lootattributes line that decided a path, lists what it passed over so first-match-wins is visible rather than asserted, reports which .lootignore line covered it, and answers for a path with no file on disk, which is the mis-seal question asked before the mistake rather than after. Because explain and the real decision are projections of one traversal they cannot drift, and that is pinned twice rather than assumed: one test deletes the line explain named and asserts the real visibility moves, then deletes each line explain called skipped and asserts it does not, hardcoding nothing about which rule should win; the other runs a real capture and asserts that for every recorded path the sealed tier equals explain's token, with the ignored file absent and a positive control that the sealed set is non-empty. Eight breakages were each proved red before the tests were trusted, including giving explain its own matcher while the decision kept the old one. And loot explain . would have printed a fully furnished answer about a path that is not one, because ** matches the empty string in this glob dialect and the door rebases a bare dot at the repo root to the empty relative path, so in any repo carrying the catch-all ADR 0038 says every real repo wants it would have named a tier and a line for nothing at all - refused at the door now, with a control showing the rules really would have answered. Found by running it rather than reading it. Five censuses needed updating where the plan named two, and the two nobody would predict are the README verb block, which is a docs file failing in the bin suite, and the derived impl-width sentence in verbs/mod.rs, because adding a single inherent method to Workspace reddens a doc comment (#1521)
ebd3285f · dbf3dbe6…diff - the spelling a newcomer and an agent both try first stops printing the whole wall: loot help <verb> ignored its argument and rendered the 157-line overview, so the discoverable form - the one that exists because git help <verb> does - was the one that did not work, while the working form had to be guessed. It is a routing fix and not a feature, because the per-verb printer already existed and reads FlagSpec::flags so a verb's help cannot drift from what its gate accepts; what was missing is that the help arm never looked at what followed it. help_for now returns the same name, spec and tail triple the table's own help gate builds, so there is exactly one printer and this verb reaches it rather than a copy - and help_sub splits into help_sub_at returning the index, because the routing needs what follows the token and two scans walking argv by their own rules is the class #1612 records. The pin is byte-identity rather than shape: loot help X and loot X --help produce the same bytes for all 69 dispatched verbs and every completed subcommand leaf, and the list is derived from loot completions bash, itself built from COMMANDS, so a verb added later cannot quietly get one spelling and not the other. Four breakages were each proved red before the tests were trusted, including reverting the arm to the unconditional wall and leaving the verb in the tail. A typo now gets the nearest verb and a stranger gets none, which required writing the first edit distance in this repo rather than reusing one - the brief said not to write a second and there was no first, loot-first hardcodes a five-name try list and loot's own unknown-command path prints the entire USAGE, which is the same damage this ticket fixes and is left to its own ticket rather than smuggled in. Two counts this change had to touch were both wrong in the same direction: a comment saying all 66 verb lines was caught by the census that reads USAGE, which holds 105, and the table is now 67 rows rather than the 66 recorded three lands ago, since a verb_of row landed in between (#1520)
8e103df8 · dbf3dbe6…diff - the fix-up a review sweep asked for, and the sweep's own premise was wrong twice: explain's porcelain row stops being two frozen positional booleans and becomes the comma-joined open set heads and log already document, with policy-file added as a genuine third flag so the growth is demonstrated rather than promised - and that flag makes a policy file distinguishable from an ordinary not-ignored path for the first time, since false true said nothing, so the #62 answer reaches porcelain at all. The pin asserts one row width AND at least three distinct flag cardinalities, which is what makes it discriminating: reverting to the two booleans leaves the width clause green because the row is still constant and fails only the cardinality clause, where a literal-string pin would have passed the revert entirely. ADR 0023 gains the explain amendment its own rule required, describing the corrected shape rather than the one being replaced, since #1540 wrote one verb at a time each with an amendment here and #1521 then cited this ADR while amending 0066 only. The stale verb_of figures turn out to sit in three places rather than the one the ticket named, and the third is inside the doc comment of the census whose whole subject is that a count is not a census, saying the table holds 66 rows where it holds 67 - so the number is deleted and the property asserted instead: a test derives both spellings, pins that 54 plus 13 accounts for the table, and reads emit.rs's warning back to assert it still exists exactly once and carries no count at all, which fired on its author's own first draft over the word two. PROSE_ONLY gains a ceiling so appending a name goes red rather than being the cheapest green, and the claim narrows to exactly that rather than inventing fifty reasons no assertion can read - the sentence is kept because it carries the argument for the list existing instead of forty-six tickets. source_walk.rs moved because the new census is a sixth consumer of the one walker and its header is generated by the test that counts them, which is the mechanism #1676 built after that header said four for too long. And this ticket argued urgency from a contract that froze on landing: explain is after the v0.4.19 tag, so no released binary ever emitted the old row and no consumer could have read one - the shape is still better, and it was cheaper to change than the ticket claimed (#1696)
0dc35b55 · dbf3dbe6…diff - loot show becomes the fourth rendering over the one delta seam and grows no differ of its own: it calls Endpoint::base, ws.content_delta and render_delta, produces no hunk, counts no line and matches no content, and the pin is byte equality rather than shape - show ends with exactly the bytes loot diff --content <parent> <sel> produces in both content and stat modes, with the leftover prefix asserted to name no path, hold no hunk header and hold no disclosure line, so a second opinion about the parent or a renderer choosing its own words for the seam's tally both go red. Four breakages proved it, and a fifth attempt was a false negative caught by its author: truncating to the last newline is a no-op, so it passed, which is exactly as misleading as a vacuous test and is why it is named here rather than quietly replaced. Three of this ticket's premises were wrong. The two commands it says reading a change takes are really one command plus reading a parent id out of loot log by eye, because <version-prefix>~1 is not a form the #305 grammar spells and only HEAD~<n> is. Subject does not mean what the criteria assume: the tree defines it as the first line of the message and this repo writes single-line paragraphs, so the first run printed a two-thousand-character subject row - nothing truncates anywhere in the CLI, so rather than invent a rule the subject moves to the last header row, above the diff where git show puts the body, instead of pushing the lineage off screen. And the no-selector arm was ambiguous in a way that made the first message false: loot status prints a live working-change row computed off the disk under ADR 0030 while working_id and the @ selector both say there is none, so saying nothing is in progress contradicted the verb next door about the same tree - the message now names which absence it is, explains the live row, and points at diff and status, and show is deliberately not widened to render anchor against disk, because that delta is bare loot diff's by construction and a second verb answering it under a header claiming a non-durable version id is the duplication #1541 deleted. A merge names which parent it is taken against and prints the command for the other side rather than pretending the diff is whole. PROSE_ONLY_CEILING rises 50 to 51 deliberately with its reason written beside it, which is the mechanism landed one change ago doing what it was built for, and a pre-existing comment claiming the arity gate is a no-op for all but one verb is repointed at the property since it has been four for some time (#1492)
7ff52d8e · dbf3dbe6…diff - user-declared gates arrive as data rather than as hooks, and the decision the ticket owed splits rather than picking one of its three options, because the gate vocabulary is two halves with different properties. The mechanism moves down into loot-cli, since its value is concentrated in the fake - a FakeSpawner that panics on an unstubbed step is what turns no process spawned from a claim into a failure, and two fakes drift with a fake that has quietly stopped intercepting being indistinguishable from one that works; loot-first keeps a re-export shim so no orchestrator caller changed. The phase axis is duplicated deliberately because it is not the same axis: loot-first asks about two trees, one of which only a land produces, while the CLI asks about two commitments, and a shared enum would carry four variants of which each consumer must refuse two - a rule nothing checks, where two small enums are checked by the type system. A new crate was rejected on arithmetic rather than taste, since loot-first already depends on loot-cli and there is no pair needing a bridge; loot-core was rejected because only half of #1682's criterion transfers, and that half is now a census asserting the engine spawns nothing rather than a sentence. A gate runs after the capture and can never prevent it: it does not decide whether the tree is recorded, only whether it is signed or sent, so a refusal costs the signature and nothing else, which is the ordering #174 already chose. On the confidentiality boundary the absolute form of the criterion is unachievable by any mechanism that runs a command in the working tree, and saying so is better than a strong claim that will not hold - loot surface writes and never deletes, so a path whose grant lapsed stays on disk as stale plaintext readable by cat, grep, the editor and every process running as the operator. The guarantee is therefore the narrow checkable one, that a gate is granted no read the operator does not already have, held up by four facts each of which a regression would have to break first: the runner never sees a Workspace, nothing is materialized, a Step carries program args and cwd with no environment channel, and there is no shell. A fifth fact answers a different attack - the declaration is untracked, so a pull cannot write one, which is git's own reason for never transporting hooks and matters more here since ADR 0075 has the forge holding proposals. This ticket also understated the surface that already exists: loot resolve --tool hands a user command decrypted plaintext today via LOOT_BASE and its siblings, so gates are not the first such door but they are the one that opens none (#1538)
44d4869a · dbf3dbe6…diff - a contributor can offer work to a repo they do not own: loot propose over a three-path family - posture, propose and withdraw - and none of them is the ref-moving endpoint, which stays single because ADR 0075 refused putting the contribution path one flag away from the path that advances someone's tip. The bytes ride /stow unchanged, since the forge's stow literally calls the same push, so the batches are byte-identical and there is no new transport. One premise had to be WIDENED and the reason is a real replay: the ticket says sign the claim exactly as the head declaration does, but a head declaration names no repo and does not have to, because require_pusher catches a replay at another namespace - while a proposal is BY DEFINITION a write by a non-owner, so every repo with an open door admits the same signer, and a tip-and-base-only envelope is replayable by any observer at every one of them, in the contributor's name. The claim therefore carries the repo path and the server compares it against the URL's namespace, proved by RUNNING the replay and watching the second namespace admit it rather than by arguing it. The disclosure is an ORDERING rule and the posture route exists only to make it one: a client that learned metadata_public from the act's own answer could only ever speak afterwards, so posture comes first, the disclosure is printed second, and only then the ref read, the stow and the claim. It is pinned twice - once by a transport that records every call into the SAME trace as the output lines, so the assertion compares positions rather than presence, and once by byte offsets in real CLI stdout against a real forge - and a shut door still discloses first and sends nothing at all. Withdrawal is the tip change's AUTHOR and only them, where visibility admits three readers, because seeing is not acting and the carrier's remedies are the author's withdrawal or the owner's decline; it does not consult the door, since a shut door must not strand what is already offered, and absent and concealed refuse in byte-identical sentences. The lane refusal keeps every one of its old assertions, including that nothing reaches the forge before it fires, and gains the sentence naming the alternative. Twelve breaks were each run and read before being restored byte-identical, and the ones worth naming are the replay above, deleting the author check so a presenter can withdraw work they did not write, adding the door to the withdraw path so a shut door strands an offer already disclosed, and declaring the tip as a head so the CAS counter moves. The ref-moving path is asserted untouched rather than described as untouched: a proposal leaves generation and head set alone, and the SAME fixture then lands a change the ordinary way and the generation does move, which is the control (#1651)
83c5afa5 · dbf3dbe6…diff - loot merge <head> is converge restricted to one head rather than a second fold: merge_head and converge_heads both reach the graph through one private fold_onto, so the three-way content merge is the ADR 0044 seam unchanged, the conflict record is the one loot conflicts and loot resolve already read, the merge node carries a minted mechanical subject naming the head it folded, and loot undo steps it back through an op recorded off the merge node rather than off an outcome map that can be empty. What it deliberately does not do is retire anything: converge drops superseded heads on the way past, and a retirement is a change to a head the operator did not name, which is the whole of what separates this verb from the one it restricts. A flat head is refused rather than silently skipped, because naming a divergent co-version does not make divergence content-mergeable. Two premises of the ticket were wrong. The un-described-change refusal does not apply as it does to the other merges: that refusal belongs to adopt, raised inside fold_line_in, which captures and signs its own merge parent, while converge never reaches it at all because capture-first defers on any working change one branch earlier - so this verb inherits the deferral, and the refusal is unreachable by construction rather than reimplemented. And loot heads resolves no selector, it only lists, so the spelling merge takes comes from loot abandon --head, which routes through resolve_selector. A third premise is narrower: conflicts are recorded and nothing proceeds, but a single named head has no loop to stop, so that criterion is merge_tips behaviour unchanged rather than anything this change makes true. One inherited shape is named rather than altered - a deferred merge --porcelain prints nothing and exits 0, exactly as a deferred converge does, because a deferral rides the human field while the machine contract is verdict rows (#1505)
6107561a · dbf3dbe6…diff - loot format-patch lands as the fourth rendering over the one delta seam rather than a second differ: a header block plus Rendering::Patch, which drops the four-space body indent every other rendering files its content under and prints git's extended header, so what comes out is a real unified diff that git apply --check accepts and a real git apply reconstructs byte for byte, proved by applying it in a scratch repo and by a strict applier in the tests that checks every context and deletion line against the base at the line the @@ header declares, since a shape assertion cannot see a wrong range. Two of the ticket's premises did not survive contact. AC3 asks to omit AND NAME, and a sealed row has no name to give: the #306 rule already withholds it, so what is named is the row, its class and content address and tier and the seam's own four-way reason, and the wording of the decision promised a path this export is not allowed to print. And a rendering over seam ROWS alone is not quite sufficient for a faithful patch: the hunks come off blame::lcs_match's line space, which ate both the trailing newline and the carriage return, so the no-newline marker is recovered from the row's raw bytes instead, and a CRLF file's patch is LF, named as a limit rather than closed with the second differ this ticket was rescoped to avoid. One neighbouring behaviour is deliberately not inherited, the sanitizer: a patch exists to reconstruct a file, so caging a control character would emit a patch that rebuilds different bytes than the change recorded, silently, and content therefore leaves verbatim with the usage line saying so. Everything else is read rather than re-derived, the path set and each row's rung and the closing in-the-clear count all being the seam's, and the header is loot show's Header widened by the author pubkey and the whole message rather than a second read of the graph. That header is designed as loot apply-patch's input: its own format number on the first line, full-width ids because a patch outlives the store a prefix would resolve against, absence spelled none rather than the dash every human header uses, and the message folded RFC-822 style so that no body can collide with a delimiter. (#1512)
9b32538e · dbf3dbe6…diff - loot count-objects lands, and the constraint it is built under is that its number has to be the number gc acts on rather than a second opinion about it: the unreachable count and the reclaim figure are gc OWN pruner and sweeper called with dry_run, over a keep set extracted out of gc as DagRepo::gc_keep_set the way 1505 pulled fold_onto out of converge, so a census that disagrees with a collection is not a thing that can be written, and the pin measures a store, really collects it, then asserts the two halves and the sum all match. One premise of the ticket was wrong in the direction that saved work: nothing reported how large a store is as a VERB, but the totals half was already factored, since RepoStore::objects_size has been the perf record load-bearing counter since 632, so this adds no third walk of the object directory at all, it adds a caller. The extraction surfaced something gc had been getting away with: its keep set was position-dependent, because the primary own working change is the one root in neither the shared graph file nor the lane registry and verify has been adding it by hand since 1309. gc could not notice, since it refuses from anywhere but the primary, where those oids ride in the loaded graph anyway; a read-only census can run from a lane, so the root moved into the shared function and the answer stopped depending on who asks. Machine output is this verb product rather than a garnish, which is why the porcelain is one row per metric keyed by a leading mark instead of one wide row: ADR 0023 makes a column unaddable, and a census is the shape most likely to be asked for one more number. The anti-vacuity pin is the same fixture reading 1 and then 0 with nothing changed but a change naming the orphan, and the read-only pin fingerprints every file under .loot rather than trusting the op log, which would never have shown a pruned object. On this lane own store it reads 7910 objects at 146.7 MiB with 2330 of them unreachable at 36.9 MiB, which is the 1524 baseline (#1523)
f10ba1d9 · dbf3dbe6…diff - format-patch stops filing re-seals under a heading that promises the opposite, and its header starts saying in numbers what the body could not carry. the omission list headed itself so that a path left out is never read as a path that did not change, while rung 2 - content is byte-identical, only the seal moved - was filed under it, so the closing N path(s) omitted added withheld paths and re-seals together and neither reader could recover their own number. there are now two headed lists and two closing counts, and patch_omission returns which list a row belongs on rather than a bare reason string; the bytes-differ-but-lines-do-not row stays on the withheld side, because something really changed there and this patch cannot carry it. the header gains omitted, omitted-reasons, unchanged and lossy, always present and 0 or none when empty so that nothing was left out and this writer does not say stop being one observation, all read off a single PatchFidelity the body's own classifier produces rather than a second walk that could call a patch complete while the rendering withheld three paths. PATCH_FORMAT stays 1: the rows are additive and the format had zero consumers, so there was no reader a bump could protect, and that reasoning expires the day 1513 ships. included stanzas deliberately carry no tier - it would have to ride in git's extended-header region, the one place that can cost the git apply promise, and a row's visibility is today's standing .lootattributes policy rather than the version's recorded marker, so exporting it would ship the sending repo's policy as if it were a property of the change. lossy is measured from the included stanzas' own bytes rather than declared, and the end-to-end smoke shows it firing on a real crlf file whose rebuilt bytes are lf. the git apply promise is pinned at last by a test that shells out to real git and skips with a printed reason where git is absent, and the mutation proving it non-vacuous found something worth recording: git apply is lenient about line numbers - shifting every hunk start by one left it perfectly happy - so the strict in-tree applier sees a defect this test cannot, and neither is redundant. beside that: converge gains merge's N path(s) need resolution summary from a shared helper, since the fold a no-remote repo reaches was the one printing conflict rows and then converged onto one line with no next move; hunk_body's comment stops claiming to be the only place file bytes reach a terminal, which patch_hunks made false the day it landed; the fourth-versus-second rendering count is settled at fourth, which is what Rendering's own members count; and the census seam records that a census run from a lane roots the primary's working change only through the working-change blob, so a torn one over-reports unreachable while costing nothing, the run being dry and a real gc re-rooting those oids from the primary (#1786)
e41bae33 · dbf3dbe6…diff - loot restore grows --source and it is the same verb with a different base rather than a second reader or a second writer: Workspace::delta_against takes the base mirrored_delta used to hardcode, so the disclosure gate, the four-kind absence ladder and the refuse-whole rule are inherited rather than restated, and what the seam hands back is now the base TREE rather than its key set because a caller re-sealing restored content has to be able to say what the base recorded it as. AC3 is plan-then-apply: every action is materialized from verdicts that were all decided above the refusal gate, so an openable path named FIRST beside an unopenable one is not written, which is the assertion a write-as-you-go loop would fail and the pin names it in that order. Three things are decided rather than inherited. A path the SOURCE does not record is reported and its local copy kept unless --delete-absent is passed - refused without --source, where it could only be a silent no-op wearing a destructive word - but a path in neither the source nor the working tree still refuses, because that is a typo and #67 says a typo must not exit zero looking clean, and AC4s deletion half presupposes a local copy to delete. The loss sentence is a different sentence: what a --source write replaces is recorded content plus whatever uncaptured edit sat on top of it, so #1498s nothing ever recorded them would tell an operator they had lost something loot cat prints in one command. And AC6 is a sentence rather than a behaviour, which is the ticket premise that turned out wrong in the useful direction: restore writes plaintext and seals nothing, so the next capture already derives the tier from todays .lootattributes and there was no publication decision to get backwards - what was missing was the report saying so, per path and on the dry side too. Two further premises were wrong. The raw --dry-run scan #1612 replaced was correct only by a coincidence this ticket expired, since --source is the verbs first valued flag and loot restore --source --dry-run a.rs now reads --dry-run as the selectors value. And the shared recoverability sentence opened in the past tense, so a dry run printed running this without --dry-run REPLACES ... what those paths held on disk is gone - found by running the shipped binary, not by review, and the tense now lives in the real runs frame where it is true (#1499)
c193cf35 · dbf3dbe6…diff - loot grep grows the options that make a search usable, and two of the ticket's acceptance criteria turned out to ask for something this verb already did unconditionally: -n wants line numbers, which loot grep has printed since #391 with no flag at all, so making them conditional would silently change the output of every invocation ever typed and -n ships instead as a pinned compatibility spelling of the default, exactly as -F now spells out the fixed-string matcher that stays the default with the regex behind -E. the same compatibility argument decides the row shape, so the change column is printed only when the selector was a REVSET, a thing this verb could not search before, while a point selector keeps the three-column row it always had. the pathspec is the declared option group and not a fourth hand-rolled one, which cost the leaf its NO_PATHS declaration first: a group's trailing list begins where the leaf's own arity ends, so grep declares two opaque positionals and attaches PATHSPEC, and because its selector is optional a bare trailing path still reads as the selector and the operator says which they meant with the separator. -E is a hand-written POSIX ERE over bytes rather than the regex crate, which is loot-cli's standing dependency-light no-clap stance applied rather than a new decision; that stance is argued in the ctrlc note in the workspace manifest, not by adr 0041, which governs loot-core and the server-side dependencies and does not reach this; and the cost is measured rather than guessed, by resolving regex alone and diffing its closure against this lock: three new entries, regex and regex-syntax and aho-corasick, since memchr and regex-automata are already in the graph through bstr from gix, though regex-automata is linked there with default features off and carries no dependencies at all in this lock, so regex would compile far more of it than anything does today, all to serve one optional flag on one read verb. AC7 is a real requirement and it is keyed on the content address rather than the path, since repo.put mints a fresh address per call and only a carried tree entry reuses one: measured, a two-change revset over three paths each opens four objects where the natural walk opens six and returns byte-identical rows, which is why the pin counts ObjectGets instead of reading the answer. AC6's aggregation needed a fixture the engine refuses to build the obvious way, because a path this identity cannot open cannot be re-captured while it sits on disk, and the count is per change and path, so one sealed path carried through two changes is two skips. the per-object read is loot-core's own readable_tree classification lifted out rather than copied, so grep and surface cannot come to disagree about what a burned path is (#1500)
c1ad796e · dbf3dbe6…diff - every conflict-stopping verb gets a named way out, and building it corrected the ticket in both directions rather than just adding flags: merge does not stop on a conflict at all, since merge_tips records the triples and fold_onto then signs the merge node and materializes the merged tree, so an abort there is a view step off signed history rather than the forgetting of a stop; while cherry-pick, revert and squash do the opposite and return before writing anything of their own, so their stop leaves even the paths that merged cleanly unapplied. the five named verbs are also not the whole set: converge, pull, adopt and ferrys carry bounce reach the same fold and the same ingest, and the stale-disk settle files a conflict on the way past any capturing verb with no operation behind it at all, so the covered set is now a census rather than an assumption. the state is one position-owned artifact at .loot/position/in-progress carrying the verb, its operand, and the op-log index the view sat at before the verb ran, declared Refuse on malformed content because a torn record read as absent answers nothing to abort over a tree that really is mid-operation, which is the single failure the record exists to prevent. it is a real artifact rather than a derivation because conflicts-are-non-empty-and-the-newest-op-is-a-stopping-verb answers no the instant the last resolve lands, which is exactly when continue must still work. nothing here re-implements a rollback: the inventory row is Captured at ordinal 7, so abort is that one index handed to op restore, which already restores the view, re-materializes the tree, refuses over edits made after the stop, and appends the compensating operation the op-log criterion asked for; force is declared on the five verbs and refused without abort. continue closes the stop rather than replaying it, because the re-run was built first and measured to reproduce the identical conflict: these verbs are capture-first and a resolve on the pre-dock home position never writes the tree, so the closure states instead that the operation applied nothing. status and conflicts name the operation through one derivation, and the pin asserts the identical line in both (#1506)
21b9faf1 · dbf3dbe6…diff - three copies of one false sentence collapse into one statement the other five are pinned to: the help for cherry-pick and revert said --continue re-runs the operation while squash, apply and merge said it closes the stop three lines away, so the shipped help disagreed with ADR 0080 section 4, with resume::cont, and with itself, on exactly the two verbs whose re-run was built first and measured wrong. USAGE is a literal and there is no const concatenation, so the fact now lives once as resume::USAGE_NOTE and a new census asserts all five USAGE lines carry those bytes verbatim, which means a reword turns five red together where three hand-typed copies turned none; the comments in pick.rs and in_progress.rs now point at that statement instead of paraphrasing it. ere.rs cited the help of loot grep for a claim the help never made, so the sentence exists now as ere::HELP_NOTE and is pinned the same way: a dot matches one BYTE, and [:alpha:], \d, \b and \s are not implemented. the help for restore asserted that nothing ever recorded those bytes of every run, while the module it documents splits IRRECOVERABLE from OVERWRITTEN precisely because that is false under --source. item 5 was wrong twice: a stop cannot be the FIRST operation of a repo, because loot init records a genesis floor op, and the refusal is not an off-by-one either, since op_index answers 0 for an empty log, no operation 0 exists to hold a captured view, and the pre-operation pointer bytes were never snapshotted, so no other number reaches them. what is true is that .loot/ops is lane-owned and nothing plants a floor in a spawned lane, making the unabortable case an ordinary one: the first view-changing command an agent runs inside a fresh lane. so the refusal stands, the message now says that and names --continue, and it is pinned through a really spawned lane whose empty op log is the control. the ATTACHED census control is now derived from the census rather than kept as a second hand-written list, and resolves each censused group name to its canonical const so an empty group of the same name cannot satisfy it vacuously, proven by a mutation that leaves the census green and turns the control red on grep, the one attached leaf that had no control at all (#1799)
51aea3d1 · dbf3dbe6…diff - loot config grows a second scope and the local one is untracked because of where it sits rather than because a rule says so: .loot/config.local is skipped by the tree walk at name == DOT, ahead of every ignore call, which is a stronger guarantee than the obvious alternative - a root-level file plus a .lootignore line could not have held, since .lootignore is itself tracked and a fresh loot init writes none, so the first status in every new repo would capture a per-operator default identity as internal, and the dialect has no negation to walk it back with. verified rather than argued: the same content under .loot/ is invisible to status while at the repo root it lands as internal on the very next command. precedence is one function and the merged listing is defined through it, so the file a row names and the value that --get prints cannot disagree; --get exits non-zero on an unset key, pinned through the shipped binary because a library Result proves nothing about what a shell branches on, and a flag-shaped --get value is refused rather than looked up, since the door hands a valued flag whatever follows it. bare set and unset write GLOBAL, inverting git deliberately: loot config set identity is the route init own refusal advertises as one name across every repo on this machine. the settings are read where they matter - remote is consulted by resolve_remote, so a bare push, pull, grants, pull-grants or propose now contacts what the repo names, with origin still the answer when nothing says otherwise. two of the ticket premises were wrong. identity cannot do what it asks: a repo identity is already repo-local in .loot/identity, a config key that overrode it would manufacture the exact name-to-key drift doctor checks for, and loot init can never read a local answer at all because the file lives inside the .loot it is about to create - so the live case is clone run from inside a repo, which is what the pin asserts against a repo configuring nothing as its control. and the local file is not a store artifact: ON_MALFORMED has nothing to choose between, because the key = value dialect cannot produce an undecodable value - the real hazard was an unreadable file reading as absent and falling through to the other scope, which Layer::load now refuses outright. one argv finding rode along: config is the first family whose flags may precede its subcommand word, and args[0] was wrong in both directions, so the leaf now resolves from the first token that is neither a flag nor a flag value (#1507)
2c12d0f2 · dbf3dbe6…diff - nearest-landmark naming lands, and the one real trap in this ticket is that distance is not well defined on a DAG: the number printed is the count of changes reachable from the selector and not from the landmark, which is what git rev-list counts for landmark..selector, and it is not a path length - pinned over a diamond where one input reads 5 as a count, 3 as the shortest path and 4 as the longest, because a straight-line fixture cannot tell the three apart and would have pinned nothing. the briefing had this backwards: git describe does not count the longest path, its number is the cardinality of the same set difference, so taking the count here agrees with git rather than departing from it. a shortest path would call you 2 changes past a landmark that fifty changes have landed on top of, and a count is the size of a set, so nothing in it depends on a traversal order or on a clock - which matters on a lane where authored_at is advisory and never an ordering input. the candidate set is deliberately not the one loot buoy resolves: buoy keeps only the maximal attested change per name, so running nearest over that answer would report no landmark for every change below the tip of a name that has moved, when an older v1.0 is exactly what a change under the newer one comes after - so attested applies the same three ADR 0025 filters across every non-reserved role and skips the maximality reduction, and that split is pinned by asserting both halves in one test. ties are refused rather than broken: two landmark NAMES at the minimum distance exit 3 and are named, the ground ADR 0025 already refuses an ambiguous buoy on, while one name on two tied changes is not ambiguous at all, since every tied change renders the identical string and there is nothing to pick between. three things were widened rather than copied - the ancestor walk now has one home in buoy::ancestors, reached by the maximality reduction, by the new distance and by Workspace::ancestors_of, where there were about to be three walks; the three attestation filters are one predicate; and --dirty asks the same reflected-tree question the converge chokepoint and bisect start already ask. --nearest is the first valued flag this verb has ever had, so the door hands it the token that follows and loot buoy --nearest --dirty would have resolved a selector spelled --dirty, silently dropping the marker: refused by name, the remedy config --get landed, with the door handing the flag over asserted as the control. no landmark anywhere in the ancestry falls back to the short version id and still exits 0, because an answer was produced rather than withheld. (#1509)
b7b0b031 · dbf3dbe6…diff - loot apply-patch lands as the inbound half of patch interchange, and the limit 1512 deliberately left open is CLOSED BY REFUSING: CRLF is refused on both sides, because the two halves are not the same kind of problem. A patch declaring lossy: crlf-normalized cannot reconstruct the bytes the change it names recorded - that is fidelity, read off the exporters own measurement rather than re-derived, since by the time the content is in the file it is LF. The half that actually bites is the LOCAL one: a hunks line space is LF, so writing an applied result back over a CRLF working file rewrites EVERY line ending in it, a whole-file change no hunk describes, that no report would show as more than the lines the patch touched, and that the next capture would sign. Reconstruction was rejected as a guess - the line space carries no endings at all, so an inserted line in a mixed file has nothing to decide from - and documentation as too weak for a silent whole-file rewrite. The cost is named rather than hidden: a CRLF repo cannot use patch interchange in either direction, and the remedy is a bundle, which carries content verbatim because it carries objects. Refusing is also what makes the round-trip pin true rather than approximately true. This is a separate verb from loot apply and not a mode of it, since a bundle carries objects, keys and signed change nodes while a patch carries none of those; dispatch is an exact-name lookup so nothing can collide there, and the place that DOES match loosely is verb_usage, which compares the token after loot rather than a prefix - pinned, because a prefix match would have dragged the whole apply-patch usage line into loot apply --help. Hunks apply strictly at the lines they declare, and what does not falls back to the ADR 0044 three-way against the change the patch names in its own parent row, which is the git am -3 shape and the reason that row is full width; loot-core gains one narrow door, converge::compose, because the theirs side of a patch is text at NO address while three_way_merge exists to pick between two addresses. A contended path is recorded and the run stops with the tree untouched, which makes this the sixth member of the 1506 resume family, and the census is what said so. Three of the ticket premises moved under it. The AC3 re-seal under the local .lootattributes is a SENTENCE and not a step, exactly as 1499 found for restore: the verb writes plaintext and then captures, so the tier is todays local policy by construction, and the export side already carries no tier for it to have inherited - the work was pinning it and saying it in the report. The AC4 no-partial-application rule forced the capture-first snapshot to run AFTER the plan rather than before, found by a fixture rather than reasoned out: a repo holding a path it cannot open has that paths plaintext on disk, capture_uncaptured_edits refuses to re-seal content it cannot read, and with the capture first the verbs own refusal was never reached at all. And a hunk body must be consumed by the counts in its @@ header and never by scanning for the closing separator, because a deleted line whose content is exactly a dash renders as two dashes, byte-identical to it. ADR 0082 freezes the header grammar now that a reader exists, which is why the 1786 fidelity rows went in before this ticket rather than after; the machine shape 1512 expected this ticket to name is deferred, with the reason written into ADR 0066 and PROSE_ONLY_CEILING, because it is one shape shared by both verbs and freezing the import side alone is the same half-contract 1512 declined on its own side (#1513)
d5fba81e · dbf3dbe6…diff - loot fetch lands as the first half of pull by call rather than by imitation: the negotiate-fetch-apply loop comes out of pull_via_declaring as Workspace::fetch_via, and pull now runs capture-first, that call, then the fold, so the two verbs cannot drift about what arriving means - the same extraction merge made of fold_onto. the custody routing comes out of pull_routed the same way, so a locked fetch takes the exact fork pull takes, relay unsigned and forge refused by name, and fetch becomes the second caller of the sealing door, which the custody gate now enumerates by name instead of counting to one. safe on a dirty tree is structural rather than guarded, because capture-first sits above the extracted half, and it is pinned by byte-comparing status porcelain across the call with a modified tracked file in the fixture - the live version id is what moves, and a capture-first mutation reddens it while the path list stays identical. no re-fetch is measured rather than asserted: the test relay now records the raw wants count per round, a later pull is required to ask for zero addresses, and the non-zero ask the fetch itself made is the positive control - an ingest-free mutation makes that number read 2. what integrating would involve is read off the head partition converge executes, through a shared converge_partition, so fast-forward means no merge node rather than one head. two premises of the ticket are wrong. a subsequent converge or pull completes the GRAPH integration but not the working tree when the repo was strictly behind, because converge_heads returns before materializing on a single head and the auto-surface of pull fires only on a non-empty apply, so the content lands in the graph and never on disk while the pull prints nothing new - the same un-materialized state a no-surface pull and a locked pull leave, and fetch now prints that and names loot rehome first. and --remote is the first valued flag of six verbs rather than of this one, so loot fetch --remote --json resolved a remote named --json; the refusal went into remote_sel, where push, pull, propose, grants and pull-grants all reach it (#1514)
577ae796 · dbf3dbe6…diff - the cardinal beside a growing list is deleted rather than corrected, and a census now refuses its return: every prose count of the stopping verbs is gone from resume.rs, pick.rs, lib.rs, main.rs and ADR 0080, and no_prose_beside_stopping_states_a_count fails any comment paragraph whose subject is that family and that puts a quantifier within three words of a noun the family comes one of per member. scope is a paragraph rather than a file because a cardinal is ordinary english and this crate is full of correct ones; run over the whole crates tree it selected the wrong sentences and nothing else, in both directions, and both controls sit in the body. the ADR half is amended by hand and left unguarded, said so rather than implied, because the same gate cannot be made quiet over a document whose every paragraph is about that family and which states correct cardinals about other closed sets. the class was also wider than the eleven sites the ticket listed: hand written verb enumerations in change.rs, in resume.rs and in ADR 0080 had gone false on the same day and are now derived or generalised. the detached doc block is reattached to resolve_sub with subcommand_word given its own, and the sweep the ticket asked for found a SECOND instance it did not name, since 1799 inserted STOPPING between the no positionals census and its 45 line doc, leaving that test undocumented ever since; both are repaired and no guard is offered, because a correct sentence attached to the wrong item is lexically identical to a wrapped paragraph. ADR 0082 is brought to house style and pinned, and what is pinned is less than the ticket claimed: 81 of 82 do not open with a numbered heading, only 25 do, so the census holds the three lines all 82 agree on, an h1 title, a blank line and the Status section. buoy --nearest name is kept and recorded in CONTEXT.md as the deliberate git describe match pattern it is, named there as having shipped undisclosed in 1509 so the record carries the finding rather than the flag alone. and apply-patch stops refusing a patch that only DELETES a CRLF file: a deletion writes no result and so rewrites no line ending, which is the entire cost ADR 0082 states, so the check moved below the deletion arm, the ADR sentence was narrowed to match, and the pin carries a modify patch refused over the very same bytes in the same fixture as its control (#1807)
eff3dc92 · dbf3dbe6…diff - loot clean lands, and the two nevers the ticket asks for are properties of how the candidate set is BUILT rather than filters over one that already exists: the protected set is the union over every change tree in the graph, consulted inside the keep predicate of the tree walk itself, so a recorded path never enters a list at all, and nothing under .loot can be reached because that same walk already skips it at name == DOT ahead of every rule, which is why this file adds no second check free to drift from the first. the walk grew a WalkFilter trait so that skip stayed one line with two implementations rather than a second walk beside it. AC6 is the one that eats work and it has its own fixture: a delta compares path-and-address pairs, so a file the graph records which currently holds an uncaptured edit reads as absent and gets deleted - which is why the set is keyed by PATH and drawn from ANY change rather than the current one. a second fixture pins the other half of that word: a path only the FIRST of two changes records, which a set read off the tip or off surface_target calls unrecorded. both were run red by named mutations before being left green, and deleting the DOT skip put twenty three .loot/objects entries in the candidate list, so the pin is not vacuous. the default is a listing and --force is the only thing that deletes; -n is accepted as the explicit spelling of that default rather than a fourth spelling of the preview idea 1809 is open about, and -n --force is refused because one of the two readings deletes files. -x and -X choose which side of .lootignore the run acts on, and the rules are read by the same parser a capture uses so first-match-wins and the absence of negation hold; -e is repeatable through a new FlagSpec::values that value now delegates to, is parsed by that same parser, holds under -x the way git keeps its own, refuses a leading bang because unlike an inherited ignore file the operator is authoring it right now, and refuses a value spelled like a flag because a valued flag swallowing the bare flag after it has been the defect five times this run. the verb is prose-only and the ceiling moved to 55 with its reason beside it: the shape worth freezing is shared with loot view --prune, which has none, and the column that matters is the per-path spared-reason neither remover computes yet (#1515)
c0b301ed · dbf3dbe6…diff - blame grows six options and the one that mattered was -w, because ignore whitespace when attributing a line reads like a second comparison and this crate has exactly one line matcher: MatchOpts is a parameter of lcs_match_with, lcs_match is defined as that function at the default options rather than as a copy of its body, and the only place two lines are ever compared is match_key inside it, so textdiff, blame and hunks stay byte identical and the whitespace rule could reach diff tomorrow without anything gaining a compare of its own. -L slices a FINISHED annotation rather than narrowing the walk, which is why a range costs exactly the object reads a bare blame costs and the 98 content-address collapse still fires under it, and its rows carry the line numbers of the file rather than of the slice. --since bounds the walk by authored_at, which ADR 0043 makes advisory and never an ordering input, so two rules keep an advisory clock from dropping history silently: a change recording NO clock never cuts the walk, and the first version past the bound is KEPT as a boundary, marked with a caret, so a line older than the bound says the walk stopped instead of being re-attributed to the oldest change the bound happened to keep. --ignore-rev and --ignore-revs-file send a line to the next OLDER version of the path, marked with a tilde, which is a list filter over the versions and not the nearby-line guess git makes there; an unreadable ignore file is REFUSED rather than read as empty, because that failure is silent and inverted, the annotation that comes back blaming exactly the change the operator wrote the file to skip. the ticket got one criterion wrong and it is the porcelain one: it asked for the contract version on that channel like the other machine shapes, but the other machine shapes do not do that, ADR 0023 settled in its 1540 amendment that json carries contract and porcelain stays header-free, and the sentence claiming otherwise is one ADR 0066 records as false by design and guards off the shipped surfaces. so both channels ship and the version rides json, and the spelling is --porcelain rather than -p because sixty verbs answer to one pair of machine selectors. sealed is pinned under every flag rather than once, and --since was the hardest of them: a bound tight enough to stop short of the seal legitimately yields no sealed row at all, so what it must not do is NAME those lines, and it does not (#1516)
b8cb568b · dbf3dbe6…diff - ahead and behind against a remote without pushing to find out, and the thesis constraint is met as a property of the REQUEST rather than as advice: the question carries an EMPTY PAYLOAD and every comparison is local. a relay is asked POST /haves with a zero length body, a forge POST /ref with a signed envelope over zero bytes, so a repo of one path emits BYTE IDENTICAL bytes to a repo of ten thousand and the question has no room to encode paths, object addresses, or even our own heads. the two endpoints that could are forbidden on this path and ADR 0021 now records why: /wants sends our object addresses, which are per-content identifiers, so a status in a loop hands the relay a per-path edit-frequency profile of ciphertext it cannot open - ADR 0083 refusal at a higher call rate - and /offer sends our head ids, which a push may do because a push is a CHOSEN act and status is not. the pin asserts the recorded path-and-body list SET EQUAL to exactly /info and /haves both empty, asserts the two recordings equal across two very different positions, and carries a positive control that a real push through the same stub records a NON-EMPTY body; mutated into the rejected design it goes red with /wants carrying a literal 32 byte address. offline is split POSITIVELY, the site gate SKIPPED-OFFLINE rule: only reqwest own is_connect and is_timeout may be called offline, and anything that ARRIVED - a refusal, a 404, a proxy page, a truncated body - is unusable, because those are different facts and collapsing them is how a guess gets reported as a measurement. the relay answered and holds nothing is a THIRD thing and reads as declared with a count. every unknown count renders dash or null and NEVER zero, so a machine that never reached the network cannot emit a number, and log --unpushed REFUSES rather than printing an empty listing, because a listing has no row meaning I could not ask and an empty one reads as everything is pushed. the asymmetry is stated rather than faked: unpushed is exact, being the same change lane a push would send through the same ancestor_closure, while unpulled is zero exactly when every declared head is held here and UNKNOWN otherwise, since a head declaration says what the tips are and not how deep they run. no FORMAT_MAJOR bump and the reason is recorded: ADR 0019 marker exists to prevent misparse of a durable or on-wire artifact, an unasked status emits byte identical porcelain, and the R row lives behind a flag that did not exist when the shape froze. no revset predicate either, because loot-revset is handed a GraphView and a KeyOracle and nothing else (#868), so a predicate answering over HTTP would put a network round trip inside revset::select and therefore inside grep and format-patch too (#1522)
2237a331 · dbf3dbe6…diff - the first review sweep of this run finds three live defects and six sentences that read wider than their subject, and the one worth the sweep is that status and log DISAGREED: a working change is a head, so RemoteDelta counted it, while scoped_view drops it from the listing - status said 3 where log listed 2, and the extra was a change push would never send. it was unpinned because every fixture FINALIZES, so the fixture came first and settled it rather than the argument; the subtraction is spelled as the two node projections, author present and signature absent, which is the same discriminator the bundle uses and ferry already reads. verify graded a pack I/O failure as object CORRUPTION while the loose arm propagated the identical fact, twelve lines under a doc saying cannot-read must never pass as verified - now verifies() is one predicate literally shared by both arms rather than two spellings that agree today, and scan_one_pack is split out so the propagating half is TESTABLE at all: an index read and a body read are two opens of a file not immutable at its name, so the only deterministic way to fail the second and not the first is to take the index and then remove the file. and [info] swallowed a real failure, since any resolve_remote error read as unconfigured including an unreadable settings file, so the arm now asks settings() a second time rather than matching on prose, keeping unconfigured for a genuine solo repo. the prose half is the doc scope-drift class six times over. read_index claimed to open a store in fewer syscalls WITHOUT reading a body and reads the whole file into memory; the syscall half was true and the bytes half was not, and the ranged read is left to 1525 with the obligation recorded there, including that a span past the end is currently checked against a buffer read_index already holds. two operator surfaces still said LOOSE objects after 1524 taught the census to count packed, and a THIRD site the ticket never named was found in DagRepo::census own doc. ADR 0023 claimed nothing changes what an unasked status emits, which was false twice - the JSON gained a field and 1524 moved contract 13 to 14 on the same shape in the same diff - so the surviving claim is the narrow one, that neither channel can be misparsed, for two DIFFERENT reasons. ADR 0019 now records that contract 14 names two status JSON shapes, because the marker buys a REFUSAL rather than shape identity, a floor and not an equality. the verbs census sentence had gone wrong one run after 1799 guarded the number beside it and warned in that very file that a derived figure can sit next to a claim that has not moved. the forge arm of the privacy pin was prose only and is now tested, and deliberately NOT by byte equality, because a forge envelope names its asker under ADR 0061 - what is pinned is that the payload unwrapped through the real unwrap_envelope is ZERO bytes on both positions, with the greater-than-64-byte envelope beside it refuting the recorder cannot see bodies (#1819)
124faca4 · dbf3dbe6…diff - loot gc --pack lands the write half, and the claim ADR 0083 deferred to this ticket is taken and comes back NEGATIVE: there is no size win and there cannot be one. a packed object is byte identical to the loose file it replaces, so a pack weighs its bodies plus 11 plus 44 per entry and the census total always RISES - measured through the CLI on a throwaway fixture, 12 objects in 11 buckets, 12094 to 12743 bytes. the groupings that WOULD compress are exactly the ones the ADR rejects as leaks, so the absence is the PRICE OF THE ANTI-LEAK RULE rather than an optimization nobody wrote, and the verb says so at the number instead of leaving a true pair that reads as a loss. the repack IS the collector: a rewritten bucket carries forward only the keep set, so an unreachable entry is simply not copied and there is no delete-a-span path to get wrong. what that leaves is a plain gc unable to collect packed garbage, and the hazard is the SILENCE rather than the retention, because nothing to prune - every stored object is referenced by a change is true of the files the pruner walked and reads as a claim about the store, printed at the operator; so a plain gc now OBSERVES it, one header and table read per pack, and prints what it could not reach beside the flag that reaches it. the census asymmetry is untouched: reclaimable_bytes stays a dry run of the plain collector, because a census agreeing with the collection is the whole reason it is trustworthy. the ranged read is implemented and it reintroduced a hazard the ticket never named - a table sized from a corruptible u32 is u32::MAX times 44, 188 GiB of prealloc - bounded now by the file, and the mutation removing that bound ABORTS the test process rather than failing it. burn still refuses a packed address and the remedy now exists and is pinned, since gc_keep_set already drops burned oids so the ordinary collector rewrites the bucket without one and no burn-shaped branch exists in the packer; it is deliberately NOT wired into destroy_objects, which runs from save taking a shared reference, because a bucket rewrite moves every OTHER member offset and the live index spans for the burned object innocent bucket-mates would go stale instantly - a destruction path that repairs itself by breaking unrelated reads is not the fix. crash safety is temp, read back through this module own reader, rename, caller unlinks, and the invariant at every instant is that every address being carried forward is readable from at least one complete file at a stable name, both of them in between; an interrupted repack is not a new failure mode but one of 817 torn writes, since the stage name is skipped by read_all and collected by the existing sweeper. two pins the mutation sweep caught as weak are fixed, one vacuous because a repo that put its own objects holds them in memory where the re-point deliberately does not reach, and one mis-named because its injected failure fires BEFORE the ordering it claimed to prove; the read-back-before-rename ordering has NO fault injection pin and that is said rather than invented, argued structurally from write_bucket having exactly one rename as its last act. and the verbs census attribution went stale a THIRD run running, caught in review here rather than after landing (#1525)
bbbe984d · dbf3dbe6…diff - loot verify --unreachable lands the dangling report and closes the packing family, and the thing it does NOT do is walk: it answers from gc_keep_set, so the whole new computation is a SET DIFFERENCE over the scan the integrity check was already doing - no third reachability walk and no second directory pass either. 1523 refused to write the second one and this refuses to write the third, which matters because a report that could disagree with the collection it names as the remedy is worse than no report. the two directions deliberately take DIFFERENT root sets and that is the subtle half: verify own node list stays the roots for MISSING, because it carries the referencing change and path that a bare oid set cannot, while the keep set is the roots for UNREACHABLE, because it additionally holds the loaded graph and it DROPS burned oids under ADR 0038 - so using it for missing would hide deliberate destruction, and reporting a burned object as missing damage is exactly the inversion burn exists to avoid. the primary own working change is seeded once in each, by the code that already seeded it. the exit rule is structural rather than a renderer discipline: is_clean does not read unreachable at all, so garbage alone cannot fail a verify however the report is rendered, and the process pin deletes an object the listing did NOT name, chosen by subtraction, so its missing half is a positive control on its unreachable half rather than a second assertion of the same thing. size is the SPAN length for a packed object and the file length for a loose one, never plaintext, which loot cannot know without keys - and the two halves are reported APART rather than summed, because that is what keeps each one checkable against its own collector: the packed half is GcReport packed_orphans by construction and the loose half is the census unreachable figure, and the census must go on excluding packed garbage to keep the promise it is pinned to. ObjectScan now carries how each present object is stored, which costs ZERO extra syscalls because both arms already held both facts at the instant they graded an address. one residual is documented rather than hidden: an address stored both loose and packed, the window inside a repack, is attributed once to its loose copy. and the empty case prints that every stored object is referenced by a change - the exact sentence 1525 had to qualify at gc as scope drift - where here it is NOT drift, because the enumeration behind it is scan_objects and that sees packed spans too; the asymmetry is pinned as a test rather than asserted in prose. the verbs census moved to 345 and its sentence moved with it, the first run in four where that did not go stale (#1526)
061af7f3 · dbf3dbe6…diff - the second review sweep of this run finds gc --pack crediting packing with the PRUNE bytes and staying silent about it, and the repair is a NARROWER SIGNATURE rather than a wider condition: size_note took the before and after totals and returned nothing whenever the store shrank, so on this repo own store, 2330 unreachable objects against roughly 250 KB of table, it would print 146.7 MiB falling to 110 MiB and say NOTHING - the exact misreading ADR 0083 warns about in its own text, in the verb written to honour that warning. packing_note now takes the REPORT and cannot see the totals at all, so no combined direction is able to suppress it, and it fires on buckets_repacked. the figure is measured by the packer rather than derived from the pair: overhead_of is header plus table, encode sizes its first body offset FROM it, and the report carries the table a rewritten bucket now holds LESS the one it replaced, computed above the dry-run bail so a dry run reports what a real run does. the pin the attribution actually rests on is new and runs a real repack over a store holding a loose orphan AND a packed one, asserting that before minus pruned minus dropped plus table equals after with every term from its own producer and the sum checked against a census none of them took, 11012 - 4141 - 4141 + 275 = 3005, and asserting the two directions genuinely disagree, which is the defect in miniature on real bytes. the pin that encoded the WRONG rule is deleted rather than preserved. four weak pins have now been found in one run and the last two were invisible to the mutation sweeps, because a sweep only tests the pins you AIM it at and therefore cannot find a pin that tests nothing - the count rule of 1812 proves a pin is LIVE, not that it is aimed at anything, and that limit is now written down. two mutations make the point by experiment rather than argument: under a copy-instead-of-rename and under a helper that unlinks a bucket without writing its pack, the OLD tests stay GREEN. one proposed fix is refused with its reason, that routing a reader fixture through the writer would assert only that the two agree and go on agreeing if both move together, which is the same vacuity the membership pin was written to avoid and which eight reader tests depend on not having; the real complaint is fixed by asserting a structural post-condition instead. the walk census contradicted itself inside one commit and the count is now stated ONCE in CONTEXT.md with the code referring to it, choosing live set as the load-bearing word because verify own node list is a ROOT SET FOR ABSENCE that is never consulted about deletion and deliberately KEEPS burned oids where a live set drops them. the commit record of c6b1cae carries the wrong version and cannot be fixed, so CONTEXT.md records that too (#1823)
907d2a9d · dbf3dbe6…diff - shallow clone lands with the cut on the RECEIVING side, and that is why no wire moves and no format major does either: the fetch body is a format-marked pair of oid runs, so a depth field would be a new wire shape and therefore a bump, where taking the whole change lane and KEEPING n generations needs no new field, no new endpoint and no server change - a shallow client works against every relay and forge already deployed, including older ones. the price is stated rather than glossed: the change lane metadata crosses once in full on the first round, and what is saved is the object bodies, which is where a history bytes are, exact from the second round onward. the no-false-absence guard lives in THREE places and none of them is a verb - assemble, which every CLI open lands in, measures the frontier; apply_bundle_reaching, the only thing that can move one, re-measures it; and the dispatcher states it on stderr after BOTH the success and the refusal arm, because a refusal is a false absence WORST shape. it cannot be bypassed by a verb that forgets to ask: there is no path from the CLI to history that skips assemble, and the one way to open without measuring is to declare RepoNeed without graph, which makes the first history read PANIC - so the declaration that would silence the notice is the same one that aborts the verb. it rides stderr rather than the shape, so json and porcelain stay byte identical under ADR 0023, and a complete position emits nothing at all. depth never reaches the remote AT ALL, pinned three ways: every recorded request re-encoded through the real codec is exactly header plus 32 bytes per id with no room for a depth or a path, the union of every have and wants is a SUBSET of what the relay itself named in a prior answer, and the aimed one is that two positions cloning the same history at the same depth and differing ONLY in their sparse view emit BYTE IDENTICAL requests. the test relay recorder had to start capturing IDS rather than counts, because a privacy claim about a request cannot be checked against a length. two findings came out of the sweep rather than the design. one mutation stayed GREEN and refuted a claim already written into four files - that shallowness is stable because the frontier id rides the declared closure - since a declared have IS a closure claim and the held tips therefore already subtract everything behind the cut; every occurrence is now the narrow true sentence with the refutation beside it. and a count assertion caught a silent no-op: the obvious deepen posture, the closure minus the frontier, comes back with an EMPTY change lane REPORTING SUCCESS, because the remaining ids are still descendants of the cut - a deepen must declare NOTHING, and the posture is now derived from the bound so the wrong pair cannot be spelled. the body-deferring filter is NOT attempted and is the one criterion left: it needs a lazy object read on every get, surface and diff path plus a policy for what happens offline, and half-building it would put a FIFTH kind of not-here into a store that already distinguishes four (#1527)
2cccbe27 · dbf3dbe6…diff - 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 - the no-false-absence guard 1527 landed was BYPASSABLE, and the bypass was neither of the two escape routes its doc ruled out: buoy and bisect return from main before the notice single call site, so assemble measured the frontier and threw it away unprinted, and a plain loot buoy on a shallow position exits 2 with no buoy - a REFUSAL-SHAPED false absence, which the comment four lines up calls the shape an operator most needs the line for. the repair is SHAPE rather than a third bullet: main is now a two statement wrapper and the whole former body is dispatch(), which is the only expression in main that produces an exit code, so every exit code the binary emits is that call return value and a return anywhere inside it - any depth, any verb, ahead of the table or behind it - returns INTO the notice. a new early return is not a new escape route, it is the SAME route, and because nothing is enumerated nothing has to be maintained. that matters because the old doc argued unbypassability by ENUMERATING escape routes and closing each, which reads as exhaustive, is not, and failed on the first route nobody listed - the same shape as a hand written count beside a derived number, which this run watched go stale three times. the residual is STATED rather than enumerated away: a process that never returns through main, process::exit or abort or panic, which is a property of process TERMINATION and not of the verb table, so nothing a verb does BY RETURNING can miss the notice. a census pinning exactly one raw exit was considered and refused, because that walker own consumer count is itself a derived-and-pasted census and adding a consumer churns it. the failed enumeration is kept as HISTORY, marked as the thing that failed, with the shape argument replacing it, and a THIRD site carrying the same claim was found in CONTEXT.md glossary which the ticket never named. FRONTIER_WIDTH stops being a process global read by verbs that hold the authority: both callers now ask their own workspace, and the public spelling no longer compiles from outside the module, which is the narrowing made structural rather than promised. three pins that asserted nothing are gone. the re-encoding privacy pin is DELETED because no byte seam is reachable - the transport is a TYPED seam, so the test re-encoded its own recorded id vectors and asserted the codec is linear in their length, true for every possible input - and its doc now records what it did against what it claimed, with the criterion standing on the other two pins, which carry positive controls. the partition pin is read out of the RENDERED sentence now rather than from literal triples the constructor never saw. two mutations make the case by experiment: moving the notice back to the end of dispatch reddens the new pin while log and show stay GREEN, and re-adding the old partition body under a scratch name PASSES, so vacuity is demonstrated rather than argued. an eighth pin was written and then deleted, because a mutation proved the existing assertion already sits on the live case (#1828)
b5eb05df · dbf3dbe6…diff - 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 - loot move lands as the sixth member of the REFIT FAMILY rather than as machinery of its own, and the whole re-anchor is ONE DELTA over tree entries: the shift, the new parent tree against the old parent, applied to every node of the moved line - so node N tree becomes old_parent then delta N, which is why DESCENDANTS NEED NO SEPARATE RULE and why the clash set is decidable from trees BEFORE anything is minted. the naming decision is the ticket own proposal taken rather than a third option invented on taste: loot move <selector> --onto <selector>, with no collision to find - loot has no mv, and a rename is something a capture RECORDS since v12 - and it is recorded in CONTEXT.md as a new Move entry that states the decision against the word rebase, which the glossary deliberately avoids. AC6 follows an EXISTING mechanism rather than a new one: split moves an entry whole and duplicate reuses entries entry-for-entry under ADR 0004, so move reuses that shape, and the structural proof is that there is NO put_sealed and NO ObjectStore::get on the move path at all - a re-seal would require acquiring a key the code never asks for. its pin carries a POSITIVE CONTROL, open_object failing before AND after, because without one an entry comparing equal proves nothing. the clash arm is the squash clash copied in shape - record conflicts, persist, op, mint nothing - and move joins STOPPING so it carries the abort, continue and force trio. two things are reported rather than papered over: the ticket parent line calls this the start of the jj rebase -r item but AC2 asks for the OPPOSITE of -r, descendants travelling WITH the change rather than re-parenting onto the old parent, and the AC as WRITTEN is what shipped. and the working change is itself a descendant, so it travels, re-recorded as a superseding version but deliberately UNSIGNED, because signing it would mint permanent history from work nobody named - which also means a clean move reports two descendants where the operator sees one, and that is pinned as true rather than hidden. eleven pins, each reddened by a named mutation with the count read rather than the word ok (#1517)
7ff4ef4d · dbf3dbe6…diff - the branch and reorder forms land as FLAGS ON MOVE rather than as verbs of their own, and the branch form is a SELECTOR SUBSTITUTION rather than a second planner: branch_root walks the first-parent line and stops at the first node the destination already descends from, the change just below it becomes the source, and the ordinary plan_move decides everything else - so the branch form CANNOT DRIFT from the subtree form refusals, clash set or sealed-carry rule, because there is one planner under both. reorder is DELTA COMPOSITION: the moved manifest becomes anchor then lower-over-upper, the overtaken one anchor then lower then upper, which equals its old tree exactly when the two touch disjoint paths - which is WHY descendants are re-parented with their manifests UNTOUCHED - and the intersection of the two deltas is the clash set, decided from trees before anything is minted. a DEFECT IN WHAT LANDED AN HOUR AGO is fixed here rather than filed: loot move <finalized> --onto @ was NOT refused, because @ on a different fork passes every #1517 guard - not self, not a descendant, not the current parent, not a merge - so it would have minted SIGNED HISTORY PARENTED ON THE UNSIGNED WORKING CHANGE, a version no push, bundle or fetch carries. the repair is stated as a rule over the parent-to-child EDGES a restructure creates, asked of the DRAFT POINTER and never the signature, because a keyless repo signs nothing - so both forms and any seventh Refit inherit the refusal instead of each enumerating its own cases. AC1 was already satisfied by #1517 and is CITED rather than re-pinned, verified against move_onto live-descendant closure rather than taken on trust. non-adjacent insertion is REFUSED BY NAME rather than half-built, because inserting at an arbitrary point re-parents the line it lands in front of, which is the -r shape #1836 holds. fourteen pins, each reddened by a named mutation with the count read - including two draft pins that go red together, which is the census proving the rule sits on the edge rather than on the verb. and two prose censuses caught the USAGE text itself, where a -b inside a jj rebase citation tokenized as an undeclared flag (#1518)
066564a1 · dbf3dbe6…diff - the draft rule stops being a claim about planners and becomes a property of the ONE SEAM EVERY REFIT CROSSES: refuse_building_on_the_draft is asked unconditionally as the first statement of apply_refit, over a new minted_edges whose match is WILDCARD-FREE, one arm per variant, each declaring the pre-existing parents it would record on. the proof is a MUTATION THAT DOES NOT COMPILE - adding an eighth variant fails with non-exhaustive patterns AT minted_edges, so a seventh Refit cannot forget the rule the way a census or a call-site list would have let it. the two planner calls STAY and are documented as ORDERING ONLY, naming an illegal destination before a clash set is computed, so forgetting one costs a MESSAGE rather than the rule. and duplicate is ruled a DEFECT rather than a named exception, because the artifact it shipped was genuinely broken: the copy is SIGNED so it travels, and it named as parent a version resolve_live_version excludes BY CONSTRUCTION - loot show on that parent exits 1 with no live version matching - with latent harm besides, since any later describe, squash or abandon of the draft makes it permanent rather than repairable by a later loot new. #398 SURVIVES CONTACT and its hard constraint was in a COMMENT its resolution never restates: duplicate must not reproduce a change at its own parent, because a content-addressed version id would collide, which is why the jj default of the source own parents is unreachable here and why #398 reached for the draft at all - the anchor satisfies that constraint just as well and is a version a receiver can resolve, so nothing in #398 required the draft specifically. the planner is no longer TOLD which version the draft is, its view loses that field entirely, which is the narrowing made structural rather than promised. two user-visible changes are stated rather than slipped in: a duplicate with work in progress now lands BESIDE the draft on the finalized anchor rather than on top of it, and --after @ now REFUSES. seven mutations, six red with the counts read and one a compile error, and the two halves are separable - blanking the declaration reddens both pins while neutralising the seam call reddens only one (#1841)
56bc591f · dbf3dbe6…diff - loot notes lands as a SECOND WRITER ON THE ATTESTATION LANE rather than as a record type of its own, so AC3 and AC6 arrive FREE and the wire does not move: a note is an attestation carrying a reserved role, the sibling of #1508 landmark record in the namespace the TAG field was shaped to admit, so every shipped reader parses the bytes, FORMAT_MAJOR does not move and NO RELAY OR FORGE REDEPLOY IS OWED. the visibility rule is the decision and it is DERIVED rather than chosen: the attestation lane carries no key and no grant list and rides the bundle beside the change it names, so a note audience is EVERY HOLDER OF THE CHANGE including a relay holding no content key - on a wholly-Internal change that audience can already open every byte, so no text ABOUT it can tell them anything new, and that implication FAILS THE INSTANT one path is Restricted or Embargoed. so the verb REFUSES on a change recording any path sealed narrower than Internal and names the paths, while Published PASSES because it is Internal plus world, a WIDER audience. inherit was rejected on its own terms - a Change HAS NO SINGLE VISIBILITY, that is the founding sentence of the glossary, so inheriting means a key granted to the INTERSECTION of holders which goes stale on every grant and maroon - and own-visibility was rejected because it is the only option that can OVER-disclose, making the safe answer an operator choice every time on an append-only already-delivered lane. an allow-leak hatch was declined because it would consent to nothing enforceable. the gate is the unconditional FIRST STATEMENT of the write path above a match exhaustive over the three writing ops, so a fourth does not COMPILE until it has an arm, and the limit is stated rather than implied: the check is at WRITE time, a path demoted afterwards leaves the note standing, and burn is the verb for that. seventeen mutations, each red with the count read, and both risky pins carry POSITIVE CONTROLS - the version-id pin mints a real version three lines below the assertion that notes did not, and the visibility pin has a peer who provably CANNOT open the sealed object reading the note in the clear beside it, each half the control for the other. a smoke run caught a defect this change had introduced rather than inherited: a multi-line note broke the attested-by line at column zero, so the cut is made AT THE DOOR, which covers a multi-line tag message that was latent and never exercised (#1519)
0ec3069f · dbf3dbe6…diff - the note rule moves to the LANE WRITE SEAM and stops being a property of one verb: Workspace::attest takes a Record rather than a role string, so there is NO OTHER WAY TO SPELL A ROLE IT ACCEPTS, and two wildcard-free matches answer for every variant - role mints the string and refuses an operator one in the reserved namespace, prose declares the free-text payload and Some IS the gate. adding a fifth variant produces TWO compile errors, one per match, which is the obligation put where a list cannot be forgotten. all three doors were REPRODUCED FIRST and a FOURTH was found that no ticket named - tag --retire -m writes the operator reason to the same lane - which is why the repair is a seam rather than three verbs remembering. door one is REACHABLE rather than theoretical: unwrap_or_default meant an unreadable tree read as NO SEALED PATHS and allowed the write, and a shallow position supplies one, because HEAD~n walks one parent edge past the oldest change HELD and resolves onto a FRONTIER ID - signed, named as a parent, not held - so the gate now fails CLOSED and names fetch --deepen. door two leaked real bytes end to end: notes add refused, then attest with a reserved-shape role exited 0 and show printed the secret on a Restricted change. and ADR 0025 had that door WRITTEN DOWN AS AN ACCEPTED COST, saying a user who reaches past the verb can still attest the sentinel and the signature binds it to their key - the hole was documented as the price of the encoding, and it was a DEFECT rather than a cost, because the role parses back as a note, renders as one and rides the bundle as one. tag -m is GATED rather than excepted, and the line is KEY VERSUS PAYLOAD: a landmark name is a namespace key a reader re-types and matches by equality, so gating it would take ADR 0018 sign-off and ADR 0025 resolver down on every change in a repo with one sealed path, while a message, a reason and a note body are prose ABOUT one change content. that refuses tag -m in THIS repo, where .lootattributes seals a path on every change, and the bare name, the bare retirement, buoy and attest all still work. eight mutations with counts read, and mutation three is the argument: neutering the seam reddens three pins while #1519 OWN PIN STAYS GREEN, which is why the first gate could not see any of this (#1847)
93cc4944 · dbf3dbe6…diff - hunk selection lands on split and restore and AC2 is STRUCTURAL RATHER THAN CHECKED: everything the picker shows comes off the ONE DELTA SEAM, whose sides resolve through visible_paths_at - the key check plus embargo escrow plus grant expiry, the same door grep and archive take since #1500 - and the hunk grain has EXACTLY ONE CONSTRUCTION SITE, reachable only where both sides hold bytes, so an unopenable path is an Unreadable row that HAS NO HUNKS IN IT and there is no second visibility test to keep in step. such a path is still OFFERED AS AN OPAQUE WHOLE rather than dropped, because dropping it silently is #67 at the verb where it costs content, and its entry then moves BY ADDRESS with nothing decrypted. the keys are git add -p keys derived from ONE TABLE so parser, prompt and help cannot drift, with two forced divergences stated as such: q ABANDONS THE WHOLE SELECTION, because git keeps what it staged and loot has no index to keep it in, and there is no edit or navigate arm because each would mint a second grouping rule or a screen model. the terminal is a DOOR rather than a check - demand is the only constructor of the only Answers the binary has, so a verb cannot obtain a keyboard without crossing the refusal - and -p with --dry-run refuses as a PAIR, because -p IS the preview and a dry run would ask every question then discard the answers. content that does not survive a line split is offered whole, MEASURED BY ROUND-TRIPPING rather than by enumerating hazards. eighteen mutations, all red with counts read, and AC2 carries a POSITIVE CONTROL that moves the other way: emitting no hunk grain at all leaves the never-a-sealed-hunk half GREEN and reddens nine, which is exactly what that half alone cannot see. ONE ACCEPTANCE CRITERION IS DELIBERATELY NOT BUILT and that is the finding: AC1 names shelve -p, and loot shelve does NOT EXIST - #1502 was closed NOT PLANNED as premature scaffolding, so building a shelf to hang a flag on would reverse a recorded decision sideways from inside a ticket about something else (#1529)
1eb98dc8 · dbf3dbe6…diff - range-diff pairs by the DURABLE CHANGE ID and by nothing else, which resolves a real tension between two of its own acceptance criteria rather than papering over it: AC1 asks for content similarity and AC6 forbids comparing content across a visibility boundary, and a similarity scorer is EXACTLY the construction AC6 rules out. git scores patches because a git commit has no durable identity - loot RECORDS what git infers, and the handle survives re-snapshot, amend and refit, which is precisely what a rework is. so the pairing is exact, O(n+m) instead of O(n times m) content deltas, and STRUCTURALLY INCAPABLE of comparing content: pair takes two slices of identifier pairs, no Workspace, no keyring, no clock, no tree, and its only production builder reads GraphView, which loot-core documents as excluding anything that touches stored bytes. the evidence that the property is held by the TYPES is that the pairing tests need no repo at all. the verb is the FIFTH RENDERING over the delta seam and builds no differ: each pair body is the seam spec, the seam delta and the seam renderer, byte for byte. what the rule cannot pair it SAYS SO ABOUT rather than guessing - no change id from a legacy or git-bridge ingest, copies, which mint a fresh handle by design, and divergent handles, paired in order with the surplus named. the skip count is argued rather than waved past: its three inputs are change ids that are cleartext on every node, recorded addresses the engine already compares without decrypting, and a fact about THIS READER keys, so the most it can say is that two versions differ in paths you cannot open, which any holder computes from the recorded addresses alone. fifteen mutations, all red with counts read, and AC6 positive control pairs an EMBARGOED change and a readable one in the SAME RUN by the SAME RULE, with only the rendering differing. two findings came from looking at real output rather than reasoning: an amend records the superseded version as a PARENT as well as a predecessor, so a different-bases warning fired on every amended pair until same_base learned it, and a superseded version CANNOT BE NAMED BY A SELECTOR at all, so the revset door is the only way to aim at the left-hand side of an amend - a thing this verb can do that diff cannot (#1533)
c4b0cd67 · dbf3dbe6…diff - the delta reaches the TypeScript SDK as a SHAPE rather than as prose, and it is the FIRST INSTANCE of #1763 rule rather than a bespoke design: DeltaShape::of is a PROJECTION of the seam and nothing else - the mark is the #306 gutter rather than a new alphabet, the rung is the LineDelta variant, the counts are its own counts and the tally is the disclosure verbatim - so nothing here was hand-designed and the shape cannot drift from what the human rendering shows. three decisions carry the weight. added and deleted are NEVER ZERO where the count is unknown, they are absent, exactly where --stat calls a row uncounted, and ONE function now feeds both channels so the two cannot disagree. a sealed row WITHHOLDS the path, the from-path and the recipient list in both channels, because a path name is CONTENT under #306 - the shape refuses to leak through the encoding what the prose refuses to print. and the machine channel is ONE SHAPE REGARDLESS of --content and --stat, verified byte-identical, because those flags pick WORDS over a delta while this picks an ENCODING of it. the contract number is the shared VERDICT_CONTRACT and not a per-verb one, which ADR 0023 already answered for every shape and #1516 declined explicitly for porcelain, and an SDK test asserts diff and status report the SAME number so a per-verb version would go RED. a defect was caught BEFORE the freeze, which is the only time that is cheap: the first encoder used to_string_lossy, so on Windows a FROZEN contract would have shipped a backslash path beside a human line printing a forward slash - caught by the pin that compares the two RENDERINGS rather than asserting each is non-empty, and its unit pin uses an EMBEDDED backslash rather than a nested path, because a nested-path fixture is vacuous on POSIX. wire names stay snake_case deliberately, since a camelCase mirror would be a second vocabulary for one frozen contract and the only thing it could do is drift. nine mutations with counts read, and the ceiling comes DOWN 57 to 56 - the first entry on that list to PAY rather than be excepted (#1554)
ac5700af · dbf3dbe6…diff - a newline in a path does not LOOK WRONG, it FORGES ROWS - a name spelled notes.md then newline then percent then four numbers injects a SECOND DISCLOSURE TALLY, which is the number an agent reads before it asks for content - so porcelain WITHHOLDS an unframeable name rather than escaping it, refusing it, or leaving it to a sentence: path becomes dash, flags gains unprintable, and --json carries the name escaped. the three alternatives are rejected WITH REASONS AT THE SITE - documenting JSON as the only safe channel is the trade ADR 0082 already refused for CRLF, since a porcelain consumer has no way to DETECT it; escaping mints a second spelling of one path inside one tool, the #988 class whose answer was ADR 0051 ONE spelling; and refusing denies the OTHER rows, disagrees with --stat, and lets anyone who can name a file BLIND the machine channel. a tab is deliberately NOT withheld, because the path is last and the documented split recovers it - the rule is exactly as wide as the frame it protects - and with both withholdings flagged, a bare dash with NEITHER flag is now unambiguously a file NAMED dash. flags order is frozen as the encoder own, sealed first because it is the member a reader must not miss, and later members APPEND so every combination without a new one keeps todays bytes. and the run caught itself: #1554 re-created the class #1553 had fixed FIVE HOURS EARLIER, four pub items with zero out-of-crate callers, one of them a pub wrapper returning exactly what #1553 had narrowed to pub(crate) - the narrowing undone THROUGH A NEW DOOR - so all four are narrowed and the rule gains its worked example for the FUNCTION half, the half nothing enforces. the frozen shape description is corrected in four places, including a TypeScript doc that omitted the bare restricted token a peer-received path really carries, which would have mis-parsed in a consumer. the residue attribution in #1860 doc is WITHDRAWN rather than explained, restated as unexplained with what measuring it would take, because that document own thesis is that unmeasured causal claims about the instrument are the defect. eight mutations with counts read, four compile_fail probes with positive controls, and ONE MUTATION WAS CAUGHT BEING TOO WEAK - un-backticking a single table row left the pin GREEN, so it proved nothing and was redone against all four mentions (#1870)
3dfefe4f · 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 - twenty-one verbs stop accepting a positional they silently DROP, and the two that keep it now say WHY rather than looking like debt: each of the twenty-three the census named was read HANDLER-FIRST rather than swept, because narrowing a verb whose arm actually reads a positional turns a silent drop into a WRONG REFUSAL, which is worse than the bug - nine ignore argv entirely, twelve read it only through their own FlagSpec, and two were followed into a helper, RelayKeys::from_args for serve and resume::mode for squash. describe and new stay exempt on a PERMANENT reason now recorded at the exemption: their #545 refusal inspects the stray token to say it is the tail of a -m subject an unescaping shell split, and the door gate runs BEFORE the handler, so declaring an arity would REPLACE that message rather than add a refusal - verified live, describe with a split subject still exits 1 carrying the full #545 text. the second census is not fed twenty-one new names, because appending them is the edit that turns a census into a list nobody reads: its zero-arity half is now DERIVED from USAGE by the same reader the first census uses, and heads, gates and count-objects came OFF the hand-written list, since the three paragraphs arguing them argued the thing that is now the general rule. the exemption is hoisted to one shared constant so it cannot be lifted in one census and left standing in the other. the new behaviour pin derives BOTH sides from independent sources - expected from USAGE prose, observed from live refusal calls - and takes exactly one junk token so cat, tag and merge stay out of the set. the ticket comment asking for a handler-side census is STALE and nothing was built: that census already exists in two places, and rather than trusting it, it was proved against the comment own instances by RECONSTRUCTING them - the hand-rolled positional filter in cat red at 0 passed 1 failed naming cat.rs:148, and the #1494 by-one flag skip red naming main.rs:1229. the one half of that comment that is NOT honestly buildable is named as such: a check that hand-written flag names never appear outside flags.rs could only exist with an allowlist of every legitimate flag literal, which is the hand-maintained-list-asserted-complete class, and the real defect in that instance was the hand-rolled skipping the existing clause already catches. six mutations with counts read, and the caller-visible change is stated rather than buried - a stray word now exits 1 for all twenty-one where it used to report success, with crates, sdk, tools, docs, site and the skills all grepped for an invocation that would start failing and NONE found (#1569)
62a8941d · 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 - the shorthand whose NAME states the path axis while its SHAPE states the arity axis is DELETED, so the terse spelling is now the one that REFUSES - which was this ticket whole thesis, that the wrong declaration was cheaper to write than the right one and that is why the silent-drop class kept recurring. every site that meant it now types the open constructor out, and the only shorthand left is the one that takes nothing. option A beat option B on BOTH axes, measured rather than preferred: B would have changed the constructor signature, so EVERY open call site owed a reason string - including the path-taking and the genuinely variadic ones - and the two dozen dispatch verbs would each have written the SAME sentence, which is boilerplate that teaches nothing and is itself a hand-maintained population. so A has the smaller blast radius AND the stronger property. the blast radius is ZERO BEHAVIOURAL, because the retired constant was literally that expression: no verb declared arity, no slot kind and no refusal moved - thirty-five declaration sites, eleven imports and about twenty-five prose sites, with the workspace check clean and no new warnings. the exemption list was ALREADY down to its two legitimate names before this began, since #1569 narrowed the other twenty-one hours earlier, so nothing was added to it or taken from it, and the #545 refusal that earns those two their place is untouched by construction - pinned rather than incidental, because the mutation that hands one of them the no-arguments declaration reddens all three censuses. the rename then exposed two more counts standing beside sets that MOVE, and both are fixed rather than carried: a fixture doc claiming all FOUR verbs it exists to serve are exactly this shape, where there are FIVE production attachers and NONE of them is that shape, and a line naming the four verbs that used the retired constant. both now state the rule and count nothing. the new guard refuses BINDING the zero-slot open claim to a name, which is the single edit that would undo this, while deliberately NOT refusing a leaf that spells the claim out at its own spec - the two told apart by what PRECEDES the constructor, with both run through the predicate before its answer is read. its limits are in its own header. and the control that mattered is the second: with the comment-strip removed AND the predicate control disabled, the tree scan names the flags file itself, over the retired declaration QUOTED INSIDE THE SURVIVING CONSTANT OWN DOC - so the strip is load-bearing rather than decorative. the first and third mutations are each other discrimination, one reddening only the tree arm and the other only the binding-versus-spelling arm, and the fourth proves the floor fires at zero files rather than agreeing silently (#1675)
123fdbd4 · dbf3dbe6…diff - a revset is accepted wherever a selector is, through the door that already existed - and the verb population is DERIVED from the shipped help text rather than from the dispatch table, which closes the two-spellings trap that had already cost two tickets UPSTREAM of this change, since help is pinned set-equal to the table in both directions. a placeholder-name list could undercount a DIFFERENT way, so the whole placeholder vocabulary of the help text is a SECOND set-equality: a verb spelling its change argument some new way now reddens the build instead of dropping silently out of the population. twenty-five selector-taking verbs, TWO of which a hand list would have missed and the derivation found - diff spells from and to, merge spells head, and neither says selector. four are adopted here, exactly the four #1563 named, all through the existing door via one shared consumer: no second parser and no per-verb precedence, because two verbs disagreeing about what a bare word means is worse than neither having revsets. the point-versus-set question is decided PER VERB with its reason rather than made uniform. cat is a point because its product is one path bytes written verbatim with no frame around them, so two versions arrive concatenated and indistinguishable from one file containing both. archive is a point because N trees are either N files, which is a different verb, or one file whose members collide path-for-path. blame is a point because the lineage walk STARTS there and there is no column for a second start. and ignore-rev is a SET USED WHOLE, because the only use of its result is a membership test, so every member is taken. diff is the interesting refusal: a two-member set does NOT collapse into the pair, because the pair is already spelled as the two arguments, so collapsing would give one argument an arity decided by data the operator cannot see - the same expression a comparison on one repo and a refusal on another - and a set has no SIDES, so a topological order would silently come to mean oldest-versus-newest. that is rejected explicitly and recorded rather than overlooked. the shared rule is exactly one, else REFUSE naming the count, and the door never takes a member. sixteen verbs are left, in two groups, both NAMED in the census rather than merely absent: nine where a set is a fan-out over a REWRITE, needing an order, a per-change failure rule, an undo story and a confirmation surface that do not exist - guessing on a destructive verb being the worst place to guess - and seven that are points by nature and left on scope, two of which have a coherent set arm and so are decisions rather than transcriptions. every pin RUNS THE VERB against a real three-change repo, each row in its own fresh copy with declared setup, so a refusal that fires BEFORE the selector resolves cannot pass vacuously - which is the exact failure this ticket exists downstream of, a criterion once pinned only at the flag gate. ten mutations with counts read. the control that matters is the one dropping a placeholder: the VERB census stayed GREEN, because diff line also spells another, while only the vocabulary census fired - which is what makes that second guard load-bearing rather than decorative. and the ignore-rev pin carries its own control, because without it the equality would hold just as well between two no-ops. three prose claims this change made FALSE are fixed, and one existing pin was MOVED rather than deleted, onto a verb the census names as selector-only so the choice cannot go stale silently (#1689)
5e9b9cb6 · dbf3dbe6…diff - the verb position stops answering a typo with a 157-line wall and names the nearest verb instead - the same repair #1520 made at the other spelling, at the more common position, since the verb slot is the one every invocation has. the suggester is REUSED verbatim rather than reimplemented, and the only-edit-distance-in-the-repo claim was VERIFIED before being relied on rather than taken from the ticket: every workspace rust file was swept for the vocabulary and for a distance dependency in any manifest, and the one pair stands alone. it already derives its population from the completion verb names rather than a hand list, so nothing about the suggester changed. the message, though, did NOT transfer verbatim - it hard-coded the help framing - so the lead clause becomes a two-variant enum and everything after it stays shared: one printer, not two. the no-match arm is TRANSFERRED rather than re-decided, and at the dispatch position that is a real narrowing, because a word near nothing now gets no invented guess AND NO WALL, where the wall used to be the whole answer. the exit code was measured on the shipped path both ways and is UNCHANGED at one, with stdout still empty and the json envelope keeping its contract number - and it is pinned as the VALUE rather than as not-success, so a change to it cannot pass. the ticket own premise about the sibling binary is FALSE and is corrected rather than carried: its five-word suggestion list had not gone stale at all, matching its dispatch arms and its flag set exactly, so no reader was ever misdirected. the CLASS was real, since nothing held the list to the table, so the list now derives - with the pin reading the MATCH ARMS rather than the flag set, because reading the flags to check the flags would be an identity, plus a source assertion that the arm spells no verb name at all. eleven mutations with counts read. M3 is what earns the behavioural file: reverting the dispatch arm to the wall leaves BOTH unit suites green and only the spawned-binary test reddens, which is exactly the argv-door blindness two tickets in this run had to be re-closed for. and M6 is a CONTROL that stays green on purpose - a second implementation inside a test module - which makes the census scope a stated limit rather than a hole nobody mentioned, alongside the other limit that it does not parse rust and so cannot see a matrix that is neither named nor spelled like one (#1695)
edfa4d62 · dbf3dbe6…diff - the commit that derived a list precisely so it could not go stale had typed its cardinality into the doc above it, and both counts are gone: the doc now says the refusal names the whole flag set - every verb this binary dispatches and nothing else, pinned in both directions - and that what the one-line shape rests on is READABILITY rather than a size. the obvious fix, pinning the count as well, is refused on purpose, because it would fail a land for the offence of adding a sixth verb, and that refusal is PROVED by a mutation adding one to both sides that stays green. the distance-crate check stops presenting a denylist as a property: it is widened with the five crates the review named, loses its own typed length so the constant carries no count either, and says plainly that it is a named list of common spellings and not a test for the category - naming BOTH holes in the voice its other stated limit already uses, a crate under a name nobody wrote down and a renamed dependency, the second confirmed by a mutation that stays green on purpose. matching on any dependency that merely looks like a distance crate was rejected as a guess dressed as a check. the derived method width is decided to be a TRIPWIRE and not a ledger, so its gap prose is dropped rather than extended - it was wrong both ways at once, excusing a gap that another clause already attributed while a new one had opened unnamed - and the doc now says outright that nobody keeps the per-ticket clauses summing to the figure. and the flag argument left at the vouching seam is examined rather than refactored for its own sake: the triple riding three doors IS the resolved delta still being assembled, so a new struct would only be a second name for it, and a bare bool would be the same flag one level down. the one real narrowing is taken instead - a caller that always passed Some now takes the set directly, so no door a caller can reach lets it choose to skip recording the open (#1922)
0ac07831 · dbf3dbe6…diff - every top-level verb whose usage is bounded now declares its arity, so loot burn a.txt b.txt refuses b.txt by name instead of burning a.txt and dropping it: twenty-seven verbs were narrowed, each read handler-first, and none reads more than its widest usage line shows - though a flag that narrows a verb shape is not covered, since the arity is one number per verb, so grant --relay and resolve --tool still drop a surplus word and that is filed as #1934. the census stops naming verbs - who owes an arity is DERIVED from USAGE (no ... on a positional token) with the subcommand families and the OPEN_BUT_TAKES_NONE verbs exempt by reference, and the hand-kept COUNTED list and its per-verb paragraphs are gone, their argument folded into the rule. the one USAGE reader gained a separator arm, since a [-- <path>...] tail read as a bracketed flag would have made grep look bounded, and the ... is read off positionals only so the repeatable ignore-rev flag leaves blame bounded. a new pin derives both sides - expected set and invocation width from USAGE, observed from live refusals - and also asserts the wrong-refusal direction: the refused word must be the junk one and a full documented invocation must pass. five mutations went red with counts read, and no in-repo caller passes an extra token (#1928)
3b9b0f9a · dbf3dbe6…diff - the opt-in telemetry channel is built, and off means no call because a paired test says so rather than a sentence
loot#1658, building what ADR 0074 specified and loot#1626 shipped only the
guard for. loot telemetry on | off | status | show; an append-only accumulator
that writes one line per dispatched verb, the dispatch-table name, with burn and
maroon collapsed to other AT WRITE TIME so no local trace of an incident exists
either; a sender that runs in main after the verb has printed and decided its
exit code, at most once per 24 hours, behind a 3-second hard timeout, that can
never print, never change the code, and never delay a verb; a spool that IS the
outbox, so show reads the same entry the sender transmits; and a 16-byte id
minted on opt-in and deleted on opt-out, never machine-id.
The guarantee is the pairing. One local listener, the real binary, a sandboxed
config home: init, status, doctor and show with telemetry off make ZERO
connections; on, the same verbs make EXACTLY ONE, and the same verbs again in
the same period make no more; off again makes none. A lone assert-zero passes
when the listener is broken, the binary crashed early, or it pointed at the
wrong port - the exact shape of the logrotate -f check that could never fail.
The endpoint override is https only with the loopback LITERAL the sole
exception, enforced twice, and localhost is refused because what it resolves
to is the resolver business; the refusal happens BEFORE an id is minted, so a
broken override never leaves an identifier that can never be used.
Clause 5 of the field rule is a property of the wire bytes, not a promise:
a test parses every string out of the JSON and requires each to be a
compile-time constant, a member of a fixed array, a dispatch-table name, or
the hex id. The platform is a build-script constant rather than a runtime
assembly from std consts, which is what the rule forbids spelled from constants.
The verb emits machine output - show --json IS the report - so it joins the
emitting tier rather than the prose-only debt list the ceiling forbids growing.
Every count that moved was moved in the documents that state it, because the
censuses read them: 129 verb lines in three sites, 82 dispatched and 80 in the
table and 25 emitting in ADR 0066 with telemetry in the named list, all 82
verbs and the block in the README, the CLI page, and ADR 0074 eight-entry
allowlist - because the endpoint is now the binary ONE destination it contacts
rather than prints, and its ALLOWED row says under what condition.
The destination census flagged three lines that dial nothing: a printed hint
naming http://127.0.0.1 and the two scheme-prefix tests in check_endpoint. They
are respelled rather than exempted - the check splits on the separator and
compares the scheme WORD, same rule, no allowlist row - because a row is a
sentence someone has to be able to defend and these had nothing to defend.
The site half: POST /api/telemetry validates a report against the SAME closed
sets the binary declares, pinned cross-repo by reading the Rust source, and
refuses WHOLE on any field outside them. A valid report becomes source=cli
rows, one per verb invocation under /cli/<verb> plus two bucket rows, all
stamped with the arrival time so no ordering exists to reconstruct; the
telemetry_id rides the row and expires with the 30-day partitions. It sits
behind the same /api/ limit_req the beacon needed, which is why the ADR put it
there. doctor prints on/off, the EFFECTIVE endpoint, last send and the spool.
The notice gains the CLI tier, as ADR 0074 §11 requires: sends nothing unless
you run on, the id links reports and off deletes it, what is never in a report,
show prints it before you decide, and the erasure route - quote your id within
30 days, you hold the key. The test that asserted the page does NOT collect
telemetry is inverted, as it was for the visitor key.
1214 lib and 118 bin tests, 51 suites, 634 site tests, budget and font gates
green, rendered and read.
8a919b01 · dbf3dbe6…diff - a verb arm now receives the argv door answer instead of the raw slice: VerbRun takes an Admitted value, the rebased tail bound to the spec of the row it was dispatched from, and its readers are the only way in, so argv[0] or argv.first() in an arm fails to compile (both planted, read as E0608 and E0599). spec_of is test-only and its 104 production lookups read through the admitted spec; six of the seven raw-index arms (bundle, grant-status, embargo-status, attest, resolve, completions) read positionals through the door and relay reads its leading word through a named exception; subcommand families resolve their leaf into a new Admitted, and bisect and buoy are handed one ahead of the table. the two named exceptions, leading_word for relay and bisect and pass_through_tail for the bisect run command, plus the one process argv read, are held to a named set of functions by a new census that went red on a planted leading_word read in cmd_bundle, and the source walker consumer header now counts it. refusals and output are unchanged by every suite and by a read of each migrated arm, with one exception found on the way: resolve with a trailing --tool and no command read --tool as the file and now refuses with the usage line, pinned by a new test. CONTEXT.md gains the Admitted argv term; loot-first keeps its own spec_of and DECLARED_READS is untouched (#1929)
d92e95f2 · dbf3dbe6…diff - the opt-in telemetry channel is built, and off means no call because a paired test says so rather than a sentence
loot#1658, building what ADR 0074 specified and loot#1626 shipped only the
guard for. loot telemetry on | off | status | show; an append-only accumulator
that writes one line per dispatched verb, the dispatch-table name, with burn and
maroon collapsed to other AT WRITE TIME so no local trace of an incident exists
either; a sender that runs in main after the verb has printed and decided its
exit code, at most once per 24 hours, behind a 3-second hard timeout, that can
never print, never change the code, and never delay a verb; a spool that IS the
outbox, so show reads the same entry the sender transmits; and a 16-byte id
minted on opt-in and deleted on opt-out, never machine-id.
The guarantee is the pairing. One local listener, the real binary, a sandboxed
config home: init, status, doctor and show with telemetry off make ZERO
connections; on, the same verbs make EXACTLY ONE, and the same verbs again in
the same period make no more; off again makes none. A lone assert-zero passes
when the listener is broken, the binary crashed early, or it pointed at the
wrong port - the exact shape of the logrotate -f check that could never fail.
The endpoint override is https only with the loopback LITERAL the sole
exception, enforced twice, and localhost is refused because what it resolves
to is the resolver business; the refusal happens BEFORE an id is minted, so a
broken override never leaves an identifier that can never be used.
Clause 5 of the field rule is a property of the wire bytes, not a promise:
a test parses every string out of the JSON and requires each to be a
compile-time constant, a member of a fixed array, a dispatch-table name, or
the hex id. The platform is a build-script constant rather than a runtime
assembly from std consts, which is what the rule forbids spelled from constants.
The verb emits machine output - show --json IS the report - so it joins the
emitting tier rather than the prose-only debt list the ceiling forbids growing.
Every count that moved was moved in the documents that state it, because the
censuses read them: 129 verb lines in three sites, 82 dispatched and 80 in the
table and 25 emitting in ADR 0066 with telemetry in the named list, all 82
verbs and the block in the README, the CLI page, and ADR 0074 eight-entry
allowlist - because the endpoint is now the binary ONE destination it contacts
rather than prints, and its ALLOWED row says under what condition.
The destination census flagged three lines that dial nothing: a printed hint
naming http://127.0.0.1 and the two scheme-prefix tests in check_endpoint. They
are respelled rather than exempted - the check splits on the separator and
compares the scheme WORD, same rule, no allowlist row - because a row is a
sentence someone has to be able to defend and these had nothing to defend.
The site half: POST /api/telemetry validates a report against the SAME closed
sets the binary declares, pinned cross-repo by reading the Rust source, and
refuses WHOLE on any field outside them. A valid report becomes source=cli
rows, one per verb invocation under /cli/<verb> plus two bucket rows, all
stamped with the arrival time so no ordering exists to reconstruct; the
telemetry_id rides the row and expires with the 30-day partitions. It sits
behind the same /api/ limit_req the beacon needed, which is why the ADR put it
there. doctor prints on/off, the EFFECTIVE endpoint, last send and the spool.
The notice gains the CLI tier, as ADR 0074 §11 requires: sends nothing unless
you run on, the id links reports and off deletes it, what is never in a report,
show prints it before you decide, and the erasure route - quote your id within
30 days, you hold the key. The test that asserted the page does NOT collect
telemetry is inverted, as it was for the visitor key.
1214 lib and 118 bin tests, 51 suites, 634 site tests, budget and font gates
green, rendered and read. (conflict resolution: crates/loot-cli/src/main.rs)
95b6c987 · dbf3dbe6…diff - the Admitted and VerbRun docs and CONTEXT.md stop saying every argv reader goes through the spec: the readers that parse positionals and flags do, the consents and relay keys now say on their own docs that they scan argv by flag name, and the named exceptions lose their count and point at the census, as bisect dispatch now does. the stale arity-census counts in main.rs and loot-core flags.rs are gone, and the owed-arity rule is one function that the top-tier census and the counting pin both call, and each still went red when burn was re-opened. the flag census and the Admitted census now share one src walk keyed by relative path, and the Admitted census went red on planted leading_word reads in cmd_bundle and verbs/mod.rs, naming the second by its path. the surface.rs privacy argument now matches Repo::surface reaching the walk unpromoted, CONTEXT.md and the resolve comment stop calling its refusal the usage line, the Verb and pick.rs doc comments are separated so rustdoc keeps them apart, and two rewrap leftovers are fixed (#1939)
7b04e214 · dbf3dbe6…diff - the zero-arity pin now asks owes_an_arity which verbs owe an arity instead of spelling its own narrower rule, and expects the same verbs as before; it went red when owes_an_arity alone exempted heads and when the heads row was re-opened. the OPEN_BUT_TAKES_NONE doc and CONTEXT.md stop listing or counting its readers and point at owes_an_arity. the Admitted doc and CONTEXT.md name the methods that do not read through the spec, as a reading of the impl rather than a rule, and with_leading_word now says it rewrites argv by position. the flags.rs censuses take the src half of the source_walk walk instead of a copy, and the Admitted census still went red on a planted leading_word read in verbs/change.rs; the consumer count is unchanged, and the stale main.rs consumer count in the consumer walk doc is gone. the loot-perf depth docs stop promising an equality nothing checks, change.rs points at the PolicyParses variant doc, and the rewrap leftovers the ticket lists are fixed, with a long line in the CONTEXT.md arity paragraph (#1944)
c5819020 · dbf3dbe6…diff - buoy and bisect are counted by telemetry like every table verb: both are dispatched ahead of the table and returned before its note_dispatched call, so a report that says it counts each verb never carried either, and the release that ships the telemetry client would have published that gap. each early path now makes the same call with its own spec name, after the same gates and before the run, so help is still not a run and no new string is introduced. a new integration test runs both verbs and reads them back out of telemetry show beside a table verb as the control, and went red when the bisect call was removed. the dispatch comment and the telemetry module header stop saying every reported name is a member of COMMANDS, which was false for other and is false for these two, and say what holds: a spec name literal or other. items 1 and 3 of the ticket are not touched here (#1945)
11291b75 · 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 - OPEN_BUT_TAKES_NONE gets the ceiling PROSE_ONLY got in #1696: its census compares the list to the tree by set-equality, which is symmetric, so appending a name was the cheapest way back to green, and OPEN_BUT_TAKES_NONE_CEILING now makes a list longer than it red until the number is raised as its own edit. the ceiling went red with its arithmetic when set one below the list (0 passed, 1 failed), and again when doctor was declared open and appended to the list in table order, where the set-equality passed and the ceiling alone fired (0 passed, 1 failed) and raising the ceiling turned it green. the list comment now says what the ceiling enforces, that the list cannot grow quietly, and that the reason for a raise is not checked, the census doc stops giving the distance to the list in lines, and ADR 0023 gains a dated amendment saying the gap it left open is closed. the workspace suite is green (#1761)
d9f6bed4 · dbf3dbe6…diff - the agent surface is settled as a decision rather than left as precedents: ADR 0088 records that loot has no template language and no template flag, that a verb gains machine output by deriving a shape from the value it already computes rather than by designing one, and states the porcelain vocabulary as a rule - tab-separated rows, a leading mark, a dash for an absent value and never a zero for an unknown count, yes/no facts in one open flag set, free text last, shared tokens, the version on json alone, and no new column on a frozen row. it names the shipped shapes that diverge from that rule, among them gates space-separated rows and telemetry key rows with a true/false column and no contract field, and records that count-objects and telemetry graduated with no ADR 0023 amendment and that nothing asserts the rule across every shape. ADR 0023 gains a dated amendment saying a shape is derived rather than designed and pointing at the rule, CONTEXT.md gains a porcelain vocabulary entry, and a new test fails when any table spec, the buoy spec or a subcommand leaf declares --template, -T or --pretty, which went red with --template added to archive and with -T added to lane gc (0 passed, 1 failed each). the workspace suite is green (#1763)
b29ce89a · dbf3dbe6…diff - review sweep 6 fix-up: ADR 0088 section 4 gains the divergences the sweep found, each checked against the code - the evolog E row printing a raw whole message (#1973), the status and buoy R rows and the tag name column holding free text ahead of the last column, tag and heads naming people by display name with no key in heads json, a sealed diff row printing a bare tier label, telemetry status and off printing a spool dash where V3 says null, and telemetry on beside show as report json with no contract - and says it records what was found rather than proving nothing else diverges. V7 now means no version line or preamble, and the visibility token and Delta renderer member lists point at the code that defines them. the template-flag census gains a twin in loot-first over its FLAGS table, both reading one spelling list now in loot_core flags; the twin went red with --template and with --pretty planted in loot-first tag, and the loot census with -T planted in lane gc (0 passed, 1 failed each). ADR 0023 credits the open flag set to ADR 0047, the OPEN_BUT_TAKES_NONE census is called an ordered comparison, its assertion points at the list instead of naming describe and new, a ceiling raise has one place for its reason, a stale family count and a stale entry position are dropped, rewrap leftovers in ADR 0088 and ADR 0035 are reflowed, CONTEXT.md follows, and owned_lane takes a Removal with named verb and refusing fields instead of two swappable strings. the workspace suite is green (#1974)
ecd53283 · dbf3dbe6…diff - loot gates --run now exits with the verdict its rows state: 0 when every gate in the phase cleared, 2 when a gate ran and refused, and 1 when loot could not run one, a program that never started or a malformed declaration, which outranks a refusal. a listing still runs nothing, and the porcelain and json bytes are unchanged, while the human sentence that said the verb exits 0 either way now names the code. the code rides the shape through a new Emit exit_code hook read at the same output boundary as bytes and notice, so gates stays in the dispatch table and no third early dispatch was added, and ADR 0076 gains a dated amendment recording the codes, superseding the #1538 exit 0 call, and noting that buoy could now move into the table while bisect dispatches early for another reason. CONTEXT.md follows. a new spawned-binary test pins both arms in each output format plus the listing, the malformed file and the codes the help states; with the dispatcher ignoring the shape code it went red (4 passed, 2 failed) while the unit pin stayed green (1 passed), with the precedence swapped both went red (5 passed, 1 failed; 0 passed, 1 failed), and with the refusal code moved the help pin went red (5 passed, 1 failed). the workspace suite is green (#1764)
b1126183 · 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 - review sweep 7 fix-up: loot-first now reads loot --version --json with a JSON parser, so the relay push is decided on a single JSON object whose top-level contract fits a u8 and whose top-level version is a string, in any key order and beside other fields, while a duplicated field, trailing text or a looser number refuses the push. the pusher= path now percent-encodes control characters as well as percent signs and whitespace, and workflow.md says so. land-change gains the relay=REFUSED recovery (catch the primary up, cargo build --release, check the contract loot --version --json prints against the major the landed commit declares, then loot push, and record the push output on the issue), says the refused block names both majors on a mismatch and a read failure otherwise, and stops counting the relay and branch states. afk-loop agrees that a FAILED or REFUSED land stands and owes a push, gains that push as a step, and counts the ticket done once the push is recorded on the issue. format.rs, ADR 0066 and ADR 0083 note that a land carrying a FORMAT_MAJOR bump normally comes out relay=REFUSED rather than FAILED, and the --version usage line, the print_out and dispatcher comments and a verdict test name follow. both new pins went red before the change (2 passed, 2 failed), the control-character pin went red with that arm removed (1 passed, 1 failed), and a serde_json Value reader went red on the duplicated field (1 passed, 1 failed). the workspace suite is green (#1979)
ce994e1d · dbf3dbe6…diff - ADR 0089 records the shallow-clone decision #1527 declined to mint from a lane, and a push from a shallow position to a host that cannot be shown to hold the history behind the cut is refused before anything is sent. the cut is on the receiving side because a depth on the wire would be a FORMAT_MAJOR move; the price is the change lane metadata crossing once in full on the first round; the guard is a shape, measured at assemble and at apply_bundle_reaching and stated by main around the dispatch, with the #1828 correction recorded beside the enumeration it replaced; the notice rides stderr; and the refuted frontier-rides-the-closure claim is kept as refuted. each bullet was checked against the tree rather than the landing message, and two did not survive the check: the landing pinned no-depth-on-the-wire three ways and #1828 deleted the re-encoding one as unable to fail, so the ADR names the two that remain; and #1527 did touch loot-core, which the relay is built from, so the true sentence is that neither the wire crate nor a server crate moved. the push decision was measured first through the real binaries on this desktop: bob at depth 2 pushed to an EMPTY relay at exit 0 with pushed 6 new object(s), the relay took the lane because stow appends every node without asking after its parents and the forge parent_trees treats an unseen parent as no comparison, and carol cloning that relay came out shallow without asking, with doctor reading recorded by a bound this store no longer records and a remedy that host could not answer; pushing back to the origin relay with nothing new, with a new change, and after the origin moved on all succeeded and still do. the rule is frontier subset of declared union ancestor_closure(declared) over the pusher own graph, asked of /haves at a relay and /ref at a forge, ahead of /wants, the first disclosing request, and free on a complete position, which returns before any question is asked; the declared half of the union is for the host whose tip IS the frontier id, which the closure walk seed filter drops and which holds everything behind it. loot pull is named only when the host declares a head this position does not hold, the moved-on shape, and fetch --deepen all is named on every refusal; a host that cannot say what it holds is refused with its own words, the ADR 0084 fail-closed clause. nothing, a warning, a receiver-side refusal and an override flag are each rejected in the ADR with the reason. red first, three ways: the gate call removed reddens the two end-to-end pins (0 passed, 2 failed, the push exiting 0 as it did) and the forge pin (0 passed, 1 failed); the declared half of the union dropped reddens its own pin and leaves the three-shape pin green (1 passed, 1 failed); the pull remedy dropped reddens the moved-on pin and leaves the empty-host pin green (1 passed, 1 failed). the body-deferring filter is NOT done and is deliberately not decided in the ADR: the ticket itself calls it a ticket-sized change against ObjectStore that introduces a fifth absence state, and the coordinating session splits it into its own issue. CONTEXT.md Shallow position points at the ADR instead of restating the cut argument, and its not-yet-guarded paragraph is now the guard. 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 (3868 passed over 120 binaries, 7 ignored) (#1826)
7a09ceb6 · dbf3dbe6…diff - the Refit family reports which version each re-anchored change became, and move, squash and absorb emit that mapping through one machine shape instead of counting it. the executor already built the old-version to rebuilt-version remap to thread each line, and each arm reported its length as re-anchored N descendant(s) or intervening change(s): the column the #1517 census named as missing from three verbs at once and declined to freeze for one of them. the reports carry the list now as Reanchored rows (from, to, the durable change id both carry, and carried for a change rewritten only because it sat above one the verb was aimed at), and the human lines print the same bytes derived from that list rather than a count kept beside it - measured through the spawned v0.4.20 primary binary against the lane build over nine fixtures across move --onto, move --before, squash and absorb (the clean arms, the move and squash stops with their aborts, the absorb stay and a mixed absorb) with minted ids masked: the transcripts are identical. the shape is spelled once in loot_cli::refit_shape over the executor seam, by the rule that filed delta_shape beside its seam: R rows for the pairs in the order minted, C rows for the paths a stop recorded, and the A and S rows only absorb fills for where each hunk went and which stayed with its reason, following ADR 0088 at every rule, with a path the frame cannot carry withheld under unprintable through the predicate delta_shape already decides with, and change null rather than a dash string on a keyless repo. the handle is read off the rebuilt version because rebuild_node drops a superseded draft from the graph, which the first cut of the pins found by reading None off the original id. squash and move declare the machine pair beside the resume trio, absorb takes OUT, the usage lines advertise it, PROSE_ONLY loses the three and the ceiling comes down 56 to 53 as the #1517 entry paid, ADR 0066 tiers move 25 to 28 and 55 to 52 with the #1517 amendment marked paid rather than left stale, ADR 0023 gains the amendment recording the value, the rows, the flag members and the JSON fields, and CONTEXT.md the Refit entry. the pins resolve both ends against the graph in-process for every report and through a separate loot evolog process for every R row off the spawned binary, where a carried working draft is asked about as @ because the hex door excludes the working change by design, and the superseded end refusing by name is asserted as the reason the mapping exists. red first: the move arm marking every row aimed-at reddens the two workspace pins and the two spawned move pins (42 passed, 2 failed; 5 passed, 2 failed), the squash arm marking its intervening rows aimed-at reddens the squash workspace pin and both spawned squash pins (43 passed, 1 failed; 5 passed, 2 failed), and the porcelain R row printing from in the to column reddens the encoder pin and the two spawned resolve pins (43 passed, 1 failed; 5 passed, 2 failed). no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green over the final tree (loot-cli 1789 passed over 52 binaries, 3 ignored; the remaining crates 2094 passed over 69 binaries, 4 ignored) (#1837)
1be80495 · dbf3dbe6…diff - sweep 1 fix-up over #1826, #1837 and #1839: a live change only a superseded head reaches is listed in the fork view, in a third section of its own, rather than in no section at all. LogGraph::retain_heads leaves such a change with an empty reachable_from, and history() partitioned the fork on exactly one head and more than one, so after a move --onto the ancestor the old tip alone reaches was printed nowhere - reproduced first through the spawned binary with two live lines (0 passed, 1 failed), the fork and the non-empty superseded head both holding as controls. GraphHistory gains unreached, the renderer prints it under behind a superseded tip after the shared ancestry, drawn_rows leads with it because such a change can only be a child of a lane or shared row, the path filter, retain_versions and all_rows walk it, and both machine shapes carry the row with no new column. HEAD on an untracked position with no working change now counts the heads Liveness::is_superseded rejects and answers the sole counted head by name, because Position::anchor answers the first graph head, which can be the superseded one; measured at the other two sites the ticket named, propose already reads is_live and IntegrationPreview deliberately counts the whole partition with retire for the superseded ones, so the liveness doc now points at the callers of the predicate rather than listing two readers. the move --onto usage line gets its space back; refit_shape spells its path columns through treepath::store_key with a backslash pin, and the #2033 census records the tree-path axis it is blind to by design; the shallow clause has one spelling, workspace::parents_not_held, read by the notice and the push refusal, with the pin the suite lacked; carried is carried_count, Reanchored.change is change_id, the squash and absorb op-log lines say re-anchored, frontier_beyond_the_host is private, the one-flag-member sentences drop one, ADR 0089 names both readers of .loot/shallow and both frontier spellings, and the one raw exit ADR 0089 rests on is held to one by a census in tutorial.rs. own > 0 for the move --onto stale head is pinned off the loot heads row. red first, six mutations in one round over disjoint pins: the unreached filter blinded, drawn_rows reordered, the HEAD filter blinded, store_key undone and a second raw exit added redden five pins with the sync shallow control green (1 passed, 5 failed) and the spawned fork pin (2 passed, 1 failed); the noun-verb swap survived tests/shallow (10 passed, 0 failed), which is why the clause has a pin now (0 passed, 1 failed under the swap). ADR 0066 keeps its gated 28 and points at the gate instead of at tickets. no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3895 passed over 122 binaries, 7 ignored) (#2046)
0b55a20f · dbf3dbe6…diff - the attestation lane has one machine row, and it says whether the listing is raw or folded. loot manifest and loot notes show gain --porcelain/--json through loot_cli::attestation_shape, one row for a lane record spelled once over the record rather than per verb: the change and the attester key as full hex, a closed kind word (role, landmark-note, landmark-retire, note, reserved), a note generation, an open flag set (removed, unprintable) and the payload last, marked R for manifest listing every record as held and F for notes show listing the trusted live register, with folded in --json; manifest also emits its grant book as G rows ahead of them, by the absorb precedent of #1837. what a role carries is read through a new loot_codec::reserved::ReservedRecord::parse that reads the envelope tag once, and render::role_display now matches on it without a wildcard instead of trying the landmark parser and then the note parser - the door #1850 asks for, built only as far as this row needs; a scratch variant fails to compile at both matches. only the notes show leaf takes the flags, and the writing leaves refuse them naming it. the prose of both verbs is byte-identical, measured against the v0.4.20 primary binary on one fixture, where both verbs refused a machine flag with unknown_flag, so no frozen row moved. PROSE_ONLY_CEILING comes down 53 to 51 as the #1519 entry paid, ADR 0066 tiers move 28 to 30 and 52 to 50 with the #1519 amendment marked paid, ADR 0023 gains the amendment recording the rows, flags and JSON fields, and CONTEXT.md the Attestation entry. red first, four ways: every row marked R reddens the shape pin, the notes pin and the spawned pin (6 passed, 2 failed; 2 passed, 1 failed); notes show building its rows from the raw lane reddens the notes pin and the spawned pin (7 passed, 1 failed; 2 passed, 1 failed); manifest dropping its grant book reddens the manifest pin (7 passed, 1 failed); and the door misreading the note tag reddens the codec pin (4 passed, 1 failed), five cli pins (4 passed, 5 failed) and two spawned pins (1 passed, 2 failed). no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3911 passed over 123 binaries, 7 ignored) (#1846)
5f1a824d · dbf3dbe6…diff - every place that repeats a stop now says what --continue will and will not do, and an F row carries the role as it was signed. resume::continue_clause is the one wording, read through ways_out, once_resolved and stop_advice by the stops, by the in-progress note status and conflicts print (which said only --continue once loot conflicts is empty), by the wrong-verb refusal (which said --continue finishes it), by apply-patch (whose own copy never said its clean paths stay unwritten), and by the apply and merge stops, which now name both ways out without the disclaimer, the reason written at stop_advice. a census in resume.rs refuses a production line of loot-cli outside that module that spells --continue, USAGE lines carrying USAGE_NOTE excepted, and its first run caught the move --before USAGE line paraphrasing the note without its disclaimer, which every_stopping_verb_documents_one_resume_sentence misses because it reads one line per verb. the note fold carries each live record role as signed (loot_core::note::LiveNote), so notes show --json role is the signed bytes for a generation spelled +2 where it was re-encoded as 2; every other row byte is pinned unchanged through the spawned binary, ADR 0023 records the amendment, and Note::parse is not made stricter, since this repo lane held no records to measure and refusing would reclassify signed records held elsewhere. manifest prose names an attester through attestation_shape::attester_name, the naming its JSON used, so the own key stops printing as hex there; unprintable is one const in delta_shape; ADR 0066 says notes stands for notes show; the ChangeGraph insert doc states its rule instead of a caller list; Pathspec::matches names the crate-private policy items instead of linking them, which removes five cargo doc warnings. declined: compile_fail,E0624, measured inert on the pinned stable toolchain (a probe edited to E0599 stayed green) and honoured only under RUSTC_BOOTSTRAP=1, the reason written at the probe. red first: the new pins failed before the fix (resume 21 passed, 2 failed; attestation_machine_shape 3 passed, 1 failed; apply_patch 12 passed, 1 failed); with the fixes undone in two rounds, the in-progress note, apply-patch copy, apply advice, re-encoded role and hex naming redden the census (1267 passed, 1 failed), apply_patch (12 passed, 1 failed), attestation_machine_shape (2 passed, 2 failed) and resume (21 passed, 2 failed), and the fold re-encoding, the old wrong-verb sentence and the old merge sentence redden the codec pin (158 passed, 1 failed), the census (1267 passed, 1 failed), attestation_machine_shape (3 passed, 1 failed) and resume (22 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 ran 3917 tests over 122 binaries with 7 ignored, and all passed but concurrent_stage_of_same_address_does_not_tear, an os error 5 in untouched persist_codec code that passed on a loot-core rerun (637 passed) and three times alone (#2052)
d5fad7c8 · 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 - what makes a line of loot --help a verb declaration is now one rule, in crates/loot-cli/src/usage.rs: the column a verb row is printed in, plus the two spaces before its description, instead of the word loot at the head of a trimmed line. the ticket named three readers of that string and the tree holds more - besides the arity census, the flags-only census and the site page pin it names, the help router, the migrate vis-spec reader, the #67 flag census, the delta-shape rung check and the revset census over the printed output all read it, most with a spelling of their own - and they key on the one function now, the site test included. the typescript cannot call a rust function in the site gate, so it reads DECLARATION and DESCRIPTION_GAP out of that module rather than respelling them, and README is the weld for the code that applies them: its verb block is pinned to the declared set from the rust side and cross-read from the site side, so a reader that drifts from the rule disagrees with README and goes red rather than measuring another population in silence. the second half is a refusal where the line is written: a USAGE line that opens with the word loot either declares a verb or is named in the failure, because such a line prints as a command to the reader of the help even when nothing mints a phantom from it. measured rather than inspected, both directions. with a wrapped description planted in USAGE and the old reading restored, five rust guards and three site assertions went red naming an invented verb changes, one of them telling the reader to give it a row on the docs page (118 passed and 5 failed in the bin, 3 failed and 4 passed on the site); with the rule in place the same line reddens the refusal alone, which prints it (122 passed and 1 failed, site 7 passed), and so does the same wrap landing in the declaration column itself. the other direction still bites: one real verb line deleted reddens seven rust guards and three site assertions (116 passed and 7 failed, site 3 failed and 4 passed). what is not claimed is a wrap that lands in that column and carries a description gap too - that is a verb row by every reading, the reader of the help included. loot view was the row whose shape ran long enough to leave a single space before its description, which made usage_signature answer with the whole line and read description words as positionals, so it gets its gap and that exception leaves the comment; ADR 0066 loses the half of its placement reason this closes. 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 (3975 passed over 122 binaries, 7 ignored) and the site gate is green end to end (662 passed and 61 skipped over 60 files, 62 surfaces) with no ceiling moved (#1883)
2f9db8e6 · dbf3dbe6…diff - loot seek carries less when asked and its refusals carry a code: under --text, -l keeps the P rows of the paths that hit and nothing under them, --count puts one N row under each P row with how many lines hit, and --max-line <bytes> cuts a hit line at a character boundary into a C row of T arity, clipped true under --json, each emitted only under a flag that did not exist before, the rule the R row set in #1522, so a consumer that never passes them reads the bytes it always read. the three are one scan rendered three ways, so skipped_sealed, truncated and the row order never move, and an uncut hit carries no clipped key so every hit shipped before renders byte for byte as it did. measured on this repo, a search for fn over crates/loot-cli/src is 259,793 bytes in 3,888 rows whole, 9,923 bytes in 92 rows under -l and 10,369 bytes in 183 rows under --count. the refusals join the #430 coded channel: a refusal under --json is one object with contract first and a code the CliError was built with, never a parse of its sentence, declared beside the verb in seek::code as conflicting_flags, read_is_bytes, no_identity, multi_head, bad_revision and no_such_remote, while a directory that is neither repo keeps no_repo and an unusable --limit or --max-line keeps bad_flag_value because a second spelling of one fault is the #1597 defect; there is no unreadable slug because --read refuses every machine format before it reads and a listing or a search counts a sealed path rather than refusing. the multi-head refusal names each head with its id prefix and subject so the next call pins one with --at in one round trip; the ticket first asked for the newest head by default and that cannot be honest from a depth-one cache, where every head reads generation 0, and authored_at is never an ordering input (ADR 0043), so the ticket was corrected before the lane opened and ADR 0023 records why. error.rs stops listing the three CLI slugs as the whole set and names what defines it, and loses a two-raiser census that was stale before this change. the pin on that one object found a defect #2092 shipped: every remote loot seek printed the dispatcher shallow note after its answer, because the body-less cache is refreshed at depth one by design and the width it records is the process-wide one main reads, so the note was wrong twice, about a cut nobody can close and naming a loot fetch --deepen that runs where the operator stands; the cache open and its object rounds now run outside_shallow_notice and a remote answer leaves stderr empty. the two-axis review before landing also refused --max-line beside -l or --count where it was silently inert, mapped the selector door stringified refusals to bad_revision on a loot target so a typo in --at carries the same code on both backends, pinned the multi-head refusal through the verb and the prefix it prints as a selector the verb accepts, pinned the cut through the scan on a multi-byte line and on an invalid byte the lossy decode widens, read the three remaining driven refusals back under --json, and narrowed the code claim to the refusals the verb decides, a stringified failure staying error as #430 says. pinned in the shape tests on every format, on a multi-byte line for the cut, on the code of each parse refusal, and on two signed heads over a spawned relay, and through the spawned binary on the seek fixture with every refusal it drives under --json read back as one object with its code. red under mutation, counts read each time: -l still writing the T rows (0 passed, 1 failed), --count off by one (0 passed, 1 failed), the JSON hit_count dropped (0 passed, 1 failed), the clip ignoring character boundaries (0 passed, 1 failed), the clip marking nothing in JSON (0 passed, 1 failed), the clipped row keeping the T mark (0 passed, 1 failed), --max-line 0 accepted (0 passed, 1 failed), the shaping flags riding a listing (0 passed, 1 failed), -l with --count accepted (0 passed, 1 failed), a parse refusal on the generic code (0 passed, 1 failed), the read refusal losing its code on the wire (0 passed, 1 failed), the multi-head refusal naming no head (0 passed, 1 failed), the multi-head refusal on the generic code (0 passed, 1 failed), the git bad-revision code generic (0 passed, 1 failed), --max-line beside -l or --count accepted and inert (0 passed, 1 failed), a bad --at on a loot target left on the generic code (0 passed, 1 failed), the multi-head guard unwired from the verb (0 passed, 1 failed), the clip through the scan measuring raw bytes (0 passed, 1 failed), and the cache refresh recording its width for the shallow note (0 passed, 1 failed). no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3984 passed over 124 binaries, 7 ignored) (#2105)
25d946e7 · dbf3dbe6…diff - ADR 0076 amended by #2101: .loot/gates gains a land phase and two hand-offs, and git hook names are mapped or refused by name. Phase::ALL has three members and every sentence that spells the set derives from it (Phase::words, Phase::usage_alternatives), so the parser refusal, the loot gates usage and the --phase errors moved without a second edit. A land gate is the user-gates row of orchestrator::GATES, between the text gates and the expensive ones, PreLandAndMerged and Outside: it runs on the lane tree and again on the merged tree when the converge moved the position, --skip-tests never silences it, and loot-first land gains --skip-gates which honors honored and never required per declared posture and records the skip in the op log after each pass, because a gate holds no Workspace. A finalize or land gate may write {message}, which expands to the path of a temp file holding the message about to be signed, newline-terminated and unlinked the moment the child returns, and {paths}, the touched paths root-relative with forward slashes, sorted, deduplicated, one argument each, deletions included; a push gate that names either is malformed, since a push has no single change, by Phase::commits_one_change rather than a list. The hand-off is built by Workspace::gate_handoff only when a gate in the phase names a placeholder, so a declaration that never asks never pays the working-delta walk, and gates::run still takes no Workspace; that accessor is the 384th Workspace method and the verb tier module doc now says so. A {message} with no message is unstartable, never a pass. The spelling was decided by running the tools, not reading them: pre-commit 4.6.2 files stage hands a hook the paths as separate arguments and its commit-msg stage hands exactly one filename, and a bare script reads that file, so a file path and argv tokens are the compatible spellings; pre-commit itself needs a .git and so cannot run inside a lane, recorded in the amendment. git pre-commit and pre-push are refused naming finalize and push; any other word is refused by name with the one sentence that holds for every unmapped hook, no index, no checkout, no rebase, no merge commit, and GIT_HOOK_PHASES is the map, never a claim about git set; the optional init --from-git was not built. Measured through the real binary in a throwaway repo, since .loot/gates is shared over the store and a land line there would make every other session binary refuse its finalize phase as malformed, which the amendment and workflow.md now warn about: two Python gates over 13 paths and the message cost 77 ms and 64 ms wall for the whole phase against 23 ms for a listing, so a contended land pays about 140 ms; loot new through the binary handed the message file and 13 paths, and --skip-gates recorded skipped p in the op log. Five pins each broken once by a named mutation and confirmed red at 0 passed 1 failed, then green at 1 passed: the row demoted to PreLand, the message file cut to its first line, the sort dropped, pre-commit unmapped, and required skipped under the flag. CONTEXT.md Phase entry and workflow.md step 7 say the same. loot-cli gates 25 passed, change and sync 65 passed, loot-first lib 225 passed after the two table assertions learned the ninth row; two lands refused on censuses the targeted runs never selected, the module-doc method count and the help placeholder classification in revset_everywhere, and each fix is one line saying the new spelling. No wire, format or store byte moves, no migration (#2101)
5c4e3996 · dbf3dbe6…diff - loot seek pays fewer round trips: a remote target is answered from its cache inside --fresh <secs> with every round trip the open makes past the record check skipped, today the info probe, the metadata pull and the grant round, and a bare http URL a loot cache already stands for is routed to it before the probe, from a record the cache writes on every refresh, the first included, holding the clock the workspace reads and never a file mtime, and a record that is missing, will not parse or is ahead of the clock refreshes rather than trusts; the answer says so, target.refreshed under --json only under the flag, the key that did not exist before riding the flag that did not exist before as the R row rule has it, and one prose line when the refresh was skipped, on stdout beside the other summary lines and on stderr for a read whose stdout is the bytes; --fresh over a local target is conflicting_flags since there is no refresh to skip and a window that is not a number is bad_flag_value; the window skips the refresh rounds and never the fetch a question needs, and since a host answers an object round with whatever the declared closure lacks, a change pushed since the refresh rides back beside the bodies a search or a read fetched, which ADR 0090 records and the smoke pins rather than hides. the bodies a question needs travel in rounds of seek::OBJECTS_PER_FETCH, 256, through fetch_objects_via whose batch is now the caller's while the pull keeps its own 32, chosen by measurement over a relay spawned in-process holding 400 small bodies, best of three on loopback: 32 per round is 13 rounds and 243 ms, 64 is 7 and 193 ms, 128 is 4 and 158 ms, 256 is 2 and 136 ms, 512 is 1 and 126 ms, the table flattening past 256 while the memory a round holds keeps doubling; the 413 halving the ticket first asked for is not built and the ticket was corrected before the first test, because a fetch request carries ids alone and the response is unbounded, so there is no 413 to fall back from. several --text patterns are one read of each kept object and the union, a line carried once however many matched it so --count counts it once and --max-line cuts it once, with a pattern that will not parse naming itself as it did alone; a repeated --text was accepted before and silently narrowed to its first value, the one shape that moved, and ADR 0023 says so. ADR 0023 records the key and the union, ADR 0090 the window and where the record lives, and CONTEXT the three. pinned on the record round trip and the inclusive window arithmetic, the union over a fixture line two patterns hit, a git cache and a loot cache each answering stale inside a window and fresh outside it, the batch read off the test relay round count with the verb own prefetch one round, and through the spawned binary on a head pushed between two calls, unseen at the recording clock inside the window and seen at a clock outside it, with the refreshed key present under the flag and absent without. red under mutation, counts read each time: a line two patterns hit carried twice (0 passed, 1 failed), only the first --text scanned (0 passed, 1 failed), the window never holding (0 passed, 1 failed), the window exclusive (0 passed, 1 failed), the git cache ignoring the window (0 passed, 1 failed), the loot cache not recorded on the first make (0 passed, 1 failed), the refreshed key written without --fresh on a loot remote (0 passed, 1 failed) and on a git remote (0 passed, 1 failed), a window over a local target accepted and inert (0 passed, 1 failed), the refreshed key dropped from the JSON target (0 passed, 1 failed), the batch argument ignored (0 passed, 1 failed), the verb prefetching at the pull batch (0 passed, 1 failed), a record ahead of the clock trusted (0 passed, 1 failed), the git answer dropping the refresh (0 passed, 1 failed), a stale read saying nothing (0 passed, 1 failed), and a stale read fetching no body (0 passed, 1 failed); the loot-remote key mutation first ran green against a pin that read only the git remote, and the pin was widened before it was believed. no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3987 passed over 124 binaries, 7 ignored) (#2108)
83c96c6a · dbf3dbe6…diff - loot seek is easier to drive: --name <glob> matches a basename at any depth, spelled into the same pathspec as --path three ways, the glob for the root, **/glob for everything below it because the dialect **/ crosses one directory or more and never none, and **/glob/** for what a directory of that name holds because the bare-directory affordance reaches a root-level pattern only, joined by the group any-match rule, because the dialect stops * at a slash and every Rust file was a --path pair agents get wrong, with a glob carrying a separator refused as a path by code; the ambient target resolved is the root lexical absolute path rather than a dot, the same root --in <dir> prints, with no verbatim prefix on Windows, a value that moves where the key does not because a dot was never a location a later call could use, which ADR 0023 says; and loot seek --schema prints the verb machine contract as one JSON document from anywhere, contract first, derived from the code rather than typed: the flags off SPEC and the attached group, the marks and their columns off MARK_COLUMNS, the codes off code::ALL, and three examples the verb renders itself, an answer carrying every optional key, a counted answer and a coded refusal, with --json accepted and any question beside it refused. the codes come in two sets that say which is which, verb_codes off code::ALL and shared_codes off CliError::SHARED defined in error.rs where those slugs are minted, and the tables that are lists are pinned against what the code does: every mark constant read off the file is in MARK_COLUMNS with the arity a rendering under every Detail emits, code::ALL is every pub const in the module read off its own source, SHARED is every slug error.rs mints, and every key Answer::json can write is in the examples, the refusal example rendered by the multi-head raiser itself. the two-axis review before landing renamed the codes key before it froze under a name that read as the whole set, moved the --name refusal ahead of the target open so a bad glob costs a remote no round trip, said that --name is not rebased by the argv door because a basename is not a path and pinned it from a subdirectory, pinned the git-local and relative --in resolved, and corrected the ADR to the spelling the code makes. pinned in the unit tests on a nested fixture and the refusal, on the schema derivation, and through the spawned binary on --name equal to the pair it spells and not to **/ alone, unioned with --path, the ambient resolved absolute and equal to --in <root>, and --schema from a directory that is no repo. red under mutation, counts read each time: --name not spelled at any depth (0 passed, 1 failed), a --name with a separator accepted (0 passed, 1 failed), the ambient resolved staying where the verb ran (0 passed, 1 failed), --in <dir> resolved staying as typed (0 passed, 1 failed), --schema taking a question beside it (0 passed, 1 failed), MARK_COLUMNS forgetting the count row (0 passed, 1 failed), MARK_COLUMNS with an arity wrong (0 passed, 1 failed), code::ALL forgetting a slug (0 passed, 1 failed), the schema example carrying no refresh (0 passed, 1 failed), the schema omitting the bare flags (0 passed, 1 failed), SHARED forgetting a slug (0 passed, 1 failed), the schema omitting the shared codes (0 passed, 1 failed), the schema refusal example not the raiser (0 passed, 1 failed), a --name refusal waiting for the target to open (0 passed, 1 failed), and --schema answering a question instead through the binary (0 passed, 1 failed). no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3999 passed over 124 binaries, 7 ignored) (#2115)
ca25f385 · dbf3dbe6…diff - the source-walk consumer census sees a helper brought in by a use, and which methods the Admitted exceptions census watches is read off the marker on their own docs: the consumer walk keyed a reach on a call site spelled with the module path, so the spawn-seam census in main.rs, which calls the cut through an import, was counted by nobody and stood on no discrimination floor at all, and the src file walk #1944 added to flags.rs was invisible the same way, which is why #1944 reported the consumer count unchanged. a use item is resolved per file now, with a rename, a glob and an item that does not close on its line refused rather than read wrongly, and the floor is owed by the consumers that read the cut rather than by every consumer, derived from this module own source as the helpers that reach MARKER, the one spelling the cut keys on, so the file walks and the header readers owe it nothing while every census that cuts owes it as before. the derived header sentence is 15 consumers in 3 compilation units: 9 library, 5 binary, 1 integration test, and the paragraph that enumerated the censuses by name now says what a consumer is and leaves the files as an index the guard checks one way, saying which way. with_leading_word, which rewrites argv by position with no spec asked, was documented as doing so and was watched by nothing, so it carries the marker now and arrives in the roster with its one caller, bisect mark_by_term; whole-word matching moves into source_walk beside the walk, one spelling where flags.rs held the copy. red under mutation, counts read each time with the filter selecting the one test: the imported half of the reach dropped (0 passed and 1 failed), a planted use-imported bare call of the cut in main.rs (0 passed and 1 failed), the floor call removed from the spawn census (0 passed and 1 failed), the cut-reader derivation reduced to the direct namers of MARKER (0 passed and 1 failed), a renamed import planted (0 passed and 1 failed), the marker taken off with_leading_word (0 passed and 1 failed), and a planted with_leading_word caller in bisect dispatch (0 passed and 1 failed). item 3 of the ticket was already closed by #1974 and nothing there moved. no migration, no wire or format byte moves and nothing outside a test and a doc comment moves, so this owes no deploy. the workspace suite is green (4013 passed over 125 binaries, 7 ignored) (#1946)
47b0d164 · dbf3dbe6…diff - loot pipeline (#2127, ADR 0091, map #2099 ticket 3): the tracked .lootpipeline at the repo root, in the .lootattributes grain with no dependency column, one step per line as name, trigger, kind and what, where the trigger names the tree a step judges, change or main, never the moment, and the kind is run, argv through the spawn seam with {message} and {paths} as gates reads them through one shared builder gates::spawnable, or check, a loot-native gate deferred naming #2128 until the checks live where the verb can reach them, and a deferred step is a refusal exiting 1, never a pass. loot pipeline lists and spawns nothing; run judges the working tree by default or, under --version, a scratch tree of a held version written by Workspace::scratch_tree from readable_tree_at so the readability decision precedes any file, holding the version own .lootpipeline, and removed on drop, with the hand-off from Workspace::version_handoff naming the paths whose readable content differs from the first parent; --only narrows to one step and an unknown name is refused naming the declared set; status reads the pipeline/TRIGGER/KIND passes and pipeline/approve admissions trusted keys signed on a version through attester_trust, passes only. A tracked file never runs implicitly: loot new with a run step declared and no .loot/gates line reaches no spawner, pinned. Frozen shapes in ADR 0023 amendment, exit code on the shape. Measured on the release binary: a two-step run 74 ms, run --version 47 ms and a listing 18 ms on a three-file repo; on this repo of 1452 tracked files, run --version HEAD materializes the whole readable tree in 1.2 s against a 0.15 s listing. Found by that measurement, not by thought: under load a Windows scanner held a freshly written scratch file, the one-shot remove_dir_all failed and its swallowed error left plaintext in the temp dir, so ScratchTree drop now retries over half a second and names the path on stderr if it still cannot remove it. Seven censuses moved, each read off its own refusal: the verb tier method count to 387, MACHINE_OUTPUT gains pipeline in sorted position, the usage spells the list leaf as an invocation line, README block and its all 84 verbs sentence, 133 verb lines in three places, the revset placeholder lists take <change|main>, <step> and <id> and SELECTOR_ONLY takes pipeline status, and the site CLI page names the verb; ADR 0066 counts 84 dispatched, 82 in the table, 32 emit machine output with an amendment. CONTEXT.md Pipeline entry says what is built. Nine unit pins plus two in change.rs, five broken once by a named mutation and confirmed red at 0 passed 1 failed then green at 1 passed: a check made a pass, the trigger filter widened, the scratch removal removed, the approval role made a job, and the tracked file run at a finalize. loot-cli and loot-first green across 64 binaries, 2162 passed; the site verb census green. No wire, format or store byte moves, no migration (#2127)
fe693773 · dbf3dbe6…diff - loot-hygiene (#2128, map #2099 ticket 4): the land text gates move from loot-first into a crate with no dependency that builds for wasm32, re-exported as loot_first::hygiene so every gate row, checkout test and AGENTS.md path reads unchanged; a new crate rather than loot-codec because three consumers now read it, loot-first, loot-cli and loot-wasm, and the wasm one is the fact ADR 0076 §2 no-new-crate reasoning did not have. Corpus::from_entries files an in-memory tree under exactly the allowlist the directory walk applies, top-level files plus SOURCE_ROOTS filtered by TEXT_EXTENSIONS minus SKIP_DIRS and SKIP_PATHS, through one filing rule both constructors share, and a link resolves against the entry set with dot and dotdot folded, so a Worker with no directory answers what the land answers. CORPUS_CHECKS is the table a .lootpipeline check step names, keyed by the land gate names and pinned to them, holding the floor-free halves: the land refusals still refuse a vacuous walk first because a mis-rooted land must not read as clean, but a three-file repo is not mis-rooted, so a check answers about the files it was handed. loot pipeline now runs check steps in-process over one corpus walk per run, a finding is failed and exits 2, an unknown name cannot start and names the set and exits 1, and the ? deferral #2127 shipped this morning is retired the same day with the ADR 0023 amendment saying so. loot-wasm links the crate and a wasm-bindgen test runs the three checks from entries under node. Measured: the site wasm is 475667 bytes before and after, unchanged, because nothing exported calls the crate; native the three checks over a 1120-file synthetic corpus take 0.8 ms in release and 28.0 ms under wasm in node; through the release binary on this repo, roughly 1355 covered files and 20 MB by an approximate count, the three checks cost 160 ms warm against a 73 ms listing and 6.5 s on a cold page cache. Two lands worth of censuses read off their refusals: the destination allowlist row for the moved file, and no other. Five pins broken once by a named mutation, red at 0 passed 1 failed then green at 1 passed: from_entries skipping widened, link resolution made true, the table key misspelled, a finding made a pass, an unknown check made a pass; the fifth sweep needed a second pass because its first restore string matched twice and left the mutation applied, caught by the full run that followed. 66 test binaries green across loot-hygiene, loot-cli and loot-first. No wire, format or store byte moves, no migration (#2128)
858575e1 · 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 - the whole-word reading gets one home and a use this walk cannot follow is refused rather than skipped: #2126 landed census_text so that a .rs-text reading more than one census needs and none of them owns has one home, and the very doc saying the source_walk whole-word reader was shared rather than copied per census had a copy of it sitting in the temp-root census next door, drifted already, one asking char::is_alphanumeric and the other an ASCII byte test, so the sentence was false the day it was written. the reading moves into census_text as whole_word_matches, the offsets a word stands at as a whole identifier, with names_whole_word derived from it rather than written beside it, so the caller that wants the answer and the caller that wants the places cannot come to disagree about where a word begins, and the boundary is the Rust one and not the ASCII one, since a boundary that reads too narrowly lets a longer identifier answer as a whole word, which is a census reporting an offence that is not one. the copy the ticket found was not the only one: loot-first, loot-forge and loot-relayd each held the same closure inside the bare-flag census of its own crate, and each reads the shared file now through the same cross-package path attribute the other callers use, so one edit reddens every consumer of it. the docs stop naming callers and say instead what decides where a reading lives, which is the census_text admission rule, and the seam a reader arrives from now carries why the import reading stays in source_walk: it is keyed to that module own name and to what helpers_named can find afterwards, so it is not a flat question. that import reading also stops enumerating what it refuses, since the enumeration was already stale: a use item naming the module is the module under its own name, or names taken out of it, and everything else falls through to refuse_import, which is how use crate::source_walk as sw, outside both branches and skipped in silence, the under-count #1946 was filed on surviving the ticket that closed it, becomes a refusal without being named. the narrowing census in main.rs stops re-deciding which narrowing is in force and asks the door, since taking the first declared narrowing whose flag a shape requires and taking the narrowest part on a shape requiring two narrowing flags of different counts, latent while no verb writes that shape and now unreachable because the rule has one home, pinned where it lives. the forwarder that discards a door result is declined with the reason at sole_statement: a door too many is a site too many and the offenders are asserted empty, so it arrives red naming the call, while reading the discard would shrink the door set, which is the direction that loses a site in silence. one more of the same class was found beside the rest: code_mask said the two censuses that share this in the present tense, and it now speaks in the past about the two walks it replaced. red under mutation, counts read each time: the shared boundary widened to admit every character reddened all five consumers from the one edit (loot-cli lib 5 passed and 2 failed, loot-cli temp_root_census 1 passed and 2 failed, loot-first 0 passed and 1 failed, loot-forge 0 passed and 1 failed, loot-relayd 0 passed and 1 failed), the alias refusal put back to the silent skip (0 passed and 1 failed), the plain module import refused as well, which is the other direction (0 passed and 1 failed), and the door narrowing rule flipped from narrowest to widest (loot-core 0 passed and 1 failed, with the CLI census still green, which is the point of the move). no migration, no wire or format byte moves, and nothing outside test support and a doc comment moves, so this owes no deploy. the workspace suite is green (4049 passed over 129 binaries, 8 ignored) (#2148)
cb7f057f · dbf3dbe6…diff - buoy joins the dispatch table and bisect is left alone on the early path with the only reason that is still its own: #1764 made an output shape able to carry its own exit code, which was the whole of why buoy was dispatched ahead of COMMANDS, so the arm is gone and cmd_buoy returns a shape like every other verb. emit::Buoy reads ADR 0025 exit codes off the BuoyVerdict it already holds, one arm per row of that table, so the rendered answer and the code are one decision and the empty porcelain of the none row cannot come apart from its 2; buoy --nearest carries its code as a value on the message instead, having collapsed to one token with no structure to read an outcome back off; and buoy_write is deleted rather than moved, because the dispatcher outcome was already byte-for-byte the same #870 rule about a reader that left early. everything that walked the table and the buoy spec separately now walks the table alone — the telemetry note_dispatched call, the machine-output census, the verb census, the documented-flag census, the template-flag census, help_for and the completion list — and nothing is counted twice: the telemetry report is asserted to say buoy once, the table is asserted to declare no name twice, the offered completion names are asserted to hold each name once, and the census sum is now an equality with the table rather than the table plus one. going through the argv door means declaring an arity, so exactly one invocation moved: loot buoy reviewed junk refuses by name and exits 1 where an open claim used to drop the word and resolve for reviewed, which is #1562 reaching a verb that had been standing outside the gate enforcing it. every code is pinned through the spawned binary in tests/buoy_exit.rs, each outcome in every rendering it accepts. red under mutation, counts read each time: the verdict codes flattened to zero (buoy_exit 4 passed and 2 failed, emit_snapshot 12 passed and 1 failed), Message::coded discarding the code it is handed (loot-cli lib 1334 passed and 1 failed, buoy_exit 5 passed and 1 failed), the early telemetry call put back beside the one in the table (telemetry 2 passed and 1 failed, reporting buoy three times for two runs), buoy pushed onto the completion list beside its own row (loot-cli bin 125 passed and 1 failed, buoy_exit 5 passed and 1 failed), the row declared twice (loot-cli bin 121 passed and 5 failed), the row declining its arity again (loot-cli bin 123 passed and 3 failed, buoy_exit 5 passed and 1 failed), and the census sum left at the table plus one (loot-cli bin 125 passed and 1 failed). ADR 0025 records where each code now comes from and the one that moved, ADR 0076 closes the open work it had recorded and narrows the early-dispatch path to bisect, ADR 0066 moves the split inside its dispatched total, and CONTEXT.md stops naming buoy beside bisect. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4058 passed over 129 binaries, 8 ignored) (#1976)
fa68a7d3 · dbf3dbe6…diff - the filter in front of the import resolver becomes part of what it refuses, and a dated amendment gets back the figure it landed with: #2148 made the branches of imported_names refuse by default and left a filter in front of them that kept only a line starting use, and a default a filter feeds is a default over what the filter let through, so a module named on a continuation line and a pub use re-export reached no branch at all and were skipped in silence, which is the #1946 under-count surviving the ticket that closed it and then the ticket that closed that, a third time. use_items reads items and not lines, a statement being the run between one semicolon and the next and a use item being the statement the keyword stands in, so a visibility, an attribute and where the lines break decide nothing, and what would make that wrong is a statement spelling the keyword without being one, which costs a refusal naming the line rather than a reach passed over. the call walk stops deciding the same question a second way and reads the code with those items blanked by without_use_items, so an import can never also be read as a call and the use and mod line prefixes it skipped by are gone, the declaration it does look for being found by what it declares. the re-export and the wrapped item are planted as outcomes, one resolving and one refusing, beside the alias #2148 planted, and the blanking is planted with them. the ASCII identifier reading in temp_root_census is gone for census_text is_ident_char, which whole_word_matches is now bounded by too, so where a name begins has one home and the wider letter a narrow reading lets answer as a whole word is planted where that census can see it. ADR 0066 #2127 amendment is restored to 82 in the dispatch table, which is what was true on its date, and the cause is named rather than the number: the_verb_counts_stated_in_prose_are_the_ones_usage_holds held every occurrence of its phrases to the count the code holds today, records included, so the cheapest green was to rewrite the record and #1976 rewrote it; claims_only takes the amendment entries out before that census reads, an entry stating what was true on the date it carries. emit.rs stops saying buoy is dispatched ahead of the table, ADR 0088 stops walking a buoy flags constant that has no references left, three plurals about verbs dispatched ahead of the table go singular where one verb is left, the telemetry pin reads the count instead of a substring of it, since buoy 1 stands inside buoy 10, and the glossary stops spelling the secret-shaped set with two env positions where #1930 made it three. red under mutation, counts read each time: the line shape deciding again which items are read (loot-cli lib 6 passed and 2 failed), an item ending where its line does (7 passed and 1 failed), the shared boundary widened to admit every character (loot-cli lib 4 passed and 4 failed, temp_root_census 1 passed and 2 failed) and narrowed back to ASCII, which nothing caught before this (temp_root_census 2 passed and 1 failed), the ADR records read as claims again (loot-cli bin 0 passed and 1 failed at the control, and with the control stood down 0 passed and 1 failed naming 82 where the code says 83), the cut widened to take every line (0 passed and 1 failed, the phrase it guards no longer stated), and the count reader stopping at the first digit (telemetry 3 passed and 1 failed). item 6 is a correction to two landed commit records and is reported rather than made. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4062 passed over 129 binaries, 8 ignored) (#2156)
54167f6e · dbf3dbe6…diff - the forge learns runners and jobs (#2157, ADR 0091, map #2099, first of three slices of #2129): migration 0018 adds the runner and job tables, both repo-scoped, so both join 0014 row-security set and take 0017 forge_is_bound_to_one_repo policy verbatim in shape, and tests/rls.rs ENABLED_TABLES names them, which is the set equality that forces the decision rather than a list nobody maintains. A runner is its OWN keypair under NO account and that is the security property expressed as schema: require_pusher and the proposal door both resolve a signer through account_key, so a runner key is refused at push and propose BY ABSENCE, where the same key on the owner account would have been a full pusher since require_pusher admits any live key of the account; the row is written from an envelope the namespace owner signed, which is a proof needing no browser session, so tests/account_tier.rs is green UNAMENDED. A job identity is the triple version, trigger, kind, so creation is idempotent by the primary key; member and approved are computed in at creation so a later membership change cannot re-authorize queued work; a claim is claimed_by plus a lease the CLAIMANT names, exclusive by FOR UPDATE SKIP LOCKED in the driver and one lock across find-and-write in the reference store; an expired lease returns the job by a READ-TIME predicate, so there is no reaper to fall behind; a verdict is accepted whatever the lease says, because the runner did the work and a bookkeeping deadline must not discard it; a job is born unclaimed and unfinished and a creation carrying either is refused, since those are the two writes that have to be exclusive. Trigger and Kind move from loot-cli to loot_net::pipeline, the one crate the forge and the CLI both already depend on, re-exported so every path reads unchanged: the forge stores what the CLI writes, and two copies of one vocabulary would be a list to keep in step. Capabilities is exactly Kind as a set, and a bit this build cannot name is REFUSED rather than dropped, because reading a newer runner row as covering less would hand it jobs it cannot do. Five conformance cases join the roll call, so both stores answer one contract and neither can name a subset. Measured: bash ci/local.sh green end to end against Postgres 18, 129 test binaries, 0 failed and 0 SKIPPED, which is what proves the driver, the migration and both policies rather than only the reference store; the claim exclusivity and the lease expiry are proved there on real Postgres, and an EXPLAIN plan under contention is NOT measured here and is owed to #2158 where the poll load is. Three refusals found by the run rather than by thought: the destination census flagged a fixture wake-up URL, allowlisted with the reason that it is a value the store round-trips and never an address anything dials, which took the allowlist to ten and moved ADR 0074 spelled count; the shipped-predicate pin read migration 17 alone, so it now reads the migration that CREATED each table rather than recording which came from where; and the binding fixture seeds a runner and a job row per repo, without which its controls were zero and the refusal below them would have passed for the wrong reason. Five pins each broken once by a named mutation, red at 1 failed then green at 0 failed with the count above zero: the claimable predicate widened, the lease made to hold forever, retirement re-dated, the born-claim refusal disabled, and an unknown capability bit accepted. No routes, no wire and no verb here; those are #2158 and #2159. No FORMAT_MAJOR move (#2157)
ea4d34a5 · dbf3dbe6…diff - a refused keypair archive puts back the half it moved, and the cut that blanks amendment records stops blanking half the document: #2112 made the order safe and left the re-run broken, because the suffix is a tag the two halves share and the search wants both names free, so a refusal at the second rename orphaned the public half under rotated-ts and sent the next run to the next suffix, handing back a path nothing had written. archive_keypair now undoes its own first rename before it returns, so the run after it meets the repo the refused one met and archives a pair under a tag of its own, which is what a re-run at a later second needs, and where the put-back is itself refused the refusal names where the public half was left; the archived public path comes back as None where there was no id.pub to move, so what it hands back is where a file is and not a name it picked. the amendment cut now asks for a date, the date being the whole reason an entry is not read: ADR 0066 opens with an undated Amended inline header and the reader blanked from there to the end of that quote, 482 lines of 953, taking the index of the amendments and the claims around it with no guarded phrase lost and nothing to say so. its control is new and reads the other way, walking every blanked line up to the header it stands under and spelling that header itself rather than reading the constant, so a widening from either end arrives red naming the lines and the guarded phrases on them, where the old control was green over half a document and the per-row found check fires only when the cut takes the last copy of a phrase. ADR 0066 stops pointing the reader at the newest entry, which is blanked, and says instead that the figures in that block are the ones held against the code, and what makes a block an entry. use_items stops naming one cost for two callers: a false item is refused in the resolver only where it also spells the module name, and is blanked in the call walk, which is the #1946 direction in the reading written to close it. the judgement items: both sides unterminated over a line they share and an empty file in each direction are pinned through the binary, the empty-lines arm in the marker test is gone with its reason moved onto the expect that ends_clean makes true, a name bound to two types three lines apart in the apply path is split, and the walk guard in store_rename_census reads the workspace members instead of the crates directory it already walks, asserted both ways. red under mutation, counts read each time: the put-back removed (loot-identity 48 passed and 1 failed, the refusal leaving id.pub.rotated-7 orphaned), the public path returned unconditionally (48 passed and 1 failed), the date requirement removed (loot-cli bin 0 passed and 1 failed, naming line 15 and 103 lines), that same defect with the new control stood down (1 passed and 0 failed, which is what shipped), the cut never leaving the quote (0 passed and 1 failed, naming line 197 and every guarded phrase the rows carry), the member list read short (store_rename_census 3 passed and 1 failed), a member declared outside the walk (3 passed and 1 failed), the empty-side carve-out removed from ends_clean (patch_trailing_newline 3 passed and 1 failed on the expect), and the marker withheld from a kept line (3 passed and 1 failed). ADR 0016 records the put-back in a new entry rather than rewriting the one that was wrong. the deploy sentence #2112 landed is narrower than that change, and that correction is a record posted on the ticket rather than code here. no migration, no wire or format byte moves and no relay or forge behaviour moves, so this owes no deploy, though the client rotation path moves and rides the next release. the workspace suite is green (4086 passed over 130 binaries, 8 ignored) (#2169)
b17b2955 · dbf3dbe6…diff - the owner can finally see what was offered to them, and a read request had to stop being a withdrawal in disguise: propose::list had no caller outside its own tests and propose::read only handle_propose_withdraw, so ADR 0075's store half was built and unreachable and an owner could see nothing. /propose/read and /propose/list join the contribution family with loot propose --show and --list on the CLI side, flags rather than a verb because a new verb trips seven censuses here and a flag does not. the finding the ticket did not anticipate is that a read naming repo and change id would have encoded BYTE-IDENTICALLY to a withdrawal, and the route lives in the URL rather than in the bytes the envelope signs, so a captured read envelope from the tip's author - the one party withdraw admits - could have been re-posted to /propose/withdraw and ended their own offer, a non-destructive act becoming a destructive one with no forgery at all. every new payload in this family now leads with an act tag and the replay is run in both directions rather than argued. the reads dispatch before run and touch ProposeTransport nowhere, because that seam exists to make the propose-time ordering assertable and a read has no ordering to assert, and --show resolves its handle before the remote and before the key so a mistyped id is answered by the refusal for the thing mistyped. terminal rows are listed rather than filtered at the last hop, an empty list says visible to you rather than claiming the repo has none, and an underivable tip is rendered beside its row rather than as a refusal. the propose-time disclosure gains the presenting key, which ADR 0075's #2162 amendment decided is world-visible on a metadata-public repo and which the old sentence omitted while naming the author. two enums share one vocabulary by WORD rather than by numbering, pinned total and injective, because loot-forge depends on loot-net and never the reverse. three censuses refused and each was right: the dispatch table lost a row to a multi-line verb declaration, a source-walk anchor spelling [Route; 16] went stale and is now count-free per the rule its own header already states, and the selector census demanded --show declare its kind, which is evolog's - one proposal by construction, borrowing evolog's resolver rather than growing a fourth. red under mutation, counts read each time: the act tag dropped (118 passed and 1 failed), the handler telling concealed from absent (422 passed and 1 failed), the list filtering terminal rows (422 passed and 1 failed) and the disclosure reverted to naming only the author (11 passed and 1 failed), each restored to 119, 423, 423 and 12. no migration and FORMAT_MAJOR does not move, but two routes are new, so an old forge 404s them and this owes a forge deploy before the flags answer against the live host. the workspace suite is green (4156 passed over 132 binaries, 8 ignored) (#2174)
09b5ad09 · dbf3dbe6…diff - loot runners lists the runner rows a forge holds for this repo, part 2 of 2 of this ticket: name, key, the step kinds each may claim, live or retired, and when the forge last heard from it, owner-only because it is the owner configuration, retired rows included because who was allowed to run a version is what an audit asks. last seen needed a column, migration 0021 runner.last_seen_at, written by the runner door on every admitted request and throttled in its WHERE clause to one write per runner per 60 seconds, so a runner polling every second costs one write a minute; a registration never sets or clears it, so a runner registered and not started reads never seen, and deriving it from job rows was rejected because an idle runner finding no work leaves none. the first spelling of that throttle, last_seen_at <= $3 - $4, failed on Postgres 18 at the first touch, which would have made every runner request a 500 on the live forge while every in-memory test passed; the edge is computed in Rust now, and landed part 1 never wrote the column. the shape is born frozen under ADR 0023: porcelain pubkey state capabilities last_seen and the name last because it may hold a space, JSON with contract, remote and one object per runner, absent times as null, both from one value and pinned byte for byte. the verb census cost, counted: MACHINE_OUTPUT gains runners, the README block and its sentence move to 85 verbs, verb lines 133 to 134 at three main.rs sites and one ADR 0066 claim, dispatched verbs 84 to 85, dispatch table 83 to 84, machine output 32 to 33 with its named list, and two ADR 0066 lines that are dated records were left alone as the census skips them; the site verb list gains runners and its gate is green. the verb runs end to end through the spawned binary against a real forge. red under mutation, counts read each time: the touch unthrottled (1 passed and 1 failed), the door never touching (0 passed and 1 failed), the name not last (0 and 1), an absent time spelled as a dash in JSON (0 and 1), and a registration allowed to set last seen (1 and 1) once its pin moved into the store, where the defence lives, after a first attempt at the route proved unobservable; on Postgres 18 the throttle removed from the SQL went red at 0 passed and 1 failed after a green unmutated arm, each restored green. bash ci/local.sh is green against Postgres 18 (4258 passed over 136 binaries, 10 ignored). migration 0021 rides the forge binary, so the forge owes a deploy (#2158)
6dd6ae69 · dbf3dbe6…diff - loot revert --continue and loot cherry-pick --continue now replay the stopped pick instead of closing the stop, on the answers loot resolve gave to that stop and on nothing else: the in-progress record gains the version the verb applies, the conflicts its stop recorded and the answers, resolve writes an answer there only when the conflict it settles is the one the stop recorded at that path, and change_delta_merge takes only the answers its caller hands in, none from an ordinary run, so the repo settled ledger is still never read by a pick (#744). a path whose content moved after it was answered is asked again rather than merged, a path holding the answer bytes at another address keeps it, a replay that stops again keeps the record restore point, and a record written before this closes as it always did. ADR 0080 gains the #1986 amendment deciding the three questions and recording, measured through the binary, that move, squash and apply-patch stop again after a resolve for reasons that are not a ledger and that apply and merge have nothing to replay; CONTEXT.md, the resume module and the usage note say the same. the resume runs to completion through the binary on the home position, on a pinned primary and in a lane. red under mutation, counts read each time: the repo ledger handed to the resume (0 passed, 1 failed in the stale-entry pin), the repo ledger handed to the three-way (0 and 1 in the loot-core pin, 0 and 1 in the re-run pin), a moved path left to the three-way (0 and 1), the address test alone (0 and 1 in loot-core, 0 and 1 through the binary), an answer keyed on the path alone (0 and 1), the record sentence worded by verb (0 and 1), the replay branch emptied (0 and 4), the re-stop taking a new restore point (0 and 1) and the usage note dropping revert (0 and 1), each restored to green. the record gains a trailing section, no format byte moves and there is no migration, so this owes no deploy. the workspace suite is green (4302 passed over 138 test binaries, 12 ignored) (#1986)
8bd8e53c · dbf3dbe6…diff - the review-sweep fix-up over #2287, #2283 and #1986. a resumed pick now asks again about a path its stop asked about when the record holds no answer for it, rather than handing it to the three-way, which merged over the resolution and put back the edit it rejected: change_delta_merge takes the stopped record whole and takes every question on it out of the three-way, keeping an answer only where the line still holds it, so a resolve that recorded no answer (a loot from before #1986, or a failed answer write, which the workspace comment now truthfully says costs a re-ask) re-stops with nothing applied, keeps the restore point, and lands once the question is answered again; refusing --continue was weighed and rejected in the ADR 0080 #2291 amendment, since nothing would be left to resolve. the replay reads its direction from pick::SPECS, which now pairs each spec with it, and a record naming a version under a verb SPECS does not hold refuses instead of replaying as a cherry-pick. the usage note names no verb, so each stopping verb line is true of its own verb, and its test now fails if the note names a verb that replays. a cherry-pick replay to completion is pinned through the binary on the home position, a pinned primary and a lane. false sentences fixed: the in-progress operand reader, the pick stop comment, the resume test doc link, the continue_clause list, the CONTEXT.md in-progress entry and HUNT-PERF on graph file reads; rewrap leftovers in fixture.rs and engine.rs are fixed, and the replaying tests keep their resolution file outside the working tree. red under mutation, counts read each time: questions taken from the answers alone (16 passed and 1 failed in the loot-core pin, 31 and 2 through the binary), an unknown verb replayed as not inverted (20 and 1), the note naming cherry-pick and revert again (19 and 2, the second the census the usage lines no longer matched), each restored to green. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4309 passed over 138 binaries, 12 ignored) (#2291)
812c48c7 · dbf3dbe6…diff - the repo owner can decline a proposal: propose::decline beside withdraw, POST /propose/decline, and loot propose --decline <change-id>, the flag named as its siblings on the verb each spell their act. a declination is signed over the repo, the change id and the tip, and tagged Act::ProposalDecline so it can never be read as a bound withdrawal or a read; on the wire it is a bound withdrawal plus the tag byte, and neither format constant moves. the owner is the key require_pusher admits, the rule jobs already asks for an approval; a member reads the proposal and is refused in words naming the author withdrawal as the other route, and a key the read gate conceals the row from gets one sentence whether the row exists or not. may_propose is not asked, so a shut door does not stop a decline; a repeated decline is the same event and is not re-dated, a landed row refuses by name, and a withdrawn row refuses as the withdrawal refuses a declined one. a declination replayed after a revised re-proposal is refused by the tip binding, the same stack re-proposed is declined again, and one signed for another repo of the same owner is refused by the repo binding. the CLI signs the tip the forge reports at the moment of declining and prints it. uncontended, a decline is one close_proposal and no graph read, pinned on the store call log. it touches no job made for the declined tip, which ADR 0091 leaves undecided. ADR 0075 records the build and what a decline does to the bytes, with CONTEXT.md and the forge spec. red under mutation, counts read each time, each 0 passed and 1 failed and then 1 passed restored: the owner check skipped, the visibility gate skipped, the repo binding removed, the tip binding removed, a declined row refusing re-proposal, a landed row declinable, a withdrawn row declinable, the decline walking the stack, the route asking may_propose, the act tag dropped, the tip left out of the signed bytes, and the declination written under its own minor. no migration and no format byte move; the forge binary changes and an older forge answers 404 on /propose/decline, so this owes a forge deploy before the flag works against the live host. the workspace suite is green (4376 passed over 139 binaries, 12 ignored), and green under bash ci/local.sh against Postgres 18 (#2176)
b45cbbf3 · dbf3dbe6…diff - the review-sweep fix-up over #2307, #2176 and #2193. a seek busy marker is believed while its clock is within the window of the reader clock on either side, so the round that claims first with the later clock is no longer taken over, and a marker that does not parse is dated by its mtime against that same now. loot propose --decline takes the version it declines, the full tip --show prints or a selector naming a version held here, following the grammar that already reads a version where a change id is expected; a change id is refused, the CLI finds the row standing at that tip and refuses when none visible does, so a revision made after the owner read the proposal is not declined. two act flags on loot propose refuse through ProposeAct::from_args, where the first one checked ran. the format-constant pin compares the marker of each trailer and act payload with the one a client from before #2159 wrote, recorded in the frozen codec copy, so a bump of either constant reddens it; a push declaring each head its own kinds is pinned through a two-head repo; the decline cost pin asserts the whole call sequence with the gate calls measured alone; the forge door tests ask every route in Route::ALL, now public, that the router mounts. each GATES row declares what its finding is read off (Judged): a finding off files gets a merged remedy that goes to loot edit at once, with no re-run and no file lock, and the pre-land pass rather than each finding says nothing has been signed or published, so the merged pass no longer prints it beside a signed merge; the #2193 census reaches each spawn after passing ones and holds every Judged declaration against the spawner. false sentences fixed in ADR 0055, 0075, 0090 and 0091, workflow.md, npm_not_started and the census doc, including a network-specific NAT64 prefix, which the wake-up rule cannot recognise; the forge refusals of withdraw and decline spell the tip as a version id; the cmd_propose doc sits on cmd_propose; the wake slot test waits for its places to come back. red under mutation, counts read each time, each restored to green: a marker ahead of the clock read as dead (2 passed and 1 failed), the unparseable marker dated by the system clock (2 and 1), the decline signing the tip the forge holds (4 and 1), a change id accepted (4 and 1), two acts not refused (4 and 1), FORMAT_MINOR at 1 (1 and 2), the push unioning kinds (0 and 1), a second read in the decline (0 and 1), /propose/read mounted without the door (0 and 2), the pre-land state line dropped (2 and 1), the files remedy replaced by the run remedy (2 and 1), the second perf spawn failure a finding (2 and 1), the site row declared Files (2 and 1). no migration, no format byte and no wire byte moves; the forge refusal wording changes, so that is live once the forge is redeployed, the decline argument once the CLI is released, and a land runs the primary release loot-first, so the gate changes are live only once those binaries are rebuilt. the workspace suite is green (4392 passed over 139 binaries, 12 ignored), and green under bash ci/local.sh against Postgres 18 (#2325)
ff3b789d · dbf3dbe6…diff - a proposal has a conversation on the forge, stored and written and read, and no CLI flag posts to it yet: migration 0022 adds proposal_comment, keyed under the proposal row so a thread outlives a decline and a re-proposal, holding the envelope its author signed and its body, both set to NULL by a withdrawal that leaves a tombstone saying author or owner, with CHECKs that refuse a withdrawn row holding either, row security bound by the 0017 policy and the rls census widened. POST /propose/comment, /propose/comment/edit and /propose/comment/withdraw are tagged acts over propose::comment, where the rules live: a signer must see the proposal and is otherwise told what an absent one is told; the owner class writes, the accountless namespace key included; anyone else needs a live account key, so a bare keypair or a retired key is refused; members, the proposer and the tip author write through a shut door, anyone else only through an open one; a comment binds the tip on file when it arrives; a path must be in that tree and a reply must name a comment on the proposal; the body is at most 16 KiB of UTF-8, refused and not cut; only the author edits, re-signing the same id, anchors unmoved and bound to the BLAKE3 of the envelope it replaces; the author or the owner class withdraws, and members and proposers do not moderate. the comment id is signed by its author, so the same envelope again is one comment and a withdrawn one is not re-posted back. the thread rides POST /propose/read as a trailer and loot propose --show prints each body only when its envelope verifies against the author and anchors beside it. the flags that post, edit and withdraw a comment, and the disclosure printed before posting, are not built, since ADR 0095 names no flags; ADR 0095 amended and CONTEXT.md updated. red with each piece undone, counts read each time, each restored green: a bare keypair admitted, the door not asked, visibility not asked (each 0 passed and 1 failed on the writer-set pin), a member moderating, the owner editing another comment (0 and 1 each), anchors free to move (0 and 1), a withdrawal that writes nothing (0 and 1), no cap, and the body cut to the cap instead of refused (0 and 1 each), no tip binding (0 and 1), an edit not bound to its prior (0 and 1), an edit past the writer rule (0 and 1), the read sending no thread and an unverified body printed (0 and 1 each); on a throwaway Postgres 18, a withdrawal that kept the words with the CHECKs dropped left the conformance case green (1 passed) and turned the pin that reads the whole row red (0 and 1). bash ci/local.sh is green against Postgres 18 (4476 passed over 139 binaries, 13 ignored). no format constant or codec byte moves; migration 0022 rides the forge binary, so this owes a forge deploy (#2331)
6af9b14f · dbf3dbe6…diff - a decline may carry a reason signed with it, kept on the proposal row and shown as the thread closing event: ProposalDeclination gains an optional reason as a trailer on its signed bytes, so a decline with none is the bytes #2176 signs and neither format constant moves, and a reason a carrier swaps or strips no longer verifies. migration 0024 adds proposal.decline_reason with a CHECK holding it to a declined row, never empty and at most COMMENT_BODY_MAX bytes, and replaces the three 0020 proposal views with the column appended, which a text guard holds equal to 0020 once the column is taken out. propose::decline refuses an empty or oversized reason whole and a repeated decline with other words, close_proposal writes the reason only with a decline, and a re-proposal clears it with closed_at. POST /propose/read carries the reason as a trailer after the thread continuation, absent from an older forge, which would drop a reason unread, so loot propose --decline <tip> --reason <text> asks the read first and refuses a reason such a forge would drop, then prints who can read it before signing; --show prints the decline under the thread with its reason caged through render sanitize. frozen before_2336 codecs pin old client against new forge and new client against old forge both ways, and tests/proposal_read.rs compares the column for each reader class with an executable red that blanks it in one view (1 class disagreeing). red with each piece undone, counts read each time, each restored green: the reason left out of the signed bytes (0 passed and 1 failed), the read trailer not decoded (0 and 1 on each of two pins), the route dropping the reason, the read serving none (0 and 1 each), no cap and an empty reason admitted (0 and 1 each), a repeated decline changing the reason (0 and 1), a re-proposal keeping it and a reason written with any terminal state in the memory store (1 and 1 each, the pg stamp of the case green without a database), the CLI signing for a forge that would drop it, the reason printed uncaged (0 and 1 each), --reason admitted beside no decline and no disclosure before signing (0 and 1 each), the migration cap moved and a view joined to another family gate (0 and 1 each). ADR 0095 amended, CONTEXT.md and the forge spec updated. bash ci/local.sh is green against Postgres 18 (4519 passed over 139 binaries, 13 ignored). migration 0024 rides the forge binary and the read and decline routes change, so this owes a forge deploy (#2336)
c8d5a6ea · 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 - loot ticket new, loot ticket show and loot tickets ship as slice 1 of the native tracker (spec §12, map #2422): a new ticket module in loot-cli holds the layout (tickets/<id>, tickets/public/<id>, tickets/<group>/<id>, where an 8-letter k-z segment is always an id and never a space), the one file format of header lines, a blank line and a verbatim body, ids of 16 random bytes shown as 8 letters and minted again on a collision with any ticket at the read position, and causal order keyed on the space-stripped path, found by walking the change graph from the read position in descending generation to the change where each key first appeared, with a key only the working tree holds sorting after every recorded one. a read folds the tree at the harbor tip, this position and the working tree, so a lane sees a ticket a sibling landed without adopt and its own uncaptured one. new writes meta, the body and one waits-on edge per --on as plain files in the working tree and prints the id alone on stdout; the body comes only through --body-file, a file or - for stdin, with a leading byte-order mark dropped and CRLF made LF, and --title refuses a tab or a line break. show --json and tickets --porcelain are the spec §7 shapes, recorded in an ADR 0023 amendment; the JSON leads with contract as ADR 0023 has every --json do, the open form and the sealed {contract, id, sealed: true} alike, the spec field list naming the payload after it. a ticket this identity cannot open is an S row, and a field filter keeps it. --restricted-to and --published are left to #2428. red under eleven named mutations over sixteen runs, each 0 passed and 1 failed and restored: position keyed on the spaced path, the harbor tip skipped, a colliding id kept, the byte-order mark kept (once through the binary), a tab allowed in a header, an unopenable meta dropped, an id-shaped segment read as a space, labels folded add-only, a field filter dropping a sealed ticket, and contract left off the sealed form and off the open form. the verb census moves: README 87 verbs, 137 usage lines, ADR 0066 at 87 dispatched and 35 with machine output, MACHINE_OUTPUT, the placeholder census and the site verb list. cargo test green in two runs, 4576 passed with 13 ignored, and the site gate green at 819 passed (#2424)
1a26ff0a · 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.