fix(polecat): validate issue exists before starting session

Add validateIssue() to check that an issue exists and is not tombstoned
before creating the tmux session. This prevents CPU spin loops from
agents retrying work on invalid issues.

Fixes #569

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/gus
2026-01-16 15:18:54 -08:00
committed by Steve Yegge
parent 08ef50047d
commit d42a9bd6e0

View File

@@ -2,6 +2,7 @@
package polecat
import (
"encoding/json"
"errors"
"fmt"
"os"
@@ -29,6 +30,7 @@ func debugSession(context string, err error) {
var (
ErrSessionRunning = errors.New("session already running")
ErrSessionNotFound = errors.New("session not found")
ErrIssueInvalid = errors.New("issue not found or tombstoned")
)
// SessionManager handles polecat session lifecycle.
@@ -161,6 +163,14 @@ func (m *SessionManager) Start(polecat string, opts SessionStartOptions) error {
workDir = m.clonePath(polecat)
}
// Validate issue exists and isn't tombstoned BEFORE creating session.
// This prevents CPU spin loops from agents retrying work on invalid issues.
if opts.Issue != "" {
if err := m.validateIssue(opts.Issue, workDir); err != nil {
return err
}
}
runtimeConfig := config.LoadRuntimeConfig(m.rig.Path)
// Ensure runtime settings exist in polecats/ (not polecats/<name>/) so we don't
@@ -449,6 +459,32 @@ func (m *SessionManager) StopAll(force bool) error {
return lastErr
}
// validateIssue checks that an issue exists and is not tombstoned.
// This must be called before starting a session to avoid CPU spin loops
// from agents retrying work on invalid issues.
func (m *SessionManager) validateIssue(issueID, workDir string) error {
cmd := exec.Command("bd", "show", issueID, "--json") //nolint:gosec
cmd.Dir = workDir
output, err := cmd.Output()
if err != nil {
return fmt.Errorf("%w: %s", ErrIssueInvalid, issueID)
}
var issues []struct {
Status string `json:"status"`
}
if err := json.Unmarshal(output, &issues); err != nil {
return fmt.Errorf("parsing issue: %w", err)
}
if len(issues) == 0 {
return fmt.Errorf("%w: %s", ErrIssueInvalid, issueID)
}
if issues[0].Status == "tombstone" {
return fmt.Errorf("%w: %s is tombstoned", ErrIssueInvalid, issueID)
}
return nil
}
// hookIssue pins an issue to a polecat's hook using bd update.
func (m *SessionManager) hookIssue(issueID, agentID, workDir string) error {
cmd := exec.Command("bd", "update", issueID, "--status=hooked", "--assignee="+agentID) //nolint:gosec