From 39b49e83ce0a299edd2a8abadb4349db4e2aa9ba Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Wed, 11 Mar 2026 09:45:20 +0100 Subject: [PATCH] beads: reduce context overflow, simplify filters, atomic blockers - list_beads.sh/list_ready_beads.sh: filter to id,priority,title,status - Add read_bead_comments.sh tool and Comments view in Assist - Simplify Assist filters to All/Ready (was 6 status filters) - Server-side filtering: list excludes closed at DB, ready uses GetReadyWork - Default limit 100, DB-level filtering - Atomic blockers= support in new command --- cmd/Assist/main.go | 167 +++++++++++++------------------------------ internal/p9/beads.go | 95 ++++++++++++++++-------- 2 files changed, 114 insertions(+), 148 deletions(-) diff --git a/cmd/Assist/main.go b/cmd/Assist/main.go index 99c0703..171dbac 100644 --- a/cmd/Assist/main.go +++ b/cmd/Assist/main.go @@ -1870,10 +1870,9 @@ func parseBeadEdits(content string, beads []Bead) []beadEdit { return edits } -// applyBeadEdits applies a batch of bead edits, then refreshes the tasks window. -func applyBeadEdits(w *acme.Win, edits []beadEdit, beads *[]Bead, statusFilter string, mount string) { +// applyBeadEdits applies a batch of bead edits. +func applyBeadEdits(w *acme.Win, edits []beadEdit, beads *[]Bead, filter string, mount string) { if len(edits) == 0 { - refreshTasksWindowWithBeads(w, *beads, mount) return } @@ -1909,9 +1908,8 @@ func applyBeadEdits(w *acme.Win, edits []beadEdit, beads *[]Bead, statusFilter s for _, e := range errs { fmt.Fprintf(os.Stderr, "beadEdit error: %s\n", e) } - refreshed, _ := listBeadsWithFilter(statusFilter, mount) + refreshed, _ := listBeadsWithFilter(filter, mount) *beads = refreshed - refreshTasksWindowWithBeads(w, *beads, mount) } func openTasksWindow() error { @@ -1931,10 +1929,10 @@ func handleTasksWindow(w *acme.Win) { defer w.CloseFiles() var beads []Bead - statusFilter := "" - windowMount := "" // Start with no mount - beads, _ = listBeadsWithFilter(statusFilter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount) + filter := "ready" + windowMount := "" + beads, _ = listBeadsWithFilter(filter, windowMount) + refreshTasksWindowWithBeads(w, beads, windowMount, filter) for e := range w.EventChan() { switch e.C2 { @@ -1943,8 +1941,8 @@ func handleTasksWindow(w *acme.Win) { arg := strings.TrimSpace(string(e.Arg)) switch cmd { case "Get": - beads, _ = listBeadsWithFilter(statusFilter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount) + beads, _ = listBeadsWithFilter(filter, windowMount) + refreshTasksWindowWithBeads(w, beads, windowMount, filter) case "Put": body, err := w.ReadAll("body") if err != nil { @@ -1952,7 +1950,7 @@ func handleTasksWindow(w *acme.Win) { continue } edits := parseBeadEdits(string(body), beads) - applyBeadEdits(w, edits, &beads, statusFilter, windowMount) + applyBeadEdits(w, edits, &beads, filter, windowMount) case "New": if err := openNewBeadWindow("", windowMount); err != nil { fmt.Fprintf(os.Stderr, "Error opening new bead window: %v\n", err) @@ -1963,8 +1961,8 @@ func handleTasksWindow(w *acme.Win) { } else if err := deleteBead(arg, windowMount); err != nil { fmt.Fprintf(os.Stderr, "Error deleting bead: %v\n", err) } else { - beads, _ = listBeadsWithFilter(statusFilter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount) + beads, _ = listBeadsWithFilter(filter, windowMount) + refreshTasksWindowWithBeads(w, beads, windowMount, filter) } case "Init": prefix := arg @@ -1974,8 +1972,8 @@ func handleTasksWindow(w *acme.Win) { if err := initBeads(prefix, windowMount); err != nil { fmt.Fprintf(os.Stderr, "Error initializing beads: %v\n", err) } else { - beads, _ = listBeadsWithFilter(statusFilter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount) + beads, _ = listBeadsWithFilter(filter, windowMount) + refreshTasksWindowWithBeads(w, beads, windowMount, filter) } case "Mount": cwd := strings.TrimSpace(arg) @@ -1990,10 +1988,9 @@ func handleTasksWindow(w *acme.Win) { if err := mountProject(cwd, name); err != nil { fmt.Fprintf(os.Stderr, "Error mounting: %v\n", err) } else { - // "" = name windowMount = name - beads, _ = listBeadsWithFilter(statusFilter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount) + beads, _ = listBeadsWithFilter(filter, windowMount) + refreshTasksWindowWithBeads(w, beads, windowMount, filter) } } case "Umount": @@ -2003,7 +2000,6 @@ func handleTasksWindow(w *acme.Win) { } else if err := umountProject(name); err != nil { fmt.Fprintf(os.Stderr, "Error unmounting: %v\n", err) } else { - // If we unmounted the current window mount, pick first available mount if windowMount == name { data, _ := readFile("beads/mtab") lines := strings.Split(strings.TrimSpace(string(data)), "\n") @@ -2013,27 +2009,24 @@ func handleTasksWindow(w *acme.Win) { parts := strings.Split(line, "\t") if len(parts) > 0 { windowMount = parts[0] - // "" = parts[0] found = true break } } } if !found { - // No mounts left, clear the window windowMount = "" beads = nil - refreshTasksWindowWithBeads(w, beads, windowMount) + refreshTasksWindowWithBeads(w, beads, windowMount, filter) continue } } - beads, _ = listBeadsWithFilter(statusFilter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount) + beads, _ = listBeadsWithFilter(filter, windowMount) + refreshTasksWindowWithBeads(w, beads, windowMount, filter) } case "Select": name := strings.TrimSpace(arg) if name == "" { - // Display mtab data, err := readFile("beads/mtab") if err != nil { fmt.Fprintf(os.Stderr, "Error reading mtab: %v\n", err) @@ -2042,7 +2035,6 @@ func handleTasksWindow(w *acme.Win) { fmt.Fprintf(os.Stderr, "Usage: select mount name, then Select\n") } } else { - // Verify mount exists data, err := readFile("beads/mtab") if err != nil { fmt.Fprintf(os.Stderr, "Error reading mtab: %v\n", err) @@ -2057,37 +2049,20 @@ func handleTasksWindow(w *acme.Win) { if !found { fmt.Fprintf(os.Stderr, "Mount %s not found\n", name) } else { - // "" = name windowMount = name - beads, _ = listBeadsWithFilter(statusFilter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount) + beads, _ = listBeadsWithFilter(filter, windowMount) + refreshTasksWindowWithBeads(w, beads, windowMount, filter) } } } - case "Open": - statusFilter = "open" - beads, _ = listBeadsWithFilter(statusFilter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount) - case "InProgress": - statusFilter = "in_progress" - beads, _ = listBeadsWithFilter(statusFilter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount) - case "Closed": - statusFilter = "closed" - beads, _ = listBeadsWithFilter(statusFilter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount) - case "Failed": - statusFilter = "failed" - beads, _ = listBeadsWithFilter(statusFilter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount) - case "PendingReview": - statusFilter = "pending_review" - beads, _ = listBeadsWithFilter(statusFilter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount) - case "PendingApproval": - statusFilter = "pending_approval" - beads, _ = listBeadsWithFilter(statusFilter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount) + case "All": + filter = "all" + beads, _ = listBeadsWithFilter(filter, windowMount) + refreshTasksWindowWithBeads(w, beads, windowMount, filter) + case "Ready": + filter = "ready" + beads, _ = listBeadsWithFilter(filter, windowMount) + refreshTasksWindowWithBeads(w, beads, windowMount, filter) default: w.WriteEvent(e) } @@ -2106,7 +2081,7 @@ func handleTasksWindow(w *acme.Win) { } } -func refreshTasksWindowWithBeads(w *acme.Win, beads []Bead, mount string) { +func refreshTasksWindowWithBeads(w *acme.Win, beads []Bead, mount string, filter string) { if mount == "" { w.Name("/AnviLLM/Tasks") } else { @@ -2121,7 +2096,7 @@ func refreshTasksWindowWithBeads(w *acme.Win, beads []Bead, mount string) { buf.WriteString(" 1. Select a directory path, then click Mount\n") buf.WriteString(" 2. Or click Select to see available mounts\n\n") } else { - buf.WriteString("[Open] [InProgress] [Closed] [Failed] [PendingReview] [PendingApproval]\n\n") + buf.WriteString("[All] [Ready]\n\n") buf.WriteString(fmt.Sprintf("%-4s %-12s %-12s %-4s %-8s %s\n", "#", "ID", "Status", "Blk", "Assignee", "Title")) buf.WriteString(fmt.Sprintf("%-4s %-12s %-12s %-4s %-8s %s\n", "----", "------------", "------------", "----", "--------", strings.Repeat("-", 50))) @@ -2146,12 +2121,23 @@ func refreshTasksWindowWithBeads(w *acme.Win, beads []Bead, mount string) { w.Ctl("show") } -func listBeadsWithFilter(statusFilter string, mount string) ([]Bead, error) { +func listBeadsWithFilter(filter string, mount string) ([]Bead, error) { if !isConnected() { return nil, fmt.Errorf("not connected to anvilsrv") } - data, err := readFile(filepath.Join("beads", mount, "list")) + if mount == "" { + return nil, nil + } + + var endpoint string + if filter == "ready" { + endpoint = filepath.Join("beads", mount, "ready") + } else { + endpoint = filepath.Join("beads", mount, "list") + } + + data, err := readFile(endpoint) if err != nil { return nil, err } @@ -2160,38 +2146,7 @@ func listBeadsWithFilter(statusFilter string, mount string) ([]Bead, error) { if err := json.Unmarshal(data, &beads); err != nil { return nil, err } - - // Apply status filter - if statusFilter == "" { - // Default: filter out closed beads - var openBeads []Bead - for _, b := range beads { - if b.Status != "closed" { - openBeads = append(openBeads, b) - } - } - return openBeads, nil - } - - // Special case: "failed" means closed beads whose close_reason is not "completed" - if statusFilter == "failed" { - var filtered []Bead - for _, b := range beads { - if b.Status == "closed" && b.CloseReason != "completed" { - filtered = append(filtered, b) - } - } - return filtered, nil - } - - // Filter by specific status (covers open, in_progress, closed, pending_review, pending_approval) - var filtered []Bead - for _, b := range beads { - if b.Status == statusFilter { - filtered = append(filtered, b) - } - } - return filtered, nil + return beads, nil } func openNewBeadWindow(parentID string, mount string) error { @@ -2296,42 +2251,16 @@ func createBeadFromMarkdown(content, parentID string, mount string) error { return fmt.Errorf("title is required") } - // Build command: new 'title' 'description' [parent-id] + // Build command: new 'title' 'description' [parent-id] [blockers=...] cmd := fmt.Sprintf("new '%s' '%s'", title, description) if parentID != "" { cmd = fmt.Sprintf("%s %s", cmd, parentID) } - - if err := writeFile(filepath.Join("beads", mount, "ctl"), []byte(cmd)); err != nil { - return err - } - - // Add blockers if specified if blockers != "" { - // Need to find the newly created bead ID - beads, err := listBeadsWithFilter("", mount) - if err != nil { - return nil // bead created, deps failed - } - var newID string - for _, b := range beads { - if b.Title == title { - newID = b.ID - break - } - } - if newID != "" { - for _, blockerID := range strings.Split(blockers, ",") { - blockerID = strings.TrimSpace(blockerID) - if blockerID != "" { - // blockerID blocks newID - addBlocksDep(blockerID, newID, mount) - } - } - } + cmd = fmt.Sprintf("%s blockers=%s", cmd, blockers) } - return nil + return writeFile(filepath.Join("beads", mount, "ctl"), []byte(cmd)) } func openViewBeadWindow(beadID string, mount string) error { diff --git a/internal/p9/beads.go b/internal/p9/beads.go index b7707ac..7696456 100644 --- a/internal/p9/beads.go +++ b/internal/p9/beads.go @@ -231,9 +231,21 @@ func (b *BeadsFS) readFromMount(m *MountedProject, endpoint string) ([]byte, err parts := strings.Split(endpoint, "/") switch { case len(parts) == 1 && parts[0] == "list": - return b.readListFromStore(m.store) + return b.readListFromStore(m.store, 100) + case len(parts) == 2 && parts[0] == "list": + limit, _ := strconv.Atoi(parts[1]) + if limit == 0 { + limit = 100 + } + return b.readListFromStore(m.store, limit) case len(parts) == 1 && parts[0] == "ready": - return b.readReadyFromStore(m.store) + return b.readReadyFromStore(m.store, 100) + case len(parts) == 2 && parts[0] == "ready": + limit, _ := strconv.Atoi(parts[1]) + if limit == 0 { + limit = 100 + } + return b.readReadyFromStore(m.store, limit) case len(parts) == 1 && parts[0] == "pending": return b.readPendingFromStore(m.store) case len(parts) == 1 && parts[0] == "blocked": @@ -338,8 +350,12 @@ func (b *BeadsFS) readMtab() ([]byte, error) { return []byte(strings.Join(lines, "\n")), nil } -func (b *BeadsFS) readListFromStore(store bd.Storage) ([]byte, error) { - issues, err := store.SearchIssues(b.ctx, "", bd.IssueFilter{}) +func (b *BeadsFS) readListFromStore(store bd.Storage, limit int) ([]byte, error) { + filter := bd.IssueFilter{ + ExcludeStatus: []bd.Status{bd.StatusClosed}, + Limit: limit, + } + issues, err := store.SearchIssues(b.ctx, "", filter) if err != nil { return nil, err } @@ -358,7 +374,7 @@ func (b *BeadsFS) readListFromStore(store bd.Storage) ([]byte, error) { CapabilityLevel string `json:"capability_level,omitempty"` CommentCount int `json:"comment_count,omitempty"` } - result := make([]BeadWithBlockers, len(issues)) + items := make([]BeadWithBlockers, len(issues)) for i, issue := range issues { // Fetch labels if not already populated if issue.Labels == nil { @@ -367,26 +383,26 @@ func (b *BeadsFS) readListFromStore(store bd.Storage) ([]byte, error) { issue.Labels = labels } } - result[i].Issue = issue + items[i].Issue = issue blockers, err := b.getBlockersFromStore(store, issue.ID) if err != nil { logging.Logger().Warn("failed to get blockers", zap.String("issue", issue.ID), zap.Error(err)) } else if len(blockers) > 0 { - result[i].Blockers = blockers + items[i].Blockers = blockers } - result[i].CapabilityLevel = extractCapabilityLevel(issue.Labels) + items[i].CapabilityLevel = extractCapabilityLevel(issue.Labels) comments, err := store.GetIssueComments(b.ctx, issue.ID) if err == nil && len(comments) > 0 { - result[i].CommentCount = len(comments) + items[i].CommentCount = len(comments) } } - return json.MarshalIndent(result, "", " ") + return json.MarshalIndent(items, "", " ") } -func (b *BeadsFS) readReadyFromStore(store bd.Storage) ([]byte, error) { - filter := bd.WorkFilter{} +func (b *BeadsFS) readReadyFromStore(store bd.Storage, limit int) ([]byte, error) { + filter := bd.WorkFilter{Limit: limit} issues, err := store.GetReadyWork(b.ctx, filter) if err != nil { return nil, err @@ -397,7 +413,6 @@ func (b *BeadsFS) readReadyFromStore(store bd.Storage) ([]byte, error) { // Filter out blocked issues - ready means unblocked ready := []*bd.Issue{} for _, issue := range issues { - // Fetch labels if not already populated if issue.Labels == nil { labels, err := store.GetLabels(b.ctx, issue.ID) if err == nil { @@ -409,6 +424,7 @@ func (b *BeadsFS) readReadyFromStore(store bd.Storage) ([]byte, error) { ready = append(ready, issue) } } + return json.MarshalIndent(ready, "", " ") } @@ -695,18 +711,25 @@ func (b *BeadsFS) executeCtlOnStore(store bd.Storage, cmd string) error { case "new", "create": if len(args) < 1 { - return fmt.Errorf("usage: new 'title' ['description'] [parent-id] [--no-lint] [capability=low|standard|high]") + return fmt.Errorf("usage: new 'title' ['description'] [parent-id] [--no-lint] [capability=low|standard|high] [blockers=id1,id2,...]") } - // Strip --no-lint flag and capability= option from args before positional parsing. + // Strip flags and options from args before positional parsing. noLint := false capLevel := "" + var blockerIDs []string filtered := args[:0] for _, a := range args { if a == "--no-lint" { noLint = true } else if level, ok := strings.CutPrefix(a, "capability="); ok { capLevel = level + } else if ids, ok := strings.CutPrefix(a, "blockers="); ok { + for _, id := range strings.Split(ids, ",") { + if id = strings.TrimSpace(id); id != "" { + blockerIDs = append(blockerIDs, id) + } + } } else { filtered = append(filtered, a) } @@ -724,8 +747,6 @@ func (b *BeadsFS) executeCtlOnStore(store bd.Storage, cmd string) error { } // Emit lint warnings unless --no-lint or issue_type=idea. - // The new command always creates TypeTask; idea exemption applies - // when the title starts with "IDEA:" as a convention. isIdea := strings.HasPrefix(strings.ToUpper(title), "IDEA:") if !noLint && !isIdea && description != "" { for _, w := range lintDescription(description) { @@ -733,29 +754,45 @@ func (b *BeadsFS) executeCtlOnStore(store bd.Storage, cmd string) error { } } + var newID string if parentID != "" { childID, err := b.createSubtaskOnStore(store, parentID, title, description, actor) if err != nil { return err } - // Apply capability label if provided. + newID = childID if capLevel != "" { _ = store.AddLabel(b.ctx, childID, capabilityPrefix+capLevel, actor) } - return nil + } else { + issue := &bd.Issue{ + Title: title, + Description: description, + Status: bd.StatusOpen, + IssueType: bd.TypeTask, + Priority: 2, + } + if capLevel != "" { + issue.Labels = []string{capabilityPrefix + capLevel} + } + if err := store.CreateIssue(b.ctx, issue, actor); err != nil { + return err + } + newID = issue.ID } - issue := &bd.Issue{ - Title: title, - Description: description, - Status: bd.StatusOpen, - IssueType: bd.TypeTask, - Priority: 2, + // Add blockers + for _, blockerID := range blockerIDs { + dep := &bd.Dependency{ + IssueID: newID, + DependsOnID: blockerID, + Type: bd.DepBlocks, + } + if err := store.AddDependency(b.ctx, dep, actor); err != nil { + logging.Logger().Warn("failed to add blocker", zap.String("bead", newID), zap.String("blocker", blockerID), zap.Error(err)) + } } - if capLevel != "" { - issue.Labels = []string{capabilityPrefix + capLevel} - } - return store.CreateIssue(b.ctx, issue, actor) + return nil case "claim": if len(args) < 1 {