resume: hide sessions that burn started, show them with --all (#74) - #83
Conversation
`clikae resume` and the home board's picker listed every session on disk; on the operator's Mac 478 of 499 antigravity sessions were headless `clikae burn` one-shots nobody will resume. burn now records the session it started in a sidecar (state/burn-sessions/<engine>/<tank>, one `<sid>\t<run_id>\t<epoch>` line per run — claude by the id it launches with, codex and antigravity by the newest transcript after the run); the picker hides those by default, `--all` or the `a` key shows them labelled [burn]. The antigravity adapter sources its recent list from the CLI's own cache/last_conversations.json with the transcript glob as fallback. Built by antigravity (agy) on tank g; the title lookup it also touched is dropped here because PR #80 carries it. bats to follow in a separate commit — the build lane's harness forbids editing tests.
…sed (#74) tests/bats/resume-hide-burn.bats (7 cases): the sidecar line carries the sid claude was launched with; the non-tty listing hides it and --all shows it labelled [burn]; codex and antigravity record the transcript created after the run, not an older one with a later name; an agy sid missing from the CLI's cache still resolves; `clikae clean` leaves the sidecar directory byte-identical; malformed sidecar lines are ignored. Writing them against the previous commit went 0/7: the claude launch argv did not carry the session id, the sidecar was written under the wrong root, and one construct was not Bash 3.2. All three fixed minimally; the three files then pass together with burn.bats and resume.bats (112/112). Tests by codex (gpt-6-astra) on tank crazy; report in the lane worktree.
4dd2e6c to
ace416c
Compare
adapter_sid_canonical <path> is now the single place claude/codex/antigravity
compute a transcript's session id. codex's uuid embeds hyphens of its own
(8-4-4-4-12), so resume's picker (${sid##*-}) kept only the uuid's OWN last
segment while burn's sidecar recorded the full id read from the file body -
the two never matched, so a codex burn session's sidecar line was byte-correct
and the row stayed visible anyway. clean.sh's _clean_session_is_live already
trusted "a codex uuid is always the trailing 36 characters" for its
live-session guard; apply the same fact in resume.sh's _resume_session_fields
(inline, not routed through load_adapter - this runs in clean.sh's per-session
hot loop and must not pay for a re-source on every mixed-engine row).
bats: adapters/{claude,codex,antigravity}.bats assert adapter_sid_canonical
directly and (codex) that it matches adapter_recent_sids's own derivation
byte-for-byte; resume-hide-burn.bats adds the codex hide A/B the review's
own fixture already exercised but never asserted.
… P1-3) switch.sh:222-228's own gate, mirrored here. burn's extra args (after --, or a raw --session-id) could already carry resume/session identity; appending --session-id unconditionally fought claude's own rule (--session-id needs --fork-session alongside --continue/--resume, verified live 2.1.267) and turned a previously-working launch into rc=1 with no artifact at all. adapter_sid_from_args (already used by switch.sh) is asked first; only mint + append when it says the launch is genuinely fresh. A caller-supplied sid also becomes the one burn records - it is exactly as proven as a minted one. bats: burn claude T -- -p ... --resume <sid> and ... --session-id <sid> both rc=0 (were 1), sidecar holds the caller's own sid. burn.bats 142/142 green.
The cache extraction grep -E'd the whole MATCHING line, then let sed's greedy `.*:` walk to whichever `: "` came LAST in that line. Every real agy install writes last_conversations.json compact/single-line, so once it holds more than one project's pointer, that walk silently returned a different project's session - the board's Continue row (and Enter) resumed the wrong conversation, not just a wrong preview string. New shared helper json_value_for_key (lib/core/json.sh) isolates just the "<key>":"<value>" pair via grep -oE before sed ever sees it - anchored to the matched pair, not the whole line. Generalizes the same escape-aware pattern grok.sh's _grok_json_str already used for its own single-key lookups. bats: a compact two-entry cache (both key orderings) returns THIS cwd's sid, never the other project's. antigravity.bats/resume-hide-burn.bats/burn.bats all still green.
…P2-1) adapter_recent_sids's cache path returned immediately on a hit, so any limit greater than 1 still got back exactly one row - the board's Continue list (limit 10) and home.sh's exclusion-pass retries (also 10) silently collapsed to at most one agy candidate no matter how many real sessions the directory actually had, and a second bare row on the same tank fell back to the unfiltered guess R3-P2-1 had already fixed once. limit<=1 keeps the original single-stat fast path (burn's own use) exactly as it was. limit>1 keeps the cache hit and falls through to the disk scan for the rest, deduped so it is never listed twice, then both sources are ranked together by sessions_by_mtime as before. bats: limit 10 with a cached hit + 3 disk-only sessions returns all 4, no duplicate; limit 1 still short-circuits on the cache alone; a bigger ask still caps at the given limit. home.bats (62/62), resume-hide-burn.bats (14/14) and burn.bats (142/142) all still green.
… read (#74 P2-2) The burn sidecar (state/burn-sessions/<engine>/<tank>) had no GC anywhere: rename_tank_state carried the burn-order entry and dry marker across but not this; remove.sh's rm -rf never touched it (out-of-dir state, keyed by name); clikae clean deliberately left it alone. Every burn attempt (including infra retries) appends a line forever. - rename_tank_state / remove_tank_burn_sidecar (profile_store.sh): move it on rename, delete it on remove. Both translate antigravity -> "agy" for the sidecar path specifically - burn.sh has always stored agy's sidecar under that literal directory name, unlike every other piece of out-of-dir state. - clean.sh's new _clean_burn_sidecar_gc: drops lines whose sid no longer has a transcript, caps what's left at CLIKAE_BURN_SIDECAR_CAP (default 2000, newest kept - the file is append-only, so newest = the tail). A file needing neither prune is left byte-identical (untouched mtime, no rewrite). - resume.sh's picker: the O(sessions x sidecar-lines) `case` substring compare against the WHOLE accumulated sidecar per candidate is now ONE `grep -n -F -x -f` pass over every candidate at once. New shared _burn_sids_file (home.sh) is the one store read, reused by P2-5's board fix next. Measured (this host, other lanes running concurrently, uptime posted alongside): 5000-line sidecar 82ms (review's baseline: 3044ms), 20000-line 214ms (review's baseline: 8170ms) - ~38x at the reviewer's own case. bats: rename/remove tests for the sidecar; clean.bats direct-calls _clean_burn_sidecar_gc for the stale-drop, the cap, dry-run, and the byte-identical no-op case. resume-hide-burn/burn/home/antigravity.bats all still green (243/243 across this run).
#74 P2-4) `c` (open clean, come back) does three things before rescanning: closes fd 3, _home_tty_leave + drops the EXIT/INT/TERM trap, unsets _handle_key. `a` (toggle --all) did none of them - it went straight back to _resume_picker's rescan while still in the alt screen with stty -echo in effect. Not theoretical: toggling --all OFF on a store that is now all-burn makes `sessions` empty, which takes the "No sessions to resume yet" + exit 0 path further down - printed INSIDE the alt screen, then wiped by the EXIT trap's _home_tty_leave on the way out. The user pressed `a` and saw nothing happen. bats: resume.bats + resume-hide-burn.bats stay green (31/31) - this class of bug needs a real pty to observe directly (tests/tools/pty-smoke.py territory); noted in the round's report as unverified by a live pty run.
The board's `R` key already forwards to `clikae resume` (one filter, not two - the PR body's "two surfaces" framing was right about that half). But _home_recent_rows (the Continue list: this dir's newest across engines) is a SEPARATE query and had no burn filter at all - a lane's one-shot session, being by definition the newest thing in the dir it just ran in, kept showing up on the board's very first screen even with resume's own hiding in place. Reuses _burn_sids_file (added in the P2-2 commit) so both surfaces read the sidecar the same way. Filtered before the rank+cut, so a hidden row leaves its slot for a real session instead of just shrinking the list. bats: a burn session is absent from the Continue list by default and a real session takes its slot; CLIKAE_RESUME_ALL=1 shows it. home.bats (63/63) and resume-hide-burn.bats (14/14) still green.
- CLIKAE_RESUME_ALL is a plain assignment, not `export` (P3-2): it is only ever read inside this same clikae process (the picker's filter, the non-interactive list) and never needed by the resumed engine - exporting it let it leak into the engine's environment, the same class of incident claude.sh's CLIKAE_LAUNCH_SID comment already records. - docs/usage.md:58's resume row now mentions --all / `a` alongside the other picker keys it already documented. - CHANGELOG's #74 entry now describes both surfaces the fix actually covers (resume's picker AND the board's Continue list, P2-5) and the sidecar's proven-attribution + lifecycle (P1-2/P2-2), and declares agy --json's run_id going from null to a string (P3-4, already shipping in this PR, previously unannounced). bats: a new resume.bats test drives `clikae resume --all <sid>` through a stub engine that dumps its own env and asserts CLIKAE_RESUME_ALL is absent. resume.bats (18/18) green. shellcheck -S warning clean across every file this round touched.
|
P1-1 fixed (35bbdce) — codex's picker derived a session id via |
|
P1-2 / P2-3 fixed (83c2d82) — sidecar attribution is now PROVEN, not guessed. The old "newest transcript with mtime ≥ attempt start" had nothing tying it to this run's own process, so a human's concurrent session in the same tank got attributed to the lane (and then hidden) — the inverse of what #74 asked for. It also wrote a ghost sid on every dry/failed run. claude: New bats: zero-transcript runs (agy + codex) write no line; an unresolvable same-cwd collision (agy) records nothing rather than guessing wrong; codex picks the cwd-matching rollout among several. |
|
P1-3 fixed (1d1265e) — |
|
P1-4 fixed (871ffaa) — the cache extraction |
|
P2-1 fixed (0859dbf) — a cache hit |
|
P2-2 fixed (fc0b0bb) — the sidecar had no GC anywhere (rename didn't carry it, remove didn't delete it, clean deliberately left it alone) and the picker's read was O(sessions × sidecar lines) — 8170ms measured at 20,000 lines. Now: Re-measured on this shared host (other lanes running concurrently, |
|
P2-4 fixed (d395d8a) — the picker's |
|
P2-5 fixed (3521223) — confirmed the brief's "two implementations" framing didn't hold (the board's |
|
P3s fixed (af5729d) — Not carried in this round: P3-3 (regex-escaping |
|
🔴 Notice for #34 ( |
adapter_recent_sids filtered agy sessions by the session's recorded history.jsonl "workspace" field matching $PWD. But every real agy install records the same workspace ($HOME) for every conversation, never the directory it actually ran in — measured on #34/#83: 607/607 indexed conversations, one distinct workspace value. So that filter could never match outside $HOME, and the home board's Resume rows for agy were permanently empty in any real project directory. Fix (issue #34's "Option 1"): drop the $PWD filter. agy's Resume rows are now every session in the active tank, newest first by transcript mtime, relying on the caller's own cap ($limit / the board's CLIKAE_HOME_RECENT_MAX, default 10) rather than a cwd match to keep the list from flooding. Burn one-shots stay hidden through #83's sidecar, unaffected by this. The existing "last used" cache (antigravity-cli/cache/last_conversations.json) is a single pointer, not an ordered multi-item list, so it can now only answer a limit-1 lookup (burn's hot path) correctly; a multi-row request (the board's continue list) always falls through to the mtime-ranked disk scan, which is the only source that can rank multiple sessions against each other — so a cache file being present no longer silently truncates the board to one row. Documented the trade-off in the adapter's docstring and in docs/EXPECTATIONS.md ("Engines on one board"): agy's Resume rows are tank-scoped, unlike claude/codex/grok, which do match $PWD. Tests: replaced the bats test asserting the OLD (buggy) cwd-exclusion behavior with one asserting inclusion regardless of cwd, plus new coverage for newest-first + limit-cap ordering and a stale cache pointer (deleted brain dir) being skipped without error — all at the adapter level (tests/bats/adapters/antigravity.bats). Added two new end-to-end board-level tests in tests/bats/home.bats using the verify report's exact fixture (workspace=cwd, workspace=$HOME, no history entry): from a non-$HOME cwd, all three now show on the board's Resume rows, newest first, and a fourth test confirms CLIKAE_HOME_RECENT_MAX still caps the list. Confirmed all six new/changed assertions red on the old $PWD filter and green after the fix. by KITT Rebased onto #83's af5729d (#74's anchored, limit-aware cache path via json_value_for_key). That preserved #83's cache-path code as-is (the $want-keyed cache lookup + limit<=1/limit>1 split are burn's own per-directory pointer, a different mechanism from the dropped disk-scan $PWD filter) and applied this commit's tank-scoping only to the disk-scan loop's cwd check, which it removes. Two of #83's own P1-4 regression tests ("cache lookup returns THIS cwd's sid ... from a compact multi-entry cache", and its order-reversed sibling) asserted the OTHER project's sid never appears in adapter_recent_sids' output at limit=5 — true under the old cwd-scoped disk scan, no longer true under tank-scoping, where that sid's own session now legitimately surfaces via the disk scan like any other tank session. Narrowed both to limit=1, the same isolation P2-1's own tests already use to test the cache extraction alone without the disk scan folded in; the anchoring assertion itself (this cwd's sid, not a later key's) is unchanged and still passes. by KITT
…$PWD (#74 P1-1) The multi-candidate attribution tie-break compared each new transcript's recorded cwd against $PWD — this SHELL's working directory, not the cwd codex actually ran in. codex always launches with -C add_dirs[0] (default: dirname("$artifact")), so any --artifact outside $PWD, or an explicit --add-dir, made burn's OWN session stop matching here, leaving a concurrent human session in $PWD as the sole "match" — exactly what 83c2d82 was meant to prevent, reintroduced by comparing against the wrong cwd. Thread the cwd the engine was actually launched in through a single variable (_burn_launch_cwd), set where the launch argv is composed (add_dirs[0] once _burn_compose has run; $PWD is only correct when nothing overrides the engine's own cwd). agy's identical tie-break at what's now :707 never overrides its own process cwd via anything add_dirs-shaped, so $PWD stays right there — threaded through as an explicit parameter instead of a bare $PWD so both branches read from the same source of truth.
…t code (#74 P2-2) Both new #74 call points into adapter_find_session read the EXIT CODE to decide "proven" — but codex's and grok's implementations both return 0 (success) on a miss: no matching transcript on disk, empty stdout. Every other caller in the repo already does the right thing (capture stdout, check for empty), so these two were the only readers of a signal the adapters never promise. burn.sh: a caller-supplied --resume/--session-id sid (P2-3's "as proven as a minted one" path) was recorded and hidden even when codex/grok had no transcript for it at all — a ghost line, for real this time (codex accepts an unknown resume target without erroring on it). clean.sh: the sidecar GC's stale-prune read the same exit code, so it never fired for a codex or grok line — the line stayed "live" forever regardless of whether its transcript still existed.
…y ran (#74 P2-3) A caller-supplied --resume/--session-id names a sid whose transcript already exists BEFORE this run starts — "the transcript exists" (the mint path's proof) is a tautology for it, true whether or not the engine ever touched it. The old code recorded and hid that sid the moment the engine was TOLD to resume it, even when the engine refused to start and never read or wrote a single byte of the file (case 2 in the round-2 review: rc=1, "engine refused to start", sidecar written anyway). Snapshot the resume target's (mtime,size) before launch — only when it already existed pre-run, so the common mint-a-fresh-sid path (no pre-stamp) is untouched — and only enforce the "did it change" gate when the engine itself reports failure. A successful run (rc == 0) is proven the way it always was; a failed one is proven only if the transcript moved.
…r kills the whole command (#74 P2-1) load_adapter exit()s the WHOLE PROCESS on an adapter it can't find (lib/core/adapter_loader.sh) — `|| true` only catches a nonzero return, never an exit. This sidecar GC's own `load_adapter ... || true` sat directly in clean's shell, not a subshell, so a removed custom adapter (docs/adding-an-adapter.md), a downgrade, or any hand-placed directory under state/burn-sessions/ took down `clikae clean` entirely: rc=1, zero output, no GC, no Trash scan, no report — for a command that has nothing to do with the broken adapter. home.sh:193-201 already has the fix for this exact hazard: probe in a subshell first (safe to let it exit there — only the subshell dies), and only load for real in this shell once the probe has proven it won't. Same shape here, plus a warning line naming the skipped engine instead of silence.
… P3-3) The comment described "head -n 1" for a "want the first" caller, but the only caller (antigravity.sh's cache lookup) pipes into "tail -n 1" and wants the LAST match -- a repeated key there means an appended, not merged, write, so the last line is the current value. Comment-only; behavior is unchanged.
home.sh's _burn_sids_file (the picker's read side) and clean.sh's GC used TWO DIFFERENT definitions of "valid burn-sidecar line": home.sh required NF==3 with a numeric 3rd field; clean.sh's GC took whatever preceded the first tab as a sid and asked nothing else. A hand-corrupted line (trailing tab, non-numeric epoch, missing field) was dead to the READER (never hid a session — correct) but ALIVE to the GC whenever its first field happened to match a real transcript — kept forever, occupying one of CLIKAE_BURN_SIDECAR_CAP's slots. _BURN_SIDECAR_VALID_AWK is now the one definition (home.sh), used directly by _burn_sids_file's awk pass and, via the new _burn_sidecar_line_valid wrapper, by clean.sh's per-line GC loop.
P3-5) _burn_sids_file's temp file has its own lifetime (created once per picker pass) but the rm -f that cleans it up sat inside the SAME if that gated the match loop on ${#_rf_sid[@]} -gt 0 -- a zero-candidate pass would never clean it up. Unreachable today (files empty exits earlier, at :743), so this is pinned structurally rather than end-to-end: the shape is what was wrong, not an observed leak.
|
P1-1 fixed in 578394d. The cwd tie-break compared each candidate's recorded cwd against |
|
P2-2 fixed in 39d9f9c. Both new call sites read |
|
P2-3 fixed in 657508a. An explicit |
|
P2-1 fixed in eb90f4e. |
|
P3-1 fixed in 4598cef. i18n'd |
|
P3-3 fixed in 96f50a7. |
|
P3-4 fixed in 60e24a0. |
|
P3-5 fixed in 85c64a9. |
adapter_recent_sids filtered agy sessions by the session's recorded history.jsonl "workspace" field matching $PWD. But every real agy install records the same workspace ($HOME) for every conversation, never the directory it actually ran in — measured on #34/#83: 607/607 indexed conversations, one distinct workspace value. So that filter could never match outside $HOME, and the home board's Resume rows for agy were permanently empty in any real project directory. Fix (issue #34's "Option 1"): drop the $PWD filter. agy's Resume rows are now every session in the active tank, newest first by transcript mtime, relying on the caller's own cap ($limit / the board's CLIKAE_HOME_RECENT_MAX, default 10) rather than a cwd match to keep the list from flooding. Burn one-shots stay hidden through #83's sidecar, unaffected by this. The existing "last used" cache (antigravity-cli/cache/last_conversations.json) is a single pointer, not an ordered multi-item list, so it can now only answer a limit-1 lookup (burn's hot path) correctly; a multi-row request (the board's continue list) always falls through to the mtime-ranked disk scan, which is the only source that can rank multiple sessions against each other — so a cache file being present no longer silently truncates the board to one row. Documented the trade-off in the adapter's docstring and in docs/EXPECTATIONS.md ("Engines on one board"): agy's Resume rows are tank-scoped, unlike claude/codex/grok, which do match $PWD. Tests: replaced the bats test asserting the OLD (buggy) cwd-exclusion behavior with one asserting inclusion regardless of cwd, plus new coverage for newest-first + limit-cap ordering and a stale cache pointer (deleted brain dir) being skipped without error — all at the adapter level (tests/bats/adapters/antigravity.bats). Added two new end-to-end board-level tests in tests/bats/home.bats using the verify report's exact fixture (workspace=cwd, workspace=$HOME, no history entry): from a non-$HOME cwd, all three now show on the board's Resume rows, newest first, and a fourth test confirms CLIKAE_HOME_RECENT_MAX still caps the list. Confirmed all six new/changed assertions red on the old $PWD filter and green after the fix. by KITT Rebased onto #83's af5729d (#74's anchored, limit-aware cache path via json_value_for_key). That preserved #83's cache-path code as-is (the $want-keyed cache lookup + limit<=1/limit>1 split are burn's own per-directory pointer, a different mechanism from the dropped disk-scan $PWD filter) and applied this commit's tank-scoping only to the disk-scan loop's cwd check, which it removes. Two of #83's own P1-4 regression tests ("cache lookup returns THIS cwd's sid ... from a compact multi-entry cache", and its order-reversed sibling) asserted the OTHER project's sid never appears in adapter_recent_sids' output at limit=5 — true under the old cwd-scoped disk scan, no longer true under tank-scoping, where that sid's own session now legitimately surfaces via the disk scan like any other tank session. Narrowed both to limit=1, the same isolation P2-1's own tests already use to test the cache extraction alone without the disk scan folded in; the anchoring assertion itself (this cwd's sid, not a later key's) is unchanged and still passes. by KITT
… not $PWD (#74 R3 P1-1) _burn_launch_cwd stayed at its "$PWD" default for the raw '-- <cmd...>' form no matter what the user's own argv said: the only two places that ever reset it (both _burn_compose call sites) are gated on prompt_set==1, which raw mode never sets. codex's own -C can be given directly after `--` — burn --help's own raw example (burn.sh:118) is exactly `-- exec -C /tmp …` — so a caller who did that from a $PWD other than -C's target reproduced round-2's P1-1 on this path verbatim: burn's own session stopped matching the multi-candidate cwd tie-break and a concurrent human session in $PWD got recorded and hidden instead. Add adapter_cwd_from_args next to adapter_sid_from_args, one per adapter that defines the latter: codex actually scans cmd[@] for `-C <dir>`, `-C<dir>`, and `--cd <dir>`; claude/antigravity/grok define it too but always return 1 (none of the three has a cwd-override flag — codex is the only one whose launch cwd can diverge from $PWD). burn.sh's raw-mode branch (both the entry-point compose and the cross-engine reroute) now asks the loaded adapter for the cwd out of cmd[@], defaulting to EMPTY when the adapter has no such hook or the flag isn't present — an empty launch cwd matches no candidate's recorded cwd, so an unresolved raw launch records nothing rather than falling back to $PWD and misattributing a concurrent session (never hide what is not proven). Also deletes the wrong comment at the old burn.sh:2035 ("raw '-- <cmd...>' mode never overrides the engine's cwd") — it does, that's the whole bug. bats: codex burn with -C attributes to its own launch dir and never hides a concurrent human session in $PWD (the reviewer's probe B shape); raw mode without -C and two candidates records nothing; burn --help's own raw -C example still records cleanly. R3 review: 1 P1 (this).
…urrent reader (#74 R3 P3-1/P3-2) Round-2's own judgment for a caller-supplied `--resume <sid>` of an EXISTING transcript was "rc == 0 OR (mtime,size) changed" — an OR. A FAILED engine whose target transcript changed anyway (a concurrent human still typing into the SAME sid, while burn's own attempt never touched it) still got recorded and hidden: a (mtime,size)-changed check is a "someone wrote it" detector, not an "the engine ran" detector (R3 review P3-2). Tightens cheaply to three conditions, all required: 1. rc == 0 — no longer an alternative to the stamp check, required outright. 2. the transcript's byte size actually GREW — the one direction a resumed transcript's own append-only log moves in; a bare (mtime,size)-changed check can't tell the engine's own write from someone else's. 3. no OTHER live process still has the transcript open — a human's own concurrent `codex resume <sid>` / `claude --resume <sid>` on the SAME sid holds the file open the whole time burn's attempt runs, so it can grow and rc can land 0 purely from burn's side while none of the growth is burn's. Checked with fuser (preferred) or lsof; neither on PATH just skips this one check and says so via log_warn — conditions 1 and 2 still have to hold either way. Any of the three false ⇒ nothing recorded. R3-P3-1 (an explicit --resume of someone else's sid that the caller typed themselves, engine succeeds) is left as-is — the R3 review names it a deliberate scope narrowing, not a defect. bats: a --resume of an existing sid whose engine fails (rc=1) while a concurrent human keeps typing into the SAME transcript (file grows) records nothing — round-2's OR would have recorded and hidden it. R3 review: this closes the P3-1/P3-2 tightening named in the round-3 fix brief (not full closure of P3-1, which the review scopes out).
|
Round-3 fixes pushed (
Receipts (red on |
…s a match (#74 R4 P2-1) R4 review P2-1: burn.sh:2778's multi-candidate cwd tie-break compared each new codex transcript's recorded cwd against _burn_launch_cwd with a plain string equality. In raw '-- <cmd...>' mode with no -C found, _burn_launch_cwd is empty by design (round-3 P1-1: 'never hide what is not proven'). But adapter_session_cwd also returns empty when a candidate's session_meta header hasn't flushed its cwd field yet (a narrow but real race between codex creat()-ing the rollout and writing that line) — empty == empty made that candidate the sole 'match', and its session got recorded into the sidecar and hidden from the default resume view. Exactly the defect this PR exists to fix, reintroduced via a different path (5d7fe32's empty default, unguarded at the compare site). Fix: require both sides non-empty before comparing. An unknown launch cwd (or an unreadable candidate cwd) now correctly contributes zero matches, same as the already-tested 'both readable but neither equal' case just above it in the file. Adds a red-on-3bcc7af regression test to resume-hide-burn.bats: two new codex transcripts appear during a raw '-- <cmd...>' run with no -C (burn's own, cwd readable; a concurrent human's, cwd unreadable) — the human's session must stay unattributed and visible in 'clikae resume'. Co-Authored-By: Claude <noreply@anthropic.com>
…verdict is final, quoted board-legend key labels (#74 R4 P3-1/P3-3/P3-5) Three independent findings from the round-4 adversarial review, each <=10 lines of code, bundled in one commit: P3-1 — lib/adapters/codex.sh's adapter_cwd_from_args didn't recognise the clap '='-joined forms real codex 0.154.0 accepts: '--cd=<dir>' fell through to 'no match found' (an empty launch cwd — which the P2-1 fix above now correctly treats as unknown rather than misattributing), and '-C=<dir>' matched the looser -C?* case and returned '=<dir>' verbatim (a value that can never equal a real cwd, so it silently never matched anything either — direction-safe, but silently dead). Both '='-joined forms are now recognised explicitly, ahead of the looser glob. Adds six direct unit tests to tests/bats/adapters/codex.bats covering all argv shapes (spaced, attached, both '=' forms, and the empty case). P3-3 — once the --resume triple gate (rc==0 AND grew AND not open elsewhere) rejected an existing sid, control fell through to the before/after snapshot diff further down, whose own single-new-candidate branch has no rc check of its own — a DIFFERENT concurrent session minted during the same failed attempt was the sole 'new' transcript and got recorded (and hidden) despite having nothing to do with the gate's verdict. A new _burn_resume_gate_rejected flag now makes that rejection final: the snapshot-diff fallback only runs when sid_to_record is empty for a reason the gate never had an opinion on. Adds a regression test to resume-hide-burn.bats: a failed --resume of an existing sid, with a concurrent human's brand-new session as the only other new transcript. P3-5 — tests/bats/home.bats:949's 'for key in $labels' word-split an unquoted variable containing a literal '?' — bash pathname-expands it against the bats process's cwd. On a clean cwd '?' just expands to itself (a false red), but with any single-character-named file present it silently expands to that filename instead, and the '?' guard right below it never fires — the real '?' key quietly stops being checked. Switched to a quoted 'while IFS= read -r key; do ... done <<< "$labels"', which line-splits instead of word-splitting and never globs. Adds a regression test that plants stray single-char files ('h', 'x') in cwd and confirms the scan is unaffected. Co-Authored-By: Claude <noreply@anthropic.com>
|
Round 4 fixes for the round-4 adversarial review (0 P1, 1 P2, 3 of 6 P3 per this round's scope): Red-on-3bcc7af confirmed for both code findings via dedicated bats regression tests; unit-level red/green also captured directly for P3-1's argv parsing and P3-5's word-splitting divergence. Specified 11 files: 452/452 ok (443 baseline + 9 new). Full suite (84 files, 6 chunks, reverse-checked coverage): 1343/1343 ok, 0 not ok. shellcheck clean, bash 3.2 clean, merge-tree clean. CI: all 9 checks pass on 2dd80ff (run 34798830773) — shellcheck, signet, smoke test x2, pty smoke x2, pester, bats (ubuntu 11m56s), bats (macos 16m11s). PR is MERGEABLE/CLEAN. |
adapter_recent_sids filtered agy sessions by the session's recorded history.jsonl "workspace" field matching $PWD. But every real agy install records the same workspace ($HOME) for every conversation, never the directory it actually ran in — measured on #34/#83: 607/607 indexed conversations, one distinct workspace value. So that filter could never match outside $HOME, and the home board's Resume rows for agy were permanently empty in any real project directory. Fix (issue #34's "Option 1"): drop the $PWD filter. agy's Resume rows are now every session in the active tank, newest first by transcript mtime, relying on the caller's own cap ($limit / the board's CLIKAE_HOME_RECENT_MAX, default 10) rather than a cwd match to keep the list from flooding. Burn one-shots stay hidden through #83's sidecar, unaffected by this. The existing "last used" cache (antigravity-cli/cache/last_conversations.json) is a single pointer, not an ordered multi-item list, so it can now only answer a limit-1 lookup (burn's hot path) correctly; a multi-row request (the board's continue list) always falls through to the mtime-ranked disk scan, which is the only source that can rank multiple sessions against each other — so a cache file being present no longer silently truncates the board to one row. Documented the trade-off in the adapter's docstring and in docs/EXPECTATIONS.md ("Engines on one board"): agy's Resume rows are tank-scoped, unlike claude/codex/grok, which do match $PWD. Tests: replaced the bats test asserting the OLD (buggy) cwd-exclusion behavior with one asserting inclusion regardless of cwd, plus new coverage for newest-first + limit-cap ordering and a stale cache pointer (deleted brain dir) being skipped without error — all at the adapter level (tests/bats/adapters/antigravity.bats). Added two new end-to-end board-level tests in tests/bats/home.bats using the verify report's exact fixture (workspace=cwd, workspace=$HOME, no history entry): from a non-$HOME cwd, all three now show on the board's Resume rows, newest first, and a fourth test confirms CLIKAE_HOME_RECENT_MAX still caps the list. Confirmed all six new/changed assertions red on the old $PWD filter and green after the fix. by KITT Rebased onto #83's af5729d (#74's anchored, limit-aware cache path via json_value_for_key). That preserved #83's cache-path code as-is (the $want-keyed cache lookup + limit<=1/limit>1 split are burn's own per-directory pointer, a different mechanism from the dropped disk-scan $PWD filter) and applied this commit's tank-scoping only to the disk-scan loop's cwd check, which it removes. Two of #83's own P1-4 regression tests ("cache lookup returns THIS cwd's sid ... from a compact multi-entry cache", and its order-reversed sibling) asserted the OTHER project's sid never appears in adapter_recent_sids' output at limit=5 — true under the old cwd-scoped disk scan, no longer true under tank-scoping, where that sid's own session now legitimately surfaces via the disk scan like any other tank session. Narrowed both to limit=1, the same isolation P2-1's own tests already use to test the cache extraction alone without the disk scan folded in; the anchoring assertion itself (this cwd's sid, not a later key's) is unchanged and still passes. by KITT
…7 follow-up, round-2 review of #102) With main's #74/#83 merged, the bare `clikae resume` picker hides sessions that burn started, and `--all` shows them. The prefix candidate list does include burn-started sessions, so the previous hint ("run `clikae resume` to browse") could send the operator to a picker that hides the very candidate that was cut. The hint now names `clikae resume --all`. Receipt: the P3-7 test updated to the new wording; resume tests in tmux-status.bats + resume.bats (including main's own --all test) 27/27 ok on the merged tree.
…es (round-4 review P2-2) Rebasing this PR onto current main pulls in #83 (`resume: hide sessions that burn started`), which gave `cmd_burn` its own pre/post `adapter_all_transcripts` snapshot calls -- two more `find` invocations (codex.sh's `find "$1/sessions" ...`) that run inside the same timed window as the engine attempt, before `_burn_result` pins `elapsed_s`. This test's shim slept 1s on *every* `find` call, so after the merge those two unrelated calls legitimately push `elapsed_s` to 2-3s -- the semantics `elapsed_s` measures (engine time, not `_burn_left_behind` scan time) are still correct, but the test's own ruler stopped being narrow enough to isolate them. Limiting the sleep to the scan's own find shapes (discovery's `-print0`, the file-list find's `-newer` sentinel) restores the original invariant under #83's semantics instead of weakening the assertion.
…#34) _home_recent_rows asked each adapter for exactly CLIKAE_HOME_RECENT_MAX rows and only then filtered the burn-started sids out of them. Its own comment claimed the filter ran "BEFORE the rank+cut ... or a hidden row would just leave a gap instead of letting a real session take its slot" — true of the rank+cut in this function, but every adapter had already cut its answer to N on the way here. So N burn sessions newer than the human ones handed the filter N rows it had to drop and left it nothing to promote: the board's Resume block disappeared entirely. That is the exact symptom #34 exists to fix, and this PR's tank-scoping makes it reachable from ANY directory on an agy tank (before, agy's rows were cwd-scoped and therefore empty out there anyway). clikae's own dispatch doctrine runs long burn relays on agy tanks, so "more than ten burn sessions newer than every human one" is the normal shape of such a tank, not a corner case. claude and codex tanks have always had the same hole (#74's filter, #83's sidecar) — it just took a burn-heavy tank to reach it. Fix, once, at the root rather than in three adapters: read the sidecar BEFORE the tank walk, and ask each adapter for N + <hidden count> rows. At most <hidden count> rows can be dropped, so N survivors are guaranteed whenever N exist. The ask is bounded by the new CLIKAE_HOME_RECENT_SCAN_MAX (default 200) because the sidecar itself is capped at 2000 (CLIKAE_BURN_SIDECAR_CAP) and no board wants 2010 rows per tank flowing through a shell accumulator. With no sidecar, or under CLIKAE_RESUME_ALL=1, the ask stays exactly CLIKAE_HOME_RECENT_MAX — the common path is unchanged. Cost, measured on a 1,000-session agy tank (this host, two runs): adapter_recent_sids is 4.7-5.0 s per call at limit=10, at limit=200 and at limit=1000 alike — every adapter stats its whole tank before it `head -n <limit>`, so a wider ask lengthens the sort's tail and nothing else. Full board render 4.8-5.4 s with no sidecar, with a 190-line one (ask=200) and with a 2000-line one (ask clamped to 200): all within the host's own noise. The widened ask is free; the 5 s itself is the tank scan, pre-existing and out of scope here. Tests (tests/bats/home.bats): three new board-level cases, one per engine with a session adapter — agy, claude and codex — each with 3 human sessions (mtime 2020) and 12 burn sessions (mtime 2021, all recorded in state/burn-sessions), CLIKAE_HOME_RECENT_MAX=10, run from a non-$HOME directory. The board must show HUMAN-3/2/1 newest-first and no BURN row. A fourth asserts CLIKAE_RESUME_ALL=1 still shows the burns. Confirmed red on this branch's tip (all three engines: the Resume block absent entirely) and green after the fix. by KITT
… and five negations now assert Round 9 (d396968) taught scripts/test.sh and ci.yml to shellcheck `*.bats`. Main's newer test files (#83, #92, #98) were written before that gate existed, so merging main turned it red on 12 findings. Two of them are cosmetic; one is a test that did not test: * clean.bats (#83): five `! grep -qF … "$f"` lines in the middle of a test (SC2314). In bats a leading `!` does not fail the test — `set -e` ignores a negated command — so "the dead sid is gone", "the oldest three are dropped" were never asserted. Now `run ! grep …`, with `bats_require_minimum_version 1.5.0` as cockpit.bats already declares it. All 11 sidecar/burn tests in the file still pass, now with the assertions live. * wait.bats: `_mkstatus … done true` passes the word `done` as an argument (SC1010); quoted. * cockpit.bats: an unused loop variable (SC2034). No production code changes.
Round-4 narrowed the `find` shim in the `elapsed_s` test to the scan's own two shapes (`-print0` for discovery, `-newer` for the file list) so #83's transcript-snapshot `find` calls would stop polluting the number. That was right, but it bound the shim to the implementation's flags with nothing asserting the binding still holds. The day discovery stops passing `-print0`, the shim fires zero times, the scan costs ~0s, and `[ "$json_elapsed" -lt 2 ]` passes unconditionally — the test goes quietly empty instead of going red. The shim now appends a line per shape it matches, and the test requires both. Receipt: with only the shim's two patterns mutated so neither can match (the implementation untouched), every pre-existing assertion in that test still passes and the new one is the only thing that fails: not ok 1 burn #84 P2-3: elapsed_s agrees … # the scan find shim never fired — elapsed_s < 2 proved nothing which is exactly the silence this asserts against.
…eview P3-6) The P3s rounds 3, 4 and 5 deliberately did not fix existed only in the review lanes' own report files. The PR body stopped at round 2, there was no follow-up issue, and the CHANGELOG addendum said what was fixed and never what was not — so merging #87 would have dropped all of them, and the next round would have rediscovered them from scratch. This repo has the convention already (#94 -> #110, #98 -> #103/#109, #83 -> #105). Filed as #112 — ten items with file:line, what each is, and which review round raised it: per-repo git timeouts that leave a wedged repo reading `dirty 0`, `left_behind_truncated` mixing units, newline-in-filename splitting, the `dirty`/`files` scope difference, the silent no-git path, the untested single-pid fallback, two missing bats coverages, the watchdog's inherited fds 5/9, and — first among equals — that not one line of this bound has ever run on stock macOS, the platform whose bash 3.2 the whole thing was written for. CHANGELOG now carries the round-5 entry and points at that issue.
) * home: bounded board reads — session-boundary indexes and a per-file reading cache (#62) The board no longer walks transcript trees on every render. Session discovery goes through per-tank boundary indexes (claude, codex, agy, grok), recent candidates are capped per cwd and tank (CLIKAE_HOME_RECENT_MAX, default 10), and every per-file parse (title, recap, limit, success, agy email) is cached by kind+path+size+mtime, including negative readings. Snapshots are published through unique mktemp files so concurrent renders never read a half-written index. Built by codex (gpt-6-astra) on tank crazy in two runs; report in the lane worktree (REPORT-board62-build.md, not committed). by KITT * board: self-heal a stale or missing per-tank snapshot inline at render time Round-1 fix review, P1-2/P1-3: a board snapshot only got (re)built at a session boundary (clikae run/burn/agy's own switch), so it went stale the moment anything reached an engine without passing through one of those three call sites (clikae alias, clikae env, a .app bundle, relay, switch.sh's ephemeral path) - permanently, for that tank, with no way to self-correct. It also froze fuel (dry/limit) readings behind an arbitrary "snapshot published within N seconds" age gate that was never a proxy for "the underlying data is still accurate". board_generation now does one cheap freshness check per read, per tank (a directory mtime plus a bounded handful of file mtimes, both compared for equality against what board_state_refresh recorded at publish time - never ">" against a wall-clock stamp, which would read a future-dated fixture or a clock-skewed file as permanently stale) and rebuilds inline, only for the one tank actually being read, the instant that check disagrees with what was last published. board_read/board_recent/board_find all route through it, so limit.sh's age-gate heuristic is no longer needed and is removed. Also verifies board_key (a 32-bit cksum) lookups against the raw sid/scope recorded at publish time, so a hash collision reads as a miss rather than silently answering with a different session's data - the same guard reading_cache_run already uses for its own key. board_read gains an explicit <engine> argument (board_generation needs it to know which scan root/scope rules apply); its handful of callers in limit.sh and scan.sh are updated accordingly. * home/adapters: restore live-row guessing and cross-project resume lookup Round-1 fix review, P1-1: board mode disabled the whole tank-scoped title guess for an unstamped live row, and the all-projects glob fallback for a stamped row's sid outside the current PWD's project slug - not narrowing either one, turning both off outright. Live rows are bounded by how many tmux sessions exist, not by transcript count, so neither one was the O(all transcripts) cost issue #62 was written to kill. Each adapter's adapter_recent_sids/adapter_find_session (and codex/grok's _codex_find_rollout/_grok_find_summary) now try the snapshot first - cheap, and self-healing as of the previous commit - and only fall through to the original live scan when the snapshot itself has nothing to say for this exact query, never disabling the scan wholesale. home.sh's own _CLIKAE_BOARD gates around the guess passes are removed to match: the adapter functions already make the cost-vs-cost call internally now. * run/antigravity: drop the post-launch board refresh, restore exec Round-1 fix review, P2-1/P2-2: cmd_run and the agy switch used to wrap adapter_run/exec agy in a subshell so a board_state_refresh could run before and after the engine - which meant clikae stayed a resident parent for the whole session instead of exec'ing away (different signal delivery, $PPID, and a 128+N exit code instead of WIFSIGNALED when the engine is killed), and paid a full tank scan synchronously on every interactive launch and every exit. Neither refresh is needed anymore: board_generation (previous commit) rebuilds a stale tank inline the moment a render actually reads it, so there is nothing left for a boundary call here to buy. Both call sites go back to a bare tail call/exec, matching main. * clean/reading_cache: GC board snapshots and cache entries, sub-second precision Round-1 fix review, P2-3: state/board generations and state/readings entries grew unbounded and clikae clean never touched either. board_state_refresh already GC's its own tank's generations on every publish now (board_gc_generations, keeping the newest few by mtime instead of an mtime+1-day floor); clikae clean additionally sweeps state/board for tanks that have not been launched (and so never re-published) in a while, and state/readings for entries whose own source file no longer exists. P3-1/P3-5: reading_cache_run's key only had whole-second mtime precision, so a same-size overwrite within the same wall-clock second could read as a cache hit. Its stat call now asks for GNU's fractional-seconds modifier / BSD's F sub-format, detected once via the same _clikae_statv this repo already uses elsewhere (never `stat -c ... || stat -f ...`: on a GNU machine `-f` means --file-system and prints something else entirely, which is exactly what _clikae_statv's own comment warns against). * tests: cover the board self-heal, live-row, and exec-semantics fixes - home-bounded.bats: the "missing state never triggers discovery" test encoded the P1-2 bug (an unpublished tank stays empty forever) as the expected behaviour; rewritten to assert the self-heal instead (discovers once, then reads bounded again). The "live rows read only the stamped transcript" test likewise encoded the P1-1 bug (an unstamped row never guesses in board mode); rewritten to assert the guess reads at most the one candidate it settles on. The run.sh test updated to assert cmd_run no longer calls board_state_refresh at all (exec semantics restored). - home.bats: "bare clikae changes nothing on disk" now excludes state/ from the before/after comparison - the board cache's own inline self-heal is a deliberate write to clikae's OWN derived cache, not to session/profile source data, which is what the invariant actually protects. * P1-A: files_mtime_size reports nanosecond precision, not whole seconds board_stale (board_state.sh) compares this against what it recorded at publish time to decide staleness. Whole-second precision made any write landing in the same wall-clock second as the last publish invisible in either direction: a limit landing mid-second read as fresh, and a resolved limit stuck reading stale. _reading_cache_keyv already made this exact fix for its own cache key; this brings files_mtime_size in line with it. * P1-C: add _codex_today_scan_dir, computed not discovered Rollouts live three levels under sessions/ (sessions/YYYY/MM/DD/), so sessions/ itself never took a new session's mtime as its own dirent count didn't change. This adds a helper that resolves the deepest EXISTING directory on today's date path (walking up from DD to MM to YYYY to sessions/ itself) so board_state.sh's freshness check has a directory whose mtime genuinely moves when a new rollout appears today, whether it's the first session of an existing day (bumps the DD dir) or the first of a new day (bumps whichever ancestor already exists). No `find`: this runs on every render via board_stale, and this repo's own test suite hard-fails any find call on a warm render. * board62 round-2: close P1-A/P1-B/P1-C, tidy P3-1/P3-4 P1-A: recent/<key> rows now carry a size column alongside the nanosecond-precision mtime (files_mtime_size), and board_stale compares both. Previously it kept only a whole-second mtime and threw away the size files_mtime_size already handed back, so a write landing in the same wall-clock second as the last publish was invisible either way. board_recent still hands callers the older, public whole-second "<mtime>\037<sid>" contract (a separate display-mtime field, never the staleness one) so home.sh's _human_age arithmetic keeps working. P1-B: claude's fuel reading (claude-usage) scans projects/ in full — it's account-level — but the only freshness signals were PWD-scoped, so a limit landing in a different project directory never invalidated this one's board. board_state_refresh now also records projects/'s own mtime (catches a brand new project directory) and the (mtime, size) of every file that was inside the -mmin -300 window at publish time (catches an append to a session in a project that already existed) — both O(bounded), never a tree walk. _claude_usage_stale checks these BEFORE board_stale's per-scope early return, so a scope with no recorded recent sessions still answers for an account-level limit. P1-C: _board_scan_root's codex case now points at _codex_today_scan_dir (see its own commit) instead of sessions/ itself. P3-1: board_gc_generations' sort now breaks ties on directory name. Several publishes inside the same wall-clock second all get whole-second mtimes, which made the sort's outcome non-deterministic (mktemp's XXXXXX suffix is random, so this doesn't guarantee `current` survives a prune, but it does make the same disk state sort the same way every time). P3-4: load_adapter now runs before mktemp -d, not after — a load failure no longer leaves an orphan generation directory behind. * P2-A: adapter_session_cwd caches by its OWN transcript, not history.jsonl The reading_cache identity used to be the shared history.jsonl, not the one session's own transcript file — so any write to history.jsonl (one new agy session anywhere) invalidated every OTHER session's cached cwd at once. On a synthetic 500-session tank, adding one new session turned into a 500-entry cache stampede: board_state_refresh went from ~5.2s (history.jsonl unchanged) to ~9.9-10.1s (one new line), because 499 unrelated sessions each re-paid a grep over history.jsonl they didn't need to. Keying on the session's own transcript file means an unrelated session's cache entry survives a write elsewhere; only the session whose own transcript actually changed re-derives its cwd. Measured before/after with board_state_refresh timed directly on the same 500-session store: refresh cost after adding one session drops from ~9.9s back to the ~5.2s unchanged baseline. * P2-B: remove burn's two remaining full board_state_refresh calls run.sh and antigravity.sh already dropped their boundary-call board_state_refresh invocations in round-1, for the reason recorded in run.sh's own comment: board_generation now rebuilds a stale tank inline, right when a render actually reads it, so a boundary call buys nothing but a synchronous full tank scan. burn.sh's two call sites (one per attempt, before launch and again before classifying the reply) were left in place and the round-1 fix report never mentioned them. Measured directly: one claude board_state_refresh on a 50-session store costs ~800ms; the agy equivalent on a 500-session store costs 4.7-10s (see the antigravity.sh commit). burn was paying that twice, unconditionally, per attempt. * tests: raise the bounded-IO ceiling for P1-B's two extra signals claude's account-level fuel freshness check now costs two more bounded stat calls per render section (projects/'s own mtime, plus one batched files_mtime_size over the -mmin -300 file set) — still O(one freshness check per render section), never O(transcripts), but board_generation's per-process cache does not survive a command-substitution subshell boundary, so a real render pays this per section. Measured: 165 lines on the existing fixture, up from a ceiling tuned to 160 before this signal existed. The equal-cost-at-100-vs-1000-transcripts assertion (the actual boundedness claim) is unchanged and still passes. * P1-1: rebase onto main, adapt to PR 79's dry rc=2 contract and the read-only boundary Rebased onto origin/main (ab6fb52): PR 80's antigravity title lookup now wraps in the cached-title path instead of being replaced by it, and the two textual home.bats conflicts kept main's idiom. PR 79 (dry-reset-expiry) landed a third return code on _limit_codex_dry: rc=2 is positive recovery evidence, distinct from rc=1 (nothing found). This PR's own codex-snapshot test predates that change and still expected rc=1 for a later success; update it to assert rc=2, matching the contract PR 79 established. Ruling: a bare render MAY write its own derived cache under CLIKAE_HOME/state/ - never under any tank's profile dir. The read-only test now asserts exactly that boundary: profiles/ (the tank's source data) is byte-for-byte untouched by a bare render; state/ is excluded from the comparison because it is allowed, not required, to change. * P1-2: claude account-level fuel sees a file that appears after a publish _claude_usage_stale had two signals: projects/'s own mtime (a brand new project directory) and the recorded (mtime, size) of each file that was inside the -mmin -300 window at the last publish. Neither sees a brand new FILE landing inside an EXISTING project directory: creating a file does not move projects/'s own mtime, and the file was never in the -mmin -300 set at the last publish (it did not exist yet), so there was no per-file record to re-stat either. Once that happened the account's claude tank stayed green forever, with no timeout to recover it. Record each existing projects/<slug> directory's own mtime too, bounded by the project count, folded into the same files_mtime_size call as the tracked files (one fork, not two) so a new file always moves a signal this check is watching. Separately, an empty tracked-file set at publish time used to read as "nothing to compare against, call it fresh" - permanently, since an append to an OLD file (already excluded from the -mmin -300 window) does not move any directory's mtime either. Zero evidence is not evidence of freshness: that exit now rebuilds instead. * P2-1: codex's scan-root chain is read off disk, not guessed from the clock _codex_today_scan_dir computed "today" from the OBSERVER's own `date`, then walked up to the deepest existing ancestor of that path. This is only correct when the observer's local "today" agrees with whatever clock actually wrote the newest rollout's date directory - true by default (codex uses the machine's own local time), false the moment clikae runs under an explicit TZ, across a timezone during travel, or under a CI/cron invocation pinned to a different zone than the interactive session that wrote the rollout. When the two disagree, the old code settled on a directory that could be a SIBLING of where the real newest rollout landed; a new rollout inside an existing sibling day directory only bumps that day directory's own mtime, never its parent's, so the freshness check watched the wrong thing and the board never rebuilt. Replace it with _codex_newest_chain, which reads the actual newest YYYY/MM/DD chain off disk (bounded glob at each of three levels, no `find`) instead of computing a path from the wall clock. board_state.sh generalizes _board_scan_root and its scanroot-mtime bookkeeping from a single path to a list, batched through one files_mtime_size call, so codex's multi-level chain and the other three engines' single path share the same publish/check code. Receipt (probe-p21.sh): a rollout in an existing sessions/2026/09/12 directory, rendered with TZ=Asia/Tokyo throughout (observer's own "today" is already 2026/09/13) - the old code left the tank frozen fresh; the new code sees it on the next render, same as TZ=UTC (where the fixture's date agrees with the observer's). * P2-2: antigravity's cwd lookup is a bulk index, not one fork per session antigravity records cwd IN the file (history.jsonl), not in the path, so board_state_refresh's rebuild loop scans the WHOLE account's sessions on a genuine miss, never just this PWD's. Deriving each session's cwd used to cost one reading_cache_run call - a stat, a cksum, and a subshell read even on a cache HIT - per session. On a synthetic 500-session tank that measured as a ~5s fixed cost on top of whatever a real rebuild needs, standing between the round-2 fix (the cache-key stampede is gone) and beating main's per-render cost on the very first render after any change on the tank. Add adapter_session_cwd_index: one awk pass over history.jsonl building the whole sid -> workspace map at once. board_state_refresh's rebuild loop and adapter_recent_sids's discovery fallback both use it when available, falling back to the old per-file adapter_session_cwd otherwise (a test's minimal stub adapter, or a session missing from the index). Same source of truth, same "first occurrence per sid wins" semantics as the per-file lookup - this removes the per-session forks around an unchanged read, not the read itself. Receipt (perf-p22.sh, the round-3 review's synthetic shape: 500 agy + 50 claude sessions, ~103KB each, one cwd): first render after adding one agy session with a limit line - before this fix, ~7.1s; after, ~3.7-3.9s, matching main's own ~3.6-3.9s baseline on the same store. * P3-1/P3-4: nanosecond directory mtimes for claude's fuel signal, align GC sorts P3-1: P1-A upgraded every FILE mtime comparison in this file to nanosecond precision, but the one DIRECTORY comparison left over (projects/'s own mtime, _claude_usage_stale's signal 1) still used file_mtime's whole-second precision. A project directory created in the SAME wall-clock second as the last publish was invisible to that signal in either direction. Both the publish and the check side now read it via files_mtime_size, the same nanosecond stat this file's other two signals already use. P3-4: board_gc_generations gained a directory-name tie-break for its sort (several publishes in the same wall-clock second get the same whole-second mtime), but clean.sh's _clean_board_gc runs the equivalent sweep over the same directories with the old two-key sort. Align it so the two GC paths rank the same set of generations the same way and never disagree on which one keep-N protects. * round-4 fix: P1-1/P1-2/P2-1/P3-4 in board_state.sh Round-4 adversarial review, 2 P1s + 2 P2s + P3-4: - P1-1: an idle claude account (no transcript touched in the last 300 minutes) paid a full rebuild on EVERY frame, forever. Round-3's fix for "the -mmin -300 window is empty at publish" made an empty signal-3 set rebuild instead of trusting it fresh, but rebuilding never changes whether the window is still empty five hours later, so it never stopped. Signal 3 now records the newest K files of EACH project directory instead of the -mmin -300 hit set (never empty unless the account has no claude session at all), and the empty-set exit goes back to fresh. - P1-2: a codex tank with no sessions/ directory yet at its first render (right after `clikae init codex`, before it has ever run) froze its snapshot permanently — no limit it hit afterward ever reached the board, and `clikae clean` could not recover it. board_state_refresh silently dropped a scan-root level that did not exist yet at publish time. Every level _board_scan_root names is now recorded, existing or not: a missing one gets a MISSING sentinel, and board_stale reading it checks existence (has it appeared since?) instead of a mtime that was never there. - P2-1: board_stale used to pay up to five separate stat forks per claude tank (scanroot, claude-usage's root/projdirs/files, recent files) on every single warm read. It now gathers every path all of its signals need first (plain reads, no fork) and pays ONE batched stat call per tank for the union. - P3-4: files_mtime_size's positional zip misattributes a row the moment a path vanishes between building the argument list and the stat call landing — GNU/BSD stat both print one fewer line for a missing arg, silently shifting every zip index after it. Added _board_mtime_size_map, keyed off the path stat itself echoes back instead of argument position, and routed every zip in this file through it (scanroot-mtime, claude-usage's projdirs/files, recent-files, the publish-time file-mtime map). * round-4 fix P2-2: codex chain records every DD under the newest MM A new rollout landing in an existing day directory that is not the lexically-newest one was permanently invisible — the same shape as round-3's P2-1, one level down (needs a clock set back, a cross-timezone local-time rewrite, or a synced/restored CODEX_HOME, not the default path, but the consequence is just as permanent once hit). _codex_newest_chain now records every DD directory under the newest MM (bounded: at most 31 per month) instead of only the newest one, so a write to ANY of them bumps a directory board_stale already watches. * round-4 fix P1-1/P2-1: share one board_generation across a whole render board_generation's per-process memo does not survive a `$( )` command substitution — it can only be read by a forked subshell, never written back to. All four render sections (_home_items' live/tanks/recent, plus _home_refresh's own dry-set and board_total) each fork at least one of their own, so a naive per-section call paid board_stale's freshness check, and on a genuine miss a full per-tank rebuild, once PER SECTION PER TANK, every single render (measured: 3 claude tanks, idle fuel window, 24 find calls a frame — 3 tanks x 2 finds x 4 sections). _home_refresh now primes board_generation for every tank once, before any of those subshells exist, so each of them inherits an already-warm cache instead of paying for its own. This also makes board_total ride the same cache as the other three sections (P2-1), instead of walking every tank's generation a fourth time. * round-5 fix P1/P2-2: one whole-tank stat fingerprint replaces every bounded staleness signal Round 1-4 each shipped a bounded, per-signal freshness approximation (a scan-root directory mtime, an account-level top-K of recorded files, a codex day/month/year chain) meant to avoid re-listing a tank's whole transcript tree on every render. Every round found one more write shape the approximation could not see: a codex limit landing via an append to the current session's rollout was invisible to a board opened from any other cwd (P1); claude's per-project top-K missed an append to the 11th-newest session in its project, and any append to an agent-*.jsonl subagent transcript, both reading as a false green fuel dot until the account's next unrelated write rescued them (P2-2). Design decision: stop approximating. board_stale now re-lists every transcript file under the tank (the exact find board_state_refresh already runs) and re-stats every one of them in a single batched call, reducing the whole set to one opaque fingerprint (count + a CRC over each file's own path/mtime_ns/size, sorted). Equality between that, recomputed fresh on every read, and what board_state_refresh recorded at publish is now the entire staleness signal, for every engine. This is real O(files) work again, but find+stat only, never a parsed line - measured at a few ms per fork for 500 files on this host, well under the parsing costs #62 was written to kill. Deleted outright rather than left beside the new signal: the per-project top-K bookkeeping, _claude_usage_stale and its three sub-signals, every per-engine scan-root mtime/chain (_board_scan_root, _codex_newest_chain), the scanroot-mtime/claude-usage-* on-disk records, and the MISSING-sentinel handling that existed only to keep that catalogue alive across a not-yet-existing directory. board_recent's on-disk row also shrank from 4 fields to 2 - the two staleness-only fields had no other reader once the per-file comparison they backed was gone. * round-5 fix P2-1: _home_refresh clears its generation memo before priming _home_refresh primes _BOARD_GEN_CACHE once per render so every subshell inherits an already-warm freshness check instead of paying for its own - but it never cleared that memo first. In a long-lived process (the interactive TUI's _home_pick, which calls _home_refresh again after every c/m/n/a/d/l), the second priming pass just re-read the first call's already-populated memo and skipped board_generation entirely, so a session or limit that landed between two refreshes stayed invisible until the process exited. _home_refresh now resets _BOARD_GEN_CACHE=() immediately before the priming loop, so every refresh in the same process asks fresh. * round-5 fix: bats receipts for the P1/P2-1/P2-2 findings and the new staleness signal Adds one bats case per round-5 finding, each verified red against a578aa0 and green on the fixed tree: a codex limit appended to a running rollout is visible from a different cwd (P1); a claude limit appended to the 11th-newest session in its project is visible (P2-2); a claude limit landing only in an already-existing agent-*.jsonl is visible (P2-2); _home_refresh called twice in one process picks up a session added between the two calls (P2-1). _board_shims no longer hard-fails find - the new staleness signal expects one bounded find+stat per tank on every read, which is the correct new invariant this round's design replaces the old 'never discover transcripts' one with. Assertions across the file are updated from 'no find call at all' to 'find/stat are bounded, but head/tail never touch a transcript's content on a warm read.' * round-6 fix P1-1/P2-1/P1-2: batch the fingerprint through find -exec, make rebuilds incremental Round-6 review found the fix5 fingerprint pushed every transcript path into one stat argv: past ARG_MAX, stat died E2BIG under 2>/dev/null and the fingerprint silently collapsed to a file count, so an append to an existing file went permanently invisible (P1-1). Building that argv also cost a bash while-read loop over find's output through a process substitution -- 500-575ms at 5,000 files even when stat succeeded (P2-1). _board_stat_rows replaces both: one `find ... -exec stat ... {} +` piped straight into `sort | cksum`. find batches its own argv per the real ARG_MAX, so E2BIG is now structurally impossible, and no path is ever pulled into a bash variable. Separately, any write anywhere in a tank forced board_state_refresh to rebuild every file's sid/scope/resume-row and re-run every file's rate-limit reading through reading_cache_run, even though only one file changed -- 39.8s for one append in a 5,000-session store (P1-2), exactly the class of regression #62 exists to fix. board_state_refresh now diffs the fresh stat rows against the previous generation's own manifest via one awk pass instead of a bash loop; sids/ and recent/ start as a cp -al (hard link) of the previous generation; only a changed, new, or removed file pays a fresh parse. A cold build (no previous generation to diff against) keeps the old bulk shape. The same argv exposure the review flagged at sessions_by_mtime's call site is also gone: the incremental path never calls it, and the cold path sorts from the stat rows it already has instead of re-stating via argv. board_stale's own equality check against the single fingerprint is unchanged, and no bounded per-signal approximation (top-K, -mmin windows, scanroot sentinels, day-dir chains) was reintroduced. * round-6 fix: bats receipts for the ARG_MAX fingerprint and the incremental rebuild Three new cases in home-bounded.bats: - a 20,000-transcript fixture (built in the test, under a throwaway HOME/CLIKAE_HOME) proves the fingerprint still sees a one-line append at a scale well past the ARG_MAX threshold an argv-based stat would hit, and separately re-runs the fingerprint's own find+stat pipeline without swallowing stderr, asserting rc=0 and an empty stderr capture -- proving stat never fails, not just that the fingerprint happens to still work. - stubbing the per-file rate-limit parser and appending to 1 of 50 sessions asserts the stub runs exactly once on the next board_state_refresh, not 50 times -- a unit-level proof that a rebuild only pays for the files that changed. - deleting one of two sessions and appending to the other (forcing a real rebuild) asserts the deleted session's resume row is gone and the kept session's is still there -- coverage for the one new failure mode the carry-forward design introduces that a pure performance receipt would not catch. * tests(compat): gate declare -g / -A / -n before porting board_state.sh off bash 4 PR #78 CI (run 34754505915) is red on all three macOS jobs since this branch's first commit: `declare -gA _BOARD_GEN_CACHE` in lib/core/board_state.sh runs at SOURCE time, and macOS bash 3.2 has neither `declare -g` (4.2+) nor associative arrays (`-A`, 4.0+) — `lib/core/board_state.sh: line 205: declare: -g: invalid option`, exit 2 before a single bats runs. compat.bats already source-scans for mapfile/${var,,}/readlink -f/&> and missed this. Extend it: `declare -[gAn]`, `local -[An]`, `typeset -[An]` (deliberately NOT `[[ … =~ …]]` — 3.2 has that), with the same whole-line-comment exclusion the file already uses, plus a negative control planting `local -A x=()` in a temp copy (same shape as the existing readlink -f control). Red on this commit (4 real sites: board_state.sh:205,437,457, adapters/antigravity.sh:230) — the port lands next commit. Co-authored-by: Claude <noreply@anthropic.com> * fix(board_state): port off declare -gA / local -A for macOS bash 3.2 CI (gh run view 34754505915): `declare -gA _BOARD_GEN_CACHE` runs at SOURCE time, and macOS bash 3.2 has neither `declare -g` (4.2+) nor associative arrays (`-A`, 4.0+) — the file failed to source at all, `clikae version` exited 2, ~1,000 bats red on every macOS job since this branch's first commit. Three more `local -A` sites (added by round-3's P2-2 bulk-index fix and round-6's incremental classifier, both correct in design, bash-4-only in implementation) had the same problem, just not yet exercised on the job that would have caught them first. PORTED, not shimmed — no version check, no bash-4-only fast path, one code path for 3.2 and 5.x: - board_generation's memo -> plain globals keyed by a sanitized (engine,dir) name, read back through `eval` indirection; a sanitize-collision is caught by writing the raw cachekey back alongside the value and comparing on read (same guard board_recent/board_find already use for their own hashed keys). `_board_gen_cache_clear` replaces the old single-assignment `_BOARD_GEN_CACHE=()` reset _home_refresh needs every redraw (home.sh updated to call it). - `_agy_ws`/`_ws` (antigravity's bulk cwd index) -> `_agy_ws_load`/ `_agy_ws_lookup`/`_agy_ws_varname`, one shared naming scheme so board_state.sh and antigravity.sh can never drift onto two different variables for the same sid. Still one fork per RENDER (`adapter_session_cwd_index`), zero forks per session — round-3 P2-2's own invariant, re-verified. - the cold-build classifier's five per-path maps (cur_mtime, cur_size, sid_of, scope_of, reading_of) -> parallel INDEXED arrays keyed by the file's position in stat_rows, not by path — bash 3.2 has always had indexed arrays, nothing here needed a hash. The INCREMENTAL classifier already used one awk join with no bash maps (round-6) and needed no change. tests/bats/compat.bats (previous commit) now source-scans for `declare -[gAn]|local -[An]|typeset -[An]`; this commit takes it from red to green. Receipts (full detail in /home/kitt/lanes/REPORT-board62-fix7.md): compat.bats green, home-bounded.bats 17/17 (incl. the round-5 P2-1 cache-reuse regression test, which exercises _board_gen_cache_clear directly), antigravity/agy-email/board-width 42/42, shellcheck -S warning clean on all three files, no bash-4+ construct left per a full repo grep. Co-authored-by: Claude <noreply@anthropic.com> * fix(board_state): move _agy_ws_* helpers into antigravity.sh, guard call sites CI (run 34760883667, both bats jobs) caught what my targeted local testing missed: `_agy_ws_load`/`_agy_ws_lookup`/`_agy_ws_varname` lived in board_state.sh, but antigravity.sh's `adapter_recent_sids` called them unconditionally. `tests/bats/adapters/antigravity.bats` sources lib/adapters/antigravity.sh directly, WITHOUT board_state.sh (a real, tested, previously self-contained configuration — the original `local -A _ws` block had zero dependency on board_state.sh) -> "_agy_ws_load: command not found", one test failure (an exact `[ -z "$output" ]` check; two sibling tests using substring matches papered over the same stderr noise). Fix: the three functions move to lib/adapters/antigravity.sh, next to `adapter_session_cwd_index` (their natural neighbour — this is antigravity-specific bookkeeping, not board-generic). board_state.sh keeps calling them but every call site is now guarded with `declare -F`, the same pattern this file already used for `adapter_session_cwd_index` itself before this port (and that the original `local -A _agy_ws` population block had too, and my first draft of this port dropped). board_state.sh must stay usable when an adapter has not been loaded — not just when antigravity specifically has not. Re-verified: tests/bats/adapters/ (95/95, including the regression test), tests/bats/compat.bats, home-bounded.bats (17/17), antigravity.bats + agy-email.bats + board-width.bats (42/42), shellcheck -S warning clean on both files. Co-authored-by: Claude <noreply@anthropic.com> * fix(board_state): generations chain instead of sharing inodes (P1-1/P2-1) Round-7 fix review, P1-1 and P2-1, which are the same defect seen from two sides: `cp -al` carried `sids/`/`recent/` forward as HARD LINKS, and the two in-place writes (`> "$base"` in _board_merge_recent_row, `> "$gen/sids/$key"` in the changed loop) then wrote through those links into the generation `current` was still pointing at. The reviewer's three-way probe caught it directly: gen1's recent row changed the moment gen2 published. The same copy cost one `link()` per transcript PRESENT — 5,001 at 5,000 files, measured by strace — which is the O(tank) bookkeeping #62 exists to remove. Both go away by not copying: - A generation holds ONLY the entries this refresh changed, plus `parent` (the generation it was built from) and `depth`. - `_board_gen_entry` resolves one entry by walking gen -> parent -> …, first hit wins. No fork, no `$( )`: an out-variable, like `_agy_ws_lookup`. - The walk is bounded by `_BOARD_GEN_MAX_DEPTH` (8). The publish that would reach it MATERIALISES instead — `cp -a`, no `-l`, oldest ancestor first — and starts a fresh chain. Reads are <= 8 lookups; writes are proportional to CHANGED files, amortised against one copy every 8 publishes. - Removal writes a zero-byte TOMBSTONE, because `rm -f` on this generation's own name would leave the ancestor answering for a deleted transcript. - Every entry write is temp + `mv -f` (`_board_gen_put`), the pattern `_board_purge_recent_row` already used: `mv` replaces a NAME and never opens an ancestor's inode. - `board_gc_generations` learned about the chain: keep-N alone would have unlinked an ancestor `current` still resolves through, which is a silently empty Resume list, not a dangling pointer a rebuild heals. `_clean_board_gc` now calls the same `_board_gc_candidates` rather than keeping a second copy of a rule that just grew a second clause. - Generations carry a `format` stamp; an older layout is treated as stale and as no previous generation at all, so an upgrade rebuilds once instead of reading entries under a naming scheme that moved. Receipts (tests/bats/home-bounded.bats): the superseded generation is byte-identical after the next publish; no entry has a link count above 1; the chain materialises and never exceeds the depth bound over 20 publishes; GC (and `clikae clean`'s sweep) never unlinks a live chain link. * fix(board_state): one canonical fingerprint, so an empty tank can be fresh (P1-2) Round-7 fix review P1-2. The staleness fingerprint had two spellings. The reader (`board_stale`) piped `_board_stat_rows` straight into `cksum`; the publisher captured the same rows in `$( )` — which strips trailing newlines — and re-emitted them with `printf '%s\n'`. With at least one transcript the two byte streams happen to agree. With NONE they cannot: the pipeline sends zero bytes, `printf '%s\n' ""` sends one newline, so the saved value was cksum("\n") = "3515105045 1" and the live one cksum("") = "4294967295 0". Every read therefore said STALE. A freshly `clikae init`'d tank — the first screen a new user sees, and the state every user is in right after install — rebuilt and published a whole new generation on EVERY frame, forever, never self-healing: `mktemp -d`, four files, a pointer swap and a GC per frame, plus `agy_email` per frame on an agy tank and `_limit_codex_rate_limits_cached` per frame on a codex one. Measured at 2.2x main's time to do nothing, with the generation count climbing by two per render. `:612` made it worse: a zero-file tank wrote no manifest, so the publisher's own `[ -f "$oldgen/manifest" ]` gate then treated the generation it had just published as unusable and cold-built again next frame. Fixes both: - `_board_fingerprint_rows` is the ONE function, called by the reader and the publisher, so "the empty set" has one canonical value by construction rather than by two authors agreeing. - `board_state_refresh` spools the stat rows to a file instead of `$( )`, so the bytes hashed at publish are exactly the bytes `_board_stat_rows` produced. The classifier awk reads that file directly too, which drops a process substitution. - An empty tank writes an empty manifest. That is a fact about the tank, not a missing file, and it lets the next refresh take the incremental path. Receipts: two freshly-init'd tanks (claude + codex) with zero transcripts hold exactly ONE generation after five renders, both sides report `cksum("")`, and `board_stale` says fresh; publish and `board_stale` agree at 0, 1 and 2 files. * perf(board_state): cold build under 1 s at 5,000 files (P2-2) Round-7 fix review P2-2. #62's acceptance text is "Board render on the machine above under 1 s cold and near-instant warm". Rounds 5-7 measured 3.4 s at 500 transcripts and 35.6 s at 5,000 on an IDLE host and framed cold as "not a target". It is the target, and the reviewer was right that noise explains the upper end of the old spread, not the lower one. The cost was never the tree walk (`find … -exec stat … {} +` over 5,000 files is ~30 ms). It was FORKS PER FILE: two `board_key`s (`printf | cksum`, two processes each) to name the `sids/` and `recent/` entries, plus a `reading_cache_run` + parser pipeline for every file inside the engine's rate-limit window. 5,000 x ~4 forks x ~1.8 ms is the 35 s, to the second. The cold path now has no per-file fork at all: - `_board_entry_key` names an entry with parameter expansion in bash and one `gsub` in awk — the same rule, written twice and pinned against itself by a test. It is a sanitisation, not a hash, and collisions are handled exactly as before (the raw sid/scope is written into the entry and compared back, so a collision is a MISS, never another session's transcript). It also collides far less than what it replaces: a uuid survives it intact, while a 32-bit cksum has ~0.3% odds of a colliding pair in a 5,000-session tank. `board_key` still names the per-tank root, where it runs once per tank. - claude keeps a session's id in its FILENAME and its scope in its parent directory, so the whole sid/scope table is one awk pass over the stat rows this function already has — no file is opened. - codex and grok keep the id IN the file, so the table comes from ONE batched bounded read: `head -c 512` with `/dev/null` first (so the `==>` banners are unconditional even for a one-file batch) over `xargs -0` batches, parsed by one awk. Any file the bound does not resolve falls back to the real `_board_engine_sidscope`, so 512 bytes costs speed on a pathological file, never an answer. - antigravity's id is in its path and its scope comes from the bulk `_agy_ws_load` index, wired straight into the table with no `$( )`. - The rate-limit scan is bounded to the newest `CLIKAE_HOME_RECENT_MAX` files per PROJECT DIRECTORY inside the window (by directory, not by scope, so claude's `agent-*.jsonl` subagent transcripts stay in the set — round-5 P2-2). The list is published as `readings-bounded`, so what was read is inspectable rather than inferred. DEVIATION from the round-8 brief, with the measurement that forced it: the brief asked for those readings to be computed lazily by the incremental path on the NEXT render. Built that way first, and it turns four receipts this branch has carried since round 5 RED — "warm reads zero transcript bytes", "self-heals inline once, then reads are bounded (list+stat, never content) again", "an incremental rebuild re-reads only the ONE file that changed", and "live rows … at most ONE candidate" — because the render that consumes a pending list is, from outside, a warm render running `tail -c 524288` over ten transcripts. The bound is the brief's; it is consumed before the generation publishes instead of one render later, which keeps the property #62 actually asks for: a render that changes nothing reads nothing. Receipts: bash and awk agree on the entry name over nine adversarial inputs (including `..`, empty, 140 chars, and a path with slashes, which must not escape the entry directory); a cold build over 40 in-window transcripts parses at most 10 and still resolves all 40; the batched read agrees with the per-file parser on codex and grok fixtures, including a codex meta line past the 512-byte bound that only the fallback can resolve. * fix(board_state): purge removed entries BEFORE applying changed ones (P2-3) Round-7 fix review P2-3. A transcript that moves to a new path while keeping its session id is classified as BOTH removed (at the old path) and changed (at the new one). The removed loop ran last, so the changed loop wrote `sids/<key>` and the removed loop then deleted the very entry it had just written. The file was still on disk and its `recent/` row was still there, but `board_find` could no longer resolve it — the session vanished from `clikae resume` with nothing to explain it. Ordering is the whole fix: purge what the previous generation knew, then apply this generation's facts on top. A moved file's entry is tombstoned and immediately rewritten with its new path; a genuinely deleted file's entry stays tombstoned. The trigger surface is not narrow. claude encodes the sid in the filename, so only a whole-file move between project directories hits it there — but codex, grok and antigravity read the sid out of the file's CONTENT, and a rollout moved, a grok session directory renamed or an agy brain directory renamed all keep the sid and change the path. Receipt: a session moved between project directories resolves to its NEW path after the rebuild, and a genuinely deleted one still stops resolving (the tombstone works through the chain, where `rm -f` on this generation's own name would not). * fix(board_state,antigravity,compat): the four round-7 P3s P3-1 — the bash-4 compat gate's negative control was a COPY of the ruler, not the ruler. `tests/bats/compat.bats` re-spelled `scan()`'s greps and the `declare -[gAn]` pattern inline against a probe tree, so it proved "this regex fires", not "THIS RULER fires": the moment `scan` changed its roots or its comment exemption, the control would have gone on vouching for a ruler that no longer existed. The pattern is written once now and the control points the REAL `scan` at the probe tree. P3-2 — `_agy_ws_load` populates plain globals (bash 3.2 has no associative arrays), and the `local -A _ws=()` it replaced was FUNCTION-scoped, so every call started from an empty map for free. Globals do not. Two consequences: a long-lived TUI accumulated one global per session id it had ever seen and kept answering for ids that had since left `history.jsonl`, and a lookup made after loading a SECOND tank could be answered by the first tank's index — the sid is written back and checked, but the same sid genuinely existing in two tanks is not a collision that write-back can catch. `_agy_ws_clear` is the twin of `_board_gen_cache_clear`, and the variable name is namespaced by tank. P3-3 — `_board_transcript_find` ended in `2>/dev/null`, documented as "only ever hides 'no such directory' for an engine this tank has never used". A redirection on `find` covers the stderr of whatever it `-exec`s, so a `stat` that failed on an INDIVIDUAL file dropped that file's row silently, and its absence is indistinguishable to the fingerprint from the file not existing. Nothing reads wrong (both sides run the same function, so a dropped row is dropped symmetrically), but it should not be invisible. The case the redirect existed for is answered directly (`[ -d "$root" ]`); everything else reaches the caller. P3-4 — grok's fingerprint file is `summary.json` and grok REWRITES it, where the other three engines append to a `.jsonl` and always move its size. On a filesystem with whole-second mtime granularity (not this host's ext4/tmpfs, measured at 2-3 ms, but HFS+, SMB and some NFS are), a same-size rewrite inside one second changes neither mtime nor size and the fingerprint cannot see it. Recorded in `_board_engine_name`'s own docstring, where the file that has the problem is named, with the bound on the consequence: Resume ORDER for that session goes one edit stale, no answer is wrong. Receipts: the agy index stops answering for a sid removed from `history.jsonl` and never lets one tank answer for another's identical sid; an engine root this tank never created stays silent at rc=0 while a directory the walk cannot descend into is now audible. * fix(board_state): one entry-name implementation, byte-wise (macOS CI, non-ASCII) Found by this PR's own macOS job, run 34769949650 `bats (macos-latest)`, within hours of pushing round 8 — and not reproducible on Linux at any locale, so nothing local would ever have shown it. `_board_entry_key` names a snapshot entry and the cold build's awk names the same entry, and they were TWO SPELLINGS of one rule — the exact shape that let this file's own staleness fingerprint drift (see _board_fingerprint_rows). On a non-ASCII sid they disagreed: bash (macOS 3.2) "unicode-<2 non-ASCII chars>" bracket matched nothing awk "unicode-__" matched both bytes A bracket expression over multibyte input is not portable: bash 3.2's and each awk's notion of "one character" differ, and the locale changes the answer again. The consequence is invisible rather than loud — the cold build writes `sids/unicode-__`, a reader looks up `sids/unicode-<chars>`, the lookup MISSES, and the session drops out of `clikae resume` with its transcript still on disk. A tank whose project paths are Chinese or accented — normal, not exotic — would lose its whole Resume list on macOS. The rule is now written once, as awk source in `_BOARD_EKEY_AWK`, and both callers run it under `LC_ALL=C` so it is byte-oriented on every platform and every locale. The reader pays one fork per lookup, which is exactly what `board_key`'s `printf | cksum` cost before it; the cold path, the one that runs per file, still pays none — it runs that same source inline in the awk it was already running. The receipt is replaced too, because the old one was the defect in miniature: it compared bash's answer against a re-spelling of the awk. It now writes real transcripts whose ids include Chinese and accented text, cold-builds, and asserts the name ON DISK is the name the reader COMPUTES and that `board_find` resolves each one — the real coupling, through the real path, on whatever platform and locale the suite runs under. Green here at LC_ALL=C, POSIX and en_US.UTF-8. * perf(board_state): a rebuilding render walks the tank once, not twice Round-8 timing, idle host, 5,000 transcripts: one-append came out at 2.15x main, over the 2x bar this round set for itself. Breaking the 127 ms incremental rebuild down rather than guessing at it: 26 ms of it is the tree walk (`find` + one batched `stat`), and `board_generation` was paying that twice back to back — once in `board_stale` to decide whether to rebuild, then again inside `board_state_refresh` to do it. `board_stale` now takes an optional output path and leaves its rows there; `board_state_refresh` takes an optional rows file and uses it instead of walking. Direct callers (tests, adapters) pass neither and are unchanged. This is the safe direction, not a shortcut. The published fingerprint then describes a disk state a few milliseconds OLDER than the parse, so anything that changed in between makes the NEXT board_stale disagree and rebuild. The failure mode it cannot produce is the dangerous one — a fingerprint NEWER than the data it vouches for, which would read as fresh forever. Two smaller forks went with it: `_board_entry_key` uses a here-string instead of `printf | awk` (a pipeline forks both sides, and this runs once per lookup on a render's path), and the two single-`printf` entry writes no longer go through `_board_gen_put`'s `cat`. Receipt: a render that DOES rebuild invokes `find` exactly once (shim-counted through the real front door), and the generation it publishes still matches `_board_transcript_fingerprint` computed fresh, still reads not-stale, and still resolves both the changed session and an untouched one. * fix(board_state,limit): the rate-limit scan is bounded by the WINDOW, not by a count Round-8 fix review P1-1 / P2-1. The cold build recorded, per PROJECT DIRECTORY, the newest CLIKAE_HOME_RECENT_MAX transcripts that fell inside the engine's rate-limit window, and scanned only those. A count and a window are different DIMENSIONS, so no count is ever "provably >= the window" — and the reviewer measured the consequence on the real front door, not in a unit: a limit sitting in a session that had gone quiet behind twelve newer neighbours in the same project directory was invisible, this branch drew a FULL fuel dot where main drew "○ … resets 11pm", and the control (the same fixture with five neighbours instead of twelve) drew the red dot again. It is not only a picture: claude's _limit_tank_dry_raw deliberately never falls back to dry_store, so `clikae burn` dispatched into a tank that was dry. It did not self-heal — only a further write to that abandoned file would have fixed it — and the cold build is exactly the render every user gets once, on the first launch after upgrading. Twelve `agent-*.jsonl` in one project directory is ordinary (this repo's own fleet writes one per subagent), and codex's "project directory" is a DATE directory with a seven-day window. So the bound is the window: every transcript whose mtime is inside it is scanned, however many share a directory. CLIKAE_HOME_RECENT_MAX goes back to bounding only the Resume rows a scope SHOWS. The cost that made a count tempting is paid off a different way. The old scan forked `reading_cache_run` + `tail` + `awk` per file (~10-11 ms each — the whole slope the reviewer measured), so the set had to be small. `_limit_batched_readings` reads all of them with one `tail` per `xargs` batch and folds them in one `awk`: same per-file bound ($CLIKAE_TX_TAIL_BYTES, i.e. exactly the bytes limit.sh's own per-file parser reads), same matching rules, one spelling of those rules (_LIMIT_READING_AWK, now shared by the per-file parsers and the batched one — two spellings of one rule is how this file's twin drifted). Receipts, each RED on 26cac7b: * a claude limit in a session that is NOT the newest in its project (the file is never written again — what an abandoned dry session leaves on disk), behind RECENT_MAX+2 newer neighbours: visible, and the board agrees with the non-board scan it approximates. * the codex twin, twelve newer rollouts in one day directory. * a cold build scans all 40 in-window transcripts, none of the 5 outside the window, in <= 5 tail processes (not one per file). * the batched scan agrees with the per-file parser, claude and codex, including a transcript whose last byte is not a newline. The round-5 receipt at home-bounded.bats:345 is replaced by the first of those: it APPENDED the limit to the "11th-newest" session, and an append makes that file the newest, so it ranked 1st by the time anything read it and could not tell a count bound from a window bound. A ruler that could not reach its own specimen — green on a branch whose cold build dropped exactly the shape it was named for. Also removes the comment block describing `readings-pending` and a "the fuel dots land on the NEXT render" mechanism that has not existed since round 8 (round-8 P3-1): `grep -rn readings-pending lib bin tests` is now empty. * fix(board_state): entry names are an injective escape, so a non-ASCII path cannot answer for its neighbour Round-8 fix review P1-2. `_board_scope_raw` hands the raw `$PWD` to every engine but claude, and round 8 named each `recent/` entry by folding every byte outside [A-Za-z0-9._-] to `_`. Two sibling directories of the same BYTE LENGTH therefore got the SAME entry name — and two CJK characters are six bytes, so `~/Developer/專案` and `~/Developer/文件` are one key. This machine's owner works in exactly such paths. The consequence was not a miss. The cold build grouped by the entry name, so both scopes' sessions went into ONE file under whichever `#scope` header was written first: that scope's Resume list answered with its neighbour's sessions (4 rows where main showed 2) and the neighbour's list went silently empty (0 rows). The file's own header promises "a collision reads as a MISS, never as another session's transcript", and for `recent/` that was false. Two changes, because there were two defects: * the name is now an ESCAPE (`%XX` per unsafe byte), not a fold. It is injective for every input short enough not to hit the 100-character rule, so the collision class is removed rather than made rare. Still ONE implementation, still awk, still run by both the cold build and every reader under LC_ALL=C — the property round 8 paid for with a macOS CI job, kept. * the cold build groups `recent/` by the RAW SCOPE, not by the name, and the incremental path's merge/purge verify the resolved entry's `#scope` header before rewriting it. So even where a name IS shared (the >100 truncation, the one case left) the loser reads as a miss, which is what the header claims — never a merged list. DEVIATION from the brief, measured. The brief asked for `cksum` of the raw bytes. The reader can call `board_key` outright, but the cold build names every entry inside one awk pass, so awk needs its own CRC-32. I wrote one (table-driven POSIX cksum, byte-exact against `cksum` on ASCII and CJK — verified against the coreutils output) and timed it: 177 ms for 5,000 sids on this host's gawk, three runs, against a cold build that has to fit in #62's one second and costs ~500 ms without it; macOS's one-true-awk is slower still. The escape costs 3 ms for the same input, needs no second implementation anywhere, and kills the class instead of shrinking it. The receipts and the reasoning are in lib/core/board_state.sh's own header. Receipt (RED on 26cac7b, where 專案一 listed s1-0/s1-1 — its neighbour's sessions — and 專案二 listed none): two CJK siblings and two accented siblings, each with two codex sessions; every scope's `board_recent` returns its own two rows and nobody else's, cold AND after an incremental rebuild; and the two scopes' entry names differ. * fix(board_state): GC protects the chain of every live generation, not only current's Round-8 fix review P2-2. `board_gc_generations` protected the ancestors `current` resolves through, and applied keep-N to everything else. But `current` is not the only generation someone is still reading: `board_generation` memoizes a generation PATH for the life of the process, so a TUI frame, a `clikae burn`, or any second shell holds one while another publishes. The reviewer measured what one further publish does to such a holder. Hold a generation at depth 7; publish once more — that publish is the MATERIALISING one, so the new chain starts fresh and the held chain is no longer reachable from `current`; keep-5 then unlinks its three oldest links. The held generation still exists, `current` is valid, `board_stale` still says "fresh", and 49 of its 50 entries stop resolving. That is exactly the consequence _board_gc_candidates' own header names ("a silently EMPTY Resume list … with `current` still valid") — the defence was simply one generation too narrow. Round 7's design had five publishes of slack here (every generation was self-contained); the chain took that to zero. keep-N now counts CHAINS rather than directories: the roots are what `current` points at plus the newest <keep> generations, and every ancestor any root still resolves through is protected with it. Cost is at most keep x _BOARD_GEN_MAX_DEPTH reads of a one-line `parent` file, all builtins; the chains overlap almost entirely, so the number of directories on disk barely moves. Receipt (RED on 26cac7b, which removed a held chain link on the first materialising publish): 50 sessions written once and never touched again, the chain driven to depth _BOARD_GEN_MAX_DEPTH-1 by appending to ONE unrelated file, then one more publish — every link of the held chain is still on disk, all 50 entries resolve FROM THE HELD GENERATION, and `clikae clean`'s own sweep (same rule, one implementation) does not undo it. * chore(board_state,test): the round-8 P3s — readings-bounded on both paths, and a gate that can see .bats Round-8 fix review P3-2 and P3-3. (P3-1 — the comment describing `readings-pending` and a "the fuel dots land on the NEXT render" mechanism that no longer existed — went with the commit that replaced the mechanism it described; `grep -rn readings-pending lib bin tests` is empty. P3-4 — the round-5 receipt that could no longer reach its own specimen — went with the same commit, rewritten as the window receipt.) P3-2 · `readings-bounded` was published by the cold build and by nothing else, so the presence of that file meant two different things ("this is what cold read" vs "this generation is incremental") and nobody auditing a frame could tell which. Both paths write it now, with one meaning on both: the transcripts THIS refresh opened for a rate-limit reading. Receipt (RED on 26cac7b, where the file does not exist after an incremental rebuild): after a 50-file tank's one changed file, `readings-bounded` is exactly that one path. P3-3 · `scripts/test.sh` read `find lib tests scripts -name '*.sh'`, which LOOKS like it covers tests/ and does not, and CI's shellcheck action does not scan `.bats` either. So two findings this branch introduced in test files — home-bounded.bats SC2155, antigravity.bats SC2154 — sat under a green light on both sides, as did compat.bats' SC1087, which came in from main. The gate now scans `.bats` too, CI grew the matching step (so the two agree by construction, not by remembering), and everything it found is fixed: 73 findings across 20 files, all of them either real (24 SC2155 `local x="$(…)"` splits, dead assignments in gate-stamp.bats and limit-codex-status.bats, `done` as an unquoted argument in wait.bats, `VAR= cmd` in suite-lock.bats, `$n[` read as an array in compat.bats) or a named, reasoned `# shellcheck disable=` where the idiom is deliberate (a destructuring `read`, a name the code under test reads out of its caller's scope, an out-variable). Two of them were dead assertions, not style: `! declare -F …` and `! grep -q …` in burn.bats. In bats, a `!`-negated command never fails the test — SC2314 is an ERROR for that reason — so both had been asserting nothing. They are `… || false` now, and burn.bats is green with them live. * fix(board_state): same-scope is byte equality, so macOS's collation cannot merge sibling directories Round 9b. PR #78's macOS job (run 34804107952, `bats (macos-latest)`, test 720) went red on round 9's own receipt: the 專案一 scope's Resume list held s0-0, s0-1, s1-0, s1-1 — its neighbour 專案二's two sessions among them. Linux was green. It was not the entry name: Apple's awk computes round 9's escape byte-exactly (the golden-name asserts added below check that on the runner itself). The merge happened one step later, in the awk that groups the cold build's `recent/` rows by scope: if (sc != cur) { ... start a new entry ... } POSIX awk compares strings in the collation order of the caller's locale, and macOS's awk does exactly that — apple-oss-distributions/awk `src/run.c` relop() is `strcoll(getsval(x), getsval(y))`, for `!=` and `==` as well as `<`. That awk ran without `LC_ALL=C`, the runner's collation is UTF-8, and UTF-8 collation on macOS has no weights for most CJK ideographs, so `專案一 != 專案二` was false: the scope never "changed", 專案二's rows went into 專案一's entry, and 專案二 had no entry at all. No Linux awk can show this, and no glibc locale can either: glibc's strcoll never returns 0 for two distinct strings (measured on C.utf8 and en_US.utf8 for the CJK pair, NFC/NFD café, soft hyphen, ZWSP, U+2060), and gawk/mawk/onetrue-awk use a byte compare for `!=` whatever the locale. So it was reproduced with the real thing instead: Apple's awk (awk-40, "version 20200816") built from source in a Debian container, run under a compiled locale whose collation gives 二 the weight of 一. The unfixed branch then fails test 720 with the IDENTICAL four rows macOS printed; the same build under C.UTF-8 is green, so the collation is the one variable. Fix, twice over: * both awks that ask "is this the same scope / sid?" — the `recent/` grouping and `_board_purge_recent_row`'s filter — run under `LC_ALL=C`, where collation is byte order by definition; * and they no longer ask with `==`/`!=` at all: `bsame(a, b)` (length + index, which never consult LC_COLLATE) is defined once next to `ekey` in `_BOARD_EKEY_AWK` and used by both. The test grows the shapes the brief asked for and one it did not: café in NFC AND NFD as sibling scopes (two directories on Linux, one on APFS, two `$PWD` byte strings either way — nothing may normalize either side); golden escapes for 專案一 and both cafés, asserted on whatever awk and locale the suite runs under; every scope's `recent/` entry on disk; and a PURGE pass (remove one NFD session: only that scope loses a row), which is the second awk this changes. Receipts (in the PR body): Apple-awk + hostile collation, test 720 on fb91dcd RED with macOS's four rows, on this commit GREEN; control arm green on both. * fix(board_state): GC ranks generations in publish order, not by a same-second coin toss Round 9b, found while re-running the r8 review's P2-2 receipt on HEAD as the brief asked (r8/p3b-hold.sh, a reader holding a depth-7 generation while publishing continues). Round 9 protected the chain of every generation in "the newest <keep>" — and "newest" was whole-second mtime with the directory name as the tie-break (round-2 P3-1). The name is mktemp's random suffix. Rebuilds inside one wall-clock second are the normal case (every tight refresh loop, every bats run), so which generations counted as the newest five was a coin toss, and the generation a reader still HOLDS could fall out of the roots and be unlinked on the very next publish — chain protection and all. Measured, same probe, four runs each (no code change between the two trees on this path; the merge did not cause it): d396968 (round 9 tip) first publish: held gone in 2 of 4 runs (chain-alive 6/8 and 7/8, 50/50 unresolvable) 3c1b4f2 (after merge) same probe: held survived 4/4 on the first publish, died early in 2 of 4 by publish 7 The round-9 bats receipt for P2-2 was therefore passing on the toss. Fix: every generation records `seq` at publish — the previous `current`'s seq + 1 — and `_board_gc_candidates` (which `clikae clean`'s sweep also calls) ranks by seq, then mtime, then name. Order no longer depends on the clock; mtime and name only break a tie between two processes that published from the same `current`, or rank a generation written before `seq` existed (seq 0, swept first). Materialising copies only sids/ and recent/, so a fresh chain never inherits a stale seq. The receipt stops relying on luck: before the materialising publish it gives the held (newest) generation the OLDEST mtime of all, so the clock disagrees with publish order on every run. RED on 3c1b4f2 (the held generation itself is unlinked), GREEN here. * test: the bats files merged from main pass the bats shellcheck gate — and five negations now assert Round 9 (d396968) taught scripts/test.sh and ci.yml to shellcheck `*.bats`. Main's newer test files (#83, #92, #98) were written before that gate existed, so merging main turned it red on 12 findings. Two of them are cosmetic; one is a test that did not test: * cle…
Closes #74.
What
burnrecords the session it started in a sidecarstate/burn-sessions/<engine>/<tank>(<sid>\t<run_id>\t<epoch>per run): claude by the id it launches with (or the id the caller itself supplied), codex / antigravity by a before/after transcript-set diff — the ONE new transcript this run produced, never a guess. Ambiguous or empty runs record nothing.resume's picker AND the home board's Continue list hide sidecar sessions by default;--all(or theakey in the picker) shows them, rows labelled[burn].adapter_recent_sidssources candidates from the CLI's owncache/last_conversations.json(anchored key lookup, honourslimit), transcript glob as fallback.clikae renamecarries it,clikae removedeletes it,clikae cleanprunes stale lines and caps it at 2000 (newest kept).adapter_sid_canonical), shared by burn's writer and resume's picker, so a burn session can actually be found and hidden.Round-1 review fixes (this round)
Fixed all 4 P1 + 5 P2 from the round-1 adversarial review, one commit per finding:
adapter_sid_canonicalper engine now.burn claude … -- --resume <sid>(or-- --session-id <sid>) used to fail (rc=1): burn appended--session-idunconditionally, clashing with claude's own rule. It now checksadapter_sid_from_argsfirst, the same gateswitch.shalready uses.sed 's/.*:…'that, on a real (compact, single-line) cache with more than one project's pointer, silently returned a DIFFERENT project's session. Anchored via a new sharedjson_value_for_keyhelper.limit, so the board's Continue list (limit 10) got back at most one agy candidate. Now falls through to the disk scan for the rest.cleanprunes + caps it, and the read is onegrep -n -F -x -fpass (214ms measured at 20,000 lines on this shared host, vs the review's 8170ms baseline).akey skipped the terminal-leaving cleanup theckey already does, so toggling--alloff on an all-burn store printed "No sessions" inside the alt screen and it got wiped on the way out. Same three-step cleanup ascnow.Rkey already forwarded toclikae resume(one filter), but the board's own Continue list (a separate query) had no filter at all. Now shares the same sid-membership read as the picker.CLIKAE_RESUME_ALLis a plain assignment, notexport(it never needs to reach the resumed engine);docs/usage.mddocuments--all/a; the CHANGELOG declares agy--json'srun_idgoing fromnullto a string.🔴 Stacked branch notice:
fix/agy-resume-visible-34(#34) is built on this branch and independently rewritesadapter_recent_sids/adapter_resume_argsinlib/adapters/antigravity.sh(dropping the$PWDcwd filter, aprintfoption-parsing fix). This round's P1-4/P2-1 commits touch the same function surgically (the cache-lookup anchor and the limit fallthrough) without restructuring it, but #34 will still need a rebase — it forked before these commits existed.Tests
tests/bats/resume-hide-burn.batsgrew from 7 to 28 cases covering every finding above (codex actually hides in resume, not just the sidecar; zero-transcript runs write no line; an unresolvable same-cwd collision records nothing; the claude argv-clash cases; etc.), plus new coverage inadapters/{claude,codex,antigravity}.bats,rename.bats,remove.bats,clean.bats,home.bats, andresume.bats. Full local suite: 1322/1322 across all 84 files, rc=0.shellcheck -S warningclean on every touched file.git merge-tree --write-tree origin/main HEAD— no conflicts.Round-2 review fixes (this round)
Fixed the 1 P1 + 3 P2 from the round-2 adversarial review, one commit per finding, plus 4 of its P3s:
$PWD(this shell's cwd), not the cwd codex actually launched in (-C add_dirs[0], defaultdirname("$artifact")). Any--artifactoutside$PWD, or an explicit--add-dir, made burn's OWN session stop matching — leaving a concurrent human session in$PWDas the sole "match", recorded and hidden. Threaded through a single_burn_launch_cwdvariable, set where the launch argv is composed; agy's identical tie-break (which never overrides its own cwd) now reads the same variable instead of a bare$PWD.adapter_find_session's exit code, but codex/grok both return 0 on a miss (empty stdout). Both new call sites now read the output.--resume <sid>named a transcript that already exists before the run starts, so "the transcript exists" was a tautology — recorded and hidden even when the engine refused to start. Now proven by rc == 0 OR the transcript's (mtime, size) changing across the run.state/burn-sessions/(a removed adapter, a downgrade) took the entireclikae cleandown with it:load_adapterexit()s the process, and|| trueonly catches a nonzero return. Same subshell-probe-then-load shapehome.sh:193-201already uses.json.sh's stale comment (sayshead -n 1, the only caller usestail -n 1); unified "valid sidecar line" into one definition shared byhome.shandclean.sh's GC (a hand-corrupted line was dead to the reader but alive to the GC, occupying a cap slot forever); fixed a temp-file cleanup that was structurally scoped to the wrong branch (unreachable today, pinned so it stays that way).Not fixed in this round (left for a future pass, per the review's own scoping): P3-2 (a
sedescape inantigravity.sh's cache-key regex misses+ ? ( ) { } |— falls through to a correct disk scan today) and P3-6 (grok sessions never appear in_resume_all_sessions' glob at all, so its sidecar participation is currently moot).🔴 #93 stacked-branch note:
fix/agy-resume-visible-34(#93) must rebase onto this branch's new HEAD and keep this round'sjson_value_for_keylookup inadapter_recent_sids— #93's own cache-lookup rewrite (grep -oE ... | tail -n 1, no key match) would silently reintroduce round-1's P1-4 (a compact multi-project cache returning the WRONG project's session) the moment a merge conflict resolves toward its side. See the review for the full three-point semantic-conflict analysis.Round-2 test additions
New coverage in
tests/bats/resume-hide-burn.bats,tests/bats/clean.bats(including two direct_clean_burn_sidecar_gcunit tests), andtests/bats/i18n.bats's existing completeness check (now covering the two newT_K_TOGGLE_*keys across all 9 locales). Full local suite: 1330/1330 across all 84 files except one pre-existing, unrelated failure (home.bats's "every key the board binds is listed in the ? help overlay" — reproduced identically onaf5729ditself, before any of this round's changes; not touched by this PR).shellcheck -S warningclean on every touched.shfile.git merge-tree --write-tree origin/main HEAD— no conflicts.Round-3 review fixes (this round)
Fixed the 1 P1 from the round-3 adversarial review, plus its named P3-1/P3-2 tightening, two commits:
--prompt/--prompt-filepath. Raw-- <cmd...>mode's_burn_launch_cwdstayed at its$PWDdefault no matter what the user's own argv said (the only two resets are both gated onprompt_set==1, which raw mode never sets) — but codex's own-Ccan be given directly after--(burn --help's own raw example,burn.sh:118, is exactly-- exec -C /tmp …), so a caller who did that from a$PWDother than-C's target reproduced round-2's exact bug on this path: burn's own session stopped matching the cwd tie-break and a concurrent human session in$PWDgot recorded and hidden instead. Addedadapter_cwd_from_argsnext toadapter_sid_from_args(codex scanscmd[@]for-C <dir>/-C<dir>/--cd <dir>; claude/antigravity/grok define it too but always return empty — none of the three has a cwd-override flag). Raw mode now derives_burn_launch_cwdfrom the adapter, defaulting to EMPTY (never$PWD) when the adapter has no such hook or the flag isn't present, so an unresolved raw launch matches nothing and records nothing — never hide what is not proven. Also deleted the wrong comment this bug's own fix had introduced ("raw mode never overrides the engine's cwd" — it does).--resume <sid>of an EXISTING transcript was recorded on "rc == 0 OR (mtime,size) changed" (an OR): a FAILED engine whose target transcript changed anyway (a concurrent human still typing into the SAME sid, untouched by burn's own attempt) still got recorded and hidden — a (mtime,size)-changed check is a "someone wrote it" detector, not an "the engine ran" detector. Now requires all three:rc == 0(no longer an alternative), the transcript's byte size actually GREW, and no OTHER live process still has it open (fuser/lsofif present, else the one check is skipped with alog_warnsaying so — conditions 1 and 2 still apply either way). R3-P3-1 itself (an explicit--resumeof someone else's sid that the caller typed themselves, engine succeeds) is left as-is per the review's own scoping — a deliberate narrowing, not a defect.home.bats's "every key the board binds is listed in the ? help overlay" test "reproduced identically onaf5729ditself." Re-investigated this round: the test's ownfor key in $labelsword-splits an UNQUOTED variable containing a literal?— which bash then glob-expands against the invoking shell's cwd. On a cwd that happens to contain a single-character-named file (this machine runs many lanes out of a shared~/lanes/, and one left a strayh),?silently expands to that filename and the test misreports a bound-but-undocumented key that doesn't exist. It is not a real difference between any two commits — running from a cleanmktemp -dcwd, the six-file targeted set is a real 443/443ok, rc=0 (six files:resume-hide-burngrew from 20 to 23 cases with this round's additions), and the full 84-file suite is 1334/1334okacross 4 chunks, all rc=0 (roam.bats's own previously-reported flake also did not reproduce this run). Filed as a finding, not fixed (it's a pre-existing bug in the test itself, outside this PR's touched files).R3 review: 1 P1 (closed), the named P3-1/P3-2 tightening (closed), P3-3 (report/PR-body correction above), P3-4 (
roam.bats's pre-existing red — confirmed unrelated to this PR, and did not reproduce at all this run: full suite is clean). The rest of P3-1's scope, P3-5 (agywant_escescaping) and P3-6 (grok's non-participation in the sidecar) remain deliberately out of scope, per the review's own scoping — unchanged from round-2.Round-3 test additions
New coverage in
tests/bats/resume-hide-burn.bats: raw-- <cmd...>codex burn with an explicit-Cattributes to its own launch dir and never hides a concurrent human session in$PWD(the round-3 review's probe B shape); the same raw mode without-Cand two ambiguous candidates records nothing;burn --help's own raw-Cexample still records cleanly; a--resume <existing sid>whose engine fails (rc=1) while a concurrent human keeps typing into the SAME transcript (file grows) records nothing, where round-2's OR would have recorded and hidden it.shellcheck -S warning -xclean on every touched.shfile and on the fulllib/**+bin/clikae. bash 3.2 GNU-ism scan: zero new hits (same two pre-existing comments as every prior round).docker run bash:3.2 bash -nclean on every touched file.git merge-tree --write-tree origin/main HEAD— no conflicts.Round-4 review fixes (this round)
Fixed the 1 P2 and three of the six P3s from the round-4 adversarial review (0 P1), two commits:
burn.sh:2778's multi-candidate cwd tie-break compared each new codex transcript's recorded cwd against_burn_launch_cwdwith a plain string equality. In raw-- <cmd...>mode with no-Cfound,_burn_launch_cwdis empty by design (round-3 P1-1: "never hide what is not proven"). Butadapter_session_cwdalso returns empty when a candidate'ssession_metaheader hasn't flushed itscwdfield yet (a narrow but real race between codexcreat()-ing the rollout and writing that line) — empty == empty made that candidate the sole "match", and its session got recorded into the sidecar and hidden from the default resume view. Exactly the defect this PR exists to fix, reintroduced via a different path. Fixed by requiring both sides non-empty before comparing.codex.sh'sadapter_cwd_from_argsdidn't recognise the clap=-joined forms real codex 0.154.0 (measured) accepts:--cd=<dir>fell through to "no match" (an empty launch cwd, now correctly treated as unknown by the P2-1 fix), and-C=<dir>matched the looser-C?*case and returned=<dir>verbatim (direction-safe but silently dead — could never equal a real cwd). Both forms now handled explicitly.--resumetriple gate (rc==0 AND grew AND not open elsewhere) rejected an existing sid, control fell through to the before/after snapshot diff further down, whose own single-new-candidate branch has no rc check of its own — a DIFFERENT concurrent session minted during the same failed attempt was the sole "new" transcript and got recorded (and hidden) despite having nothing to do with the gate's verdict. A_burn_resume_gate_rejectedflag now makes that rejection final.tests/bats/home.bats:949'sfor key in $labelsword-split an unquoted variable containing a literal?, which bash pathname-expands against the bats process's cwd — a stray single-character-named file silently swaps the?token for that filename, and the?guard right below never fires. Switched to a quotedwhile IFS= read -r key; do … done <<< "$labels". Fixed even thoughhome.shitself isn't in this PR's touched-file list, because the review's finding was specifically requested in this round's brief.Left for a future pass, per the review's own scoping: R4-P3-2 (the third gate check has no positive-path test in the repo — only my own throwaway probes exercised it), R4-P3-4 (codex's app-server daemon may hold the rollout fd after the engine exits, making the third gate check a permanent no-op on codex — direction-safe, unverified), and R4-P3-6 (three items the review explicitly says are pre-existing and deliberately kept: an explicit
--resume <sid>a caller typed themselves; antigravity's cache-key regex escape gap; grok's non-participation in the sidecar).Round-4 test additions
New coverage:
tests/bats/resume-hide-burn.bats— a raw-- <cmd...>codex burn WITHOUT-C, where one of two new candidates' recorded cwd is unreadable (id-onlysession_metaline), records nothing and the human stays visible (P2-1); a--resume <existing sid>whose engine fails the triple gate while a DIFFERENT concurrent session is minted records nothing (the gate-rejected fix).tests/bats/adapters/codex.bats— six direct unit tests foradapter_cwd_from_argscovering every real codex argv shape (-C <dir>,-C<dir>,--cd <dir>,--cd=<dir>,-C=<dir>, and no flag at all).tests/bats/home.bats— the existing key-legend-parity test's loop is now quoted, plus a new test that plants stray single-char files (h,x) in cwd and confirms the scan is unaffected.shellcheck -S warning -xclean on every touched.shfile and on the fulllib/**+bin/clikae. bash 3.2 GNU-ism scan: zero new hits (same two pre-existing comments as every prior round).docker run bash:3.2 bash -nclean on every touched file.git merge-tree --write-tree origin/main HEAD— no conflicts. Specified 11 files (resume-hide-burn/burn/resume/home/clean/adapters/*): 452/452ok, rc=0 (443 baseline + 9 new this round). Full suite (84 files, 6 chunks): 1343/1343ok, 0not ok(6 chunks, reverse-checked coverage against the exact 84-file list).Round-2 fixes by KITT.
Built by antigravity (tank g); tests by codex (tank crazy). Round-1 fixes by KITT.
Round-3 fixes by KITT.
Round-4 fixes by KITT.
🤖 Generated with Claude Code