Unify agent startup with Manager pattern
Refactors all agent startup paths (witness, refinery, crew, polecat) to use a consistent Manager interface with Start(), Stop(), IsRunning(), and SessionName() methods. Includes: - Witness manager with GUPP propulsion nudge for startup - Refinery manager for engineer sessions - Crew manager for worker agents - Session/polecat manager updates - claude_settings_check doctor check for settings validation - Settings management consolidated from rig/manager.go - Settings location moved outside source repos to prevent conflicts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -344,8 +345,10 @@ func ensureRefinerySession(rigName string, r *rig.Rig) (bool, error) {
|
||||
refineryRigDir = r.Path
|
||||
}
|
||||
|
||||
// Ensure Claude settings exist (autonomous role needs mail in SessionStart)
|
||||
if err := claude.EnsureSettingsForRole(refineryRigDir, "refinery"); err != nil {
|
||||
// Ensure Claude settings exist in refinery/ (not refinery/rig/) so we don't
|
||||
// write into the source repo. Claude walks up the tree to find settings.
|
||||
refineryParentDir := filepath.Join(r.Path, "refinery")
|
||||
if err := claude.EnsureSettingsForRole(refineryParentDir, "refinery"); err != nil {
|
||||
return false, fmt.Errorf("ensuring Claude settings: %w", err)
|
||||
}
|
||||
|
||||
@@ -762,25 +765,6 @@ func runStartCrew(cmd *cobra.Command, args []string) error {
|
||||
crewGit := git.NewGit(r.Path)
|
||||
crewMgr := crew.NewManager(r, crewGit)
|
||||
|
||||
// Check if crew exists, create if not
|
||||
worker, err := crewMgr.Get(name)
|
||||
if err == crew.ErrCrewNotFound {
|
||||
fmt.Printf("Creating crew workspace %s in %s...\n", name, rigName)
|
||||
worker, err = crewMgr.Add(name, false) // No feature branch for crew
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating crew workspace: %w", err)
|
||||
}
|
||||
fmt.Printf("%s Created crew workspace: %s/%s\n",
|
||||
style.Bold.Render("✓"), rigName, name)
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("getting crew worker: %w", err)
|
||||
} else {
|
||||
fmt.Printf("Crew workspace %s/%s exists\n", rigName, name)
|
||||
}
|
||||
|
||||
// Ensure crew workspace is on default branch
|
||||
ensureDefaultBranch(worker.ClonePath, fmt.Sprintf("Crew workspace %s/%s", rigName, name), r.Path)
|
||||
|
||||
// Resolve account for Claude config
|
||||
accountsPath := constants.MayorAccountsPath(townRoot)
|
||||
claudeConfigDir, accountHandle, err := config.ResolveAccountConfigDir(accountsPath, startCrewAccount)
|
||||
@@ -791,68 +775,18 @@ func runStartCrew(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf("Using account: %s\n", accountHandle)
|
||||
}
|
||||
|
||||
// Check if session exists
|
||||
t := tmux.NewTmux()
|
||||
sessionID := crewSessionName(rigName, name)
|
||||
hasSession, err := t.HasSession(sessionID)
|
||||
// Use manager's Start() method - handles workspace creation, settings, and session
|
||||
err = crewMgr.Start(name, crew.StartOptions{
|
||||
Account: startCrewAccount,
|
||||
ClaudeConfigDir: claudeConfigDir,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking session: %w", err)
|
||||
}
|
||||
|
||||
if hasSession {
|
||||
// Session exists - check if Claude is still running
|
||||
agentCfg, _, err := config.ResolveAgentConfigWithOverride(townRoot, r.Path, startCrewAgentOverride)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving agent: %w", err)
|
||||
}
|
||||
if !t.IsAgentRunning(sessionID, config.ExpectedPaneCommands(agentCfg)...) {
|
||||
// Claude has exited, restart it with "gt prime" as initial prompt
|
||||
fmt.Printf("Session exists, restarting Claude...\n")
|
||||
startupCmd, err := config.BuildCrewStartupCommandWithAgentOverride(rigName, name, r.Path, "gt prime", startCrewAgentOverride)
|
||||
if err != nil {
|
||||
return fmt.Errorf("building startup command: %w", err)
|
||||
}
|
||||
if err := t.SendKeys(sessionID, startupCmd); err != nil {
|
||||
return fmt.Errorf("restarting claude: %w", err)
|
||||
}
|
||||
if errors.Is(err, crew.ErrSessionRunning) {
|
||||
fmt.Printf("%s Session already running: %s\n", style.Dim.Render("○"), crewMgr.SessionName(name))
|
||||
} else {
|
||||
fmt.Printf("%s Session already running: %s\n", style.Dim.Render("○"), sessionID)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// 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_RIG", rigName)
|
||||
_ = 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(rigName)
|
||||
_ = t.ConfigureGasTownSession(sessionID, theme, rigName, name, "crew")
|
||||
|
||||
// Wait for shell to be ready after session creation
|
||||
if err := t.WaitForShellReady(sessionID, constants.ShellReadyTimeout); err != nil {
|
||||
return fmt.Errorf("waiting for shell: %w", err)
|
||||
}
|
||||
|
||||
// Start claude with skip permissions and proper env vars for seance
|
||||
// Pass "gt prime" as initial prompt so context is loaded immediately
|
||||
startupCmd, err := config.BuildCrewStartupCommandWithAgentOverride(rigName, name, r.Path, "gt prime", startCrewAgentOverride)
|
||||
if err != nil {
|
||||
return fmt.Errorf("building startup command: %w", err)
|
||||
}
|
||||
if err := t.SendKeys(sessionID, startupCmd); err != nil {
|
||||
return fmt.Errorf("starting claude: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Started crew workspace: %s/%s\n",
|
||||
style.Bold.Render("✓"), rigName, name)
|
||||
}
|
||||
@@ -926,54 +860,14 @@ func startCrewMember(rigName, crewName, townRoot string) error {
|
||||
return fmt.Errorf("rig '%s' not found", rigName)
|
||||
}
|
||||
|
||||
// Create crew manager
|
||||
// Create crew manager and use Start() method
|
||||
crewGit := git.NewGit(r.Path)
|
||||
crewMgr := crew.NewManager(r, crewGit)
|
||||
|
||||
// Check if crew exists, create if not
|
||||
worker, err := crewMgr.Get(crewName)
|
||||
if err == crew.ErrCrewNotFound {
|
||||
worker, err = crewMgr.Add(crewName, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating crew workspace: %w", err)
|
||||
}
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("getting crew worker: %w", err)
|
||||
}
|
||||
|
||||
// Ensure crew workspace is on default branch
|
||||
ensureDefaultBranch(worker.ClonePath, fmt.Sprintf("Crew workspace %s/%s", rigName, crewName), r.Path)
|
||||
|
||||
// Create tmux session
|
||||
t := tmux.NewTmux()
|
||||
sessionID := crewSessionName(rigName, crewName)
|
||||
|
||||
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_RIG", rigName)
|
||||
_ = t.SetEnvironment(sessionID, "GT_CREW", crewName)
|
||||
|
||||
// Apply rig-based theming (non-fatal: theming failure doesn't affect operation)
|
||||
theme := getThemeForRig(rigName)
|
||||
_ = t.ConfigureGasTownSession(sessionID, theme, rigName, crewName, "crew")
|
||||
|
||||
// Set up C-b n/p keybindings for crew session cycling (non-fatal)
|
||||
_ = t.SetCrewCycleBindings(sessionID)
|
||||
|
||||
// Wait for shell to be ready
|
||||
if err := t.WaitForShellReady(sessionID, constants.ShellReadyTimeout); err != nil {
|
||||
return fmt.Errorf("waiting for shell: %w", err)
|
||||
}
|
||||
|
||||
// Start claude with proper env vars for seance
|
||||
// Pass "gt prime" as initial prompt so context is loaded immediately
|
||||
// (SessionStart hook fires, then Claude processes "gt prime" as first user message)
|
||||
claudeCmd := config.BuildCrewStartupCommand(rigName, crewName, r.Path, "gt prime")
|
||||
if err := t.SendKeys(sessionID, claudeCmd); err != nil {
|
||||
return fmt.Errorf("starting claude: %w", err)
|
||||
// Start handles workspace creation, settings, and session all in one
|
||||
err = crewMgr.Start(crewName, crew.StartOptions{})
|
||||
if err != nil && !errors.Is(err, crew.ErrSessionRunning) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user