Files
gastown/internal/cmd/crew_at.go
Steve Yegge b172fd4787 feat: Add rig infra cycling (witness ↔ refinery)
Extended the unified cycle system to include rig infrastructure sessions:
- Witness ↔ Refinery (per rig) now cycle with C-b n/p

Also moved SetCycleBindings into ConfigureGasTownSession so ALL Gas Town
sessions automatically get the unified cycle bindings. Removed redundant
individual calls from crew, mayor, and deacon startup code.

Cycle groups are now:
- Town: Mayor ↔ Deacon
- Crew (per rig): All crew members in same rig
- Infra (per rig): Witness ↔ Refinery

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 16:36:38 -08:00

179 lines
5.9 KiB
Go

package cmd
import (
"fmt"
"time"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/constants"
"github.com/steveyegge/gastown/internal/crew"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/tmux"
"github.com/steveyegge/gastown/internal/workspace"
)
func runCrewAt(cmd *cobra.Command, args []string) error {
var name string
// Determine crew name: from arg, or auto-detect from cwd
if len(args) > 0 {
name = args[0]
// Parse rig/name format (e.g., "beads/emma" -> rig=beads, name=emma)
if rig, crewName, ok := parseRigSlashName(name); ok {
if crewRig == "" {
crewRig = rig
}
name = crewName
}
} else {
// Try to detect from current directory
detected, err := detectCrewFromCwd()
if err != nil {
return fmt.Errorf("could not detect crew workspace from current directory: %w\n\nUsage: gt crew at <name>", err)
}
name = detected.crewName
if crewRig == "" {
crewRig = detected.rigName
}
fmt.Printf("Detected crew workspace: %s/%s\n", detected.rigName, name)
}
crewMgr, r, err := getCrewManager(crewRig)
if err != nil {
return err
}
// Get the crew worker
worker, err := crewMgr.Get(name)
if err != nil {
if err == crew.ErrCrewNotFound {
return fmt.Errorf("crew workspace '%s' not found", name)
}
return fmt.Errorf("getting crew worker: %w", err)
}
// Ensure crew workspace is on main branch (persistent roles should not use feature branches)
ensureMainBranch(worker.ClonePath, fmt.Sprintf("Crew workspace %s/%s", r.Name, name))
// If --no-tmux, just print the path
if crewNoTmux {
fmt.Println(worker.ClonePath)
return nil
}
// Resolve account for Claude config
townRoot, err := workspace.FindFromCwd()
if err != nil {
return fmt.Errorf("finding town root: %w", err)
}
accountsPath := constants.MayorAccountsPath(townRoot)
claudeConfigDir, accountHandle, err := config.ResolveAccountConfigDir(accountsPath, crewAccount)
if err != nil {
return fmt.Errorf("resolving account: %w", err)
}
if accountHandle != "" {
fmt.Printf("Using account: %s\n", accountHandle)
}
// Check if session exists
t := tmux.NewTmux()
sessionID := crewSessionName(r.Name, name)
hasSession, err := t.HasSession(sessionID)
if err != nil {
return fmt.Errorf("checking session: %w", err)
}
if !hasSession {
// Create new session
if err := t.NewSession(sessionID, worker.ClonePath); err != nil {
return fmt.Errorf("creating session: %w", err)
}
// Set environment (non-fatal: session works without these)
_ = t.SetEnvironment(sessionID, "GT_ROLE", "crew")
_ = t.SetEnvironment(sessionID, "GT_RIG", r.Name)
_ = t.SetEnvironment(sessionID, "GT_CREW", name)
// Set CLAUDE_CONFIG_DIR for account selection (non-fatal)
if claudeConfigDir != "" {
_ = t.SetEnvironment(sessionID, "CLAUDE_CONFIG_DIR", claudeConfigDir)
}
// Apply rig-based theming (non-fatal: theming failure doesn't affect operation)
// Note: ConfigureGasTownSession includes cycle bindings
theme := getThemeForRig(r.Name)
_ = t.ConfigureGasTownSession(sessionID, theme, r.Name, name, "crew")
// Wait for shell to be ready after session creation
if err := t.WaitForShellReady(sessionID, 5*time.Second); err != nil {
return fmt.Errorf("waiting for shell: %w", err)
}
// Get pane ID for respawn
paneID, err := t.GetPaneID(sessionID)
if err != nil {
return fmt.Errorf("getting pane ID: %w", err)
}
// Use respawn-pane to replace shell with Claude directly
// This gives cleaner lifecycle: Claude exits → session ends (no intermediate shell)
// Pass "gt prime" as initial prompt so Claude loads context immediately
// Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes
bdActor := fmt.Sprintf("%s/crew/%s", r.Name, name)
claudeCmd := fmt.Sprintf(`export GT_ROLE=crew GT_RIG=%s GT_CREW=%s BD_ACTOR=%s && claude --dangerously-skip-permissions "gt prime"`, r.Name, name, bdActor)
if err := t.RespawnPane(paneID, claudeCmd); err != nil {
return fmt.Errorf("starting claude: %w", err)
}
fmt.Printf("%s Created session for %s/%s\n",
style.Bold.Render("✓"), r.Name, name)
} else {
// Session exists - check if Claude is still running
// Uses both pane command check and UI marker detection to avoid
// restarting when user is in a subshell spawned from Claude
if !t.IsClaudeRunning(sessionID) {
// Claude has exited, restart it using respawn-pane
fmt.Printf("Claude exited, restarting...\n")
// Get pane ID for respawn
paneID, err := t.GetPaneID(sessionID)
if err != nil {
return fmt.Errorf("getting pane ID: %w", err)
}
// Use respawn-pane to replace shell with Claude directly
// Pass "gt prime" as initial prompt so Claude loads context immediately
// Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes
bdActor := fmt.Sprintf("%s/crew/%s", r.Name, name)
claudeCmd := fmt.Sprintf(`export GT_ROLE=crew GT_RIG=%s GT_CREW=%s BD_ACTOR=%s && claude --dangerously-skip-permissions "gt prime"`, r.Name, name, bdActor)
if err := t.RespawnPane(paneID, claudeCmd); err != nil {
return fmt.Errorf("restarting claude: %w", err)
}
}
}
// Check if we're already in the target session
if isInTmuxSession(sessionID) {
// We're in the session at a shell prompt - just start Claude directly
// Pass "gt prime" as initial prompt so Claude loads context immediately
fmt.Printf("Starting Claude in current session...\n")
return execClaude("gt prime")
}
// If inside tmux (but different session), don't switch - just inform user
if tmux.IsInsideTmux() {
fmt.Printf("Started %s/%s. Use C-b s to switch.\n", r.Name, name)
return nil
}
// Outside tmux: attach unless --detached flag is set
if crewDetached {
fmt.Printf("Started %s/%s. Run 'gt crew at %s' to attach.\n", r.Name, name, name)
return nil
}
// Attach to session
return attachToTmuxSession(sessionID)
}