a73x

16d61346

Promote retro lessons into CLAUDE.md and restructure RETRO.md around actions

a73x   2026-09-06 08:27

Commit message
Promote retro lessons into CLAUDE.md and restructure RETRO.md around actions

CLAUDE.md
Old New
@@ -34,4 +34,122 @@ Rust 2021 edition: Follow standard conventions
34 34
35 35
36 <!-- MANUAL ADDITIONS START --> 36 <!-- MANUAL ADDITIONS START -->
37
38 ## Invariants — do not break, they are load-bearing
39
40 Stated here because a *plan* once instructed the opposite and was implemented
41 faithfully (2026-09-06, lease tokens). A step that contradicts one of these is
42 a bug in the plan, to be fixed in the plan document.
43
44 - **Contributing code requires nothing but plain git.** Everything else may
45 require the server. This replaced the older "if it only works with
46 `git-collab-server`, it is not a git-collab feature" test on 2026-09-05; see
47 `docs/superpowers/specs/2026-09-05-server-authoritative-collab-design.md`.
48 - **The event DAG is append-only.** A correction is a new event, never an edit
49 of an old one, so the record stays an audit trail. `patch log --timeline` is
50 where that is visible.
51 - **Every historical `event.json` shape stays readable.** Events are signed, so
52 nothing can rewrite one; making a field required broke 39 patches in the
53 user's repositories once (see the `PatchCreate.commit` comment in
54 `src/event.rs`, which is load-bearing prose). "Absent" is a state the types
55 must be able to express.
56 - **Unknown repo and unauthorized repo answer identically.** Same bytes, same
57 exit code, over SSH and HTTP alike, so an error cannot be used to probe which
58 private repositories exist. Enforced in `handle_release_command`,
59 `handle_lease_command` and the HTTP 404-collapsing in `git_http.rs`.
60 - **Lease rows are a per-issue tenure ledger and are never deleted.** Freeing
61 one (release *or* expiry) clears `holder`; `token` only ever increments.
62 Deleting a row, or sweeping an expired one, resets the counter and destroys
63 the only property a fencing token has. Reads filter, they do not reap.
64 - **Revision refs are named by commit OID, not revision number.** `r/<n>` does
65 not survive two clones revising offline.
66 - **One rule per question.** Liveness of a lease is `Row::held` and nothing
67 else; two predicates for one question is how they drift.
68 - **The `commit-msg` hook never fails a commit.** It runs inside `git commit`;
69 the one thing it must not do is give git a reason to abort. No matching
70 patch, several, a detached HEAD, or no binary on `PATH` all leave the message
71 exactly as written.
72 - **Prose abbreviates ids; `--json` carries all forty characters.** The JSON id
73 is what a script holds on to. `abbrev::for_issues`/`for_patches` compute the
74 display width from the whole id set, never from the filtered rows on screen.
75
76 ## Working rules
77
78 Each of these exists because of a specific failure; the date is when it bit.
79
80 - **Never touch the index or HEAD to answer a question.** No
81 `stash`/`switch`/`reset` to check whether a failure pre-dates your work — use
82 `git worktree add --detach /tmp/base-check <ref>`. Under Delta's replicating
83 VFS a broken `&&` chain left the tree clean and the work in `stash@{0}`,
84 twice in one session, the second time two hours after writing the lesson down
85 (2026-09-06).
86 - **Branch before the first commit of a slice.** Delta's Changes view diffs the
87 thread's branch against its base, so committing onto `main` renders an empty
88 diff while the work piles up invisibly (2026-09-06, five commits).
89 - **Stage by path, never `git add -A`.** It swept `.codex`, `.superpowers/`
90 scratch and two unrelated untracked docs into a one-file commit (2026-09-06).
91 - **Wrap remote probes in `timeout`, and look for a local clone before cloning
92 fresh.** A bare `ssh` probe of git.a73x.sh hung two minutes (2026-08-25).
93 - **Read the existing resolution before proposing a design.** A display-time
94 merge-base recomputation was proposed before reading `patch.rs`, which
95 already prefers the *recorded* base — a recomputed merge-base equals head
96 once a patch lands, rendering an empty diff.
97 - **Do not write a claim in a comment you have not tested.** A comment asserted
98 `SORT_NONE` was unstable for same-timestamp commits; only `TIME` is.
99 - **Enumerate the boundary members before the happy path.** Empty, single,
100 first/root, duplicate or already-seen, freed-then-reused. Five separate
101 file-history bugs and one lease bug were all one shape: the rule was right
102 for the common case and wrong at a boundary.
103 - **Ask the outside about the inside.** A component reporting on itself cannot
104 catch itself being wrong: keys come from `ssh-keygen`, not our own encoder;
105 interop is asserted through the real `ssh` client and real `git`.
106 - **Capture `$?` before piping.** `cargo test | grep -c ok` reports grep's exit
107 status, not the build's.
108 - **A green suite is not user acceptance.** Say what was verified and what was
109 only asserted.
110
111 ## Gates
112
113 `make lint` (fmt + check + clippy) before a commit; `make ci` (lint + test +
114 build) before delivery. `cargo clippy --all-targets` is expected to be silent.
115
116 **The baseline is not green, and was not before you arrived** (measured at
117 `702a1e6`, 2026-09-06): nine `trust::tests` fail because they call
118 `load_trust_policy`, which reads the developer's real
119 `~/.config/git-collab/trusted-keys` rather than using the
120 `load_trust_policy_with_global` seam that exists for injecting one; and
121 `a_sync_authenticates_to_the_server_once` fails with `matches reject pattern`.
122 Both are non-hermetic tests reading real user state. Until they are fixed,
123 `cargo test --no-fail-fast` is the only way to see the other targets, and a new
124 failure has to be told apart from these ten by name. Fixing them is an open
125 action in `RETRO.md`.
126
127 ## Reading this repo without burning context
128
129 `wc -l` on 2026-09-06; re-measure before trusting a figure. Prefer `grep -n`
130 for the symbol, then `sed -n 'A,Bp'` for a window.
131
132 - **Do not `cat` these:** `src/tui/mod.rs` (2.7k), `src/state.rs` (2.4k,
133 the DAG-replay materialisers), `src/patch.rs` (2.1k),
134 `src/server/ssh/session.rs` (1.9k), `src/lib.rs` (1.8k, the command
135 dispatch), `src/tui/widgets.rs` (1.5k), `src/server/governance/conf.rs`
136 (1.3k), `src/cli.rs` (1.2k), `src/sync.rs` (1.1k),
137 `src/server/repos.rs` (1.1k), `src/server/setup.rs` (1.1k). Tests too:
138 `tests/sync_test.rs` (2.6k), `tests/common/mod.rs` (2.0k, the harness),
139 `tests/collab_test.rs` (1.8k).
140 - Plans under `docs/superpowers/plans/` are implementation scripts and run to
141 thousands of lines with code inline; read the task you are on.
142 - Pipe command output (`| tail -30`, `grep -c`, `2>/dev/null`). A full
143 `cargo test` run is thousands of tokens of re-billed noise.
144
145 ## Where the answers live
146
147 `docs/superpowers/specs/` designs, `docs/superpowers/plans/` implementation
148 plans (the current direction is the 2026-09-05 server-authoritative design and
149 its five phases) · `docs/design-debt.md` accepted debt and when to revisit ·
150 `RETRO.md` open actions and retained debt, **read before scoping a slice** ·
151 `README.md` user-facing behaviour, and the place a claim about the CLI's output
152 gets checked · `~/code/rad/workshop` the agent workshop this server is one
153 piece of (`heimdall`, `durin`).
154
37 <!-- MANUAL ADDITIONS END --> 155 <!-- MANUAL ADDITIONS END -->
RETRO.md
Old New
@@ -0,0 +1,135 @@
1 # Retrospectives
2
3 One section per delivered slice, newest last. Each carries what was delivered,
4 what worked, what to change, and **actions the next slice must close** — marked
5 `[x]` with evidence, or carried forward with a reason. Retained debt gets an
6 owner and a *trigger* rather than a date, so it fires when someone next touches
7 that area instead of rotting.
8
9 This file is an input, not a diary: read the open actions before scoping the
10 next slice. Lessons that generalise past their slice get promoted into
11 `CLAUDE.md`, which is loaded every session — this file is not. The format is
12 borrowed from `~/code/rad/mux`, whose `RETRO.md` has closed 49 actions this
13 way.
14
15 ## file-history view (date not recorded)
16
17 Retrofitted into this structure from flat notes; the lessons are verbatim, and
18 no narrative has been reconstructed around them.
19
20 ### Lessons
21
22 - the `git2` revwalk API is `set_sorting`, not `set_sort_mode` (API drift from docs I'd seen); wasted a compile cycle on the rename.
23 - wrote a comment claiming `SORT_NONE` was unstable for same-timestamp commits — never tested it. The demo proved both `NONE` and `TOPOLOGICAL` are stable; only `TIME` is broken. A review caught the unverified claim.
24 - `.take(limit)` was before `.filter_map()`, so a repo with 200+ commits could show an empty history if early commits didn't touch the file. Caught in self-review, not by a test.
25 - 404'd on deleted files because the handler validated the path exists at HEAD. A deletion test caught it — the existence check was wrong.
26 - (P1) compared only parent 0, so a clean non-fast-forward merge whose entry matched a non-first parent was listed alongside the source-branch commit. Fixed by comparing all parents (git's TREESAME rule: suppress if matching any). A merge test caught it.
27 - (P2) compared only blob OIDs, so a mode-only change (chmod +x, same oid) was omitted. Fixed by including the tree-entry mode in the comparison. A chmod test caught it.
28 - the all-parents comparison made a root commit with no parents and no file vacuously "differs from all parents" — a never-tracked path showed the initial commit. Fixed by requiring root commits to actually introduce the path.
29
30 ### What to change
31
32 Five of these six are one shape: **the rule was right for the common case and
33 wrong at a boundary** — first parent vs all parents, blob oid vs oid+mode, the
34 vacuous root commit, `take` before `filter`. Enumerate the boundary members of
35 a set before writing the happy path.
36
37 ## push-to-create, rejected — 2026-08-25
38
39 ### Lessons
40
41 - probing git.a73x.sh with bare ssh hung 2 min (no timeout) and I cloned settings fresh before checking ~/code/settings existed; wrap remote probes in `timeout` and look for a local clone first.
42
43 ## Patch web diff (date not recorded)
44
45 ### Lessons
46
47 - proposed recomputing merge-base at display time before reading `patch.rs`, which already prefers the *recorded* base (a recomputed merge-base equals head once a patch lands, rendering empty) — reading the CLI's resolution first would have skipped a wrong design.
48
49 ## Issue leases — phase 1 of server-authoritative collab — 2026-09-06
50
51 Delivered atomic TTL work claims on issues: a SQLite lease store, the
52 `collab-lease` SSH exec verb (acquire/renew/release/list), `issue
53 claim|unclaim|renew|claims` on the CLI, and claims shown in the web UI. The
54 SSH key is the credential — no tokens were added. 8 planned tasks, 6 commits,
55 57 new tests; `cargo clippy --all-targets` clean. Plan:
56 `docs/superpowers/plans/2026-09-05-issue-leases.md`.
57
58 ### What worked
59
60 - Writing the plan before implementing gave every task a failing-test-first
61 shape, and the e2e tests were what caught the one real design bug.
62 - Extracting `remote_ssh` before the CLI needed it kept `release` and `lease`
63 on one SSH client path rather than two.
64 - Deriving the lease `repo` key in one function (`leases::repo_key`) once two
65 callers appeared. The symptom of skipping that would have been a claim that
66 exists over SSH and is invisible on the page.
67 - Writing the README section forced a check of what the commands actually
68 print, which is how the un-abbreviated ids were caught.
69
70 ### Lessons
71
72 - the plan told me to `DELETE` a lease row on release and to reap expired rows on every read. Both reset the per-issue `token` to 1, destroying the one property a fencing token has (tenure 1's zombie passing a fresh tenure 1's check). The reaping path was worse: *reading* the lease list would have silently reset fencing. An e2e test asserting the second tenure gets token 2 caught it; the fix is that rows are a tenure ledger, never deleted — `holder IS NULL` means free.
73 - two liveness predicates (`Lease::live` and `Row::held`) existed briefly for the same question. Deleted the unused one rather than leaving both to drift.
74 - to check whether some unrelated `trust::tests` failures pre-dated my work I did `git stash -u` + `switch main` + `switch back` + `stash pop` inside a Delta worktree. The `&&` chain didn't complete, and I landed on `main` with an apparently-empty stash list — the WIP looked lost (it wasn't; `git reflog stash` still had it). Under a replicating VFS, don't stash-and-switch to inspect a baseline: read the file, or use a throwaway `git worktree`. The failures were in `trust.rs`, which I had never touched — the check wasn't worth the risk in the first place.
75 - repeated the stash-and-switch mistake I had *just* written up, two hours later, to check the same kind of pre-existing failure. The `&&` chain broke at the same place and the tree came back clean with the work in `stash@{0}` again. The right tool was there all along: `git worktree add --detach /tmp/base-check <base>` gives a pristine checkout with no effect on the working tree, and that is what finally confirmed both failures were pre-existing. Writing the lesson down is not the same as applying it — when a command would touch the index or HEAD to answer a question, answer the question a different way.
76 - committed the first five commits straight onto `main`, which is the branch Delta's Changes view diffs *against*: the UI showed nothing while five commits sat there, and the work had to be moved onto `leases` after the fact. Branch before the first commit of a slice, never onto the base.
77 - `git add -A` swept `.codex`, `.superpowers/` scratch and two unrelated untracked docs into a commit that was supposed to be one plan file. Stage by path.
78
79 ### What to change
80
81 - **A plan is a hypothesis, not an authority.** This is the first bug here whose
82 source was the plan document itself, faithfully implemented. Plans are very
83 detailed in this repo (the release plan is ~2,400 lines with code), which
84 mostly helps but means a wrong instruction gets built exactly as written. So
85 plans state **invariants** beside steps, and a step that contradicts one is a
86 plan bug to fix in the document (done: `571c422`).
87 - Lessons were being filed in this file, which nothing reads at decision time.
88 Anything that generalises now goes to `CLAUDE.md`.
89
90 ### Actions for the next slice
91
92 Owner: whoever scopes the next slice. Mark `[x]` with evidence, or carry
93 forward with a reason. A passing test does not by itself close an action about
94 judgement or process.
95
96 - [x] Promote the generalisable rules from this file into `CLAUDE.md`'s manual
97 section, each naming the failure it prevents. Evidence: the Invariants and
98 Working rules sections added 2026-09-06.
99 - [x] State the invariants that existed only in my head or in scattered code
100 comments (lease token monotonicity, the identical-reply rule, append-only
101 events, plain-git contributing). Evidence: `CLAUDE.md` Invariants section.
102 - [ ] **Make `cargo test` green so the gate stops being decorative.** Nine
103 `trust::tests` read the developer's real `~/.config/git-collab/trusted-keys`
104 instead of using the existing `load_trust_policy_with_global` seam;
105 `a_sync_authenticates_to_the_server_once` fails on refs/collab pushes with
106 `matches reject pattern`. Both confirmed failing at base `702a1e6`, so
107 neither is new. Six of this file's entries end with "a test caught it" —
108 tests are the primary defence here, and a permanently-red baseline means new
109 red cannot be told from old red. That is what cost two near-losses of working
110 state above.
111 - [ ] Add a `make base-check REF=<ref>` target wrapping
112 `git worktree add --detach` + test + `worktree remove`, so the safe way to
113 inspect a baseline is one command. A note alone demonstrably did not hold.
114 - [ ] Add the boundary-member checklist (empty, single, first/root,
115 duplicate/already-seen, freed-then-reused) to the plan-writing routine, and
116 require plans to state invariants beside steps.
117
118 ### Retained debt
119
120 - **Lease owner, next lease change:** fencing tokens are stored and reported
121 but nothing verifies them — there is no server-mediated write to fence until
122 phase 3. When patch revisions start flowing through the server, every
123 holder-authored write must carry its token and stale ones must be refused.
124 - **Web UI owner, next issue-page change:** an ungoverned server shows the raw
125 key fingerprint (`key:SHA256:…`) as the claim holder. Correct, unreadable.
126 Governance already supplies a person's name; ungoverned has nothing better
127 without a local alias map.
128 - **Lease owner, when a repo is deleted:** lease rows are keyed by repo path
129 and never reclaimed, so deleting a repository leaves its rows behind. Harmless
130 at any plausible size (one row per ever-claimed issue) and deliberate — the
131 rows carry the fencing high-water mark — but it is unbounded growth with no
132 sweeper.
133 - **Whoever tracks this file:** `RETRO.md` is still untracked in git. An
134 untracked retro cannot be reviewed, referenced from a commit, or read by
135 anyone else, which is part of why its lessons did not propagate.