Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6e236bd
Add internal/registry package with host-local canonical store
iautom8things Apr 12, 2026
04dcd77
Auto-register beadwork repos + bw registry list/prune
iautom8things Apr 12, 2026
1f15c3a
Remove Prefix from registry Entry; read live from repo
jallum Apr 24, 2026
9a76f56
Collapse registry to plain text path list (~/.bw)
jallum Apr 24, 2026
575b13e
Rename BEADWORK_HOME to BW_REGISTRY
jallum Apr 24, 2026
980056f
Remove redundant canonical path resolution from registry
jallum Apr 24, 2026
e252f1b
Integrate registry into global config
jallum Apr 24, 2026
e4ef771
Add internal/registry package with config-backed helpers
jallum Apr 24, 2026
88f25e4
Gate auto-registration on registry.auto config flag
jallum Apr 26, 2026
ee8cdd5
Move registry.auto check into registry.Auto accessor
jallum Apr 26, 2026
cd0373a
Record unblocked events in issue history
iautom8things Apr 12, 2026
72701db
Add bw recap with tree/JSON renderers and cross-repo fan-out
iautom8things Apr 12, 2026
d5abb39
Polish recap output
iautom8things Apr 13, 2026
ac817d5
recap: skip cursor advance on gapped explicit windows
iautom8things Apr 16, 2026
f530f27
Remove unused treefs_test helpers
iautom8things Apr 16, 2026
cce1951
Move recap cursor to refs/beadwork/recap-cursor
jallum Apr 24, 2026
756a217
Fix acceptance tests after registry-v2 rebase
jallum May 7, 2026
55f9892
Merge main into distill/recap, resolving conflicts
jallum May 7, 2026
a6a46ca
gofmt internal/recap/recap.go internal/treefs/treefs_test.go
jallum May 7, 2026
030cf23
recap: bump cursor mtime on no-event runs
iautom8things May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 40 additions & 13 deletions cmd/bw/close.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package main

import (
"errors"
"fmt"

"github.com/jallum/beadwork/internal/config"

"github.com/jallum/beadwork/internal/issue"
"github.com/jallum/beadwork/internal/md"
"github.com/jallum/beadwork/internal/treefs"
)

type CloseArgs struct {
Expand All @@ -30,28 +32,53 @@ func parseCloseArgs(raw []string) (CloseArgs, error) {
}, nil
}

const closeMaxRetries = 3

