From 30e65b5ca7e7ff6c00f6395322eb6e3a2a89453f Mon Sep 17 00:00:00 2001 From: dennis Date: Sun, 25 Jan 2026 13:33:21 -0800 Subject: [PATCH] refactor(convoy): replace SQLite queries with bd dep list commands Replace direct sqlite3 CLI calls in getTrackedIssues and isTrackedByConvoy with bd dep list commands that work with any beads backend (SQLite or Dolt). - getTrackedIssues: Use bd dep list --direction=down --type=tracks --json - isTrackedByConvoy: Use bd dep list --direction=up --type=tracks --json This enables convoy commands to work with the Dolt backend. Fixes: hq-e4eefc.2 Co-Authored-By: Claude Opus 4.5 --- internal/cmd/convoy.go | 106 ++++++++++------------------------- internal/cmd/sling_convoy.go | 45 ++++++++------- 2 files changed, 54 insertions(+), 97 deletions(-) diff --git a/internal/cmd/convoy.go b/internal/cmd/convoy.go index 77ff29b4..2b73fa4c 100644 --- a/internal/cmd/convoy.go +++ b/internal/cmd/convoy.go @@ -1310,104 +1310,58 @@ type trackedIssueInfo struct { WorkerAge string `json:"worker_age,omitempty"` // How long worker has been on this issue } -// getTrackedIssues queries SQLite directly to get issues tracked by a convoy. -// This is needed because bd dep list doesn't properly show cross-rig external dependencies. -// Uses batched lookup to avoid N+1 subprocess calls. +// getTrackedIssues uses bd dep list to get issues tracked by a convoy. +// Returns issue details including status, type, and worker info. func getTrackedIssues(townBeads, convoyID string) []trackedIssueInfo { - dbPath := filepath.Join(townBeads, "beads.db") - - // Query tracked dependencies from SQLite - // Escape single quotes to prevent SQL injection - safeConvoyID := strings.ReplaceAll(convoyID, "'", "''") - queryCmd := exec.Command("sqlite3", "-json", dbPath, - fmt.Sprintf(`SELECT depends_on_id, type FROM dependencies WHERE issue_id = '%s' AND type = 'tracks'`, safeConvoyID)) + // Use bd dep list to get tracked dependencies + // Run from town root (parent of .beads) so bd routes correctly + townRoot := filepath.Dir(townBeads) + depCmd := exec.Command("bd", "--no-daemon", "dep", "list", convoyID, "--direction=down", "--type=tracks", "--json") + depCmd.Dir = townRoot var stdout bytes.Buffer - queryCmd.Stdout = &stdout - if err := queryCmd.Run(); err != nil { + depCmd.Stdout = &stdout + if err := depCmd.Run(); err != nil { return nil } + // Parse the JSON output - bd dep list returns full issue details var deps []struct { - DependsOnID string `json:"depends_on_id"` - Type string `json:"type"` + ID string `json:"id"` + Title string `json:"title"` + Status string `json:"status"` + IssueType string `json:"issue_type"` + Assignee string `json:"assignee"` + DependencyType string `json:"dependency_type"` + Labels []string `json:"labels"` } if err := json.Unmarshal(stdout.Bytes(), &deps); err != nil { return nil } - // First pass: collect all issue IDs and track which rig they belong to - type issueRef struct { - ID string - RigName string // empty for local issues, rig name for external - } - issueRefs := make([]issueRef, 0, len(deps)) - idToDepType := make(map[string]string) - + // Collect non-closed issue IDs for worker lookup + openIssueIDs := make([]string, 0, len(deps)) for _, dep := range deps { - issueID := dep.DependsOnID - rigName := "" // Local issue by default - - // Handle external reference format: external:rig:issue-id - if strings.HasPrefix(issueID, "external:") { - parts := strings.SplitN(issueID, ":", 3) - if len(parts) == 3 { - rigName = parts[1] // Extract rig name - issueID = parts[2] // Extract the actual issue ID - } - } - - issueRefs = append(issueRefs, issueRef{ID: issueID, RigName: rigName}) - idToDepType[issueID] = dep.Type - } - - // Query issues, grouped by rig - detailsMap := make(map[string]*issueDetails) - for _, ref := range issueRefs { - var details *issueDetails - if ref.RigName != "" { - // External reference: query the rig database - details = getExternalIssueDetails(townBeads, ref.RigName, ref.ID) - } else { - // Local reference: query town database - details = getIssueDetails(ref.ID) - } - if details != nil { - detailsMap[ref.ID] = details - } - } - - // Get workers for these issues (only for non-closed issues) - openIssueIDs := make([]string, 0) - for _, ref := range issueRefs { - id := ref.ID - if details, ok := detailsMap[id]; ok && details.Status != "closed" { - openIssueIDs = append(openIssueIDs, id) + if dep.Status != "closed" { + openIssueIDs = append(openIssueIDs, dep.ID) } } workersMap := getWorkersForIssues(openIssueIDs) - // Second pass: build result using the batch lookup + // Build result var tracked []trackedIssueInfo - for _, ref := range issueRefs { - issueID := ref.ID + for _, dep := range deps { info := trackedIssueInfo{ - ID: issueID, - Type: idToDepType[issueID], - } - - if details, ok := detailsMap[issueID]; ok { - info.Title = details.Title - info.Status = details.Status - info.IssueType = details.IssueType - info.Assignee = details.Assignee - } else { - info.Title = "(external)" - info.Status = "unknown" + ID: dep.ID, + Title: dep.Title, + Status: dep.Status, + Type: dep.DependencyType, + IssueType: dep.IssueType, + Assignee: dep.Assignee, } // Add worker info if available - if worker, ok := workersMap[issueID]; ok { + if worker, ok := workersMap[dep.ID]; ok { info.Worker = worker.Worker info.WorkerAge = worker.Age } diff --git a/internal/cmd/sling_convoy.go b/internal/cmd/sling_convoy.go index 8982ae4e..51429a84 100644 --- a/internal/cmd/sling_convoy.go +++ b/internal/cmd/sling_convoy.go @@ -3,6 +3,7 @@ package cmd import ( "crypto/rand" "encoding/base32" + "encoding/json" "fmt" "os" "os/exec" @@ -29,32 +30,34 @@ func isTrackedByConvoy(beadID string) string { return "" } - // Query town beads for any convoy that tracks this issue - // Convoys use "tracks" dependency type: convoy -> tracked issue - townBeads := filepath.Join(townRoot, ".beads") - dbPath := filepath.Join(townBeads, "beads.db") + // Use bd dep list to find what tracks this issue (direction=up) + // Filter for open convoys in the results + depCmd := exec.Command("bd", "--no-daemon", "dep", "list", beadID, "--direction=up", "--type=tracks", "--json") + depCmd.Dir = townRoot - // Query dependencies where this bead is being tracked - // Also check for external reference format: external:rig:issue-id - query := fmt.Sprintf(` - SELECT d.issue_id - FROM dependencies d - JOIN issues i ON d.issue_id = i.id - WHERE d.type = 'tracks' - AND i.issue_type = 'convoy' - AND i.status = 'open' - AND (d.depends_on_id = '%s' OR d.depends_on_id LIKE '%%:%s') - LIMIT 1 - `, beadID, beadID) - - queryCmd := exec.Command("sqlite3", dbPath, query) - out, err := queryCmd.Output() + out, err := depCmd.Output() if err != nil { return "" } - convoyID := strings.TrimSpace(string(out)) - return convoyID + // Parse results and find an open convoy + var trackers []struct { + ID string `json:"id"` + IssueType string `json:"issue_type"` + Status string `json:"status"` + } + if err := json.Unmarshal(out, &trackers); err != nil { + return "" + } + + // Return the first open convoy that tracks this issue + for _, tracker := range trackers { + if tracker.IssueType == "convoy" && tracker.Status == "open" { + return tracker.ID + } + } + + return "" } // createAutoConvoy creates an auto-convoy for a single issue and tracks it.