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
This commit is contained in:
Levi Neely 2026-03-11 09:45:20 +01:00
parent 3896b3db43
commit 39b49e83ce
2 changed files with 114 additions and 148 deletions

View File

@ -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 {

View File

@ -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 {