Files
gastown/internal/cmd/patrol_helpers.go
Steve Yegge 1a20012e35 Refactor prime.go: extract patrol helpers
- Create patrol_helpers.go with shared patrol logic
- Refactor 3 patrol context functions (~500 lines) to use shared helpers
- prime.go: 1561 → 1128 lines (28% reduction)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 19:23:45 -08:00

203 lines
6.2 KiB
Go

package cmd
import (
"bytes"
"fmt"
"os/exec"
"strings"
"github.com/steveyegge/gastown/internal/style"
)
// PatrolConfig holds role-specific patrol configuration.
type PatrolConfig struct {
RoleName string // "deacon", "witness", "refinery"
PatrolMolName string // "mol-deacon-patrol", etc.
BeadsDir string // where to look for beads
Assignee string // agent identity for pinning
HeaderEmoji string // display emoji
HeaderTitle string // "Patrol Status", etc.
WorkLoopSteps []string // role-specific instructions
CheckInProgress bool // whether to check in_progress status first (witness/refinery do, deacon doesn't)
}
// findActivePatrol finds an active patrol molecule for the role.
// Returns the patrol ID, display line, and whether one was found.
func findActivePatrol(cfg PatrolConfig) (patrolID, patrolLine string, found bool) {
// Check for in-progress patrol first (if configured)
if cfg.CheckInProgress {
cmdList := exec.Command("bd", "--no-daemon", "list", "--status=in_progress", "--type=epic")
cmdList.Dir = cfg.BeadsDir
var stdoutList bytes.Buffer
cmdList.Stdout = &stdoutList
cmdList.Stderr = nil
if cmdList.Run() == nil {
lines := strings.Split(stdoutList.String(), "\n")
for _, line := range lines {
if strings.Contains(line, cfg.PatrolMolName) && !strings.Contains(line, "[template]") {
parts := strings.Fields(line)
if len(parts) > 0 {
return parts[0], line, true
}
}
}
}
}
// Check for open patrols with open children (active wisp)
cmdOpen := exec.Command("bd", "--no-daemon", "list", "--status=open", "--type=epic")
cmdOpen.Dir = cfg.BeadsDir
var stdoutOpen bytes.Buffer
cmdOpen.Stdout = &stdoutOpen
cmdOpen.Stderr = nil
if cmdOpen.Run() == nil {
lines := strings.Split(stdoutOpen.String(), "\n")
for _, line := range lines {
if strings.Contains(line, cfg.PatrolMolName) && !strings.Contains(line, "[template]") {
parts := strings.Fields(line)
if len(parts) > 0 {
molID := parts[0]
// Check if this molecule has open children
cmdShow := exec.Command("bd", "--no-daemon", "show", molID)
cmdShow.Dir = cfg.BeadsDir
var stdoutShow bytes.Buffer
cmdShow.Stdout = &stdoutShow
cmdShow.Stderr = nil
if cmdShow.Run() == nil {
showOutput := stdoutShow.String()
// Deacon only checks "- open]", witness/refinery also check "- in_progress]"
hasOpenChildren := strings.Contains(showOutput, "- open]")
if cfg.CheckInProgress {
hasOpenChildren = hasOpenChildren || strings.Contains(showOutput, "- in_progress]")
}
if hasOpenChildren {
return molID, line, true
}
}
}
}
}
}
return "", "", false
}
// autoSpawnPatrol creates and pins a new patrol wisp.
// Returns the patrol ID or an error.
func autoSpawnPatrol(cfg PatrolConfig) (string, error) {
// Find the proto ID for the patrol molecule
cmdCatalog := exec.Command("bd", "--no-daemon", "mol", "catalog")
cmdCatalog.Dir = cfg.BeadsDir
var stdoutCatalog bytes.Buffer
cmdCatalog.Stdout = &stdoutCatalog
cmdCatalog.Stderr = nil
if err := cmdCatalog.Run(); err != nil {
return "", fmt.Errorf("failed to list molecule catalog")
}
// Find patrol molecule in catalog
var protoID string
catalogLines := strings.Split(stdoutCatalog.String(), "\n")
for _, line := range catalogLines {
if strings.Contains(line, cfg.PatrolMolName) {
parts := strings.Fields(line)
if len(parts) > 0 {
// Strip trailing colon from ID (catalog format: "gt-xxx: title")
protoID = strings.TrimSuffix(parts[0], ":")
break
}
}
}
if protoID == "" {
return "", fmt.Errorf("proto %s not found in catalog", cfg.PatrolMolName)
}
// Create the patrol wisp
cmdSpawn := exec.Command("bd", "--no-daemon", "wisp", "create", protoID)
cmdSpawn.Dir = cfg.BeadsDir
var stdoutSpawn, stderrSpawn bytes.Buffer
cmdSpawn.Stdout = &stdoutSpawn
cmdSpawn.Stderr = &stderrSpawn
if err := cmdSpawn.Run(); err != nil {
return "", fmt.Errorf("failed to create patrol wisp: %s", stderrSpawn.String())
}
// Parse the created molecule ID from output
var patrolID string
spawnOutput := stdoutSpawn.String()
for _, line := range strings.Split(spawnOutput, "\n") {
if strings.Contains(line, "Root issue:") || strings.Contains(line, "Created") {
parts := strings.Fields(line)
for _, p := range parts {
if strings.HasPrefix(p, "wisp-") || strings.HasPrefix(p, "gt-") {
patrolID = p
break
}
}
}
}
if patrolID == "" {
return "", fmt.Errorf("created wisp but could not parse ID from output")
}
// Pin the wisp to the agent
cmdPin := exec.Command("bd", "--no-daemon", "update", patrolID, "--status=pinned", "--assignee="+cfg.Assignee)
cmdPin.Dir = cfg.BeadsDir
if err := cmdPin.Run(); err != nil {
return patrolID, fmt.Errorf("created wisp %s but failed to pin", patrolID)
}
return patrolID, nil
}
// outputPatrolContext is the main function that handles patrol display logic.
// It finds or creates a patrol and outputs the status and work loop.
func outputPatrolContext(cfg PatrolConfig) {
fmt.Println()
fmt.Printf("%s\n\n", style.Bold.Render(fmt.Sprintf("## %s %s", cfg.HeaderEmoji, cfg.HeaderTitle)))
// Try to find an active patrol
patrolID, patrolLine, hasPatrol := findActivePatrol(cfg)
if !hasPatrol {
// No active patrol - auto-spawn one
fmt.Printf("Status: **No active patrol** - creating %s...\n", cfg.PatrolMolName)
fmt.Println()
var err error
patrolID, err = autoSpawnPatrol(cfg)
if err != nil {
if patrolID != "" {
fmt.Printf("⚠ %s\n", err.Error())
} else {
fmt.Println(style.Dim.Render(err.Error()))
fmt.Println(style.Dim.Render(fmt.Sprintf("Run `bd mol catalog` to troubleshoot.")))
return
}
} else {
fmt.Printf("✓ Created and pinned patrol wisp: %s\n", patrolID)
}
} else {
// Has active patrol - show status
fmt.Println("Status: **Patrol Active**")
fmt.Printf("Patrol: %s\n\n", strings.TrimSpace(patrolLine))
}
// Show patrol work loop instructions
fmt.Printf("**%s Patrol Work Loop:**\n", strings.Title(cfg.RoleName))
for i, step := range cfg.WorkLoopSteps {
fmt.Printf("%d. %s\n", i+1, step)
}
if patrolID != "" {
fmt.Println()
fmt.Printf("Current patrol ID: %s\n", patrolID)
}
}