refactor: Move pending spawn tracking to gt spawn pending (hq-466n)
- Moved pending.go from deacon/ to polecat/ package - Changed storage location from deacon/pending.json to spawn/pending.json - Added 'gt spawn pending' subcommand for listing/clearing pending spawns - Deprecated 'gt deacon pending' (still works, shows deprecation notice) This decouples pending spawn observation from the Deacon role - anyone can now observe pending polecats (Mayor, humans, debugging, etc.). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+66
-167
File diff suppressed because one or more lines are too long
+16
-20
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/steveyegge/gastown/internal/deacon"
|
"github.com/steveyegge/gastown/internal/deacon"
|
||||||
|
"github.com/steveyegge/gastown/internal/polecat"
|
||||||
"github.com/steveyegge/gastown/internal/style"
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
"github.com/steveyegge/gastown/internal/tmux"
|
"github.com/steveyegge/gastown/internal/tmux"
|
||||||
"github.com/steveyegge/gastown/internal/workspace"
|
"github.com/steveyegge/gastown/internal/workspace"
|
||||||
@@ -112,21 +113,16 @@ This command is typically called by the daemon during cold startup.`,
|
|||||||
|
|
||||||
var deaconPendingCmd = &cobra.Command{
|
var deaconPendingCmd = &cobra.Command{
|
||||||
Use: "pending [session-to-clear]",
|
Use: "pending [session-to-clear]",
|
||||||
Short: "List pending spawns with captured output (for AI observation)",
|
Short: "[DEPRECATED] Use 'gt spawn pending' instead",
|
||||||
Long: `List pending polecat spawns with their terminal output for AI analysis.
|
Long: `DEPRECATED: Use 'gt spawn pending' instead.
|
||||||
|
|
||||||
This is the ZFC-compliant way for the Deacon (AI) to observe polecats:
|
This command has been moved to 'gt spawn pending' since pending spawn
|
||||||
1. Run 'gt deacon pending' to see pending spawns and their output
|
tracking is not Deacon-specific - anyone might need to observe pending
|
||||||
2. Analyze the output to determine if Claude is ready (look for "> " prompt)
|
polecats (Mayor, humans, debugging, etc.).
|
||||||
3. Run 'gt nudge <session> "Begin."' to trigger ready polecats
|
|
||||||
4. Run 'gt deacon pending <session>' to clear from pending list
|
|
||||||
|
|
||||||
This replaces the regex-based trigger-pending for steady-state operation.
|
The functionality is identical:
|
||||||
The AI makes the readiness judgment, not hardcoded regex.
|
gt spawn pending # List all pending with output
|
||||||
|
gt spawn pending gastown/p-abc123 # Clear specific session from pending`,
|
||||||
Examples:
|
|
||||||
gt deacon pending # List all pending with output
|
|
||||||
gt deacon pending gastown/p-abc123 # Clear specific session from pending`,
|
|
||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
RunE: runDeaconPending,
|
RunE: runDeaconPending,
|
||||||
}
|
}
|
||||||
@@ -364,7 +360,7 @@ func runDeaconTriggerPending(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: Check inbox for new POLECAT_STARTED messages
|
// Step 1: Check inbox for new POLECAT_STARTED messages
|
||||||
pending, err := deacon.CheckInboxForSpawns(townRoot)
|
pending, err := polecat.CheckInboxForSpawns(townRoot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("checking inbox: %w", err)
|
return fmt.Errorf("checking inbox: %w", err)
|
||||||
}
|
}
|
||||||
@@ -377,7 +373,7 @@ func runDeaconTriggerPending(cmd *cobra.Command, args []string) error {
|
|||||||
fmt.Printf("%s Found %d pending spawn(s)\n", style.Bold.Render("●"), len(pending))
|
fmt.Printf("%s Found %d pending spawn(s)\n", style.Bold.Render("●"), len(pending))
|
||||||
|
|
||||||
// Step 2: Try to trigger each pending spawn
|
// Step 2: Try to trigger each pending spawn
|
||||||
results, err := deacon.TriggerPendingSpawns(townRoot, triggerTimeout)
|
results, err := polecat.TriggerPendingSpawns(townRoot, triggerTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("triggering: %w", err)
|
return fmt.Errorf("triggering: %w", err)
|
||||||
}
|
}
|
||||||
@@ -398,7 +394,7 @@ func runDeaconTriggerPending(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Prune stale pending spawns (older than 5 minutes)
|
// Step 3: Prune stale pending spawns (older than 5 minutes)
|
||||||
pruned, _ := deacon.PruneStalePending(townRoot, 5*time.Minute)
|
pruned, _ := polecat.PruneStalePending(townRoot, 5*time.Minute)
|
||||||
if pruned > 0 {
|
if pruned > 0 {
|
||||||
fmt.Printf(" %s Pruned %d stale spawn(s)\n", style.Dim.Render("○"), pruned)
|
fmt.Printf(" %s Pruned %d stale spawn(s)\n", style.Dim.Render("○"), pruned)
|
||||||
}
|
}
|
||||||
@@ -427,7 +423,7 @@ func runDeaconPending(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: Check inbox for new POLECAT_STARTED messages
|
// Step 1: Check inbox for new POLECAT_STARTED messages
|
||||||
pending, err := deacon.CheckInboxForSpawns(townRoot)
|
pending, err := polecat.CheckInboxForSpawns(townRoot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("checking inbox: %w", err)
|
return fmt.Errorf("checking inbox: %w", err)
|
||||||
}
|
}
|
||||||
@@ -491,12 +487,12 @@ func runDeaconPending(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
// clearPendingSession removes a session from the pending list.
|
// clearPendingSession removes a session from the pending list.
|
||||||
func clearPendingSession(townRoot, session string) error {
|
func clearPendingSession(townRoot, session string) error {
|
||||||
pending, err := deacon.LoadPending(townRoot)
|
pending, err := polecat.LoadPending(townRoot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("loading pending: %w", err)
|
return fmt.Errorf("loading pending: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var remaining []*deacon.PendingSpawn
|
var remaining []*polecat.PendingSpawn
|
||||||
found := false
|
found := false
|
||||||
for _, ps := range pending {
|
for _, ps := range pending {
|
||||||
if ps.Session == session {
|
if ps.Session == session {
|
||||||
@@ -510,7 +506,7 @@ func clearPendingSession(townRoot, session string) error {
|
|||||||
return fmt.Errorf("session %s not found in pending list", session)
|
return fmt.Errorf("session %s not found in pending list", session)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := deacon.SavePending(townRoot, remaining); err != nil {
|
if err := polecat.SavePending(townRoot, remaining); err != nil {
|
||||||
return fmt.Errorf("saving pending: %w", err)
|
return fmt.Errorf("saving pending: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/steveyegge/gastown/internal/config"
|
"github.com/steveyegge/gastown/internal/config"
|
||||||
@@ -35,6 +36,9 @@ var (
|
|||||||
spawnMolecule string
|
spawnMolecule string
|
||||||
spawnForce bool
|
spawnForce bool
|
||||||
spawnAccount string
|
spawnAccount string
|
||||||
|
|
||||||
|
// spawn pending flags
|
||||||
|
spawnPendingLines int
|
||||||
)
|
)
|
||||||
|
|
||||||
var spawnCmd = &cobra.Command{
|
var spawnCmd = &cobra.Command{
|
||||||
@@ -44,6 +48,8 @@ var spawnCmd = &cobra.Command{
|
|||||||
Short: "Spawn a polecat with work assignment",
|
Short: "Spawn a polecat with work assignment",
|
||||||
Long: `Spawn a polecat with a work assignment.
|
Long: `Spawn a polecat with a work assignment.
|
||||||
|
|
||||||
|
Use 'gt spawn pending' to view spawns waiting to be triggered.
|
||||||
|
|
||||||
Assigns an issue or task to a polecat and starts a session. If no polecat
|
Assigns an issue or task to a polecat and starts a session. If no polecat
|
||||||
is specified, auto-selects an idle polecat in the rig.
|
is specified, auto-selects an idle polecat in the rig.
|
||||||
|
|
||||||
@@ -67,6 +73,27 @@ Examples:
|
|||||||
RunE: runSpawn,
|
RunE: runSpawn,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var spawnPendingCmd = &cobra.Command{
|
||||||
|
Use: "pending [session-to-clear]",
|
||||||
|
Short: "List pending spawns with captured output (for AI observation)",
|
||||||
|
Long: `List pending polecat spawns with their terminal output for AI analysis.
|
||||||
|
|
||||||
|
This shows spawns waiting to be triggered (Claude is still initializing).
|
||||||
|
The terminal output helps determine if Claude is ready.
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
1. Run 'gt spawn pending' to see pending spawns and their output
|
||||||
|
2. Analyze the output to determine if Claude is ready (look for "> " prompt)
|
||||||
|
3. Run 'gt nudge <session> "Begin."' to trigger ready polecats
|
||||||
|
4. Run 'gt spawn pending <session>' to clear from pending list
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
gt spawn pending # List all pending with output
|
||||||
|
gt spawn pending gastown/p-abc123 # Clear specific session from pending`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
RunE: runSpawnPending,
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
spawnCmd.Flags().StringVar(&spawnIssue, "issue", "", "Beads issue ID to assign")
|
spawnCmd.Flags().StringVar(&spawnIssue, "issue", "", "Beads issue ID to assign")
|
||||||
spawnCmd.Flags().StringVarP(&spawnMessage, "message", "m", "", "Free-form task description")
|
spawnCmd.Flags().StringVarP(&spawnMessage, "message", "m", "", "Free-form task description")
|
||||||
@@ -78,6 +105,11 @@ func init() {
|
|||||||
spawnCmd.Flags().BoolVar(&spawnForce, "force", false, "Force spawn even if polecat has unread mail")
|
spawnCmd.Flags().BoolVar(&spawnForce, "force", false, "Force spawn even if polecat has unread mail")
|
||||||
spawnCmd.Flags().StringVar(&spawnAccount, "account", "", "Claude Code account handle to use (overrides default)")
|
spawnCmd.Flags().StringVar(&spawnAccount, "account", "", "Claude Code account handle to use (overrides default)")
|
||||||
|
|
||||||
|
// spawn pending flags
|
||||||
|
spawnPendingCmd.Flags().IntVarP(&spawnPendingLines, "lines", "n", 15,
|
||||||
|
"Number of terminal lines to capture per session")
|
||||||
|
|
||||||
|
spawnCmd.AddCommand(spawnPendingCmd)
|
||||||
rootCmd.AddCommand(spawnCmd)
|
rootCmd.AddCommand(spawnCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -741,3 +773,108 @@ func buildWorkAssignmentMail(issue *BeadsIssue, message, polecatAddress string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runSpawnPending shows pending spawns with captured output for AI observation.
|
||||||
|
// This is the ZFC-compliant way to observe polecats waiting to be triggered.
|
||||||
|
func runSpawnPending(cmd *cobra.Command, args []string) error {
|
||||||
|
townRoot, err := workspace.FindFromCwdOrError()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("not in a Gas Town workspace: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If session argument provided, clear it from pending
|
||||||
|
if len(args) == 1 {
|
||||||
|
return clearSpawnPending(townRoot, args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Check inbox for new POLECAT_STARTED messages
|
||||||
|
pending, err := polecat.CheckInboxForSpawns(townRoot)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("checking inbox: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pending) == 0 {
|
||||||
|
fmt.Printf("%s No pending spawns\n", style.Dim.Render("○"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t := tmux.NewTmux()
|
||||||
|
|
||||||
|
fmt.Printf("%s Pending spawns (%d):\n\n", style.Bold.Render("●"), len(pending))
|
||||||
|
|
||||||
|
for i, ps := range pending {
|
||||||
|
// Check if session still exists
|
||||||
|
running, err := t.HasSession(ps.Session)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Session: %s\n", ps.Session)
|
||||||
|
fmt.Printf(" Status: error checking session: %v\n\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !running {
|
||||||
|
fmt.Printf("Session: %s\n", ps.Session)
|
||||||
|
fmt.Printf(" Status: session no longer exists\n\n")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture terminal output for AI analysis
|
||||||
|
output, err := t.CapturePane(ps.Session, spawnPendingLines)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Session: %s\n", ps.Session)
|
||||||
|
fmt.Printf(" Status: error capturing output: %v\n\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print session info
|
||||||
|
fmt.Printf("Session: %s\n", ps.Session)
|
||||||
|
fmt.Printf(" Rig: %s\n", ps.Rig)
|
||||||
|
fmt.Printf(" Polecat: %s\n", ps.Polecat)
|
||||||
|
if ps.Issue != "" {
|
||||||
|
fmt.Printf(" Issue: %s\n", ps.Issue)
|
||||||
|
}
|
||||||
|
fmt.Printf(" Spawned: %s ago\n", time.Since(ps.SpawnedAt).Round(time.Second))
|
||||||
|
fmt.Printf(" Terminal output (last %d lines):\n", spawnPendingLines)
|
||||||
|
fmt.Println(strings.Repeat("─", 50))
|
||||||
|
fmt.Println(output)
|
||||||
|
fmt.Println(strings.Repeat("─", 50))
|
||||||
|
|
||||||
|
if i < len(pending)-1 {
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%s To trigger a ready polecat:\n", style.Dim.Render("→"))
|
||||||
|
fmt.Printf(" gt nudge <session> \"Begin.\"\n")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearSpawnPending removes a session from the pending list.
|
||||||
|
func clearSpawnPending(townRoot, session string) error {
|
||||||
|
pending, err := polecat.LoadPending(townRoot)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading pending: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var remaining []*polecat.PendingSpawn
|
||||||
|
found := false
|
||||||
|
for _, ps := range pending {
|
||||||
|
if ps.Session == session {
|
||||||
|
found = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
remaining = append(remaining, ps)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("session %s not found in pending list", session)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := polecat.SavePending(townRoot, remaining); err != nil {
|
||||||
|
return fmt.Errorf("saving pending: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s Cleared %s from pending list\n", style.Bold.Render("✓"), session)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Package deacon provides the Deacon agent infrastructure.
|
// Package polecat provides polecat lifecycle management.
|
||||||
package deacon
|
package polecat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -36,7 +36,7 @@ type PendingSpawn struct {
|
|||||||
|
|
||||||
// PendingFile returns the path to the pending spawns file.
|
// PendingFile returns the path to the pending spawns file.
|
||||||
func PendingFile(townRoot string) string {
|
func PendingFile(townRoot string) string {
|
||||||
return filepath.Join(townRoot, "deacon", "pending.json")
|
return filepath.Join(townRoot, "spawn", "pending.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadPending loads the pending spawns from disk.
|
// LoadPending loads the pending spawns from disk.
|
||||||
Reference in New Issue
Block a user