fix: add gt witness process command to invoke polecat cleanup handlers (gt-h3gzj)
The Witness handlers (HandlePolecatDone, HandleMerged, etc.) existed in Go code but were never called - there was no CLI command to invoke them. This caused polecats to remain in 'done' state after MR merge because POLECAT_DONE messages were never processed. Changes: - Add `gt witness process <rig>` command to process Witness mail - Fix --wisp flag to --ephemeral in cleanup wisp creation - Command processes POLECAT_DONE, MERGED, HELP, SWARM_START messages - Auto-nukes clean polecats, creates cleanup wisps for dirty ones 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
74409dc32b
commit
a3bccc881b
+225
-2
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/steveyegge/gastown/internal/claude"
|
"github.com/steveyegge/gastown/internal/claude"
|
||||||
"github.com/steveyegge/gastown/internal/config"
|
"github.com/steveyegge/gastown/internal/config"
|
||||||
"github.com/steveyegge/gastown/internal/constants"
|
"github.com/steveyegge/gastown/internal/constants"
|
||||||
|
"github.com/steveyegge/gastown/internal/mail"
|
||||||
"github.com/steveyegge/gastown/internal/rig"
|
"github.com/steveyegge/gastown/internal/rig"
|
||||||
"github.com/steveyegge/gastown/internal/session"
|
"github.com/steveyegge/gastown/internal/session"
|
||||||
"github.com/steveyegge/gastown/internal/style"
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
@@ -22,8 +23,9 @@ import (
|
|||||||
|
|
||||||
// Witness command flags
|
// Witness command flags
|
||||||
var (
|
var (
|
||||||
witnessForeground bool
|
witnessForeground bool
|
||||||
witnessStatusJSON bool
|
witnessStatusJSON bool
|
||||||
|
witnessProcessJSON bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var witnessCmd = &cobra.Command{
|
var witnessCmd = &cobra.Command{
|
||||||
@@ -105,6 +107,30 @@ Examples:
|
|||||||
RunE: runWitnessRestart,
|
RunE: runWitnessRestart,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var witnessProcessCmd = &cobra.Command{
|
||||||
|
Use: "process <rig>",
|
||||||
|
Short: "Process witness mail",
|
||||||
|
Long: `Process protocol messages in the Witness's mailbox.
|
||||||
|
|
||||||
|
Reads unread messages and handles each based on protocol type:
|
||||||
|
|
||||||
|
POLECAT_DONE - Auto-nuke if clean, create cleanup wisp if dirty
|
||||||
|
LIFECYCLE:Shutdown - Auto-nuke if clean
|
||||||
|
MERGED - Verify and complete cleanup
|
||||||
|
MERGE_FAILED - Notify polecat of failure
|
||||||
|
HELP - Assess and escalate if needed
|
||||||
|
SWARM_START - Initialize swarm tracking
|
||||||
|
|
||||||
|
This command invokes the Go handlers that perform the actual cleanup
|
||||||
|
operations (killing tmux sessions, removing worktrees, etc.).
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
gt witness process gastown
|
||||||
|
gt witness process gastown --json`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runWitnessProcess,
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Start flags
|
// Start flags
|
||||||
witnessStartCmd.Flags().BoolVar(&witnessForeground, "foreground", false, "Run in foreground (default: background)")
|
witnessStartCmd.Flags().BoolVar(&witnessForeground, "foreground", false, "Run in foreground (default: background)")
|
||||||
@@ -112,12 +138,16 @@ func init() {
|
|||||||
// Status flags
|
// Status flags
|
||||||
witnessStatusCmd.Flags().BoolVar(&witnessStatusJSON, "json", false, "Output as JSON")
|
witnessStatusCmd.Flags().BoolVar(&witnessStatusJSON, "json", false, "Output as JSON")
|
||||||
|
|
||||||
|
// Process flags
|
||||||
|
witnessProcessCmd.Flags().BoolVar(&witnessProcessJSON, "json", false, "Output as JSON")
|
||||||
|
|
||||||
// Add subcommands
|
// Add subcommands
|
||||||
witnessCmd.AddCommand(witnessStartCmd)
|
witnessCmd.AddCommand(witnessStartCmd)
|
||||||
witnessCmd.AddCommand(witnessStopCmd)
|
witnessCmd.AddCommand(witnessStopCmd)
|
||||||
witnessCmd.AddCommand(witnessRestartCmd)
|
witnessCmd.AddCommand(witnessRestartCmd)
|
||||||
witnessCmd.AddCommand(witnessStatusCmd)
|
witnessCmd.AddCommand(witnessStatusCmd)
|
||||||
witnessCmd.AddCommand(witnessAttachCmd)
|
witnessCmd.AddCommand(witnessAttachCmd)
|
||||||
|
witnessCmd.AddCommand(witnessProcessCmd)
|
||||||
|
|
||||||
rootCmd.AddCommand(witnessCmd)
|
rootCmd.AddCommand(witnessCmd)
|
||||||
}
|
}
|
||||||
@@ -458,3 +488,196 @@ func runWitnessRestart(cmd *cobra.Command, args []string) error {
|
|||||||
fmt.Printf(" %s\n", style.Dim.Render("Use 'gt witness attach' to connect"))
|
fmt.Printf(" %s\n", style.Dim.Render("Use 'gt witness attach' to connect"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WitnessProcessResult tracks the result of processing witness mail.
|
||||||
|
type WitnessProcessResult struct {
|
||||||
|
MessageID string `json:"message_id"`
|
||||||
|
ProtocolType witness.ProtocolType `json:"protocol_type"`
|
||||||
|
From string `json:"from"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Handled bool `json:"handled"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
WispCreated string `json:"wisp_created,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWitnessProcess(cmd *cobra.Command, args []string) error {
|
||||||
|
rigName := args[0]
|
||||||
|
|
||||||
|
townRoot, err := workspace.FindFromCwdOrError()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("not in a Gas Town workspace: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify rig exists
|
||||||
|
_, r, err := getWitnessManager(rigName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get witness mailbox
|
||||||
|
witnessAddr := fmt.Sprintf("%s/witness", rigName)
|
||||||
|
router := mail.NewRouter(townRoot)
|
||||||
|
mailbox, err := router.GetMailbox(witnessAddr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting witness mailbox: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get unread messages
|
||||||
|
messages, err := mailbox.ListUnread()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listing unread messages: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(messages) == 0 {
|
||||||
|
if witnessProcessJSON {
|
||||||
|
fmt.Println("[]")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s No pending messages\n", style.Dim.Render("○"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !witnessProcessJSON {
|
||||||
|
fmt.Printf("%s Processing %d message(s) for %s\n", style.Bold.Render("●"), len(messages), rigName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []WitnessProcessResult
|
||||||
|
for _, msg := range messages {
|
||||||
|
result := processWitnessMessage(townRoot, r.Path, rigName, msg, router)
|
||||||
|
results = append(results, result)
|
||||||
|
|
||||||
|
if !witnessProcessJSON {
|
||||||
|
// Print result
|
||||||
|
if result.Error != "" {
|
||||||
|
fmt.Printf(" %s [%s] %s: %s\n",
|
||||||
|
style.Error.Render("✗"),
|
||||||
|
result.ProtocolType,
|
||||||
|
msg.Subject,
|
||||||
|
result.Error)
|
||||||
|
} else if result.Handled {
|
||||||
|
fmt.Printf(" %s [%s] %s\n",
|
||||||
|
style.Bold.Render("✓"),
|
||||||
|
result.ProtocolType,
|
||||||
|
result.Action)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" %s [%s] %s\n",
|
||||||
|
style.Dim.Render("○"),
|
||||||
|
result.ProtocolType,
|
||||||
|
result.Action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archive handled messages
|
||||||
|
if result.Handled && result.Error == "" {
|
||||||
|
_ = mailbox.Delete(msg.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output
|
||||||
|
if witnessProcessJSON {
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
return enc.Encode(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
handled := 0
|
||||||
|
errors := 0
|
||||||
|
for _, r := range results {
|
||||||
|
if r.Handled {
|
||||||
|
handled++
|
||||||
|
}
|
||||||
|
if r.Error != "" {
|
||||||
|
errors++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%s Processed %d/%d messages",
|
||||||
|
style.Bold.Render("✓"), handled, len(results))
|
||||||
|
if errors > 0 {
|
||||||
|
fmt.Printf(" (%d errors)", errors)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processWitnessMessage handles a single protocol message and returns the result.
|
||||||
|
func processWitnessMessage(townRoot, rigPath, rigName string, msg *mail.Message, router *mail.Router) WitnessProcessResult {
|
||||||
|
result := WitnessProcessResult{
|
||||||
|
MessageID: msg.ID,
|
||||||
|
From: msg.From,
|
||||||
|
Subject: msg.Subject,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classify the message
|
||||||
|
result.ProtocolType = witness.ClassifyMessage(msg.Subject)
|
||||||
|
|
||||||
|
// Handle based on type
|
||||||
|
switch result.ProtocolType {
|
||||||
|
case witness.ProtoPolecatDone:
|
||||||
|
handlerResult := witness.HandlePolecatDone(rigPath, rigName, msg)
|
||||||
|
result.Handled = handlerResult.Handled
|
||||||
|
result.Action = handlerResult.Action
|
||||||
|
result.WispCreated = handlerResult.WispCreated
|
||||||
|
if handlerResult.Error != nil {
|
||||||
|
result.Error = handlerResult.Error.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
case witness.ProtoLifecycleShutdown:
|
||||||
|
handlerResult := witness.HandleLifecycleShutdown(rigPath, rigName, msg)
|
||||||
|
result.Handled = handlerResult.Handled
|
||||||
|
result.Action = handlerResult.Action
|
||||||
|
result.WispCreated = handlerResult.WispCreated
|
||||||
|
if handlerResult.Error != nil {
|
||||||
|
result.Error = handlerResult.Error.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
case witness.ProtoMerged:
|
||||||
|
handlerResult := witness.HandleMerged(rigPath, rigName, msg)
|
||||||
|
result.Handled = handlerResult.Handled
|
||||||
|
result.Action = handlerResult.Action
|
||||||
|
result.WispCreated = handlerResult.WispCreated
|
||||||
|
if handlerResult.Error != nil {
|
||||||
|
result.Error = handlerResult.Error.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
case witness.ProtoMergeFailed:
|
||||||
|
handlerResult := witness.HandleMergeFailed(rigPath, rigName, msg, router)
|
||||||
|
result.Handled = handlerResult.Handled
|
||||||
|
result.Action = handlerResult.Action
|
||||||
|
if handlerResult.Error != nil {
|
||||||
|
result.Error = handlerResult.Error.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
case witness.ProtoHelp:
|
||||||
|
handlerResult := witness.HandleHelp(rigPath, rigName, msg, router)
|
||||||
|
result.Handled = handlerResult.Handled
|
||||||
|
result.Action = handlerResult.Action
|
||||||
|
if handlerResult.Error != nil {
|
||||||
|
result.Error = handlerResult.Error.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
case witness.ProtoSwarmStart:
|
||||||
|
handlerResult := witness.HandleSwarmStart(rigPath, msg)
|
||||||
|
result.Handled = handlerResult.Handled
|
||||||
|
result.Action = handlerResult.Action
|
||||||
|
result.WispCreated = handlerResult.WispCreated
|
||||||
|
if handlerResult.Error != nil {
|
||||||
|
result.Error = handlerResult.Error.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
case witness.ProtoHandoff:
|
||||||
|
// Handoff messages are handled by the Claude agent reading them, not by Go code
|
||||||
|
result.Handled = false
|
||||||
|
result.Action = "handoff message - read by Claude agent, not processed here"
|
||||||
|
|
||||||
|
default:
|
||||||
|
result.Handled = false
|
||||||
|
result.Action = "unknown message type, skipped"
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -397,7 +397,7 @@ func createCleanupWisp(workDir, polecatName, issueID, branch string) (string, er
|
|||||||
labels := strings.Join(CleanupWispLabels(polecatName, "pending"), ",")
|
labels := strings.Join(CleanupWispLabels(polecatName, "pending"), ",")
|
||||||
|
|
||||||
output, err := util.ExecWithOutput(workDir, "bd", "create",
|
output, err := util.ExecWithOutput(workDir, "bd", "create",
|
||||||
"--wisp",
|
"--ephemeral",
|
||||||
"--title", title,
|
"--title", title,
|
||||||
"--description", description,
|
"--description", description,
|
||||||
"--labels", labels,
|
"--labels", labels,
|
||||||
@@ -431,7 +431,7 @@ func createSwarmWisp(workDir string, payload *SwarmStartPayload) (string, error)
|
|||||||
labels := strings.Join(SwarmWispLabels(payload.SwarmID, payload.Total, 0, payload.StartedAt), ",")
|
labels := strings.Join(SwarmWispLabels(payload.SwarmID, payload.Total, 0, payload.StartedAt), ",")
|
||||||
|
|
||||||
output, err := util.ExecWithOutput(workDir, "bd", "create",
|
output, err := util.ExecWithOutput(workDir, "bd", "create",
|
||||||
"--wisp",
|
"--ephemeral",
|
||||||
"--title", title,
|
"--title", title,
|
||||||
"--description", description,
|
"--description", description,
|
||||||
"--labels", labels,
|
"--labels", labels,
|
||||||
@@ -450,7 +450,7 @@ func createSwarmWisp(workDir string, payload *SwarmStartPayload) (string, error)
|
|||||||
// findCleanupWisp finds an existing cleanup wisp for a polecat.
|
// findCleanupWisp finds an existing cleanup wisp for a polecat.
|
||||||
func findCleanupWisp(workDir, polecatName string) (string, error) {
|
func findCleanupWisp(workDir, polecatName string) (string, error) {
|
||||||
output, err := util.ExecWithOutput(workDir, "bd", "list",
|
output, err := util.ExecWithOutput(workDir, "bd", "list",
|
||||||
"--wisp",
|
"--ephemeral",
|
||||||
"--labels", fmt.Sprintf("polecat:%s,state:merge-requested", polecatName),
|
"--labels", fmt.Sprintf("polecat:%s,state:merge-requested", polecatName),
|
||||||
"--status", "open",
|
"--status", "open",
|
||||||
"--json",
|
"--json",
|
||||||
|
|||||||
Reference in New Issue
Block a user