feat(status): add compact one-line-per-worker output as default

- Add --verbose/-v flag to show detailed multi-line output (old behavior)
- Compact mode shows: name + status indicator (●/○) + hook + mail count
- MQ info displayed inline with refinery
- Fix Makefile install target to use ~/.local/bin

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/jack
2026-01-06 20:12:39 -08:00
committed by Steve Yegge
parent 6dbb841e22
commit 950e35317e
9 changed files with 264 additions and 244 deletions

View File

@@ -23,7 +23,7 @@ ifeq ($(shell uname),Darwin)
endif
install: build
cp $(BUILD_DIR)/$(BINARY) ~/bin/$(BINARY)
cp $(BUILD_DIR)/$(BINARY) ~/.local/bin/$(BINARY)
clean:
rm -f $(BUILD_DIR)/$(BINARY)

View File

@@ -29,6 +29,7 @@ var statusJSON bool
var statusFast bool
var statusWatch bool
var statusInterval int
var statusVerbose bool
var statusCmd = &cobra.Command{
Use: "status",
@@ -49,6 +50,7 @@ func init() {
statusCmd.Flags().BoolVar(&statusFast, "fast", false, "Skip mail lookups for faster execution")
statusCmd.Flags().BoolVarP(&statusWatch, "watch", "w", false, "Watch mode: refresh status continuously")
statusCmd.Flags().IntVarP(&statusInterval, "interval", "n", 2, "Refresh interval in seconds")
statusCmd.Flags().BoolVarP(&statusVerbose, "verbose", "v", false, "Show detailed multi-line output per agent")
rootCmd.AddCommand(statusCmd)
}
@@ -456,8 +458,16 @@ func outputStatusText(status TownStatus) error {
if icon == "" {
icon = roleIcons[agent.Name]
}
fmt.Printf("%s %s\n", icon, style.Bold.Render(capitalizeFirst(agent.Name)))
renderAgentDetails(agent, " ", nil, status.Location)
if statusVerbose {
fmt.Printf("%s %s\n", icon, style.Bold.Render(capitalizeFirst(agent.Name)))
renderAgentDetails(agent, " ", nil, status.Location)
fmt.Println()
} else {
// Compact: icon + name on one line
renderAgentCompact(agent, icon+" ", nil, status.Location)
}
}
if !statusVerbose && len(status.Agents) > 0 {
fmt.Println()
}
@@ -488,73 +498,86 @@ func outputStatusText(status TownStatus) error {
// Witness
if len(witnesses) > 0 {
fmt.Printf("%s %s\n", roleIcons["witness"], style.Bold.Render("Witness"))
for _, agent := range witnesses {
renderAgentDetails(agent, " ", r.Hooks, status.Location)
if statusVerbose {
fmt.Printf("%s %s\n", roleIcons["witness"], style.Bold.Render("Witness"))
for _, agent := range witnesses {
renderAgentDetails(agent, " ", r.Hooks, status.Location)
}
fmt.Println()
} else {
for _, agent := range witnesses {
renderAgentCompact(agent, roleIcons["witness"]+" ", r.Hooks, status.Location)
}
}
fmt.Println()
}
// Refinery
if len(refineries) > 0 {
fmt.Printf("%s %s\n", roleIcons["refinery"], style.Bold.Render("Refinery"))
for _, agent := range refineries {
renderAgentDetails(agent, " ", r.Hooks, status.Location)
}
// MQ summary (shown under refinery)
if r.MQ != nil {
mqParts := []string{}
if r.MQ.Pending > 0 {
mqParts = append(mqParts, fmt.Sprintf("%d pending", r.MQ.Pending))
if statusVerbose {
fmt.Printf("%s %s\n", roleIcons["refinery"], style.Bold.Render("Refinery"))
for _, agent := range refineries {
renderAgentDetails(agent, " ", r.Hooks, status.Location)
}
if r.MQ.InFlight > 0 {
mqParts = append(mqParts, style.Warning.Render(fmt.Sprintf("%d in-flight", r.MQ.InFlight)))
}
if r.MQ.Blocked > 0 {
mqParts = append(mqParts, style.Dim.Render(fmt.Sprintf("%d blocked", r.MQ.Blocked)))
}
if len(mqParts) > 0 {
// Add state indicator
stateIcon := "○" // idle
switch r.MQ.State {
case "processing":
stateIcon = style.Success.Render("●")
case "blocked":
stateIcon = style.Error.Render("○")
// MQ summary (shown under refinery)
if r.MQ != nil {
mqStr := formatMQSummary(r.MQ)
if mqStr != "" {
fmt.Printf(" MQ: %s\n", mqStr)
}
// Add health warning if stale
healthSuffix := ""
if r.MQ.Health == "stale" {
healthSuffix = style.Error.Render(" [stale]")
}
fmt.Println()
} else {
for _, agent := range refineries {
// Compact: include MQ on same line if present
mqSuffix := ""
if r.MQ != nil {
mqStr := formatMQSummaryCompact(r.MQ)
if mqStr != "" {
mqSuffix = " " + mqStr
}
}
fmt.Printf(" MQ: %s %s%s\n", stateIcon, strings.Join(mqParts, ", "), healthSuffix)
renderAgentCompactWithSuffix(agent, roleIcons["refinery"]+" ", r.Hooks, status.Location, mqSuffix)
}
}
fmt.Println()
}
// Crew
if len(crews) > 0 {
fmt.Printf("%s %s (%d)\n", roleIcons["crew"], style.Bold.Render("Crew"), len(crews))
for _, agent := range crews {
renderAgentDetails(agent, " ", r.Hooks, status.Location)
if statusVerbose {
fmt.Printf("%s %s (%d)\n", roleIcons["crew"], style.Bold.Render("Crew"), len(crews))
for _, agent := range crews {
renderAgentDetails(agent, " ", r.Hooks, status.Location)
}
fmt.Println()
} else {
fmt.Printf("%s %s (%d)\n", roleIcons["crew"], style.Bold.Render("Crew"), len(crews))
for _, agent := range crews {
renderAgentCompact(agent, " ", r.Hooks, status.Location)
}
}
fmt.Println()
}
// Polecats
if len(polecats) > 0 {
fmt.Printf("%s %s (%d)\n", roleIcons["polecat"], style.Bold.Render("Polecats"), len(polecats))
for _, agent := range polecats {
renderAgentDetails(agent, " ", r.Hooks, status.Location)
if statusVerbose {
fmt.Printf("%s %s (%d)\n", roleIcons["polecat"], style.Bold.Render("Polecats"), len(polecats))
for _, agent := range polecats {
renderAgentDetails(agent, " ", r.Hooks, status.Location)
}
fmt.Println()
} else {
fmt.Printf("%s %s (%d)\n", roleIcons["polecat"], style.Bold.Render("Polecats"), len(polecats))
for _, agent := range polecats {
renderAgentCompact(agent, " ", r.Hooks, status.Location)
}
}
fmt.Println()
}
// No agents
if len(witnesses) == 0 && len(refineries) == 0 && len(crews) == 0 && len(polecats) == 0 {
fmt.Printf(" %s\n\n", style.Dim.Render("(no agents)"))
fmt.Printf(" %s\n", style.Dim.Render("(no agents)"))
}
fmt.Println()
}
return nil
@@ -665,6 +688,165 @@ func renderAgentDetails(agent AgentRuntime, indent string, hooks []AgentHookInfo
}
}
// formatMQSummary formats the MQ status for verbose display
func formatMQSummary(mq *MQSummary) string {
if mq == nil {
return ""
}
mqParts := []string{}
if mq.Pending > 0 {
mqParts = append(mqParts, fmt.Sprintf("%d pending", mq.Pending))
}
if mq.InFlight > 0 {
mqParts = append(mqParts, style.Warning.Render(fmt.Sprintf("%d in-flight", mq.InFlight)))
}
if mq.Blocked > 0 {
mqParts = append(mqParts, style.Dim.Render(fmt.Sprintf("%d blocked", mq.Blocked)))
}
if len(mqParts) == 0 {
return ""
}
// Add state indicator
stateIcon := "○" // idle
switch mq.State {
case "processing":
stateIcon = style.Success.Render("●")
case "blocked":
stateIcon = style.Error.Render("○")
}
// Add health warning if stale
healthSuffix := ""
if mq.Health == "stale" {
healthSuffix = style.Error.Render(" [stale]")
}
return fmt.Sprintf("%s %s%s", stateIcon, strings.Join(mqParts, ", "), healthSuffix)
}
// formatMQSummaryCompact formats MQ status for compact single-line display
func formatMQSummaryCompact(mq *MQSummary) string {
if mq == nil {
return ""
}
// Very compact: "MQ:12" or "MQ:12 [stale]"
total := mq.Pending + mq.InFlight + mq.Blocked
if total == 0 {
return ""
}
healthSuffix := ""
if mq.Health == "stale" {
healthSuffix = style.Error.Render("[stale]")
}
return fmt.Sprintf("MQ:%d%s", total, healthSuffix)
}
// renderAgentCompactWithSuffix renders a single-line agent status with an extra suffix
func renderAgentCompactWithSuffix(agent AgentRuntime, indent string, hooks []AgentHookInfo, townRoot string, suffix string) {
// Build status indicator
var statusIndicator string
beadState := agent.State
sessionExists := agent.Running
beadSaysRunning := beadState == "running" || beadState == "idle" || beadState == ""
switch {
case beadSaysRunning && sessionExists:
statusIndicator = style.Success.Render("●")
case beadSaysRunning && !sessionExists:
statusIndicator = style.Error.Render("●") + style.Warning.Render(" dead")
case !beadSaysRunning && sessionExists:
statusIndicator = style.Success.Render("●") + style.Warning.Render(" ["+beadState+"]")
default:
statusIndicator = style.Error.Render("○")
}
// Get hook info
hookBead := agent.HookBead
hookTitle := agent.WorkTitle
if hookBead == "" && hooks != nil {
for _, h := range hooks {
if h.Agent == agent.Address && h.HasWork {
hookBead = h.Molecule
hookTitle = h.Title
break
}
}
}
// Build hook suffix
hookSuffix := ""
if hookBead != "" {
if hookTitle != "" {
hookSuffix = style.Dim.Render(" → ") + truncateWithEllipsis(hookTitle, 30)
} else {
hookSuffix = style.Dim.Render(" → ") + hookBead
}
} else if hookTitle != "" {
hookSuffix = style.Dim.Render(" → ") + truncateWithEllipsis(hookTitle, 30)
}
// Mail indicator
mailSuffix := ""
if agent.UnreadMail > 0 {
mailSuffix = fmt.Sprintf(" 📬%d", agent.UnreadMail)
}
// Print single line: name + status + hook + mail + suffix
fmt.Printf("%s%-12s %s%s%s%s\n", indent, agent.Name, statusIndicator, hookSuffix, mailSuffix, suffix)
}
// renderAgentCompact renders a single-line agent status
func renderAgentCompact(agent AgentRuntime, indent string, hooks []AgentHookInfo, townRoot string) {
// Build status indicator
var statusIndicator string
beadState := agent.State
sessionExists := agent.Running
beadSaysRunning := beadState == "running" || beadState == "idle" || beadState == ""
switch {
case beadSaysRunning && sessionExists:
statusIndicator = style.Success.Render("●")
case beadSaysRunning && !sessionExists:
statusIndicator = style.Error.Render("●") + style.Warning.Render(" dead")
case !beadSaysRunning && sessionExists:
statusIndicator = style.Success.Render("●") + style.Warning.Render(" ["+beadState+"]")
default:
statusIndicator = style.Error.Render("○")
}
// Get hook info
hookBead := agent.HookBead
hookTitle := agent.WorkTitle
if hookBead == "" && hooks != nil {
for _, h := range hooks {
if h.Agent == agent.Address && h.HasWork {
hookBead = h.Molecule
hookTitle = h.Title
break
}
}
}
// Build hook suffix
hookSuffix := ""
if hookBead != "" {
if hookTitle != "" {
hookSuffix = style.Dim.Render(" → ") + truncateWithEllipsis(hookTitle, 30)
} else {
hookSuffix = style.Dim.Render(" → ") + hookBead
}
} else if hookTitle != "" {
hookSuffix = style.Dim.Render(" → ") + truncateWithEllipsis(hookTitle, 30)
}
// Mail indicator
mailSuffix := ""
if agent.UnreadMail > 0 {
mailSuffix = fmt.Sprintf(" 📬%d", agent.UnreadMail)
}
// Print single line: name + status + hook + mail
fmt.Printf("%s%-12s %s%s%s\n", indent, agent.Name, statusIndicator, hookSuffix, mailSuffix)
}
// formatHookInfo formats the hook bead and title for display
func formatHookInfo(hookBead, title string, maxLen int) string {
if hookBead == "" {

View File

@@ -340,43 +340,10 @@ gt mail send mayor/ -s "Health: <rig> <component> unresponsive" \\
Reset unresponsive_cycles to 0 when component responds normally."""
[[steps]]
id = "stale-hook-check"
title = "Cleanup stale hooked beads"
needs = ["health-scan"]
description = """
Find and unhook beads stuck in 'hooked' status.
Beads can get stuck in 'hooked' status when agents die or abandon work without
properly unhooking. This step cleans them up so the work can be reassigned.
**Step 1: Preview stale hooks**
```bash
gt deacon stale-hooks --dry-run
```
Review the output - it shows:
- Hooked beads older than 1 hour
- Whether the assignee agent is still alive
- What action would be taken
**Step 2: If stale hooks found with dead agents, unhook them**
```bash
gt deacon stale-hooks
```
This sets status back to 'open' for beads whose assignee agent is no longer running.
**Step 3: If no stale hooks**
No action needed - hooks are healthy.
**Note**: This is a backstop. Primary fix is ensuring agents properly unhook
beads when they exit or hand off work."""
[[steps]]
id = "zombie-scan"
title = "Backup check for zombie polecats"
needs = ["stale-hook-check"]
needs = ["health-scan"]
description = """
Defense-in-depth check for zombie polecats that Witness should have cleaned.

View File

@@ -78,7 +78,7 @@ Gather activity data from each rig in the town.
**1. List accessible rigs:**
```bash
gt rig list
gt rigs
# Returns list of rigs: gastown, beads, etc.
```

View File

@@ -57,7 +57,7 @@ gt hook # Shows scope in hook_bead
```bash
# If town-wide
gt rig list # Get list of all rigs
gt rigs # Get list of all rigs
# If specific rig
# Just use that rig

View File

@@ -145,39 +145,20 @@ git stash list # Should be empty
If dirty, clean up first (stash or discard).
**2. Fetch latest main:**
**2. Fetch latest state:**
```bash
git fetch origin
git fetch origin {{branch}}:refs/remotes/origin/{{branch}}
```
**3. Locate the source polecat's worktree:**
The branch is local-only (not pushed to origin). Find the source polecat path
from the task metadata. The path follows the pattern:
```
~/gt/<rig>/polecats/<polecat-name>
```
Extract the source polecat name from the MR metadata:
**3. Checkout the branch:**
```bash
bd show {{original_mr}} --json | jq -r '.description' | grep -oP 'Source polecat: \K\S+'
```
**4. Fetch the branch from the source polecat's worktree:**
```bash
# The source polecat's worktree still exists (cleanup is deferred until MR merges)
SOURCE_POLECAT_PATH="$HOME/gt/{{rig}}/polecats/{{source_polecat}}"
git fetch "$SOURCE_POLECAT_PATH" {{branch}}:{{branch}}
```
**5. Checkout the branch:**
```bash
git checkout -b temp-resolve {{branch}}
git checkout -b temp-resolve origin/{{branch}}
```
Using `temp-resolve` as the local branch name keeps things clear.
**6. Verify the branch state:**
**4. Verify the branch state:**
```bash
git log --oneline -5 # Recent commits
git log origin/main..HEAD # Commits not on main
@@ -402,11 +383,3 @@ required = true
[vars.branch]
description = "The branch to rebase (extracted from task metadata)"
required = true
[vars.rig]
description = "The rig where the source polecat resides"
required = true
[vars.source_polecat]
description = "The name of the polecat whose local branch contains the work (extracted from MR metadata)"
required = true

View File

@@ -362,14 +362,20 @@ Should be empty. If not:
- Pop and commit: `git stash pop && git add -A && git commit`
- Or drop if garbage: `git stash drop`
**4. Verify nothing left behind:**
**4. Push your branch:**
```bash
git push -u origin $(git branch --show-current)
```
**5. Verify nothing left behind:**
```bash
git status # Clean
git stash list # Empty
git log origin/main..HEAD # Your commits (local only, not pushed)
git log origin/main..HEAD # Your commits
git diff origin/main...HEAD # Your changes (expected)
```
**Exit criteria:** Workspace clean, commits ready for Refinery."""
**Exit criteria:** Branch pushed, workspace clean, no cruft."""
[[steps]]
id = "prepare-for-review"
@@ -436,7 +442,7 @@ You should see output like:
**3. You're recyclable:**
Your work is in the queue. The Witness knows you're done.
Refinery will access your local branch via shared .repo.git.
Your sandbox can be cleaned up - all work is pushed to origin.
If you have context remaining, you may:
- Pick up new work from `bd ready`

View File

@@ -1,11 +1,8 @@
description = """
Full Gas Town shutdown with cleanup.
Full Gas Town shutdown and restart.
This molecule provides a clean shutdown that:
- Preserves work (no forcing, no lost work)
- Cleans ephemeral state (wisps, stale beads)
- Resets agents to clean state
- Is idempotent (safe to run multiple times)
This is an idempotent shutdown - it stops Claude sessions but preserves
polecat sandboxes and hooks. Polecats can resume their work after restart.
Use when you need to:
- Reset all state for a fresh start
@@ -17,7 +14,7 @@ Sling to Mayor when ready to reboot:
"""
formula = "mol-town-shutdown"
type = "workflow"
version = 3
version = 2
[[steps]]
id = "preflight-check"
@@ -33,8 +30,7 @@ This checks for:
- **Uncommitted changes**: Polecats with dirty git status
- **Unpushed commits**: Work that hasn't reached remote
- **Active merges**: Refinery mid-merge (could corrupt state)
- **Hooked work**: Agents with work still attached
- **In-progress beads**: Work that hasn't completed
- **Pending CI**: PRs waiting on GitHub Actions
Output is a report with warnings/blockers:
@@ -43,8 +39,7 @@ Output is a report with warnings/blockers:
| BLOCKER | Active merge in progress | Abort shutdown |
| WARNING | Uncommitted polecat changes | List affected polecats |
| WARNING | Unpushed commits | List repos needing push |
| WARNING | Hooked work | List agents with hooks |
| INFO | In-progress beads | Will be reset to open |
| INFO | Pending CI runs | Note for awareness |
If blockers exist, STOP and resolve them first.
If only warnings, decide whether to proceed (work will be preserved).
@@ -76,108 +71,37 @@ What this does NOT do:
- Remove hook attachments
- Lose any git state
After restart, polecats can be respawned and will resume from their hooks.
Note: Crew workers are NOT stopped (they're user-managed).
"""
[[steps]]
id = "clear-hooks"
title = "Clear agent hooks"
needs = ["stop-sessions"]
description = """
Detach all hooked work from agents.
```bash
# Clear hooks for all agents
for rig in $(gt rig list --names); do
gt unsling $rig/witness
gt unsling $rig/refinery
for polecat in $(gt polecat list $rig --names); do
gt unsling $rig/polecats/$polecat
done
done
# Clear deacon hook
gt unsling deacon
```
This ensures no agent will auto-start work on restart.
The beads themselves are preserved - just detached from hooks.
"""
[[steps]]
id = "reset-in-progress"
title = "Reset in-progress beads to open"
needs = ["clear-hooks"]
description = """
Reset beads that were in-progress to open status.
```bash
# Find all in-progress beads and reset
bd list --status=in_progress --format=ids | while read id; do
bd update $id --status=open --assignee=""
done
```
This ensures:
- No beads stuck in limbo after restart
- Work can be re-assigned fresh
- No orphaned assignments
"""
[[steps]]
id = "clear-inboxes"
title = "Archive and clear inboxes"
needs = ["reset-in-progress"]
needs = ["stop-sessions"]
description = """
Archive and clear all agent inboxes across all rigs.
```bash
# For each rig
for rig in $(gt rig list --names); do
for rig in $(gt rigs --names); do
gt mail clear $rig/witness --archive
gt mail clear $rig/refinery --archive
done
# Clear Mayor inbox
gt mail clear mayor --archive
# Clear Deacon inbox
gt mail clear deacon --archive
```
Messages are archived to `.beads/mail-archive/` before deletion.
Crew inboxes are NOT cleared (user manages those).
"""
[[steps]]
id = "burn-wisps"
title = "Clean up ephemeral data"
needs = ["clear-inboxes"]
description = """
Remove wisp directories and ephemeral beads.
```bash
# Remove wisp directories
for rig in $(gt rig list --paths); do
rm -rf $rig/.beads-wisp/
done
# Delete wisp beads from issues (hq-wisp-*, gt-wisp-*, etc.)
bd list --type=wisp --format=ids | while read id; do
bd delete $id --force
done
# Delete stale hooked handoff beads (more than 7 days old)
bd cleanup --stale-handoffs --age=7d
```
This prevents unbounded accumulation of ephemeral state.
"""
[[steps]]
id = "stop-daemon"
title = "Stop the daemon"
needs = ["burn-wisps"]
needs = ["clear-inboxes"]
description = """
Stop the Gas Town daemon gracefully.
@@ -204,7 +128,7 @@ Rotate logs to prevent unbounded growth.
# Rotate daemon logs
gt daemon rotate-logs
# Clean up old session captures
# Clean up old session captures (but not current sandboxes)
gt doctor --fix
```
@@ -219,12 +143,6 @@ description = """
Ensure all beads state is persisted.
```bash
# Sync all rig beads
for rig in $(gt rig list --paths); do
(cd $rig && bd sync)
done
# Sync town beads
bd sync
```
@@ -233,33 +151,10 @@ are preserved with whatever state they had. They'll commit their
own work when they resume.
"""
[[steps]]
id = "validate-clean"
title = "Validate clean state"
needs = ["sync-state"]
description = """
Verify all cleanup operations succeeded.
```bash
gt shutdown validate
```
Checks:
- [ ] All inboxes empty (except crew)
- [ ] No hooked work
- [ ] No in-progress beads
- [ ] Beads synced
- [ ] No wisp directories
- [ ] Daemon stopped
Reports any discrepancies. Non-blocking - shutdown continues
but issues are logged for manual review.
"""
[[steps]]
id = "handoff-mayor"
title = "Send Mayor handoff"
needs = ["validate-clean"]
needs = ["sync-state"]
description = """
Record shutdown context for the fresh Mayor session.
@@ -269,9 +164,6 @@ Town shutdown completed. State preserved.
Polecat sandboxes: PRESERVED (will resume from hooks)
Inboxes: ARCHIVED and cleared
Hooks: CLEARED
In-progress beads: RESET to open
Wisps: BURNED
Daemon: STOPPED
Next steps:
@@ -302,5 +194,5 @@ The daemon will:
- Resume background coordination
Polecats are NOT auto-respawned. Use `gt sling` or let Witness
restart them based on available work.
restart them based on their preserved hooks.
"""

View File

@@ -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 <message-id>\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 <name>)\n\n2. **Check cleanup_status** from the polecat's agent bead:\n```bash\n# Get the agent bead ID (format: <prefix>-agent-<rig>-polecat-<name>)\nbd show gt-agent-<rig>-polecat-<name> --json 2>/dev/null | grep cleanup_status\n```\n\n3. **If cleanup_status=clean** (branch pushed, safe to nuke):\n```bash\ngt polecat nuke <name>\ngt mail archive <message-id>\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:<name>\" \\\n --description \"Polecat <name> needs cleanup. Status: <cleanup_status>\" \\\n --labels \"cleanup,polecat:<name>,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 <name>\ngt mail archive <message-id>\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:<name>,state:merge-requested --status=open\n\n# If found, proceed with full polecat nuke:\ngt polecat nuke <name>\n\n# Burn the cleanup wisp\nbd close <wisp-id>\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: <polecat> needs help\" -m \"<details>\"\n```\nArchive after handling (escalated or resolved):\n```bash\ngt mail archive <message-id>\n```\n\n**HANDOFF**:\nRead predecessor context. Continue from where they left off.\nArchive after absorbing context:\n```bash\ngt mail archive <message-id>\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:<swarm_id>\" --description \"Tracking batch: <swarm_id>\" --labels swarm,swarm_id:<swarm_id>,total:<N>,completed:0,start:<timestamp>\n```\nArchive after creating swarm tracking wisp:\n```bash\ngt mail archive <message-id>\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 <message-id>\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 <name> → 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:<name>,state:merge-requested --status=open\n\n# If found, proceed with full polecat nuke:\ngt polecat nuke <name>\n\n# Burn the cleanup wisp\nbd close <wisp-id>\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: <polecat> needs help\" -m \"<details>\"\n```\nArchive after handling (escalated or resolved):\n```bash\ngt mail archive <message-id>\n```\n\n**HANDOFF**:\nRead predecessor context. Continue from where they left off.\nArchive after absorbing context:\n```bash\ngt mail archive <message-id>\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:<swarm_id>\" --description \"Tracking batch: <swarm_id>\" --labels swarm,swarm_id:<swarm_id>,total:<N>,completed:0,start:<timestamp>\n```\nArchive after creating swarm tracking wisp:\n```bash\ngt mail archive <message-id>\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 --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/<name>\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 <name>\n\n# Close the wisp\nbd close <wisp-id> --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 <rig>/<polecat>\" \\\n -m \"Cleanup Status: <status>\nBranch: <branch>\nIssue: <issue-id>\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 --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/<name>\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 <name>\n\n# Close the wisp\nbd close <wisp-id> --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 <rig>/<polecat>\" \\\n -m \"Cleanup Status: <status>\nBranch: <branch>\nIssue: <issue-id>\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: <rig-name>`\n- `agent_state: running|idle|stuck|done`\n- `hook_bead: <current-work-id>`\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 <hook_bead> # See current step/issue\n```\n\nYou can also verify they're responsive:\n```bash\ntmux capture-pane -t gt-<rig>-<name> -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/<name>\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 <name>\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: <polecat> has uncommitted work\\\" \\\n -m \\\"Polecat: <name>\nState: idle (no hook_bead)\nGit status: <uncommitted-files>\nUnpushed commits: <count>\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/<name> 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:<name> --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 <name> # 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 <rig>/polecats/<name> \"How's progress? Need help?\"\n```\n\n**Step 6: Escalate if needed**\n```bash\ngt mail send mayor/ -s \"Escalation: <polecat> stuck\" \\\n -m \"Polecat <name> 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: <rig-name>`\n- `agent_state: running|idle|stuck|done`\n- `hook_bead: <current-work-id>`\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 <hook_bead> # See current step/issue\n```\n\nYou can also verify they're responsive:\n```bash\ntmux capture-pane -t gt-<rig>-<name> -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/<name>\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 <name>\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: <polecat> has uncommitted work\\\" \\\n -m \\\"Polecat: <name>\nState: idle (no hook_bead)\nGit status: <uncommitted-files>\nUnpushed commits: <count>\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/<name> 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:<name> --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 <name> # 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 <rig>/polecats/<name> \"How's progress? Need help?\"\n```\n\n**Step 6: Escalate if needed**\n```bash\ngt mail send mayor/ -s \"Escalation: <polecat> stuck\" \\\n -m \"Polecat <name> 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 --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: <swarm_id>\" -m \"All <total> polecats merged.\nDuration: <minutes> minutes\nSwarm: <swarm_id>\"\n\n# Close the swarm tracking wisp\nbd close <swarm-wisp-id> --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 --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: <swarm_id>\" -m \"All <total> polecats merged.\nDuration: <minutes> minutes\nSwarm: <swarm_id>\"\n\n# Close the swarm tracking wisp\nbd close <swarm-wisp-id> --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 <message-id>\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."
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 <message-id>\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."
id = 'patrol-cleanup'
needs = ['ping-deacon']
title = 'End-of-cycle inbox hygiene'