From 7edd75021badd277a460ca6bd1aac14cb40f39cd Mon Sep 17 00:00:00 2001 From: gastown/crew/jack Date: Tue, 6 Jan 2026 13:33:12 -0800 Subject: [PATCH] fix: revert heretical gt witness process command, update formula for ZFC (gt-h3gzj) The previous commit (a3bccc8) violated ZFC by implementing molecule step logic in Go handlers. Per PRIMING.md: Agent decides. Go transports. This commit: 1. Reverts the gt witness process command (Go code should not make decisions) 2. Updates mol-witness-patrol formula with explicit CLI commands 3. Fixes --wisp to --ephemeral (bd create flag correction) 4. Removes --wisp from bd list calls (invalid flag) The Witness Claude agent now has explicit instructions: - Parse POLECAT_DONE message for polecat name - Check cleanup_status via bd show - Run gt polecat nuke or bd create --ephemeral based on status - Archive mail after handling ZFC: Agent decides. Go transports. --- internal/cmd/witness.go | 227 +----------------- .../formulas/mol-witness-patrol.formula.toml | 10 +- 2 files changed, 7 insertions(+), 230 deletions(-) diff --git a/internal/cmd/witness.go b/internal/cmd/witness.go index ee7066c4..d68d3a46 100644 --- a/internal/cmd/witness.go +++ b/internal/cmd/witness.go @@ -12,7 +12,6 @@ import ( "github.com/steveyegge/gastown/internal/claude" "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/constants" - "github.com/steveyegge/gastown/internal/mail" "github.com/steveyegge/gastown/internal/rig" "github.com/steveyegge/gastown/internal/session" "github.com/steveyegge/gastown/internal/style" @@ -23,9 +22,8 @@ import ( // Witness command flags var ( - witnessForeground bool - witnessStatusJSON bool - witnessProcessJSON bool + witnessForeground bool + witnessStatusJSON bool ) var witnessCmd = &cobra.Command{ @@ -107,30 +105,6 @@ Examples: RunE: runWitnessRestart, } -var witnessProcessCmd = &cobra.Command{ - Use: "process ", - 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() { // Start flags witnessStartCmd.Flags().BoolVar(&witnessForeground, "foreground", false, "Run in foreground (default: background)") @@ -138,16 +112,12 @@ func init() { // Status flags witnessStatusCmd.Flags().BoolVar(&witnessStatusJSON, "json", false, "Output as JSON") - // Process flags - witnessProcessCmd.Flags().BoolVar(&witnessProcessJSON, "json", false, "Output as JSON") - // Add subcommands witnessCmd.AddCommand(witnessStartCmd) witnessCmd.AddCommand(witnessStopCmd) witnessCmd.AddCommand(witnessRestartCmd) witnessCmd.AddCommand(witnessStatusCmd) witnessCmd.AddCommand(witnessAttachCmd) - witnessCmd.AddCommand(witnessProcessCmd) rootCmd.AddCommand(witnessCmd) } @@ -488,196 +458,3 @@ func runWitnessRestart(cmd *cobra.Command, args []string) error { fmt.Printf(" %s\n", style.Dim.Render("Use 'gt witness attach' to connect")) 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 -} diff --git a/internal/formula/formulas/mol-witness-patrol.formula.toml b/internal/formula/formulas/mol-witness-patrol.formula.toml index 12c612b9..2be2aa7a 100644 --- a/internal/formula/formulas/mol-witness-patrol.formula.toml +++ b/internal/formula/formulas/mol-witness-patrol.formula.toml @@ -3,12 +3,12 @@ formula = 'mol-witness-patrol' version = 2 [[steps]] -description = "Check inbox and handle messages.\n\n```bash\ngt mail inbox\n```\n\nFor each message:\n\n**POLECAT_STARTED**:\nA new polecat has started working. Acknowledge and archive.\n```bash\n# Acknowledge startup (optional: log for activity tracking)\ngt mail archive \n```\nNo action needed beyond acknowledgment - archive immediately.\n\n**POLECAT_DONE / LIFECYCLE:Shutdown**:\n\n*EPHEMERAL MODEL*: Polecats are truly ephemeral - done at MR submission,\nrecyclable immediately. Once the branch is pushed (cleanup_status=clean),\nthe polecat can be nuked. The MR lifecycle continues independently in the\nRefinery. If conflicts arise, Refinery creates a NEW conflict-resolution\ntask for a NEW polecat.\n\nPolecat lifecycle: spawning → working → mr_submitted → nuked\nMR lifecycle: created → queued → processed → merged (handled by Refinery)\n\nThe handler (HandlePolecatDone) will:\n1. Check cleanup_status from agent bead\n2. If \"clean\" (branch pushed): AUTO-NUKE immediately, archive mail\n3. If dirty: Create cleanup wisp for manual intervention\n\n```bash\n# The handler does this automatically:\n# - For clean state: gt polecat nuke → archive mail\n# - For dirty state: create wisp → process in next step\n```\n\nCleanup wisps are only created when something is wrong (uncommitted changes,\nunpushed commits). Most POLECAT_DONE messages result in immediate nuke.\n\n**MERGED**:\nA branch was merged successfully. This is informational in the ephemeral model\nsince the polecat was already nuked after MR submission.\n\nIf a cleanup wisp exists (dirty state), complete the cleanup:\n```bash\n# Find the cleanup wisp for this polecat\nbd list --wisp --labels=polecat:,state:merge-requested --status=open\n\n# If found, proceed with full polecat nuke:\ngt polecat nuke \n\n# Burn the cleanup wisp\nbd close \n```\nArchive after cleanup is complete.\n\n**HELP / Blocked**:\nAssess the request. Can you help? If not, escalate to Mayor:\n```bash\ngt mail send mayor/ -s \"Escalation: needs help\" -m \"
\"\n```\nArchive after handling (escalated or resolved):\n```bash\ngt mail archive \n```\n\n**HANDOFF**:\nRead predecessor context. Continue from where they left off.\nArchive after absorbing context:\n```bash\ngt mail archive \n```\n\n**SWARM_START**:\nMayor initiating batch polecat work. Initialize swarm tracking.\n```bash\n# Parse swarm info from mail body: {\"swarm_id\": \"batch-123\", \"beads\": [\"bd-a\", \"bd-b\"]}\nbd create --wisp --title \"swarm:\" --description \"Tracking batch: \" --labels swarm,swarm_id:,total:,completed:0,start:\n```\nArchive after creating swarm tracking wisp:\n```bash\ngt mail archive \n```\n\n**Hygiene principle**: Archive messages after they're fully processed.\nKeep only: active work, unprocessed requests. Inbox should be near-empty." +description = "Check inbox and handle messages.\n\n```bash\ngt mail inbox\n```\n\nFor each message:\n\n**POLECAT_STARTED**:\nA new polecat has started working. Acknowledge and archive.\n```bash\n# Acknowledge startup (optional: log for activity tracking)\ngt mail archive \n```\nNo action needed beyond acknowledgment - archive immediately.\n\n**POLECAT_DONE / LIFECYCLE:Shutdown**:\n\n*EPHEMERAL MODEL*: Polecats are truly ephemeral - done at MR submission,\nrecyclable immediately. Once the branch is pushed (cleanup_status=clean),\nthe polecat can be nuked. The MR lifecycle continues independently in the\nRefinery. If conflicts arise, Refinery creates a NEW conflict-resolution\ntask for a NEW polecat.\n\nPolecat lifecycle: spawning → working → mr_submitted → nuked\nMR lifecycle: created → queued → processed → merged (handled by Refinery)\n\n**YOU execute these steps** (ZFC: agent decides, Go transports):\n\n1. **Parse the message** - extract polecat name from subject (POLECAT_DONE )\n\n2. **Check cleanup_status** from the polecat's agent bead:\n```bash\n# Get the agent bead ID (format: -agent--polecat-)\nbd show gt-agent--polecat- --json 2>/dev/null | grep cleanup_status\n```\n\n3. **If cleanup_status=clean** (branch pushed, safe to nuke):\n```bash\ngt polecat nuke \ngt mail archive \n```\n\n4. **If cleanup_status is dirty** (has_uncommitted, has_unpushed, has_stash):\n```bash\n# Create cleanup wisp for manual intervention\nbd create --ephemeral --title \"cleanup:\" \\\n --description \"Polecat needs cleanup. Status: \" \\\n --labels \"cleanup,polecat:,state:pending\"\n# Don't archive yet - will be handled in process-cleanups step\n```\n\n5. **If no agent bead found** (polecat may not exist):\n```bash\n# Try to nuke anyway - gt polecat nuke will fail safely if doesn't exist\ngt polecat nuke \ngt mail archive \n```\n\nCleanup wisps are only created when something is wrong (uncommitted changes,\nunpushed commits). Most POLECAT_DONE messages result in immediate nuke.\n\n**MERGED**:\nA branch was merged successfully. This is informational in the ephemeral model\nsince the polecat was already nuked after MR submission.\n\nIf a cleanup wisp exists (dirty state), complete the cleanup:\n```bash\n# Find the cleanup wisp for this polecat\nbd list --labels=polecat:,state:merge-requested --status=open\n\n# If found, proceed with full polecat nuke:\ngt polecat nuke \n\n# Burn the cleanup wisp\nbd close \n```\nArchive after cleanup is complete.\n\n**HELP / Blocked**:\nAssess the request. Can you help? If not, escalate to Mayor:\n```bash\ngt mail send mayor/ -s \"Escalation: needs help\" -m \"
\"\n```\nArchive after handling (escalated or resolved):\n```bash\ngt mail archive \n```\n\n**HANDOFF**:\nRead predecessor context. Continue from where they left off.\nArchive after absorbing context:\n```bash\ngt mail archive \n```\n\n**SWARM_START**:\nMayor initiating batch polecat work. Initialize swarm tracking.\n```bash\n# Parse swarm info from mail body: {\"swarm_id\": \"batch-123\", \"beads\": [\"bd-a\", \"bd-b\"]}\nbd create --ephemeral --title \"swarm:\" --description \"Tracking batch: \" --labels swarm,swarm_id:,total:,completed:0,start:\n```\nArchive after creating swarm tracking wisp:\n```bash\ngt mail archive \n```\n\n**Hygiene principle**: Archive messages after they're fully processed.\nKeep only: active work, unprocessed requests. Inbox should be near-empty." id = 'inbox-check' title = 'Process witness mail' [[steps]] -description = "Process cleanup wisps (exception handling for dirty polecats).\n\nIn the ephemeral model, cleanup wisps are only created when a polecat has\ndirty state (uncommitted changes, unpushed commits) that prevented immediate\nnuke. Most polecats are nuked immediately on POLECAT_DONE and never create wisps.\n\n```bash\n# Find all cleanup wisps\nbd list --wisp --labels=cleanup --status=open\n```\n\nIf no wisps, skip this step (most common case in ephemeral model).\n\nFor each cleanup wisp, investigate and resolve the dirty state:\n\n## State: pending (needs investigation)\n\n1. **Extract polecat name** from wisp title/labels\n\n2. **Diagnose the problem**:\n```bash\ncd polecats/\ngit status # What's uncommitted?\ngit stash list # Any stashed work?\ngit log origin/main..HEAD # Any unpushed commits?\n```\n\n3. **Resolution options**:\n - **Uncommitted changes**: Commit and push, then nuke\n - **Stashed work**: Pop and commit, or discard if not valuable\n - **Unpushed commits**: Push to origin, then nuke\n - **All valuable work lost**: Escalate to Mayor for recovery\n\n4. **If resolvable locally**: Fix and nuke\n```bash\n# Example: push unpushed commits\ngit push origin HEAD\n\n# Then nuke\ngt polecat nuke \n\n# Close the wisp\nbd close --reason \"Resolved: pushed commits, nuked\"\n```\n\n5. **If needs escalation**: Send RECOVERY_NEEDED to Mayor\n```bash\ngt mail send mayor/ -s \"RECOVERY_NEEDED /\" \\\n -m \"Cleanup Status: \nBranch: \nIssue: \n\nCannot auto-resolve. Please advise.\"\n```\nLeave wisp open until Mayor resolves.\n\n## State: merge-requested (legacy, rare)\n\nThis state was used before the ephemeral model. If found, the polecat is\nwaiting for a MERGED signal. The inbox-check step handles these.\n\n**Parallelism**: Use Task tool subagents to process multiple cleanups concurrently.\nEach cleanup is independent - perfect for parallel execution." +description = "Process cleanup wisps (exception handling for dirty polecats).\n\nIn the ephemeral model, cleanup wisps are only created when a polecat has\ndirty state (uncommitted changes, unpushed commits) that prevented immediate\nnuke. Most polecats are nuked immediately on POLECAT_DONE and never create wisps.\n\n```bash\n# Find all cleanup wisps\nbd list --labels=cleanup --status=open\n```\n\nIf no wisps, skip this step (most common case in ephemeral model).\n\nFor each cleanup wisp, investigate and resolve the dirty state:\n\n## State: pending (needs investigation)\n\n1. **Extract polecat name** from wisp title/labels\n\n2. **Diagnose the problem**:\n```bash\ncd polecats/\ngit status # What's uncommitted?\ngit stash list # Any stashed work?\ngit log origin/main..HEAD # Any unpushed commits?\n```\n\n3. **Resolution options**:\n - **Uncommitted changes**: Commit and push, then nuke\n - **Stashed work**: Pop and commit, or discard if not valuable\n - **Unpushed commits**: Push to origin, then nuke\n - **All valuable work lost**: Escalate to Mayor for recovery\n\n4. **If resolvable locally**: Fix and nuke\n```bash\n# Example: push unpushed commits\ngit push origin HEAD\n\n# Then nuke\ngt polecat nuke \n\n# Close the wisp\nbd close --reason \"Resolved: pushed commits, nuked\"\n```\n\n5. **If needs escalation**: Send RECOVERY_NEEDED to Mayor\n```bash\ngt mail send mayor/ -s \"RECOVERY_NEEDED /\" \\\n -m \"Cleanup Status: \nBranch: \nIssue: \n\nCannot auto-resolve. Please advise.\"\n```\nLeave wisp open until Mayor resolves.\n\n## State: merge-requested (legacy, rare)\n\nThis state was used before the ephemeral model. If found, the polecat is\nwaiting for a MERGED signal. The inbox-check step handles these.\n\n**Parallelism**: Use Task tool subagents to process multiple cleanups concurrently.\nEach cleanup is independent - perfect for parallel execution." id = 'process-cleanups' needs = ['inbox-check'] title = 'Process pending cleanup wisps' @@ -20,7 +20,7 @@ needs = ['process-cleanups'] title = 'Ensure refinery is alive' [[steps]] -description = "Survey all polecats using agent beads (ZFC: trust what agents report).\n\n**Step 1: List polecat agent beads**\n\n```bash\nbd list --type=agent --json\n```\n\nFilter the JSON output for entries where description contains `role_type: polecat`.\nEach polecat agent bead has fields in its description:\n- `role_type: polecat`\n- `rig: `\n- `agent_state: running|idle|stuck|done`\n- `hook_bead: `\n\n**Step 2: For each polecat, check agent_state**\n\n| agent_state | Meaning | Action |\n|-------------|---------|--------|\n| running | Actively working | Check progress (Step 3) |\n| idle | No work assigned | Auto-nuke if clean (Step 3a) |\n| stuck | Self-reported stuck | Handle stuck protocol |\n| done | Work complete | Verify cleanup triggered (see Step 4a) |\n\n**Step 3: For running polecats, assess progress**\n\nCheck the hook_bead field to see what they're working on:\n```bash\nbd show # See current step/issue\n```\n\nYou can also verify they're responsive:\n```bash\ntmux capture-pane -t gt-- -p | tail -20\n```\n\nLook for:\n- Recent tool activity → making progress\n- Idle at prompt → may need nudge\n- Error messages → may need help\n\n**Step 3a: For idle polecats, auto-nuke if clean**\n\nWhen agent_state=idle, the polecat has no work assigned. Check if it's safe to nuke:\n\n```bash\n# Check git status in the polecat's worktree\ncd polecats/\ngit status --porcelain # Should be empty (clean)\ngit log origin/main..HEAD # Should have no unpushed commits\n```\n\n**If clean** (no uncommitted changes, no unpushed commits):\n```bash\n# Safe to nuke - no work to lose\ngt polecat nuke \n```\nLog the auto-nuke for audit purposes. No escalation needed.\n\n**If dirty** (uncommitted or unpushed work):\n```bash\n# Escalate to Mayor - polecat has work that might be valuable\ngt mail send mayor/ -s \\\"IDLE_DIRTY: has uncommitted work\\\" \\\n -m \\\"Polecat: \nState: idle (no hook_bead)\nGit status: \nUnpushed commits: \n\nPlease advise: recover work or discard?\\\"\n```\n\n**Rationale**: Idle polecats with clean git state are pure overhead. They have\nno work and no state worth preserving. Nuking them immediately frees resources\nand reduces noise. Only escalate when there's actual work at risk.\n\n**Step 4: Decide action**\n\n| Observation | Action |\n|-------------|--------|\n| agent_state=running, recent activity | None |\n| agent_state=running, idle 5-15 min | Gentle nudge |\n| agent_state=running, idle 15+ min | Direct nudge with deadline |\n| agent_state=stuck | Assess and help or escalate |\n| agent_state=done | Verify cleanup triggered (see Step 4a) |\n\n**Step 4a: Handle agent_state=done**\n\nIn the ephemeral model, polecats with agent_state=done and cleanup_status=clean\nshould already be nuked by HandlePolecatDone. Finding one here indicates:\n\n1. **Stale agent bead** - polecat was nuked but bead remains\n ```bash\n # Verify polecat doesn't exist anymore\n ls polecats/ 2>/dev/null || echo \"Already nuked\"\n ```\n If nuked, the agent bead is stale. Clean it up or ignore.\n\n2. **Cleanup wisp exists** - polecat has dirty state needing intervention\n ```bash\n bd list --wisp --labels=polecat: --status=open\n ```\n Process in process-cleanups step.\n\n3. **No wisp, polecat exists** - POLECAT_DONE mail was missed\n Try auto-nuke directly (ephemeral model):\n ```bash\n # Check cleanup_status and nuke if clean\n gt polecat nuke # Will fail if dirty\n ```\n If nuke fails (dirty state), create cleanup wisp for investigation.\n\n**Step 5: Execute nudges**\n```bash\ngt nudge /polecats/ \"How's progress? Need help?\"\n```\n\n**Step 6: Escalate if needed**\n```bash\ngt mail send mayor/ -s \"Escalation: stuck\" \\\n -m \"Polecat reports stuck. Please intervene.\"\n```\n\n**Parallelism**: Use Task tool subagents to inspect multiple polecats concurrently.\n\n**ZFC Principle**: Trust agent_state from beads. Don't infer state from PID/tmux." +description = "Survey all polecats using agent beads (ZFC: trust what agents report).\n\n**Step 1: List polecat agent beads**\n\n```bash\nbd list --type=agent --json\n```\n\nFilter the JSON output for entries where description contains `role_type: polecat`.\nEach polecat agent bead has fields in its description:\n- `role_type: polecat`\n- `rig: `\n- `agent_state: running|idle|stuck|done`\n- `hook_bead: `\n\n**Step 2: For each polecat, check agent_state**\n\n| agent_state | Meaning | Action |\n|-------------|---------|--------|\n| running | Actively working | Check progress (Step 3) |\n| idle | No work assigned | Auto-nuke if clean (Step 3a) |\n| stuck | Self-reported stuck | Handle stuck protocol |\n| done | Work complete | Verify cleanup triggered (see Step 4a) |\n\n**Step 3: For running polecats, assess progress**\n\nCheck the hook_bead field to see what they're working on:\n```bash\nbd show # See current step/issue\n```\n\nYou can also verify they're responsive:\n```bash\ntmux capture-pane -t gt-- -p | tail -20\n```\n\nLook for:\n- Recent tool activity → making progress\n- Idle at prompt → may need nudge\n- Error messages → may need help\n\n**Step 3a: For idle polecats, auto-nuke if clean**\n\nWhen agent_state=idle, the polecat has no work assigned. Check if it's safe to nuke:\n\n```bash\n# Check git status in the polecat's worktree\ncd polecats/\ngit status --porcelain # Should be empty (clean)\ngit log origin/main..HEAD # Should have no unpushed commits\n```\n\n**If clean** (no uncommitted changes, no unpushed commits):\n```bash\n# Safe to nuke - no work to lose\ngt polecat nuke \n```\nLog the auto-nuke for audit purposes. No escalation needed.\n\n**If dirty** (uncommitted or unpushed work):\n```bash\n# Escalate to Mayor - polecat has work that might be valuable\ngt mail send mayor/ -s \\\"IDLE_DIRTY: has uncommitted work\\\" \\\n -m \\\"Polecat: \nState: idle (no hook_bead)\nGit status: \nUnpushed commits: \n\nPlease advise: recover work or discard?\\\"\n```\n\n**Rationale**: Idle polecats with clean git state are pure overhead. They have\nno work and no state worth preserving. Nuking them immediately frees resources\nand reduces noise. Only escalate when there's actual work at risk.\n\n**Step 4: Decide action**\n\n| Observation | Action |\n|-------------|--------|\n| agent_state=running, recent activity | None |\n| agent_state=running, idle 5-15 min | Gentle nudge |\n| agent_state=running, idle 15+ min | Direct nudge with deadline |\n| agent_state=stuck | Assess and help or escalate |\n| agent_state=done | Verify cleanup triggered (see Step 4a) |\n\n**Step 4a: Handle agent_state=done**\n\nIn the ephemeral model, polecats with agent_state=done and cleanup_status=clean\nshould already be nuked (in inbox-check step). Finding one here indicates:\n\n1. **Stale agent bead** - polecat was nuked but bead remains\n ```bash\n # Verify polecat doesn't exist anymore\n ls polecats/ 2>/dev/null || echo \"Already nuked\"\n ```\n If nuked, the agent bead is stale. Clean it up or ignore.\n\n2. **Cleanup wisp exists** - polecat has dirty state needing intervention\n ```bash\n bd list --labels=polecat: --status=open\n ```\n Process in process-cleanups step.\n\n3. **No wisp, polecat exists** - POLECAT_DONE mail was missed\n Try auto-nuke directly (ephemeral model):\n ```bash\n # Check cleanup_status and nuke if clean\n gt polecat nuke # Will fail if dirty\n ```\n If nuke fails (dirty state), create cleanup wisp for investigation.\n\n**Step 5: Execute nudges**\n```bash\ngt nudge /polecats/ \"How's progress? Need help?\"\n```\n\n**Step 6: Escalate if needed**\n```bash\ngt mail send mayor/ -s \"Escalation: stuck\" \\\n -m \"Polecat reports stuck. Please intervene.\"\n```\n\n**Parallelism**: Use Task tool subagents to inspect multiple polecats concurrently.\n\n**ZFC Principle**: Trust agent_state from beads. Don't infer state from PID/tmux." id = 'survey-workers' needs = ['check-refinery'] title = 'Inspect all active polecats' @@ -32,7 +32,7 @@ needs = ['survey-workers'] title = 'Check timer gates for expiration' [[steps]] -description = "If Mayor started a batch (SWARM_START), check if all polecats have completed.\n\n**Step 1: Find active swarm tracking wisps**\n```bash\nbd list --wisp --labels=swarm --status=open\n```\nIf no active swarm, skip this step.\n\n**Step 2: Count completed polecats for this swarm**\n\nExtract from wisp labels: swarm_id, total, completed, start timestamp.\nCheck how many cleanup wisps have been closed for this swarm's polecats.\n\n**Step 3: If all complete, notify Mayor**\n```bash\ngt mail send mayor/ -s \"SWARM_COMPLETE: \" -m \"All polecats merged.\nDuration: minutes\nSwarm: \"\n\n# Close the swarm tracking wisp\nbd close --reason \"All polecats merged\"\n```\n\nNote: Runs every patrol cycle. Notification sent exactly once when all complete." +description = "If Mayor started a batch (SWARM_START), check if all polecats have completed.\n\n**Step 1: Find active swarm tracking wisps**\n```bash\nbd list --labels=swarm --status=open\n```\nIf no active swarm, skip this step.\n\n**Step 2: Count completed polecats for this swarm**\n\nExtract from wisp labels: swarm_id, total, completed, start timestamp.\nCheck how many cleanup wisps have been closed for this swarm's polecats.\n\n**Step 3: If all complete, notify Mayor**\n```bash\ngt mail send mayor/ -s \"SWARM_COMPLETE: \" -m \"All polecats merged.\nDuration: minutes\nSwarm: \"\n\n# Close the swarm tracking wisp\nbd close --reason \"All polecats merged\"\n```\n\nNote: Runs every patrol cycle. Notification sent exactly once when all complete." id = 'check-swarm-completion' needs = ['check-timer-gates'] title = 'Check if active swarm is complete' @@ -44,7 +44,7 @@ needs = ['check-swarm-completion'] title = 'Ping Deacon for health check' [[steps]] -description = "Verify inbox hygiene before ending patrol cycle.\n\n**Step 1: Check inbox state**\n```bash\ngt mail inbox\n```\n\nIn the ephemeral model, most POLECAT_DONE messages are handled immediately\n(auto-nuke) and archived. Inbox should contain ONLY:\n- Unprocessed messages (just arrived, will handle next cycle)\n- MERGED notifications (informational, archive after reading)\n\n**Step 2: Archive any stale messages**\n\nLook for messages that were processed but not archived:\n- POLECAT_STARTED older than this cycle → archive\n- POLECAT_DONE that was auto-nuked → should be archived already\n- MERGED notifications → archive after acknowledging\n- HELP/Blocked that was escalated → archive\n- SWARM_START that created tracking wisp → archive\n\n```bash\n# For each stale message found:\ngt mail archive \n```\n\n**Step 3: Verify cleanup wisp hygiene**\n\nIn the ephemeral model, cleanup wisps should be rare (only for dirty polecats):\n```bash\nbd list --wisp --labels=cleanup --status=open\n```\n\n- state:pending → Needs investigation in process-cleanups\n- state:merge-requested → Legacy state, handle in inbox-check\n\nIf cleanup wisps are accumulating, investigate why polecats aren't clean.\n\n**Goal**: Inbox should be nearly empty. Cleanup wisps should be rare." +description = "Verify inbox hygiene before ending patrol cycle.\n\n**Step 1: Check inbox state**\n```bash\ngt mail inbox\n```\n\nIn the ephemeral model, most POLECAT_DONE messages are handled immediately\n(auto-nuke) and archived. Inbox should contain ONLY:\n- Unprocessed messages (just arrived, will handle next cycle)\n- MERGED notifications (informational, archive after reading)\n\n**Step 2: Archive any stale messages**\n\nLook for messages that were processed but not archived:\n- POLECAT_STARTED older than this cycle → archive\n- POLECAT_DONE that was auto-nuked → should be archived already\n- MERGED notifications → archive after acknowledging\n- HELP/Blocked that was escalated → archive\n- SWARM_START that created tracking wisp → archive\n\n```bash\n# For each stale message found:\ngt mail archive \n```\n\n**Step 3: Verify cleanup wisp hygiene**\n\nIn the ephemeral model, cleanup wisps should be rare (only for dirty polecats):\n```bash\nbd list --labels=cleanup --status=open\n```\n\n- state:pending → Needs investigation in process-cleanups\n- state:merge-requested → Legacy state, handle in inbox-check\n\nIf cleanup wisps are accumulating, investigate why polecats aren't clean.\n\n**Goal**: Inbox should be nearly empty. Cleanup wisps should be rare." id = 'patrol-cleanup' needs = ['ping-deacon'] title = 'End-of-cycle inbox hygiene'