func cmdClose(store *issue.Store, args []string, w Writer, _ *config.Config) (*config.Config, error) {
ca, err := parseCloseArgs(args)
if err != nil {
return nil, err
}

iss, err := store.Close(ca.ID, ca.Reason)
if err != nil {
return nil, err
}
var iss *issue.Issue
var unblocked []*issue.Issue

unblocked, err := store.NewlyUnblocked(iss.ID)
if err != nil {
return nil, err
}
for attempt := range closeMaxRetries {
if attempt > 0 {
store.ClearCache()
if err := store.Refresh(); err != nil {
return nil, fmt.Errorf("refresh after conflict: %w", err)
}
}

iss, err = store.Close(ca.ID, ca.Reason)
if err != nil {
return nil, err
}

intent := fmt.Sprintf("close %s", iss.ID)
if ca.Reason != "" {
intent += fmt.Sprintf(" reason=%q", ca.Reason)
unblocked, err = store.NewlyUnblocked(iss.ID)
if err != nil {
return nil, err
}

intent := fmt.Sprintf("close %s", iss.ID)
if ca.Reason != "" {
intent += fmt.Sprintf(" reason=%q", ca.Reason)
}
for _, u := range unblocked {
intent += fmt.Sprintf("\nunblocked %s", u.ID)
}

err = store.Commit(intent)
if err == nil {
break
}
if !errors.Is(err, treefs.ErrRefMoved) {
return nil, fmt.Errorf("commit failed: %w", err)
}
}
if err := store.Commit(intent); err != nil {
return nil, fmt.Errorf("commit failed: %w", err)
if err != nil {
return nil, fmt.Errorf("commit failed after %d attempts: %w", closeMaxRetries, err)
}

if ca.JSON {
Expand Down
27 changes: 26 additions & 1 deletion cmd/bw/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,31 @@ var commands = []Command{
NeedsStore: true,
Run: cmdPrime,
},
{
Name: "recap",
Summary: "Show recent activity across issues",
Description: "Summarize beadwork activity in this repo (or --all for every registered repo).\nBy default, shows activity since the last recap — first-time recaps show the last 24 hours.\nOutput is condensed (one line per issue). Use --verbose for per-event detail.\n\nWindow tokens:\n today, yesterday, week\n durations: 15m, 1h, 3h30m, 24h, 2d, 7d, 2w\nUse --since for an explicit start (RFC3339 or YYYY-MM-DD).\n--dry-run shows activity without advancing the cursor.",
Flags: []Flag{
{Long: "--since", Value: "DATE", Help: "Start time (RFC3339 or YYYY-MM-DD)"},
{Long: "--dry-run", Help: "Show activity without advancing the cursor"},
{Long: "--all", Help: "Recap every registered repository"},
{Long: "--verbose", Short: "-v", Help: "Per-event detail tree (default is condensed)"},
{Long: "--json", Help: "Output as JSON"},
{Long: "--ascii", Help: "Use plain ASCII tree characters (with --verbose)"},
},
Examples: []Example{
{Cmd: "bw recap", Help: "Activity since last recap (or 24h if first-time)"},
{Cmd: "bw recap 15m", Help: "Last 15 minutes"},
{Cmd: "bw recap 1h"},
{Cmd: "bw recap today"},
{Cmd: "bw recap 7d --verbose", Help: "Full per-event tree"},
{Cmd: "bw recap week --json"},
{Cmd: "bw recap --since 2026-01-01"},
{Cmd: "bw recap --all", Help: "Across all registered repos"},
{Cmd: "bw recap --dry-run", Help: "Preview without advancing cursor"},
},
Run: cmdRecap,
},
{
Name: "registry",
Summary: "Manage the repository registry",
Expand Down Expand Up @@ -519,7 +544,7 @@ var commandGroups = []struct {
{"Finding Work", []string{"ready", "blocked"}},
{"Dependencies", []string{"dep"}},
{"Sync & Data", []string{"sync", "export", "import"}},
{"Cross-Repo & Activity", []string{"registry"}},
{"Cross-Repo & Activity", []string{"recap", "registry"}},
{"Setup & Config", []string{"init", "config", "upgrade", "onboard", "prime"}},
}

Expand Down
36 changes: 29 additions & 7 deletions cmd/bw/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"regexp"
"strings"

"github.com/jallum/beadwork/internal/config"
Expand Down Expand Up @@ -38,11 +39,22 @@ func parseHistoryArgs(raw []string) (HistoryArgs, error) {
return ha, nil
}

// unblockedRe matches "unblocked <id>" lines in commit messages (line >= 2).
var unblockedRe = regexp.MustCompile(`^unblocked\s+(\S+)$`)

type commitEntry struct {
Hash string `json:"hash"`
Timestamp string `json:"timestamp"`
Author string `json:"author"`
Intent string `json:"intent"`
Hash string `json:"hash"`
Timestamp string `json:"timestamp"`
Author string `json:"author"`
Intent string `json:"intent"`
Unblocked []string `json:"unblocked,omitempty"`
}

func firstLine(s string) string {
if i := strings.IndexByte(s, '\n'); i >= 0 {
return s[:i]
}
return s
}

func cmdHistory(store *issue.Store, args []string, w Writer, _ *config.Config) (*config.Config, error) {
Expand All @@ -68,12 +80,19 @@ func cmdHistory(store *issue.Store, args []string, w Writer, _ *config.Config) (
for i := len(commits) - 1; i >= 0; i-- {
c := commits[i]
if strings.Contains(c.Message, iss.ID) {
matched = append(matched, commitEntry{
entry := commitEntry{
Hash: c.Hash,
Timestamp: c.Time.UTC().Format("2006-01-02 15:04"),
Author: c.Author,
Intent: c.Message,
})
Intent: firstLine(c.Message),
}
// Parse unblocked lines from the commit message (line >= 2 only).
for _, line := range strings.Split(c.Message, "\n")[1:] {
if m := unblockedRe.FindStringSubmatch(strings.TrimSpace(line)); m != nil {
entry.Unblocked = append(entry.Unblocked, m[1])
}
}
matched = append(matched, entry)
}
}

Expand All @@ -94,6 +113,9 @@ func cmdHistory(store *issue.Store, args []string, w Writer, _ *config.Config) (

for _, e := range matched {
fmt.Fprintf(w, "%s %s %s\n", e.Timestamp, e.Author, e.Intent)
for _, uid := range e.Unblocked {
fmt.Fprintf(w, " → unblocked %s\n", uid)
}
}
return nil, nil
}
32 changes: 32 additions & 0 deletions cmd/bw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"time"

"github.com/jallum/beadwork/internal/config"
"github.com/jallum/beadwork/internal/issue"
Expand All @@ -14,13 +15,25 @@ import (

const version = "0.12.3"

// globalNoColor is set by commands (e.g. recap --no-color) to force
// non-colored output even when stdout is a TTY. Consulted at render setup.
var globalNoColor bool

// globalDryRun mirrors the --dry-run flag. For commands with NeedsStore
// it drives store.DryRun; for commands without a store (e.g. recap) it
// suppresses side-effects like advancing the registry cursor.
var globalDryRun bool

func resolveRenderMode(args []string) string {
if mode, ok := flagValue(args, "--x-render-as"); ok && mode != "" {
return mode
}
if hasFlag(args, "--x-raw") {
return "raw"
}
if hasFlag(args, "--no-color") {
return "markdown"
}
if term.IsTerminal(int(os.Stdout.Fd())) && os.Getenv("NO_COLOR") == "" {
return "tty"
}
Expand Down Expand Up @@ -55,10 +68,15 @@ func main() {

args = removeFlag(args, "--x-raw")
args, _ = removeFlagValue(args, "--x-render-as")
if hasFlag(args, "--no-color") {
globalNoColor = true
args = removeFlag(args, "--no-color")
}

dryRun := hasFlag(args, "--dry-run")
if dryRun {
args = removeFlag(args, "--dry-run")
globalDryRun = true
}

switch cmd {
Expand Down Expand Up @@ -118,6 +136,20 @@ func main() {
}
}

// bwNow returns the current time respecting BW_CLOCK.
// The returned Time preserves its original location (local time when no
// BW_CLOCK is set; whatever offset BW_CLOCK carries otherwise) so that
// day-boundary math ("today", "yesterday") uses the user's local zone.
// Callers that need UTC for storage should .UTC() themselves.
func bwNow() time.Time {
if v := os.Getenv("BW_CLOCK"); v != "" {
if t, err := time.Parse(time.RFC3339, v); err == nil {
return t
}
}
return time.Now()
}

// extractDirFlag removes all -C <dir> pairs from args and sets repoDir.
func extractDirFlag(args []string) []string {
out := make([]string, 0, len(args))
Expand Down
Loading
Loading