Skip to content

Add bw recap: unblocked events, recap parser, tree/JSON renderers, cross-repo fan-out#122

Merged
jallum merged 20 commits into
mainfrom
distill/recap
May 9, 2026
Merged

Add bw recap: unblocked events, recap parser, tree/JSON renderers, cross-repo fan-out#122
jallum merged 20 commits into
mainfrom
distill/recap

Conversation

@jallum

@jallum jallum commented Apr 24, 2026

Copy link
Copy Markdown
Owner

bw recap shows recent beadwork activity. Cursor-driven incremental by default (24h backfill on first run), with explicit window support (recap today, --since). Gap detection refuses to advance the cursor when an explicit window would skip unseen commits. --all fans out across registered repos. Cursor lives in a local git ref (refs/beadwork/recap-cursor).

@jallum jallum marked this pull request as draft April 24, 2026 11:32
@jallum jallum force-pushed the distill/recap branch 3 times, most recently from f1c0bb6 to 142e597 Compare April 24, 2026 18:57
@jallum jallum changed the base branch from distill/registry to distill/registry-v2 April 24, 2026 18:57
jallum pushed a commit that referenced this pull request May 7, 2026
## Summary

CI is currently red on `main` because `cmd/bw` fails the `staticcheck`
step. This PR makes it green.

- **Compile error fix.** `prime_test.go`'s two newer subtests still
  called the pre-config `cmdPrime` signature. Updated both to match
  the current `(*issue.Store, []string, Writer, *config.Config)
  (*config.Config, error)` shape used by every other call site.
- **Drop five U1000 helpers** that have no production callers, plus
  the tests written solely to exercise them:
    - `(*Command).valueFlags` — never had a caller; commands all pass
      hardcoded flag lists straight to `ParseArgs`
    - `validateDate` — replaced by `resolveDateNow` in #99
    - `toRFC3339Date` / `fromRFC3339Date` — `bd`-compat padding removed
      in #112 when `defer_until` / `due` switched to native YYYY-MM-DD
    - `relativeTime` (string wrapper) — only the pre-rewrite recap
      called it; the re-land in #122 uses `relativeTimeSince` directly
      with parsed `time.Time`
- **Inline three `validateDate` checks** in real defer-behavior tests
  (`TestCmdDeferRelativeDate`, `…Tomorrow`, `…NextMonday`) with
  `time.Parse("2006-01-02", ...)`.
- **Drop now-unused `"strings"` import** in `export.go`.
- **Keep `relativeTimeSince` (and its test)** behind
  `//lint:ignore U1000` referencing #122, which wires it back into
  `bw recap`. Once #122 lands, that directive can be removed.

Net: **+8 / −184** across 9 files, all in `cmd/bw/`.

## Test plan

- [x] `go build ./...`
- [x] `staticcheck ./...` (clean)
- [x] `go test ./...` (full suite passes)
@jallum jallum force-pushed the distill/registry-v2 branch from 409d3e0 to 050fc7f Compare May 7, 2026 11:18
@jallum jallum marked this pull request as ready for review May 7, 2026 12:13
@jallum jallum requested a review from iautom8things May 7, 2026 12:13
iautom8things and others added 17 commits May 7, 2026 09:14
Host-local JSON store tracking beadwork-enabled repos:
- Load/Save with atomic temp-file rename for safe concurrent writes
- Touch/TouchAndSave for registering repos with timestamps
- AdvanceCursorAndSave for incremental recap cursors
- Prune for removing stale entries by predicate
- Unknown-field preservation across load/save cycles
- Schema version check refusing newer-than-supported
- Home resolution: BEADWORK_HOME > ~/.beadwork (os.UserHomeDir honors $HOME)
- CanonicalRepoPath resolving worktrees to their main repo root

Also adds test scaffolding used throughout the series:
- newBwEnv() sets BEADWORK_HOME to an isolated temp dir
- newMultiRepoEnv(t, n) creates n repos sharing a registry dir
- seedRegistry() / registryContents() for registry state setup/assertion
- compareGolden(t, name, got) generalized golden-file comparison
- bwNow() returns the deterministic clock as time.Time
- bwFail() for testing expected-error paths
Auto-registration:
- touchRegistry() runs after every successful command, registering the
  current repo in the host-local registry
- Resolves via FindRepoAt (not NeedsStore) so it fires for all commands
- Uses CanonicalRepoPath to deduplicate worktrees to their main repo
- Gated on IsInitialized() so non-beadwork git repos aren't registered
- Respects BW_CLOCK for deterministic timestamps in tests
- Silently ignores errors (registryErrorOnce warns once on stderr)
- BEADWORK_HOME env var isolates tests from the real registry

bw registry subcommand:
- list: TTY + plain + JSON modes, MISSING flag for deleted repos, live
  prefix read from repo, sorted by path
- prune: --yes/-y for non-interactive, TTY stdin check, half-removal
  warning when >50% of entries would be removed
- Appears in bw --help under Cross-Repo & Activity
- Nested dispatch routes registry <sub> with --help support

CLI tests (cli_test.go) now set BEADWORK_HOME to t.TempDir() via a
shared bwTestEnv() helper, so shelling out the bw binary doesn't
pollute the user's real registry.
The registry should be a path+timestamp index, not a cache of
repo metadata. Always read the prefix from .bwconfig via the
repo package to eliminate drift between the two sources.
Replace the JSON-based registry (schema version, Entry structs with
timestamps/cursors) with a simple text file containing one repo path
per line. The registry is now a pure path index — no metadata caching.

