Changes touching this path
- Spec the jj-ergonomics trio: ADRs 0029/0030/0031 (#138) (#141)
* Dogfood drive: log day 2 (clean bridge day, 2/5)
Day 2 was a git-clean day: the CA epic close-out had landed on main via
PRs, so the ferry carried it across (4 commits ingested, 0 conflicts, 0
projected back; 146 objects pushed, resumable). Record the day in the
drive log + evidence table (2/5).
Also fix loot-day.ps1 doubling the "ferry:" prefix in the logged line
(loot's own output already carries it).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Spec the jj-ergonomics trio: ADRs 0029/0030/0031 (#138)
The hand-off deliverable of wayfinder map #132. Folds the three design
decisions (#134/#135/#136) and the prototype's validated verb surface (#137)
into three implementation-ready ADRs:
- 0029 stable change-ids — durable random change_id beside the content
version id; signature binds version_id ‖ change_id; divergence is a
first-class data state; letters-vs-digits display; FORMAT_MAJOR 5→6.
- 0030 implicit auto-snapshot + reconciled verb surface — snapshot on
mutating verbs only; status read-only (-m dropped); new mints+prints the
next change id; columnar log/status; divergent-change marker + `loot
abandon`; demotion guard travels on the implicit snapshot; never signs.
- 0031 operation log & undo — view-only undo over an append-only graph;
op log grows on undo (redo); grant/maroon/pull-grants/push are non-undoable
barriers; keyring/manifest/escrow/purges never touched; local-only .loot/ops.
Points CONTEXT.md's out-of-scope note at the spec. Implementation graduates
to a follow-on build map.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
4403ce13 - Merge branch 'main' of https://github.com/Connor-Miller/loot
2f8096b7 · 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 - 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 - reconcile_onto captures whenever it may materialize, not when git brought new commits (#280)
802cbf5f · 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 - Judge the tip-duplicate drop by recorded manifests, so deletions count (#289)
A change whose only content was DELETING files was silently destroyed at
finalize: same_tree_content compared tree_at ancestry overlays, and the
union resurrects an ancestor's entry for every path the child deleted, so a
deletion-only capture read as content-identical to the tip and
drop_capture_if_redundant ate it - describe message and all. This very bug
ate its discoverer's cleanup change (#284/PR #285); the workaround that
landed was riding a one-line edit alongside the deletions.
same_tree_content now compares the two changes' recorded manifests (every
change carries its complete tree, deletion = absence): path set AND
content. An empty capture is redundant only when nothing is held to compare
against - over a non-empty tip it is a delete-everything change and signs
like any other work. Same judgment at the adopt catch-up's inline site, the
live status row, and working_preview's tip slice (a clean tree no longer
reads dirty forever once a deletion lands). Truly identical captures - the
bare new on a clean tree, the co-located checkout after a git pull - still
evaporate; an undescribed deletion now hits the #174 refusal instead of
silently no-op'ing. ADR 0030 amended.
06d1880e · dbf3dbe6…diff - primary catch-up: checkout at the #291 squash (site/ scaffold) folded for the drift reconcile (#297)
e6f7079a · dbf3dbe6…diff - a view step refuses over work no change records, and a primary finalize says so on stderr (#436)
a3248ede · dbf3dbe6…diff - the three hints that speak when a git pull ran ahead of a loot pull say one thing, and the hazard names loot new, the verb that actually signs, instead of describe, which does not (#1391)
7a273f8d · dbf3dbe6…diff - re-pointing the working tree becomes one call that carries the #436 clobber guard with it, so the four bisect checkouts stop destroying an edit made while testing a midpoint, and no caller is left owing a guard that only a doc comment could name for it (#1414)
17abc3a0 · dbf3dbe6…diff
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.