From ef1f5ac2f3d52a30656aa36ba6eed3fc0cc40791 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 23 Dec 2025 00:10:39 -0800 Subject: [PATCH 1/2] Refinery startup: auto-bond mol-refinery-patrol on start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add outputRefineryPatrolContext to gt prime that automatically spawns the refinery patrol molecule when no active patrol is found. This ensures the merge queue is always monitored when Refinery starts up. Key changes: - Add RoleRefinery to outputMoleculeContext - Implement outputRefineryPatrolContext with auto-spawn logic - Check for existing in-progress or open patrol molecules - Spawn mol-refinery-patrol wisp if none found (gt-j6s8) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/prime.go | 175 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 173 insertions(+), 2 deletions(-) diff --git a/internal/cmd/prime.go b/internal/cmd/prime.go index 5b4fdcd7..05036354 100644 --- a/internal/cmd/prime.go +++ b/internal/cmd/prime.go @@ -550,8 +550,8 @@ func outputAttachmentStatus(ctx RoleContext) { // outputMoleculeContext checks if the agent is working on a molecule step and shows progress. func outputMoleculeContext(ctx RoleContext) { - // Applies to polecats, crew workers, and deacon - if ctx.Role != RolePolecat && ctx.Role != RoleCrew && ctx.Role != RoleDeacon { + // Applies to polecats, crew workers, deacon, and refinery + if ctx.Role != RolePolecat && ctx.Role != RoleCrew && ctx.Role != RoleDeacon && ctx.Role != RoleRefinery { return } @@ -561,6 +561,12 @@ func outputMoleculeContext(ctx RoleContext) { return } + // For Refinery, use special patrol molecule handling (auto-bonds on startup) + if ctx.Role == RoleRefinery { + outputRefineryPatrolContext(ctx) + return + } + // Check for in-progress issues b := beads.New(ctx.WorkDir) issues, err := b.List(beads.ListOptions{ @@ -750,6 +756,171 @@ func outputDeaconPatrolContext(ctx RoleContext) { fmt.Println(" - Loop back to spawn new wisp, or exit if context high") } +// outputRefineryPatrolContext shows patrol molecule status for the Refinery. +// Unlike other patrol roles, Refinery AUTO-BONDS its patrol molecule on startup +// if one isn't already running. This ensures the merge queue is always monitored. +func outputRefineryPatrolContext(ctx RoleContext) { + fmt.Println() + fmt.Printf("%s\n\n", style.Bold.Render("## 🔧 Refinery Patrol Status")) + + // Refinery works from its own rig clone: /refinery/rig/ + // Beads are in the current WorkDir + refineryBeadsDir := ctx.WorkDir + + // Find mol-refinery-patrol molecules (exclude template) + // Look for in-progress patrol first (resumable) + cmdList := exec.Command("bd", "--no-daemon", "list", "--status=in_progress", "--type=epic") + cmdList.Dir = refineryBeadsDir + var stdoutList bytes.Buffer + cmdList.Stdout = &stdoutList + cmdList.Stderr = nil + errList := cmdList.Run() + + hasPatrol := false + var patrolID string + var patrolLine string + + if errList == nil { + lines := strings.Split(stdoutList.String(), "\n") + for _, line := range lines { + if strings.Contains(line, "mol-refinery-patrol") && !strings.Contains(line, "[template]") { + parts := strings.Fields(line) + if len(parts) > 0 { + patrolID = parts[0] + patrolLine = line + hasPatrol = true + break + } + } + } + } + + // Also check for open patrols with open children (active wisp) + if !hasPatrol { + cmdOpen := exec.Command("bd", "--no-daemon", "list", "--status=open", "--type=epic") + cmdOpen.Dir = refineryBeadsDir + var stdoutOpen bytes.Buffer + cmdOpen.Stdout = &stdoutOpen + cmdOpen.Stderr = nil + if cmdOpen.Run() == nil { + lines := strings.Split(stdoutOpen.String(), "\n") + for _, line := range lines { + if strings.Contains(line, "mol-refinery-patrol") && !strings.Contains(line, "[template]") { + parts := strings.Fields(line) + if len(parts) > 0 { + molID := parts[0] + // Check if this molecule has open children + cmdShow := exec.Command("bd", "--no-daemon", "show", molID) + cmdShow.Dir = refineryBeadsDir + var stdoutShow bytes.Buffer + cmdShow.Stdout = &stdoutShow + cmdShow.Stderr = nil + if cmdShow.Run() == nil { + showOutput := stdoutShow.String() + if strings.Contains(showOutput, "- open]") || strings.Contains(showOutput, "- in_progress]") { + hasPatrol = true + patrolID = molID + patrolLine = line + break + } + } + } + } + } + } + } + + if !hasPatrol { + // No active patrol - AUTO-SPAWN one + fmt.Println("Status: **No active patrol** - spawning mol-refinery-patrol...") + fmt.Println() + + // Find the proto ID for mol-refinery-patrol + cmdCatalog := exec.Command("bd", "--no-daemon", "mol", "catalog") + cmdCatalog.Dir = refineryBeadsDir + var stdoutCatalog bytes.Buffer + cmdCatalog.Stdout = &stdoutCatalog + cmdCatalog.Stderr = nil + + if cmdCatalog.Run() != nil { + fmt.Println(style.Dim.Render("Failed to list molecule catalog. Run `bd mol catalog` to troubleshoot.")) + return + } + + // Find mol-refinery-patrol in catalog + var protoID string + catalogLines := strings.Split(stdoutCatalog.String(), "\n") + for _, line := range catalogLines { + if strings.Contains(line, "mol-refinery-patrol") { + parts := strings.Fields(line) + if len(parts) > 0 { + protoID = parts[0] + break + } + } + } + + if protoID == "" { + fmt.Println(style.Dim.Render("Proto mol-refinery-patrol not found in catalog. Run `bd mol register` first.")) + return + } + + // Spawn the wisp (default spawn creates wisp) + cmdSpawn := exec.Command("bd", "--no-daemon", "mol", "spawn", protoID, "--assignee", ctx.Rig+"/refinery") + cmdSpawn.Dir = refineryBeadsDir + var stdoutSpawn, stderrSpawn bytes.Buffer + cmdSpawn.Stdout = &stdoutSpawn + cmdSpawn.Stderr = &stderrSpawn + + if err := cmdSpawn.Run(); err != nil { + fmt.Printf("Failed to spawn patrol: %s\n", stderrSpawn.String()) + fmt.Println(style.Dim.Render("Run manually: bd --no-daemon mol spawn " + protoID)) + return + } + + // Parse the spawned molecule ID from output + spawnOutput := stdoutSpawn.String() + fmt.Printf("✓ Spawned patrol molecule\n") + + // Extract molecule ID from spawn output (format: "Created molecule: gt-xxxx") + for _, line := range strings.Split(spawnOutput, "\n") { + if strings.Contains(line, "molecule:") || strings.Contains(line, "Created") { + parts := strings.Fields(line) + for _, p := range parts { + if strings.HasPrefix(p, "gt-") { + patrolID = p + break + } + } + } + } + + if patrolID != "" { + fmt.Printf("Patrol ID: %s\n\n", patrolID) + } + } else { + // Has active patrol - show status + fmt.Println("Status: **Patrol Active**") + fmt.Printf("Patrol: %s\n\n", strings.TrimSpace(patrolLine)) + } + + // Show patrol work loop instructions + fmt.Println("**Refinery Patrol Work Loop:**") + fmt.Println("1. Check inbox: `gt mail inbox`") + fmt.Println("2. Check next step: `bd ready`") + fmt.Println("3. Execute the step (queue scan, process branch, tests, merge)") + fmt.Println("4. Close step: `bd close `") + fmt.Println("5. Check next: `bd ready`") + fmt.Println("6. At cycle end (burn-or-loop step):") + fmt.Println(" - Generate summary of patrol cycle") + fmt.Println(" - Squash: `bd --no-daemon mol squash --summary \"\"`") + fmt.Println(" - Loop back to spawn new wisp, or exit if context high") + if patrolID != "" { + fmt.Println() + fmt.Printf("Current patrol ID: %s\n", patrolID) + } +} + // acquireIdentityLock checks and acquires the identity lock for worker roles. // This prevents multiple agents from claiming the same worker identity. // Returns an error if another agent already owns this identity. From 8bb0ad8e4050c47ca68ffdee1bdc84dcf6d1b80e Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 23 Dec 2025 00:11:07 -0800 Subject: [PATCH 2/2] Close gt-j6s8: Refinery patrol auto-bond implemented --- .beads/issues.jsonl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index de32f5c3..71a75edf 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -643,7 +643,7 @@ {"id":"gt-upom","title":"Witness patrol: cleanup idle orphan polecats","description":"Add patrol step to find and cleanup polecats that are idle with no assigned issue. These orphans occur when polecats crash before sending DONE or Witness misses the message. Patrol should verify git is clean before removing worktree. Part of gt-rana.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-21T23:09:41.756753-08:00","updated_at":"2025-12-21T23:09:41.756753-08:00"} {"id":"gt-us8","title":"Daemon: configurable heartbeat interval","description":"Heartbeat interval is hardcoded to 60s. Should be configurable via:\n- town.json config\n- Command line flag\n- Environment variable\n\nDefault 60s is reasonable but some deployments may want faster/slower.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-18T13:38:14.282216-08:00","updated_at":"2025-12-18T13:38:14.282216-08:00","dependencies":[{"issue_id":"gt-us8","depends_on_id":"gt-99m","type":"blocks","created_at":"2025-12-18T13:38:26.704111-08:00","created_by":"daemon"}]} {"id":"gt-usy0","title":"Merge: gt-3x0z.3","description":"branch: polecat/rictus\ntarget: main\nsource_issue: gt-3x0z.3\nrig: gastown","status":"closed","priority":2,"issue_type":"merge-request","created_at":"2025-12-21T16:03:43.535266-08:00","updated_at":"2025-12-21T17:20:27.505696-08:00","closed_at":"2025-12-21T17:20:27.505696-08:00","close_reason":"ORPHANED: Branch never pushed, worktree deleted"} -{"id":"gt-utwc","title":"Self-mail should suppress tmux notification","description":"When sending mail to yourself (e.g., mayor sending to mayor/), the tmux notification shouldn't fire.\n\n**Rationale:**\n- Self-mail is intended for future-you (next session handoff)\n- Present-you just sent it, so you already know about it\n- The notification is redundant/confusing in this case\n\n**Fix:**\nSuppress tmux notification when sender == recipient address.","status":"closed","priority":3,"issue_type":"bug","created_at":"2025-12-22T17:55:39.573705-08:00","updated_at":"2025-12-22T23:58:02.827026-08:00","closed_at":"2025-12-22T23:58:02.827026-08:00","close_reason":"Skip tmux notification when sender == recipient"} +{"id":"gt-utwc","title":"Self-mail should suppress tmux notification","description":"When sending mail to yourself (e.g., mayor sending to mayor/), the tmux notification shouldn't fire.\n\n**Rationale:**\n- Self-mail is intended for future-you (next session handoff)\n- Present-you just sent it, so you already know about it\n- The notification is redundant/confusing in this case\n\n**Fix:**\nSuppress tmux notification when sender == recipient address.","status":"open","priority":3,"issue_type":"bug","created_at":"2025-12-22T17:55:39.573705-08:00","updated_at":"2025-12-22T17:55:39.573705-08:00"} {"id":"gt-uym5","title":"Implement gt mol status command","description":"Show what's on an agent's hook.\n\n```bash\ngt mol status [target]\n```\n\nOutput:\n- What's slung (molecule name, associated issue)\n- Current phase and progress\n- Whether it's a wisp\n- Next action hint\n\nIf no target, shows current agent's status.\n\nAcceptance:\n- [ ] Read pinned bead attachment\n- [ ] Display molecule/issue info\n- [ ] Show phase progress\n- [ ] Indicate wisp vs durable","status":"closed","priority":1,"issue_type":"task","assignee":"gastown/nux","created_at":"2025-12-22T03:17:34.679963-08:00","updated_at":"2025-12-22T12:34:19.942265-08:00","closed_at":"2025-12-22T12:34:19.942265-08:00","close_reason":"Implemented gt mol status command with mol alias, auto-detection, progress display, wisp detection, and next action hints"} {"id":"gt-v5hv","title":"Work on ga-y6b: Implement Refinery as Claude agent. Conve...","description":"Work on ga-y6b: Implement Refinery as Claude agent. Convert from shell to Claude agent that processes MRs in merge queue, runs tests, merges to integration branch. When done, submit MR (not PR) to integration branch for Refinery.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T22:58:17.576892-08:00","updated_at":"2025-12-19T23:23:22.778407-08:00","closed_at":"2025-12-19T23:23:22.778407-08:00"} {"id":"gt-v5k","title":"Design: Failure modes and recovery","description":"Document failure modes and recovery strategies for Gas Town operations.\n\n## Critical Failure Modes\n\n### 1. Agent Crash Mid-Operation\n\n**Scenario**: Polecat crashes while committing, Witness crashes while verifying\n\n**Detection**:\n- Session suddenly gone (tmux check fails)\n- State shows 'working' but no session\n- Heartbeat stops (for Witness)\n\n**Recovery**:\n- Doctor detects via ZombieSessionCheck\n- Capture any recoverable state\n- Reset agent state to 'idle'\n- For Witness: auto-restart via supervisor or manual gt witness start\n\n### 2. Git State Corruption\n\n**Scenario**: Merge conflict, failed rebase, detached HEAD\n\n**Detection**:\n- Git commands fail\n- Dirty state that won't commit\n- Branch diverged from origin\n\n**Recovery**:\n- gt doctor reports git health issues\n- Manual intervention recommended\n- Severe cases: remove clone, re-clone\n\n### 3. Beads Sync Conflict\n\n**Scenario**: Two polecats modify same issue\n\n**Detection**:\n- bd sync fails with conflict\n- Beads tombstone mechanism handles most cases\n\n**Recovery**:\n- Beads has last-write-wins semantics\n- bd sync --force in extreme cases\n- Issues may need manual dedup\n\n### 4. Tmux Failure\n\n**Scenario**: Tmux server crashes, socket issues\n\n**Detection**:\n- All sessions inaccessible\n- \"no server running\" errors\n\n**Recovery**:\n- Kill any orphan processes\n- tmux kill-server \u0026\u0026 tmux start-server\n- All agent states reset to idle\n- Re-spawn active work\n\n### 5. Claude API Issues\n\n**Scenario**: Rate limits, outages, context limits\n\n**Detection**:\n- Sessions hang or produce errors\n- Repeated failure patterns\n\n**Recovery**:\n- Exponential backoff (handled by Claude Code)\n- For context limits: session cycling (mail-to-self)\n- For outages: wait and retry\n\n### 6. Disk Full\n\n**Scenario**: Clones, logs, or beads fill disk\n\n**Detection**:\n- Write operations fail\n- git/bd commands error\n\n**Recovery**:\n- Clean up logs: rm ~/.gastown/logs/*\n- Remove old polecat clones\n- gt doctor --fix can clean some cruft\n\n### 7. Network Failure\n\n**Scenario**: Can't reach GitHub, API servers\n\n**Detection**:\n- git fetch/push fails\n- Claude sessions hang\n\n**Recovery**:\n- Work continues locally\n- Queue pushes for later\n- Sync when connectivity restored\n\n## Recovery Principles\n\n1. **Fail safe**: Prefer stopping over corrupting\n2. **State is recoverable**: Git and beads have recovery mechanisms\n3. **Doctor heals**: gt doctor --fix handles common issues\n4. **Emergency stop**: gt stop --all as last resort\n5. **Human escalation**: Some failures need Overseer intervention\n\n## Implementation\n\n- Document each failure mode in architecture.md\n- Ensure doctor checks cover detection\n- Add recovery hints to error messages\n- Log all failures for debugging","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-15T23:19:07.198289-08:00","updated_at":"2025-12-15T23:19:28.171942-08:00"}