- DefaultPath() returns ~/.bw (or $BEADWORK_HOME) as a file, not dir
- Registry API: Load/Save/Add/Remove/Paths
- touchRegistry() just calls reg.Add(path)
- All tests updated for new plain-text format
Matches the BW_CLOCK convention and accurately describes what the
env var points to — a registry file, not a home directory.
CanonicalRepoPath duplicated the worktree→main-repo resolution already
done by repo.FindRepoAt. Since touchRegistry already has a *Repo from
FindRepoAt, r.RepoDir() gives the canonical root directly.
Move registry.repos into the YAML config file, eliminating the
internal/registry package entirely. Auto-registration writes to
config; registry list/prune read from config. Save failures from
auto-registration are now silently ignored so commands still run
when the config path is unwritable.
Reintroduce internal/registry as a thin layer over config providing
Paths, Repos (with live prefix lookup), Resolve (prefix→path), and
Register (immutable add). autoRegister and registry commands now use
these helpers, giving the upcoming recap --all a clean API to fan
out across registered repos.
Auto-registration now only fires when registry.auto is true in the
global config, and reuses the repo already resolved by the store
instead of looking it up again.
Stamp "unblocked <id>" lines into the close commit message for each
newly unblocked dependent, and expose them via bw history. This feeds
the recap command's activity view.

- Close path scans ready-state transitions and writes unblocked events
  atomically with the close transition
- Retry on ref-update contention (shared beadwork branch can race)
- history command renders unblocked events alongside other events
- Distinguishes "unblocked" text in a reason from a real stamped event
internal/recap package:
- Data model for per-issue activity windows (events + metadata)
- Parser reading beadwork commit log for transitions
- Window resolution for today/yesterday/since-last/--hours

bw recap command:
- Single-repo: tree renderer (grouped by issue) and --json renderer
- --all: cross-repo fan-out across the registry, aggregating by repo
- Registered under "Cross-Repo & Activity" in bw --help
- Condensed default view with duration tokens (15m, 1h, 3h30m, 2d, 1w)
- Colorize both condensed and verbose tree renderers
- Fix incremental recap (since-last) window bookkeeping
- Truncate long titles; suppress TTY hints when piped
- Stamp last_recap_at on each run so the "since last recap" label
  updates monotonically
- Honor local timezone for today/yesterday windows
Explicit-window recaps (--since / today / 1h / etc.) previously stamped
the cursor to HEAD via AllCommits, even when the window started after
the current cursor. Commits between cursor_time and window.Start —
never rendered — would be marked seen and permanently skipped by future
implicit recaps.

Now: when the window starts after the cursor, the recap renders the
window as before but neither `cursor` nor `last_recap_at` is stamped,
and a stderr notice reports the count of unrendered gap commits. No-gap
explicit recaps and bare cursor-driven recaps keep their existing
stamping behavior. --dry-run still skips all stamping.

See .spec/decisions/recap-explicit-window-conditional-advance.md.
containsStr and contains were orphaned when "Record unblocked events"
swapped the CAS conflict assertion to errors.Is(err, ErrRefMoved).
Staticcheck flags them as U1000.
Store the recap cursor as a local git ref instead of in the registry.
The ref file's mtime provides LastRecapAt. This keeps per-user recap
state local to each repo clone (never replicated via push/fetch).

- Add Repo.RecapCursor/SetRecapCursor/LastRecapAt methods
- Rewrite recap.go to use repo-based cursor instead of registry
- Update acceptance tests to check git ref instead of registry JSON
registryContents/registryDir/BEADWORK_HOME were artifacts of the old
file-based registry; the new design stores repos in BW_CONFIG (YAML)
and the recap cursor in refs/beadwork/recap-cursor.

- TestRecapDryRun: check git ref instead of registry file contents
- TestRecapTodayLocalTimezone: BW_CONFIG= replaces BEADWORK_HOME=
- TestRecapNotInRepo: remove registryDir field, add isolated BW_CONFIG
- TestRecapAllWarnsOnMissing: seedRegistry() replaces file write
@iautom8things iautom8things force-pushed the distill/registry-v2 branch from 050fc7f to ee8cdd5 Compare May 7, 2026 13:16
Base automatically changed from distill/registry-v2 to main May 7, 2026 17:09
jallum and others added 3 commits May 7, 2026 13:13
All conflicts were additive: recap command registration, bwNow() helper,
and recap/close-unblocked test helpers and test cases added in the PR
vs. absent in main.
The "since last recap (Xh ago)" header derives from os.Stat ModTime of
the cursor ref file. Before this change, mtime only moved when
SetRecapCursor wrote a new hash, so a no-event recap left the timestamp
unchanged and the header lagged across quiet stretches:

  Mon 09:00  bw recap          → "since last recap (24h ago)"
  Tue 09:00  bw recap          → "no activity"   (mtime untouched)
  Wed 09:00  bw recap          → "since last recap (2d ago)"

Add TouchRecapCursor (os.Chtimes-only) and call it from runRecapSingle
and cmdRecapAll when the recap completes without new commits and a
cursor already exists. The gap-window early-return still skips both
SetRecapCursor and TouchRecapCursor, preserving the gapped-explicit
behavior from ac817d5.

Update TestRecapStampsLastRecapAtWithNoCommits to actually assert what
its name claims: backdate the cursor mtime via os.Chtimes, run a
no-event recap, verify mtime advances. Sanity-checked: removing the
fix makes the test fail with "cursor mtime did not advance".

@iautom8things iautom8things left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a fix for 'last recap' tracking. Ready for your approval @jallum 🚀 🚀 🚀 when ready

@jallum jallum merged commit 80cda69 into main May 9, 2026
1 check passed
@jallum jallum deleted the distill/recap branch May 9, 2026 18:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants