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 <noreply@anthropic.com>
This commit is contained in:
dennis
2026-01-25 13:33:21 -08:00
committed by Steve Yegge
parent b178d056f6
commit 30e65b5ca7
2 changed files with 54 additions and 97 deletions

View File

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

View File

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