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>
This commit is contained in:
202
internal/cmd/patrol_helpers.go
Normal file
202
internal/cmd/patrol_helpers.go
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -798,508 +798,70 @@ func showMoleculeProgress(b *beads.Beads, rootID string) {
|
|||||||
|
|
||||||
// outputDeaconPatrolContext shows patrol molecule status for the Deacon.
|
// outputDeaconPatrolContext shows patrol molecule status for the Deacon.
|
||||||
// Deacon uses wisps (Wisp:true issues in main .beads/) for patrol cycles.
|
// Deacon uses wisps (Wisp:true issues in main .beads/) for patrol cycles.
|
||||||
// bd wisp creates wisp-marked issues that are auto-deleted on squash.
|
|
||||||
func outputDeaconPatrolContext(ctx RoleContext) {
|
func outputDeaconPatrolContext(ctx RoleContext) {
|
||||||
fmt.Println()
|
cfg := PatrolConfig{
|
||||||
fmt.Printf("%s\n\n", style.Bold.Render("## 🔄 Patrol Status (Wisp-based)"))
|
RoleName: "deacon",
|
||||||
|
PatrolMolName: "mol-deacon-patrol",
|
||||||
// Check for active mol-deacon-patrol molecules in town beads
|
BeadsDir: ctx.WorkDir,
|
||||||
// A patrol is "active" if it has open wisp children (steps to execute)
|
Assignee: "deacon",
|
||||||
// After squash, the root stays open but has no open children - that's "completed"
|
HeaderEmoji: "🔄",
|
||||||
// Deacon uses town beads (via redirect from ~/gt/deacon/.beads/ to ~/gt/.beads/)
|
HeaderTitle: "Patrol Status (Wisp-based)",
|
||||||
beadsDir := ctx.WorkDir
|
CheckInProgress: false,
|
||||||
|
WorkLoopSteps: []string{
|
||||||
// First find mol-deacon-patrol molecules (exclude template)
|
"Check next step: `bd ready`",
|
||||||
cmdList := exec.Command("bd", "list", "--status=open", "--type=epic")
|
"Execute the step (heartbeat, mail, health checks, etc.)",
|
||||||
cmdList.Dir = beadsDir
|
"Close step: `bd close <step-id>`",
|
||||||
var stdoutList bytes.Buffer
|
"Check next: `bd ready`",
|
||||||
cmdList.Stdout = &stdoutList
|
"At cycle end (loop-or-exit step):\n - Generate summary of patrol cycle\n - Squash: `bd --no-daemon mol squash <mol-id> --summary \"<summary>\"`\n - Loop back to create new wisp, or exit if context high",
|
||||||
cmdList.Stderr = nil
|
},
|
||||||
errList := cmdList.Run()
|
|
||||||
|
|
||||||
// Find a patrol molecule with open children
|
|
||||||
hasPatrol := false
|
|
||||||
var patrolLine string
|
|
||||||
var patrolID string
|
|
||||||
if errList == nil {
|
|
||||||
lines := strings.Split(stdoutList.String(), "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
if strings.Contains(line, "mol-deacon-patrol") && !strings.Contains(line, "[template]") {
|
|
||||||
// Extract the ID (first word)
|
|
||||||
parts := strings.Fields(line)
|
|
||||||
if len(parts) > 0 {
|
|
||||||
molID := parts[0]
|
|
||||||
// Check if this molecule has open children using bd show
|
|
||||||
cmdShow := exec.Command("bd", "show", molID)
|
|
||||||
cmdShow.Dir = beadsDir
|
|
||||||
var stdoutShow bytes.Buffer
|
|
||||||
cmdShow.Stdout = &stdoutShow
|
|
||||||
cmdShow.Stderr = nil
|
|
||||||
if cmdShow.Run() == nil {
|
|
||||||
showOutput := stdoutShow.String()
|
|
||||||
// Check for "- open]" in children section (open child steps)
|
|
||||||
if strings.Contains(showOutput, "- open]") {
|
|
||||||
hasPatrol = true
|
|
||||||
patrolLine = line
|
|
||||||
patrolID = molID
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasPatrol {
|
|
||||||
// No active patrol - AUTO-SPAWN one
|
|
||||||
fmt.Println("Status: **No active patrol** - creating mol-deacon-patrol...")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// Find the proto ID for mol-deacon-patrol
|
|
||||||
cmdCatalog := exec.Command("bd", "--no-daemon", "mol", "catalog")
|
|
||||||
cmdCatalog.Dir = beadsDir
|
|
||||||
var stdoutCatalog bytes.Buffer
|
|
||||||
cmdCatalog.Stdout = &stdoutCatalog
|
|
||||||
cmdCatalog.Stderr = nil
|
|
||||||
|
|
||||||
if cmdCatalog.Run() != nil {
|
|
||||||
fmt.Println(style.Dim.Render("Failed to list molecule catalog. Run `bd mol catalog` to troubleshoot."))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find mol-deacon-patrol in catalog
|
|
||||||
var protoID string
|
|
||||||
catalogLines := strings.Split(stdoutCatalog.String(), "\n")
|
|
||||||
for _, line := range catalogLines {
|
|
||||||
if strings.Contains(line, "mol-deacon-patrol") {
|
|
||||||
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 == "" {
|
|
||||||
fmt.Println(style.Dim.Render("Proto mol-deacon-patrol not found in catalog. Run `bd mol register` first."))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the patrol wisp (step 1: create)
|
|
||||||
cmdSpawn := exec.Command("bd", "--no-daemon", "wisp", "create", protoID)
|
|
||||||
cmdSpawn.Dir = beadsDir
|
|
||||||
var stdoutSpawn, stderrSpawn bytes.Buffer
|
|
||||||
cmdSpawn.Stdout = &stdoutSpawn
|
|
||||||
cmdSpawn.Stderr = &stderrSpawn
|
|
||||||
|
|
||||||
if err := cmdSpawn.Run(); err != nil {
|
|
||||||
fmt.Printf("Failed to create patrol wisp: %s\n", stderrSpawn.String())
|
|
||||||
fmt.Println(style.Dim.Render("Run manually: bd --no-daemon wisp create " + protoID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the created molecule ID from output
|
|
||||||
spawnOutput := stdoutSpawn.String()
|
|
||||||
|
|
||||||
// Extract molecule ID from output (format: "Root issue: wisp-xxxx" or "gt-xxxx")
|
|
||||||
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 == "" {
|
|
||||||
fmt.Printf("⚠ Created wisp but could not parse ID from output\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pin the wisp to deacon (step 2: assign)
|
|
||||||
cmdPin := exec.Command("bd", "--no-daemon", "update", patrolID, "--status=pinned", "--assignee=deacon")
|
|
||||||
cmdPin.Dir = beadsDir
|
|
||||||
if err := cmdPin.Run(); err != nil {
|
|
||||||
fmt.Printf("⚠ Created wisp %s but failed to pin to deacon\n", patrolID)
|
|
||||||
} 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.Println("**Deacon Patrol Work Loop:**")
|
|
||||||
fmt.Println("1. Check next step: `bd ready`")
|
|
||||||
fmt.Println("2. Execute the step (heartbeat, mail, health checks, etc.)")
|
|
||||||
fmt.Println("3. Close step: `bd close <step-id>`")
|
|
||||||
fmt.Println("4. Check next: `bd ready`")
|
|
||||||
fmt.Println("5. At cycle end (loop-or-exit step):")
|
|
||||||
fmt.Println(" - Generate summary of patrol cycle")
|
|
||||||
fmt.Println(" - Squash: `bd --no-daemon mol squash <mol-id> --summary \"<summary>\"`")
|
|
||||||
fmt.Println(" - Loop back to create new wisp, or exit if context high")
|
|
||||||
if patrolID != "" {
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Current patrol ID: %s\n", patrolID)
|
|
||||||
}
|
}
|
||||||
|
outputPatrolContext(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// outputWitnessPatrolContext shows patrol molecule status for the Witness.
|
// outputWitnessPatrolContext shows patrol molecule status for the Witness.
|
||||||
// Witness AUTO-BONDS its patrol molecule on startup if one isn't already running.
|
// Witness AUTO-BONDS its patrol molecule on startup if one isn't already running.
|
||||||
// This ensures polecat health is always monitored.
|
|
||||||
func outputWitnessPatrolContext(ctx RoleContext) {
|
func outputWitnessPatrolContext(ctx RoleContext) {
|
||||||
fmt.Println()
|
cfg := PatrolConfig{
|
||||||
fmt.Printf("%s\n\n", style.Bold.Render("## 👁 Witness Patrol Status"))
|
RoleName: "witness",
|
||||||
|
PatrolMolName: "mol-witness-patrol",
|
||||||
// Witness works from its own rig clone: <rig>/witness/rig/
|
BeadsDir: ctx.WorkDir,
|
||||||
// Beads are in the current WorkDir
|
Assignee: ctx.Rig + "/witness",
|
||||||
witnessBeadsDir := ctx.WorkDir
|
HeaderEmoji: "👁",
|
||||||
|
HeaderTitle: "Witness Patrol Status",
|
||||||
// Find mol-witness-patrol molecules (exclude template)
|
CheckInProgress: true,
|
||||||
// Look for in-progress patrol first (resumable)
|
WorkLoopSteps: []string{
|
||||||
cmdList := exec.Command("bd", "--no-daemon", "list", "--status=in_progress", "--type=epic")
|
"Check inbox: `gt mail inbox`",
|
||||||
cmdList.Dir = witnessBeadsDir
|
"Check next step: `bd ready`",
|
||||||
var stdoutList bytes.Buffer
|
"Execute the step (survey polecats, inspect, nudge, etc.)",
|
||||||
cmdList.Stdout = &stdoutList
|
"Close step: `bd close <step-id>`",
|
||||||
cmdList.Stderr = nil
|
"Check next: `bd ready`",
|
||||||
errList := cmdList.Run()
|
"At cycle end (burn-or-loop step):\n - Generate summary of patrol cycle\n - Squash: `bd --no-daemon mol squash <mol-id> --summary \"<summary>\"`\n - Loop back to create new wisp, or exit if context high",
|
||||||
|
},
|
||||||
hasPatrol := false
|
|
||||||
var patrolID string
|
|
||||||
var patrolLine string
|
|
||||||
|
|
||||||
if errList == nil {
|
|
||||||
lines := strings.Split(stdoutList.String(), "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
if strings.Contains(line, "mol-witness-patrol") && !strings.Contains(line, "[template]") {
|
|
||||||
parts := strings.Fields(line)
|
|
||||||
if len(parts) > 0 {
|
|
||||||
patrolID = parts[0]
|
|
||||||
patrolLine = line
|
|
||||||
hasPatrol = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also check for open patrols with open children (active wisp)
|
|
||||||
if !hasPatrol {
|
|
||||||
cmdOpen := exec.Command("bd", "--no-daemon", "list", "--status=open", "--type=epic")
|
|
||||||
cmdOpen.Dir = witnessBeadsDir
|
|
||||||
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, "mol-witness-patrol") && !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 = witnessBeadsDir
|
|
||||||
var stdoutShow bytes.Buffer
|
|
||||||
cmdShow.Stdout = &stdoutShow
|
|
||||||
cmdShow.Stderr = nil
|
|
||||||
if cmdShow.Run() == nil {
|
|
||||||
showOutput := stdoutShow.String()
|
|
||||||
if strings.Contains(showOutput, "- open]") || strings.Contains(showOutput, "- in_progress]") {
|
|
||||||
hasPatrol = true
|
|
||||||
patrolID = molID
|
|
||||||
patrolLine = line
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasPatrol {
|
|
||||||
// No active patrol - AUTO-SPAWN one
|
|
||||||
fmt.Println("Status: **No active patrol** - creating mol-witness-patrol...")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// Find the proto ID for mol-witness-patrol
|
|
||||||
cmdCatalog := exec.Command("bd", "--no-daemon", "mol", "catalog")
|
|
||||||
cmdCatalog.Dir = witnessBeadsDir
|
|
||||||
var stdoutCatalog bytes.Buffer
|
|
||||||
cmdCatalog.Stdout = &stdoutCatalog
|
|
||||||
cmdCatalog.Stderr = nil
|
|
||||||
|
|
||||||
if cmdCatalog.Run() != nil {
|
|
||||||
fmt.Println(style.Dim.Render("Failed to list molecule catalog. Run `bd mol catalog` to troubleshoot."))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find mol-witness-patrol in catalog
|
|
||||||
var protoID string
|
|
||||||
catalogLines := strings.Split(stdoutCatalog.String(), "\n")
|
|
||||||
for _, line := range catalogLines {
|
|
||||||
if strings.Contains(line, "mol-witness-patrol") {
|
|
||||||
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 == "" {
|
|
||||||
fmt.Println(style.Dim.Render("Proto mol-witness-patrol not found in catalog. Run `bd mol register` first."))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the patrol wisp (step 1: create)
|
|
||||||
witnessAgent := ctx.Rig + "/witness"
|
|
||||||
cmdSpawn := exec.Command("bd", "--no-daemon", "wisp", "create", protoID)
|
|
||||||
cmdSpawn.Dir = witnessBeadsDir
|
|
||||||
var stdoutSpawn, stderrSpawn bytes.Buffer
|
|
||||||
cmdSpawn.Stdout = &stdoutSpawn
|
|
||||||
cmdSpawn.Stderr = &stderrSpawn
|
|
||||||
|
|
||||||
if err := cmdSpawn.Run(); err != nil {
|
|
||||||
fmt.Printf("Failed to create patrol wisp: %s\n", stderrSpawn.String())
|
|
||||||
fmt.Println(style.Dim.Render("Run manually: bd --no-daemon wisp create " + protoID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the created molecule ID from output
|
|
||||||
spawnOutput := stdoutSpawn.String()
|
|
||||||
|
|
||||||
// Extract molecule ID from output (format: "Root issue: wisp-xxxx" or "gt-xxxx")
|
|
||||||
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 == "" {
|
|
||||||
fmt.Printf("⚠ Created wisp but could not parse ID from output\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pin the wisp to witness (step 2: assign)
|
|
||||||
cmdPin := exec.Command("bd", "--no-daemon", "update", patrolID, "--status=pinned", "--assignee="+witnessAgent)
|
|
||||||
cmdPin.Dir = witnessBeadsDir
|
|
||||||
if err := cmdPin.Run(); err != nil {
|
|
||||||
fmt.Printf("⚠ Created wisp %s but failed to pin to witness\n", patrolID)
|
|
||||||
} 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.Println("**Witness Patrol Work Loop:**")
|
|
||||||
fmt.Println("1. Check inbox: `gt mail inbox`")
|
|
||||||
fmt.Println("2. Check next step: `bd ready`")
|
|
||||||
fmt.Println("3. Execute the step (survey polecats, inspect, nudge, etc.)")
|
|
||||||
fmt.Println("4. Close step: `bd close <step-id>`")
|
|
||||||
fmt.Println("5. Check next: `bd ready`")
|
|
||||||
fmt.Println("6. At cycle end (burn-or-loop step):")
|
|
||||||
fmt.Println(" - Generate summary of patrol cycle")
|
|
||||||
fmt.Println(" - Squash: `bd --no-daemon mol squash <mol-id> --summary \"<summary>\"`")
|
|
||||||
fmt.Println(" - Loop back to create new wisp, or exit if context high")
|
|
||||||
if patrolID != "" {
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Current patrol ID: %s\n", patrolID)
|
|
||||||
}
|
}
|
||||||
|
outputPatrolContext(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// outputRefineryPatrolContext shows patrol molecule status for the Refinery.
|
// outputRefineryPatrolContext shows patrol molecule status for the Refinery.
|
||||||
// Unlike other patrol roles, Refinery AUTO-BONDS its patrol molecule on startup
|
// Refinery AUTO-BONDS its patrol molecule on startup if one isn't already running.
|
||||||
// if one isn't already running. This ensures the merge queue is always monitored.
|
|
||||||
func outputRefineryPatrolContext(ctx RoleContext) {
|
func outputRefineryPatrolContext(ctx RoleContext) {
|
||||||
fmt.Println()
|
cfg := PatrolConfig{
|
||||||
fmt.Printf("%s\n\n", style.Bold.Render("## 🔧 Refinery Patrol Status"))
|
RoleName: "refinery",
|
||||||
|
PatrolMolName: "mol-refinery-patrol",
|
||||||
// Refinery works from its own rig clone: <rig>/refinery/rig/
|
BeadsDir: ctx.WorkDir,
|
||||||
// Beads are in the current WorkDir
|
Assignee: ctx.Rig + "/refinery",
|
||||||
refineryBeadsDir := ctx.WorkDir
|
HeaderEmoji: "🔧",
|
||||||
|
HeaderTitle: "Refinery Patrol Status",
|
||||||
// Find mol-refinery-patrol molecules (exclude template)
|
CheckInProgress: true,
|
||||||
// Look for in-progress patrol first (resumable)
|
WorkLoopSteps: []string{
|
||||||
cmdList := exec.Command("bd", "--no-daemon", "list", "--status=in_progress", "--type=epic")
|
"Check inbox: `gt mail inbox`",
|
||||||
cmdList.Dir = refineryBeadsDir
|
"Check next step: `bd ready`",
|
||||||
var stdoutList bytes.Buffer
|
"Execute the step (queue scan, process branch, tests, merge)",
|
||||||
cmdList.Stdout = &stdoutList
|
"Close step: `bd close <step-id>`",
|
||||||
cmdList.Stderr = nil
|
"Check next: `bd ready`",
|
||||||
errList := cmdList.Run()
|
"At cycle end (burn-or-loop step):\n - Generate summary of patrol cycle\n - Squash: `bd --no-daemon mol squash <mol-id> --summary \"<summary>\"`\n - Loop back to create new wisp, or exit if context high",
|
||||||
|
},
|
||||||
hasPatrol := false
|
|
||||||
var patrolID string
|
|
||||||
var patrolLine string
|
|
||||||
|
|
||||||
if errList == nil {
|
|
||||||
lines := strings.Split(stdoutList.String(), "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
if strings.Contains(line, "mol-refinery-patrol") && !strings.Contains(line, "[template]") {
|
|
||||||
parts := strings.Fields(line)
|
|
||||||
if len(parts) > 0 {
|
|
||||||
patrolID = parts[0]
|
|
||||||
patrolLine = line
|
|
||||||
hasPatrol = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also check for open patrols with open children (active wisp)
|
|
||||||
if !hasPatrol {
|
|
||||||
cmdOpen := exec.Command("bd", "--no-daemon", "list", "--status=open", "--type=epic")
|
|
||||||
cmdOpen.Dir = refineryBeadsDir
|
|
||||||
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, "mol-refinery-patrol") && !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 = refineryBeadsDir
|
|
||||||
var stdoutShow bytes.Buffer
|
|
||||||
cmdShow.Stdout = &stdoutShow
|
|
||||||
cmdShow.Stderr = nil
|
|
||||||
if cmdShow.Run() == nil {
|
|
||||||
showOutput := stdoutShow.String()
|
|
||||||
if strings.Contains(showOutput, "- open]") || strings.Contains(showOutput, "- in_progress]") {
|
|
||||||
hasPatrol = true
|
|
||||||
patrolID = molID
|
|
||||||
patrolLine = line
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasPatrol {
|
|
||||||
// No active patrol - AUTO-SPAWN one
|
|
||||||
fmt.Println("Status: **No active patrol** - creating mol-refinery-patrol...")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// Find the proto ID for mol-refinery-patrol
|
|
||||||
cmdCatalog := exec.Command("bd", "--no-daemon", "mol", "catalog")
|
|
||||||
cmdCatalog.Dir = refineryBeadsDir
|
|
||||||
var stdoutCatalog bytes.Buffer
|
|
||||||
cmdCatalog.Stdout = &stdoutCatalog
|
|
||||||
cmdCatalog.Stderr = nil
|
|
||||||
|
|
||||||
if cmdCatalog.Run() != nil {
|
|
||||||
fmt.Println(style.Dim.Render("Failed to list molecule catalog. Run `bd mol catalog` to troubleshoot."))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find mol-refinery-patrol in catalog
|
|
||||||
var protoID string
|
|
||||||
catalogLines := strings.Split(stdoutCatalog.String(), "\n")
|
|
||||||
for _, line := range catalogLines {
|
|
||||||
if strings.Contains(line, "mol-refinery-patrol") {
|
|
||||||
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 == "" {
|
|
||||||
fmt.Println(style.Dim.Render("Proto mol-refinery-patrol not found in catalog. Run `bd mol register` first."))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the patrol wisp (step 1: create)
|
|
||||||
refineryAgent := ctx.Rig + "/refinery"
|
|
||||||
cmdSpawn := exec.Command("bd", "--no-daemon", "wisp", "create", protoID)
|
|
||||||
cmdSpawn.Dir = refineryBeadsDir
|
|
||||||
var stdoutSpawn, stderrSpawn bytes.Buffer
|
|
||||||
cmdSpawn.Stdout = &stdoutSpawn
|
|
||||||
cmdSpawn.Stderr = &stderrSpawn
|
|
||||||
|
|
||||||
if err := cmdSpawn.Run(); err != nil {
|
|
||||||
fmt.Printf("Failed to create patrol wisp: %s\n", stderrSpawn.String())
|
|
||||||
fmt.Println(style.Dim.Render("Run manually: bd --no-daemon wisp create " + protoID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the created molecule ID from output
|
|
||||||
spawnOutput := stdoutSpawn.String()
|
|
||||||
|
|
||||||
// Extract molecule ID from output (format: "Root issue: wisp-xxxx" or "gt-xxxx")
|
|
||||||
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 == "" {
|
|
||||||
fmt.Printf("⚠ Created wisp but could not parse ID from output\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pin the wisp to refinery (step 2: assign)
|
|
||||||
cmdPin := exec.Command("bd", "--no-daemon", "update", patrolID, "--status=pinned", "--assignee="+refineryAgent)
|
|
||||||
cmdPin.Dir = refineryBeadsDir
|
|
||||||
if err := cmdPin.Run(); err != nil {
|
|
||||||
fmt.Printf("⚠ Created wisp %s but failed to pin to refinery\n", patrolID)
|
|
||||||
} 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.Println("**Refinery Patrol Work Loop:**")
|
|
||||||
fmt.Println("1. Check inbox: `gt mail inbox`")
|
|
||||||
fmt.Println("2. Check next step: `bd ready`")
|
|
||||||
fmt.Println("3. Execute the step (queue scan, process branch, tests, merge)")
|
|
||||||
fmt.Println("4. Close step: `bd close <step-id>`")
|
|
||||||
fmt.Println("5. Check next: `bd ready`")
|
|
||||||
fmt.Println("6. At cycle end (burn-or-loop step):")
|
|
||||||
fmt.Println(" - Generate summary of patrol cycle")
|
|
||||||
fmt.Println(" - Squash: `bd --no-daemon mol squash <mol-id> --summary \"<summary>\"`")
|
|
||||||
fmt.Println(" - Loop back to create new wisp, or exit if context high")
|
|
||||||
if patrolID != "" {
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("Current patrol ID: %s\n", patrolID)
|
|
||||||
}
|
}
|
||||||
|
outputPatrolContext(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkSlungWork checks for pinned work on the agent's hook.
|
// checkSlungWork checks for pinned work on the agent's hook.
|
||||||
|
|||||||
Reference in New Issue
Block a user