fix: handle bd --no-daemon exit code 0 bug on not-found (#339)

When bd --no-daemon show <id> does not find an issue, it incorrectly exits
with code 0 (success) but writes the error to stderr and leaves stdout empty.
This causes JSON parse failures throughout gt when code tries to unmarshal
the empty stdout.

This PR handles the bug defensively in all affected code paths:
- beads.go run(): Detect empty stdout + non-empty stderr as error
- beads.go wrapError(): Add 'no issue found' to ErrNotFound patterns
- sling.go: Check len(out) == 0 in multiple functions
- convoy.go getIssueDetails(): Check stdout.Len() == 0
- prime_molecule.go: Check stdout.Len() == 0

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/joe
2026-01-11 18:37:01 -08:00
committed by Steve Yegge
parent b9025379b7
commit d126c967a0
4 changed files with 40 additions and 4 deletions

View File

@@ -151,6 +151,13 @@ func (b *Beads) run(args ...string) ([]byte, error) {
return nil, b.wrapError(err, stderr.String(), args) return nil, b.wrapError(err, stderr.String(), args)
} }
// Handle bd --no-daemon exit code 0 bug: when issue not found,
// --no-daemon exits 0 but writes error to stderr with empty stdout.
// Detect this case and treat as error to avoid JSON parse failures.
if stdout.Len() == 0 && stderr.Len() > 0 {
return nil, b.wrapError(fmt.Errorf("command produced no output"), stderr.String(), args)
}
return stdout.Bytes(), nil return stdout.Bytes(), nil
} }
@@ -174,7 +181,9 @@ func (b *Beads) wrapError(err error, stderr string, args []string) error {
} }
// ErrNotFound is widely used for issue lookups - acceptable exception // ErrNotFound is widely used for issue lookups - acceptable exception
if strings.Contains(stderr, "not found") || strings.Contains(stderr, "Issue not found") { // Match various "not found" error patterns from bd
if strings.Contains(stderr, "not found") || strings.Contains(stderr, "Issue not found") ||
strings.Contains(stderr, "no issue found") {
return ErrNotFound return ErrNotFound
} }

View File

@@ -1186,6 +1186,10 @@ func getIssueDetails(issueID string) *issueDetails {
if err := showCmd.Run(); err != nil { if err := showCmd.Run(); err != nil {
return nil return nil
} }
// Handle bd --no-daemon exit 0 bug: empty stdout means not found
if stdout.Len() == 0 {
return nil
}
var issues []struct { var issues []struct {
ID string `json:"id"` ID string `json:"id"`

View File

@@ -44,6 +44,12 @@ func showMoleculeExecutionPrompt(workDir, moleculeID string) {
fmt.Printf(" Check status with: bd mol current %s\n", moleculeID) fmt.Printf(" Check status with: bd mol current %s\n", moleculeID)
return return
} }
// Handle bd --no-daemon exit 0 bug: empty stdout means not found
if stdout.Len() == 0 {
fmt.Println(style.Bold.Render("→ PROPULSION PRINCIPLE: Work is on your hook. RUN IT."))
fmt.Println(" Begin working on this molecule immediately.")
return
}
// Parse JSON output - it's an array with one element // Parse JSON output - it's an array with one element
var outputs []MoleculeCurrentOutput var outputs []MoleculeCurrentOutput

View File

@@ -523,6 +523,10 @@ func storeArgsInBead(beadID, args string) error {
if err != nil { if err != nil {
return fmt.Errorf("fetching bead: %w", err) return fmt.Errorf("fetching bead: %w", err)
} }
// Handle bd --no-daemon exit 0 bug: empty stdout means not found
if len(out) == 0 {
return fmt.Errorf("bead not found")
}
// Parse the bead // Parse the bead
var issues []beads.Issue var issues []beads.Issue
@@ -738,9 +742,15 @@ func verifyBeadExists(beadID string) error {
if townRoot, err := workspace.FindFromCwd(); err == nil { if townRoot, err := workspace.FindFromCwd(); err == nil {
cmd.Dir = townRoot cmd.Dir = townRoot
} }
if err := cmd.Run(); err != nil { // Use Output() instead of Run() to detect bd --no-daemon exit 0 bug:
// when issue not found, --no-daemon exits 0 but produces empty stdout.
out, err := cmd.Output()
if err != nil {
return fmt.Errorf("bead '%s' not found (bd show failed)", beadID) return fmt.Errorf("bead '%s' not found (bd show failed)", beadID)
} }
if len(out) == 0 {
return fmt.Errorf("bead '%s' not found", beadID)
}
return nil return nil
} }
@@ -764,6 +774,11 @@ func getBeadInfo(beadID string) (*beadInfo, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("bead '%s' not found", beadID) return nil, fmt.Errorf("bead '%s' not found", beadID)
} }
// Handle bd --no-daemon exit 0 bug: when issue not found,
// --no-daemon exits 0 but produces empty stdout (error goes to stderr).
if len(out) == 0 {
return nil, fmt.Errorf("bead '%s' not found", beadID)
}
// bd show --json returns an array (issue + dependents), take first element // bd show --json returns an array (issue + dependents), take first element
var infos []beadInfo var infos []beadInfo
if err := json.Unmarshal(out, &infos); err != nil { if err := json.Unmarshal(out, &infos); err != nil {
@@ -829,14 +844,16 @@ func resolveSelfTarget() (agentID string, pane string, hookRoot string, err erro
// Uses --no-daemon with --allow-stale for consistency with verifyBeadExists. // Uses --no-daemon with --allow-stale for consistency with verifyBeadExists.
func verifyFormulaExists(formulaName string) error { func verifyFormulaExists(formulaName string) error {
// Try bd formula show (handles all formula file formats) // Try bd formula show (handles all formula file formats)
// Use Output() instead of Run() to detect bd --no-daemon exit 0 bug:
// when formula not found, --no-daemon may exit 0 but produce empty stdout.
cmd := exec.Command("bd", "--no-daemon", "formula", "show", formulaName, "--allow-stale") cmd := exec.Command("bd", "--no-daemon", "formula", "show", formulaName, "--allow-stale")
if err := cmd.Run(); err == nil { if out, err := cmd.Output(); err == nil && len(out) > 0 {
return nil return nil
} }
// Try with mol- prefix // Try with mol- prefix
cmd = exec.Command("bd", "--no-daemon", "formula", "show", "mol-"+formulaName, "--allow-stale") cmd = exec.Command("bd", "--no-daemon", "formula", "show", "mol-"+formulaName, "--allow-stale")
if err := cmd.Run(); err == nil { if out, err := cmd.Output(); err == nil && len(out) > 0 {
return nil return nil
} }