From 0644fc833650c18a0ac2b9060935be9535fc7afe Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Fri, 20 Mar 2026 15:50:55 +0100 Subject: [PATCH] Remove Tasks/beads code (moved to 9beads-acme) --- main.go | 1070 +------------------------------------------------------ 1 file changed, 1 insertion(+), 1069 deletions(-) diff --git a/main.go b/main.go index 297a39c..81c5439 100644 --- a/main.go +++ b/main.go @@ -11,7 +11,6 @@ import ( "path/filepath" "regexp" "sort" - "strconv" "strings" "sync" "time" @@ -52,7 +51,6 @@ type SessionInfo struct { var ( fs *client.Fsys - beadsFs *client.Fsys // Track window IDs for prompt windows (client-side state) promptWindows = make(map[string]int) // session ID -> window ID promptWindowsMu sync.RWMutex // protects promptWindows map @@ -98,11 +96,6 @@ func main() { } } - // Connect to 9beads (optional - beads features disabled if not running) - beadsFs, err = connectToBeads() - if err != nil { - logging.Logger().Info("9beads not running, beads features disabled") - } if fs != nil { defer fs.Close() } @@ -114,7 +107,7 @@ func main() { defer w.CloseFiles() w.Name(windowName) - w.Write("tag", []byte("Get Put Attach Stop Restart Kill Alias Context Daemon Recover Inbox Archive Tasks ")) + w.Write("tag", []byte("Get Put Attach Stop Restart Kill Alias Context Daemon Recover Inbox Archive ")) refreshList(w) w.Ctl("clean") @@ -299,10 +292,6 @@ func main() { if err := openArchiveWindow("user", date); err != nil { fmt.Fprintf(os.Stderr, "Error opening archive window: %v\n", err) } - case "Tasks": - if err := openTasksWindow(); err != nil { - fmt.Fprintf(os.Stderr, "Error opening tasks window: %v\n", err) - } default: w.WriteEvent(e) @@ -366,22 +355,10 @@ func connectToServer() (*client.Fsys, error) { return client.MountService("anvillm") } -func connectToBeads() (*client.Fsys, error) { - ns := client.Namespace() - if ns == "" { - return nil, fmt.Errorf("no namespace") - } - return client.MountService("beads") -} - func isConnected() bool { return fs != nil } -func isBeadsConnected() bool { - return beadsFs != nil -} - func createSession(backend, cwd string) error { if !isConnected() { return fmt.Errorf("not connected to anvilsrv (use Daemon command to start server)") @@ -431,58 +408,11 @@ func controlSession(id, cmd string) error { return err } -func isBeadID(text string) bool { - // Match bead ID pattern: prefix-xxx or prefix-xxx.yyy (hierarchical) - // Common prefixes: bd, task, bug, etc. - matched, _ := regexp.MatchString(`^[a-zA-Z]+-[a-z0-9]+(\.[0-9]+)*$`, text) - return matched -} - -func getMountForSession(sessionID string) string { - sess, err := getSession(sessionID) - if err != nil || sess.Cwd == "" { - return "" - } - data, err := readBeadsFile("mtab") - if err != nil { - return "" - } - for _, line := range strings.Split(string(data), "\n") { - parts := strings.Split(line, "\t") - if len(parts) == 2 { - mountName, mountCwd := parts[0], parts[1] - if sess.Cwd == mountCwd || strings.HasPrefix(sess.Cwd, mountCwd+"/") { - return mountName - } - } - } - return "" -} - func sendPrompt(id, prompt string) error { - mount := "" - trimmedPrompt := strings.TrimSpace(prompt) - if isBeadID(trimmedPrompt) { - mount = getMountForSession(id) - } - return sendPromptWithMount(id, prompt, mount) -} - -func sendPromptWithMount(id, prompt, mount string) error { if !isConnected() { return fmt.Errorf("not connected to anvilsrv") } - // Check if prompt is a bead ID and construct execution prompt - trimmedPrompt := strings.TrimSpace(prompt) - if isBeadID(trimmedPrompt) { - if mount != "" { - prompt = fmt.Sprintf("Load the beads skill, and work on bead %s, mount=%s.", trimmedPrompt, mount) - } else { - prompt = fmt.Sprintf("Load the beads skill, and work on bead %s.", trimmedPrompt) - } - } - // Create message JSON msg := map[string]interface{}{ "to": id, @@ -640,44 +570,6 @@ func writeFile(path string, data []byte) error { return err } -func readBeadsFile(path string) ([]byte, error) { - if !isBeadsConnected() { - return nil, fmt.Errorf("not connected to 9beads") - } - fid, err := beadsFs.Open(path, plan9.OREAD) - if err != nil { - return nil, err - } - defer fid.Close() - - var buf []byte - tmp := make([]byte, 8192) - for { - n, err := fid.Read(tmp) - if n > 0 { - buf = append(buf, tmp[:n]...) - } - if err != nil { - break - } - } - return buf, nil -} - -func writeBeadsFile(path string, data []byte) error { - if !isBeadsConnected() { - return fmt.Errorf("not connected to 9beads") - } - fid, err := beadsFs.Open(path, plan9.OWRITE) - if err != nil { - return err - } - defer fid.Close() - - _, err = fid.Write(data) - return err -} - func refreshList(w *acme.Win) { var buf strings.Builder buf.WriteString("Backends: [Kiro] [Claude] [Ollama]\n\n") @@ -2073,966 +1965,6 @@ func sendReply(to, msgType, subject, body string) error { return nil } - -type Bead struct { - ID string `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - Status string `json:"status"` - Assignee string `json:"assignee"` - Priority int `json:"priority"` - Blockers []string `json:"blockers,omitempty"` - Labels []string `json:"labels,omitempty"` - CloseReason string `json:"close_reason,omitempty"` -} - -func (b *Bead) HasLabel(label string) bool { - for _, l := range b.Labels { - if l == label { - return true - } - } - return false -} - -// beadEdit represents a single inline action parsed from the tasks window body. -type beadEdit struct { - action string // "delete", "claim", "unclaim", "complete", "fail", "create" - beadID string // bead ID (for existing bead actions) - title string // title (for create action) -} - -// parseBeadEdits parses inline action annotations from the tasks window body. -// Recognized prefixes (by 1-based index shown in the list): -// -// - [idx] Delete bead at that index -// c [idx] Claim bead at that index (assign to user) -// u [idx] Unclaim bead: clear assignee, reset status to open -// o [idx] Open (promote deferred bead to ready) -// d [idx] Defer bead -// x [idx] Close/complete bead at that index -// f [idx] Fail bead at that index with reason 'user-closed' -// + [title] Create new bead with that title -func parseBeadEdits(content string, beads []Bead) []beadEdit { - var edits []beadEdit - for _, line := range strings.Split(content, "\n") { - line = strings.TrimSpace(line) - if len(line) == 0 { - continue - } - switch { - case strings.HasPrefix(line, "- "): - rest := strings.TrimSpace(line[2:]) - if idx := parseIndex(rest); idx > 0 && idx <= len(beads) { - edits = append(edits, beadEdit{action: "delete", beadID: beads[idx-1].ID}) - } - case strings.HasPrefix(line, "c "): - rest := strings.TrimSpace(line[2:]) - if idx := parseIndex(rest); idx > 0 && idx <= len(beads) { - edits = append(edits, beadEdit{action: "claim", beadID: beads[idx-1].ID}) - } - case strings.HasPrefix(line, "u "): - rest := strings.TrimSpace(line[2:]) - if idx := parseIndex(rest); idx > 0 && idx <= len(beads) { - edits = append(edits, beadEdit{action: "unclaim", beadID: beads[idx-1].ID}) - } - case strings.HasPrefix(line, "o "): - rest := strings.TrimSpace(line[2:]) - if idx := parseIndex(rest); idx > 0 && idx <= len(beads) { - edits = append(edits, beadEdit{action: "open", beadID: beads[idx-1].ID}) - } - case strings.HasPrefix(line, "d "): - rest := strings.TrimSpace(line[2:]) - if idx := parseIndex(rest); idx > 0 && idx <= len(beads) { - edits = append(edits, beadEdit{action: "defer", beadID: beads[idx-1].ID}) - } - case strings.HasPrefix(line, "x "): - rest := strings.TrimSpace(line[2:]) - if idx := parseIndex(rest); idx > 0 && idx <= len(beads) { - edits = append(edits, beadEdit{action: "complete", beadID: beads[idx-1].ID}) - } - case strings.HasPrefix(line, "f "): - rest := strings.TrimSpace(line[2:]) - if idx := parseIndex(rest); idx > 0 && idx <= len(beads) { - edits = append(edits, beadEdit{action: "fail", beadID: beads[idx-1].ID}) - } - case strings.HasPrefix(line, "+ "): - title := strings.TrimSpace(line[2:]) - if title != "" { - edits = append(edits, beadEdit{action: "create", title: title}) - } - } - } - return edits -} - -// applyBeadEdits applies a batch of bead edits. -func applyBeadEdits(w *acme.Win, edits []beadEdit, beads *[]Bead, filter string, mount string) { - if len(edits) == 0 { - return - } - - var errs []string - for _, edit := range edits { - switch edit.action { - case "delete": - if err := deleteBead(edit.beadID, mount); err != nil { - errs = append(errs, fmt.Sprintf("delete %s: %v", edit.beadID, err)) - } - case "claim": - if err := claimBeadAsUser(edit.beadID, mount); err != nil { - errs = append(errs, fmt.Sprintf("claim %s: %v", edit.beadID, err)) - } - case "unclaim": - if err := unclaimBead(edit.beadID, mount); err != nil { - errs = append(errs, fmt.Sprintf("unclaim %s: %v", edit.beadID, err)) - } - case "open": - if err := openBead(edit.beadID, mount); err != nil { - errs = append(errs, fmt.Sprintf("open %s: %v", edit.beadID, err)) - } - case "defer": - if err := deferBead(edit.beadID, mount); err != nil { - errs = append(errs, fmt.Sprintf("defer %s: %v", edit.beadID, err)) - } - case "complete": - if err := completeBead(edit.beadID, mount); err != nil { - errs = append(errs, fmt.Sprintf("complete %s: %v", edit.beadID, err)) - } - case "fail": - if err := failBead(edit.beadID, "user-closed", mount); err != nil { - errs = append(errs, fmt.Sprintf("fail %s: %v", edit.beadID, err)) - } - case "create": - if err := openNewBeadWindowWithTitle(edit.title, mount); err != nil { - errs = append(errs, fmt.Sprintf("create %q: %v", edit.title, err)) - } - } - } - for _, e := range errs { - fmt.Fprintf(os.Stderr, "beadEdit error: %s\n", e) - } - refreshed, _ := listBeadsWithFilter(filter, mount) - *beads = refreshed - refreshTasksWindowWithBeads(w, *beads, mount, filter) -} - -func openTasksWindow() error { - w, err := acme.New() - if err != nil { - return err - } - - w.Name("/AnviLLM/Tasks") - w.Write("tag", []byte("Get Put New Remove Init Mount Umount Select ")) - - go handleTasksWindow(w) - return nil -} - -func handleTasksWindow(w *acme.Win) { - defer w.CloseFiles() - - var beads []Bead - filter := "all" - windowMount := "" - beads, _ = listBeadsWithFilter(filter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount, filter) - - for e := range w.EventChan() { - switch e.C2 { - case 'x', 'X': - cmd := string(e.Text) - arg := strings.TrimSpace(string(e.Arg)) - switch cmd { - case "Get": - beads, _ = listBeadsWithFilter(filter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount, filter) - case "Put": - body, err := w.ReadAll("body") - if err != nil { - fmt.Fprintf(os.Stderr, "Error reading tasks body: %v\n", err) - continue - } - edits := parseBeadEdits(string(body), beads) - 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) - } - case "Remove": - if arg == "" { - fmt.Fprintf(os.Stderr, "Usage: select bead ID, then Remove\n") - } else if err := deleteBead(arg, windowMount); err != nil { - fmt.Fprintf(os.Stderr, "Error deleting bead: %v\n", err) - } else { - beads, _ = listBeadsWithFilter(filter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount, filter) - } - case "Init": - prefix := arg - if prefix == "" { - prefix = "bd" - } - if err := initBeads(prefix, windowMount); err != nil { - fmt.Fprintf(os.Stderr, "Error initializing beads: %v\n", err) - } else { - beads, _ = listBeadsWithFilter(filter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount, filter) - } - case "Mount": - cwd := strings.TrimSpace(arg) - if cwd == "" { - fmt.Fprintf(os.Stderr, "Usage: select path, then Mount\n") - } else { - if name, err := mountProject(cwd); err != nil { - fmt.Fprintf(os.Stderr, "Error mounting: %v\n", err) - } else { - windowMount = name - beads, _ = listBeadsWithFilter(filter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount, filter) - } - } - case "Umount": - name := strings.TrimSpace(arg) - if name == "" { - fmt.Fprintf(os.Stderr, "Usage: select mount name, then Umount\n") - } else if err := umountProject(name); err != nil { - fmt.Fprintf(os.Stderr, "Error unmounting: %v\n", err) - } else { - if windowMount == name { - data, _ := readBeadsFile("mtab") - lines := strings.Split(strings.TrimSpace(string(data)), "\n") - found := false - for _, line := range lines { - if line != "" { - parts := strings.Split(line, "\t") - if len(parts) > 0 { - windowMount = parts[0] - found = true - break - } - } - } - if !found { - windowMount = "" - beads = nil - refreshTasksWindowWithBeads(w, beads, windowMount, filter) - continue - } - } - beads, _ = listBeadsWithFilter(filter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount, filter) - } - case "Select": - name := strings.TrimSpace(arg) - if name == "" { - data, err := readBeadsFile("mtab") - if err != nil { - fmt.Fprintf(os.Stderr, "Error reading mtab: %v\n", err) - } else { - fmt.Fprintf(os.Stderr, "Current mounts:\n%s\n", string(data)) - fmt.Fprintf(os.Stderr, "Usage: select mount name, then Select\n") - } - } else { - data, err := readBeadsFile("mtab") - if err != nil { - fmt.Fprintf(os.Stderr, "Error reading mtab: %v\n", err) - } else { - found := false - for _, line := range strings.Split(string(data), "\n") { - if strings.HasPrefix(line, name+"\t") { - found = true - break - } - } - if !found { - fmt.Fprintf(os.Stderr, "Mount %s not found\n", name) - } else { - windowMount = name - beads, _ = listBeadsWithFilter(filter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount, filter) - } - } - } - case "Deferred": - filter = "deferred" - beads, _ = listBeadsWithFilter(filter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount, filter) - case "Ready": - filter = "ready" - beads, _ = listBeadsWithFilter(filter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount, filter) - case "All": - filter = "all" - beads, _ = listBeadsWithFilter(filter, windowMount) - refreshTasksWindowWithBeads(w, beads, windowMount, filter) - default: - w.WriteEvent(e) - } - case 'l', 'L': - text := strings.TrimSpace(string(e.Text)) - if idx := parseIndex(text); idx > 0 && idx <= len(beads) { - if err := openViewBeadWindow(beads[idx-1].ID, windowMount); err != nil { - fmt.Fprintf(os.Stderr, "Error opening bead window: %v\n", err) - } - } else { - w.WriteEvent(e) - } - default: - w.WriteEvent(e) - } - } -} - -func refreshTasksWindowWithBeads(w *acme.Win, beads []Bead, mount string, filter string) { - if mount == "" { - w.Name("/AnviLLM/Tasks") - } else { - w.Name(fmt.Sprintf("/AnviLLM/Tasks [%s]", mount)) - } - var buf strings.Builder - - if mount == "" { - buf.WriteString("No project mounted.\n\n") - buf.WriteString("Use Mount to mount a project directory, or Select to choose an existing mount.\n\n") - buf.WriteString("Example:\n") - 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("[Deferred] [Ready] [All]\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))) - - for i, b := range beads { - assignee := b.Assignee - if assignee == "" { - assignee = "-" - } - blk := "-" - if len(b.Blockers) > 0 { - blk = fmt.Sprintf("%d", len(b.Blockers)) - } - buf.WriteString(fmt.Sprintf("%-4d %-12s %-12s %-4s %-8s %s\n", i+1, b.ID, b.Status, blk, assignee, b.Title)) - } - } - - w.Addr(",") - w.Write("data", []byte(buf.String())) - w.Ctl("clean") - w.Addr("0") - w.Ctl("dot=addr") - w.Ctl("show") -} - -func listBeadsWithFilter(filter string, mount string) ([]Bead, error) { - if !isConnected() { - return nil, fmt.Errorf("not connected to anvilsrv") - } - - if mount == "" && filter != "deferred" && filter != "ready" { - return nil, nil - } - - var endpoint string - switch filter { - case "deferred": - if mount == "" { - endpoint = "deferred" - } else { - endpoint = filepath.Join(mount, "deferred") - } - case "ready": - if mount == "" { - endpoint = "ready" - } else { - endpoint = filepath.Join(mount, "ready") - } - default: - endpoint = filepath.Join(mount, "list") - } - - data, err := readBeadsFile(endpoint) - if err != nil { - return nil, err - } - - var beads []Bead - if err := json.Unmarshal(data, &beads); err != nil { - return nil, err - } - return beads, nil -} - -func openNewBeadWindow(parentID string, mount string) error { - w, err := acme.New() - if err != nil { - return err - } - - if parentID != "" { - w.Name(fmt.Sprintf("/AnviLLM/Tasks/%s/+new", parentID)) - } else { - w.Name("/AnviLLM/Tasks/+new") - } - w.Write("tag", []byte("Put ")) - - template := `--- -title: -blockers: ---- -` - w.Write("body", []byte(template)) - w.Ctl("clean") - - go handleNewBeadWindow(w, parentID, mount) - return nil -} - -// openNewBeadWindowWithTitle opens a new bead creation window pre-filled with the given title. -func openNewBeadWindowWithTitle(title string, mount string) error { - w, err := acme.New() - if err != nil { - return err - } - - w.Name("/AnviLLM/Tasks/+new") - w.Write("tag", []byte("Put ")) - - body := fmt.Sprintf("---\ntitle: %s\nblockers:\n---\n", title) - w.Write("body", []byte(body)) - w.Ctl("clean") - - go handleNewBeadWindow(w, "", mount) - return nil -} - -func handleNewBeadWindow(w *acme.Win, parentID string, mount string) { - defer w.CloseFiles() - - for e := range w.EventChan() { - if (e.C2 == 'x' || e.C2 == 'X') && string(e.Text) == "Put" { - body, err := w.ReadAll("body") - if err != nil { - fmt.Fprintf(os.Stderr, "Error reading body: %v\n", err) - continue - } - if err := createBeadFromMarkdown(string(body), parentID, mount); err != nil { - fmt.Fprintf(os.Stderr, "Error creating bead: %v\n", err) - continue - } - w.Ctl("delete") - return - } else { - w.WriteEvent(e) - } - } -} - -func createBeadFromMarkdown(content, parentID string, mount string) error { - if !isConnected() { - return fmt.Errorf("not connected to anvilsrv") - } - - // Parse frontmatter - content = strings.TrimSpace(content) - if !strings.HasPrefix(content, "---") { - return fmt.Errorf("missing frontmatter") - } - - parts := strings.SplitN(content[3:], "---", 2) - if len(parts) < 1 { - return fmt.Errorf("invalid frontmatter") - } - - frontmatter := strings.TrimSpace(parts[0]) - description := "" - if len(parts) > 1 { - description = strings.TrimSpace(parts[1]) - } - - // Parse YAML frontmatter (simple key: value parsing) - var title, blockers string - for _, line := range strings.Split(frontmatter, "\n") { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "title:") { - title = strings.TrimSpace(strings.TrimPrefix(line, "title:")) - } else if strings.HasPrefix(line, "blockers:") { - blockers = strings.TrimSpace(strings.TrimPrefix(line, "blockers:")) - } - } - - if title == "" { - return fmt.Errorf("title is required") - } - - // Build command: new 'title' 'description' [parent-id] [blockers=...] - cmd := fmt.Sprintf("new '%s' '%s'", shellEscape(title), shellEscape(description)) - if parentID != "" { - cmd = fmt.Sprintf("%s %s", cmd, parentID) - } - if blockers != "" { - cmd = fmt.Sprintf("%s blockers=%s", cmd, blockers) - } - - return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(cmd)) -} - -func openViewBeadWindow(beadID string, mount string) error { - w, err := acme.New() - if err != nil { - return err - } - - w.Name(fmt.Sprintf("/AnviLLM/Tasks/%s", beadID)) - w.Write("tag", []byte("Get Put New Blocks Comments Comment ")) - - go handleViewBeadWindow(w, beadID, mount) - return nil -} - -func handleViewBeadWindow(w *acme.Win, beadID string, mount string) { - defer w.CloseFiles() - - // Track original blockers for diff on Put - bead, _ := getBead(beadID, mount) - var origBlockers []string - if bead != nil { - origBlockers = bead.Blockers - } - - refreshViewBeadWindow(w, beadID, mount) - - for e := range w.EventChan() { - switch e.C2 { - case 'x', 'X': - cmd := string(e.Text) - arg := strings.TrimSpace(string(e.Arg)) - switch cmd { - case "Get": - bead, _ = getBead(beadID, mount) - if bead != nil { - origBlockers = bead.Blockers - } - refreshViewBeadWindow(w, beadID, mount) - case "Put": - body, err := w.ReadAll("body") - if err != nil { - fmt.Fprintf(os.Stderr, "Error reading body: %v\n", err) - continue - } - if err := updateBead(beadID, string(body), origBlockers, mount); err != nil { - fmt.Fprintf(os.Stderr, "Error updating bead: %v\n", err) - } else { - bead, _ = getBead(beadID, mount) - if bead != nil { - origBlockers = bead.Blockers - } - w.Ctl("clean") - } - case "New": - if err := openNewBeadWindow(beadID, mount); err != nil { - fmt.Fprintf(os.Stderr, "Error opening new bead window: %v\n", err) - } - case "Blocks": - if arg == "" { - fmt.Fprintf(os.Stderr, "Usage: select bead ID, then Blocks\n") - } else if err := addBlocksDep(beadID, arg, mount); err != nil { - fmt.Fprintf(os.Stderr, "Error adding blocks dep: %v\n", err) - } - case "Comments": - if err := openCommentsWindow(beadID, mount); err != nil { - fmt.Fprintf(os.Stderr, "Error opening comments window: %v\n", err) - } - case "Comment": - if err := openCommentWindow(beadID, mount); err != nil { - fmt.Fprintf(os.Stderr, "Error opening comment window: %v\n", err) - } - default: - w.WriteEvent(e) - } - default: - w.WriteEvent(e) - } - } -} - -func refreshViewBeadWindow(w *acme.Win, beadID string, mount string) { - bead, err := getBead(beadID, mount) - if err != nil { - w.Addr(",") - w.Write("data", []byte(fmt.Sprintf("Error reading bead: %v\n", err))) - w.Ctl("clean") - return - } - - var buf strings.Builder - buf.WriteString("---\n") - buf.WriteString(fmt.Sprintf("id: %s\n", bead.ID)) - buf.WriteString(fmt.Sprintf("title: %s\n", bead.Title)) - buf.WriteString(fmt.Sprintf("status: %s\n", bead.Status)) - if bead.Assignee != "" { - buf.WriteString(fmt.Sprintf("assignee: %s\n", bead.Assignee)) - } - if bead.Priority != 0 { - buf.WriteString(fmt.Sprintf("priority: %d\n", bead.Priority)) - } - buf.WriteString(fmt.Sprintf("blockers: %s\n", strings.Join(bead.Blockers, ", "))) - if bead.HasLabel("requires_approval") { - buf.WriteString("requires_approval: yes\n") - } else { - buf.WriteString("requires_approval: no\n") - } - if bead.HasLabel("requires_review") { - buf.WriteString("requires_review: yes\n") - } else { - buf.WriteString("requires_review: no\n") - } - buf.WriteString("---\n") - if bead.Description != "" { - desc, _ := strconv.Unquote(`"` + bead.Description + `"`) - if desc == "" { - desc = bead.Description - } - buf.WriteString(desc) - } - - w.Addr(",") - w.Write("data", []byte(buf.String())) - w.Ctl("clean") - w.Addr("0") - w.Ctl("dot=addr") - w.Ctl("show") -} - -func openCommentsWindow(beadID string, mount string) error { - w, err := acme.New() - if err != nil { - return err - } - w.Name(fmt.Sprintf("/AnviLLM/Tasks/%s/comments", beadID)) - w.Write("tag", []byte("Get ")) - - go handleCommentsWindow(w, beadID, mount) - return nil -} - -func openCommentWindow(beadID string, mount string) error { - w, err := acme.New() - if err != nil { - return err - } - w.Name(fmt.Sprintf("/AnviLLM/Tasks/%s/comment", beadID)) - w.Write("tag", []byte("Put ")) - - go handleCommentWindow(w, beadID, mount) - return nil -} - -func handleCommentWindow(w *acme.Win, beadID string, mount string) { - defer w.CloseFiles() - for e := range w.EventChan() { - switch e.C2 { - case 'x', 'X': - if string(e.Text) == "Put" { - body, err := w.ReadAll("body") - if err != nil { - fmt.Fprintf(os.Stderr, "Error reading body: %v\n", err) - continue - } - text := strings.TrimSpace(string(body)) - if text == "" { - continue - } - cmd := fmt.Sprintf("comment %s '%s'", beadID, text) - if err := writeBeadsFile(filepath.Join( mount, "ctl"), []byte(cmd)); err != nil { - fmt.Fprintf(os.Stderr, "Error adding comment: %v\n", err) - } else { - w.Addr(",") - w.Write("data", nil) - w.Ctl("clean") - } - } else { - w.WriteEvent(e) - } - default: - w.WriteEvent(e) - } - } -} - -func handleCommentsWindow(w *acme.Win, beadID string, mount string) { - defer w.CloseFiles() - refreshCommentsWindow(w, beadID, mount) - - for e := range w.EventChan() { - switch e.C2 { - case 'x', 'X': - if string(e.Text) == "Get" { - refreshCommentsWindow(w, beadID, mount) - } else { - w.WriteEvent(e) - } - default: - w.WriteEvent(e) - } - } -} - -func refreshCommentsWindow(w *acme.Win, beadID string, mount string) { - data, err := readBeadsFile(filepath.Join( mount, beadID, "comments")) - if err != nil { - w.Addr(",") - w.Write("data", []byte(fmt.Sprintf("Error reading comments: %v\n", err))) - w.Ctl("clean") - return - } - - var comments []struct { - Author string `json:"author"` - Text string `json:"text"` - CreatedAt string `json:"created_at"` - } - if err := json.Unmarshal(data, &comments); err != nil { - w.Addr(",") - w.Write("data", []byte(fmt.Sprintf("Error parsing comments: %v\n", err))) - w.Ctl("clean") - return - } - - var buf strings.Builder - if len(comments) == 0 { - buf.WriteString("No comments.\n") - } else { - for _, c := range comments { - buf.WriteString(fmt.Sprintf("--- %s (%s) ---\n%s\n\n", c.Author, c.CreatedAt, c.Text)) - } - } - - w.Addr(",") - w.Write("data", []byte(buf.String())) - w.Ctl("clean") - w.Addr("0") - w.Ctl("dot=addr") - w.Ctl("show") -} - -func getBead(beadID string, mount string) (*Bead, error) { - if !isConnected() { - return nil, fmt.Errorf("not connected to anvilsrv") - } - - data, err := readBeadsFile(filepath.Join( mount, beadID, "json")) - if err != nil { - return nil, err - } - - var bead Bead - if err := json.Unmarshal(data, &bead); err != nil { - return nil, err - } - - return &bead, nil -} - -func addBlocksDep(blockerID, blockedID string, mount string) error { - if !isConnected() { - return fmt.Errorf("not connected to anvilsrv") - } - // dep means parent blocks child - // So "blockerID blocks blockedID" means blockedID depends on blockerID - cmd := fmt.Sprintf("dep %s %s", blockedID, blockerID) - return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(cmd)) -} - -func removeBlocksDep(beadID, blockerID string, mount string) error { - if !isConnected() { - return fmt.Errorf("not connected to anvilsrv") - } - cmd := fmt.Sprintf("undep %s %s", beadID, blockerID) - return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(cmd)) -} - -func deleteBead(beadID string, mount string) error { - if !isConnected() { - return fmt.Errorf("not connected to anvilsrv") - } - cmd := fmt.Sprintf("delete %s", beadID) - return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(cmd)) -} - -func claimBeadAsUser(beadID string, mount string) error { - if !isConnected() { - return fmt.Errorf("not connected to anvilsrv") - } - return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(fmt.Sprintf("claim %s user", beadID))) -} - -func unclaimBead(beadID string, mount string) error { - if !isConnected() { - return fmt.Errorf("not connected to anvilsrv") - } - return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(fmt.Sprintf("unclaim %s", beadID))) -} - -func completeBead(beadID string, mount string) error { - if !isConnected() { - return fmt.Errorf("not connected to anvilsrv") - } - return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(fmt.Sprintf("complete %s", beadID))) -} - -func failBead(beadID, reason string, mount string) error { - if !isConnected() { - return fmt.Errorf("not connected to anvilsrv") - } - return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(fmt.Sprintf("fail %s %s", beadID, reason))) -} - -func openBead(beadID string, mount string) error { - if !isConnected() { - return fmt.Errorf("not connected to anvilsrv") - } - return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(fmt.Sprintf("open %s", beadID))) -} - -func deferBead(beadID string, mount string) error { - if !isConnected() { - return fmt.Errorf("not connected to anvilsrv") - } - return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(fmt.Sprintf("defer %s", beadID))) -} - -func updateBead(beadID, content string, origBlockers []string, mount string) error { - content = strings.TrimSpace(content) - if !strings.HasPrefix(content, "---") { - return fmt.Errorf("missing frontmatter") - } - - parts := strings.SplitN(content[3:], "---", 2) - if len(parts) < 1 { - return fmt.Errorf("invalid frontmatter") - } - - frontmatter := strings.TrimSpace(parts[0]) - description := "" - if len(parts) > 1 { - description = strings.TrimSpace(parts[1]) - } - - // Parse frontmatter - var title string - var newBlockers []string - requiresApproval := -1 // -1=unset, 0=false, 1=true - requiresReview := -1 - for _, line := range strings.Split(frontmatter, "\n") { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "title:") { - title = strings.TrimSpace(strings.TrimPrefix(line, "title:")) - } else if strings.HasPrefix(line, "blockers:") { - val := strings.TrimSpace(strings.TrimPrefix(line, "blockers:")) - if val != "" { - for _, b := range strings.Split(val, ",") { - b = strings.TrimSpace(b) - if b != "" { - newBlockers = append(newBlockers, b) - } - } - } - } else if strings.HasPrefix(line, "requires_approval:") { - val := strings.TrimSpace(strings.TrimPrefix(line, "requires_approval:")) - if val == "yes" || val == "true" || val == "1" { - requiresApproval = 1 - } else { - requiresApproval = 0 - } - } else if strings.HasPrefix(line, "requires_review:") { - val := strings.TrimSpace(strings.TrimPrefix(line, "requires_review:")) - if val == "yes" || val == "true" || val == "1" { - requiresReview = 1 - } else { - requiresReview = 0 - } - } - } - - // Update title and description - if title != "" { - cmd := fmt.Sprintf("update %s title '%s'", beadID, shellEscape(title)) - writeBeadsFile(filepath.Join( mount, "ctl"), []byte(cmd)) - } - if description != "" { - cmd := fmt.Sprintf("update %s description '%s'", beadID, shellEscape(description)) - writeBeadsFile(filepath.Join( mount, "ctl"), []byte(cmd)) - } - - // Update blockers - origSet := make(map[string]bool) - for _, b := range origBlockers { - origSet[b] = true - } - newSet := make(map[string]bool) - for _, b := range newBlockers { - newSet[b] = true - } - - for _, b := range newBlockers { - if !origSet[b] { - addBlocksDep(b, beadID, mount) - } - } - for _, b := range origBlockers { - if !newSet[b] { - removeBlocksDep(beadID, b, mount) - } - } - - // Toggle requires_approval label - if requiresApproval == 1 { - writeBeadsFile(filepath.Join( mount, "ctl"), []byte(fmt.Sprintf("label %s requires_approval", beadID))) - } else if requiresApproval == 0 { - writeBeadsFile(filepath.Join( mount, "ctl"), []byte(fmt.Sprintf("unlabel %s requires_approval", beadID))) - } - - // Toggle requires_review label - if requiresReview == 1 { - writeBeadsFile(filepath.Join( "", "ctl"), []byte(fmt.Sprintf("label %s requires_review", beadID))) - } else if requiresReview == 0 { - writeBeadsFile(filepath.Join( "", "ctl"), []byte(fmt.Sprintf("unlabel %s requires_review", beadID))) - } - - return nil -} - -func initBeads(prefix string, mount string) error { - if !isConnected() { - return fmt.Errorf("not connected to anvilsrv") - } - return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(fmt.Sprintf("init %s", prefix))) -} - -// mountProject mounts cwd via beads ctl and returns the generated mount name. -func mountProject(cwd string) (string, error) { - if !isBeadsConnected() { - return "", fmt.Errorf("not connected to 9beads") - } - // Generate mount name (first segment of UUID) - mount := fmt.Sprintf("%08x", time.Now().UnixNano()&0xffffffff) - cmd := fmt.Sprintf("mount %s %s", cwd, mount) - if err := writeBeadsFile("ctl", []byte(cmd)); err != nil { - return "", fmt.Errorf("mount failed: %w", err) - } - return mount, nil -} - -func umountProject(name string) error { - if !isConnected() { - return fmt.Errorf("not connected to anvilsrv") - } - return writeBeadsFile("ctl", []byte(fmt.Sprintf("umount %s", name))) -} - -// searchMail searches mail files for a pattern func searchMail(agentID, pattern, date string) ([]byte, error) { homeDir, _ := os.UserHomeDir() mailDir := filepath.Join(homeDir, ".local/share/anvillm/mail", agentID)