refactor: unify agent startup with Manager pattern
- Create mayor.Manager for mayor lifecycle (Start/Stop/IsRunning/Status) - Create deacon.Manager for deacon lifecycle with respawn loop - Move session.Manager to polecat.SessionManager (clearer naming) - Add zombie session detection for mayor/deacon (kills tmux if Claude dead) - Remove duplicate session startup code from up.go, start.go, mayor.go - Rename sessMgr -> polecatMgr for consistency - Make witness/refinery SessionName() public for status display All agent types now follow the same Manager pattern: mgr := agent.NewManager(...) mgr.Start(...) mgr.Stop() mgr.IsRunning() mgr.Status() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,26 +1,14 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/mayor"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
|
||||
// getMayorSessionName returns the Mayor session name.
|
||||
func getMayorSessionName() string {
|
||||
return session.MayorSessionName()
|
||||
}
|
||||
|
||||
var mayorCmd = &cobra.Command{
|
||||
Use: "mayor",
|
||||
Aliases: []string{"may"},
|
||||
@@ -95,21 +83,31 @@ func init() {
|
||||
rootCmd.AddCommand(mayorCmd)
|
||||
}
|
||||
|
||||
func runMayorStart(cmd *cobra.Command, args []string) error {
|
||||
t := tmux.NewTmux()
|
||||
|
||||
sessionName := getMayorSessionName()
|
||||
|
||||
// Check if session already exists
|
||||
running, err := t.HasSession(sessionName)
|
||||
// getMayorManager returns a mayor manager for the current workspace.
|
||||
func getMayorManager() (*mayor.Manager, error) {
|
||||
townRoot, err := workspace.FindFromCwdOrError()
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking session: %w", err)
|
||||
return nil, fmt.Errorf("not in a Gas Town workspace: %w", err)
|
||||
}
|
||||
if running {
|
||||
return fmt.Errorf("Mayor session already running. Attach with: gt mayor attach")
|
||||
return mayor.NewManager(townRoot), nil
|
||||
}
|
||||
|
||||
// getMayorSessionName returns the Mayor session name.
|
||||
func getMayorSessionName() string {
|
||||
return mayor.SessionName()
|
||||
}
|
||||
|
||||
func runMayorStart(cmd *cobra.Command, args []string) error {
|
||||
mgr, err := getMayorManager()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := startMayorSession(t, sessionName, mayorAgentOverride); err != nil {
|
||||
fmt.Println("Starting Mayor session...")
|
||||
if err := mgr.Start(mayorAgentOverride); err != nil {
|
||||
if err == mayor.ErrAlreadyRunning {
|
||||
return fmt.Errorf("Mayor session already running. Attach with: gt mayor attach")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -120,93 +118,18 @@ func runMayorStart(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// startMayorSession creates and initializes the Mayor tmux session.
|
||||
func startMayorSession(t *tmux.Tmux, sessionName, agentOverride string) error {
|
||||
// Find workspace root
|
||||
townRoot, err := workspace.FindFromCwdOrError()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a Gas Town workspace: %w", err)
|
||||
}
|
||||
|
||||
// Mayor runs in mayor/ subdirectory to keep its files (CLAUDE.md, settings)
|
||||
// separate from child agents that inherit the working directory
|
||||
mayorDir := filepath.Join(townRoot, "mayor")
|
||||
if err := os.MkdirAll(mayorDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating mayor directory: %w", err)
|
||||
}
|
||||
|
||||
// Create session in mayor directory
|
||||
fmt.Println("Starting Mayor session...")
|
||||
if err := t.NewSession(sessionName, mayorDir); err != nil {
|
||||
return fmt.Errorf("creating session: %w", err)
|
||||
}
|
||||
|
||||
// Set environment (non-fatal: session works without these)
|
||||
_ = t.SetEnvironment(sessionName, "GT_ROLE", "mayor")
|
||||
_ = t.SetEnvironment(sessionName, "BD_ACTOR", "mayor")
|
||||
|
||||
// Apply Mayor theme (non-fatal: theming failure doesn't affect operation)
|
||||
// Note: ConfigureGasTownSession includes cycle bindings
|
||||
theme := tmux.MayorTheme()
|
||||
_ = t.ConfigureGasTownSession(sessionName, theme, "", "Mayor", "coordinator")
|
||||
|
||||
// Launch Claude - the startup hook handles 'gt prime' automatically
|
||||
// Use SendKeysDelayed to allow shell initialization after NewSession
|
||||
// Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes
|
||||
// Mayor uses default runtime config (empty rigPath) since it's not rig-specific
|
||||
startupCmd, err := config.BuildAgentStartupCommandWithAgentOverride("mayor", "mayor", "", "", agentOverride)
|
||||
if err != nil {
|
||||
return fmt.Errorf("building startup command: %w", err)
|
||||
}
|
||||
if err := t.SendKeysDelayed(sessionName, startupCmd, 200); err != nil {
|
||||
return fmt.Errorf("sending command: %w", err)
|
||||
}
|
||||
|
||||
// Wait for Claude to start (non-fatal)
|
||||
if err := t.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil {
|
||||
// Non-fatal
|
||||
}
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
// Inject startup nudge for predecessor discovery via /resume
|
||||
_ = session.StartupNudge(t, sessionName, session.StartupNudgeConfig{
|
||||
Recipient: "mayor",
|
||||
Sender: "human",
|
||||
Topic: "cold-start",
|
||||
}) // Non-fatal
|
||||
|
||||
// GUPP: Gas Town Universal Propulsion Principle
|
||||
// Send the propulsion nudge to trigger autonomous coordination.
|
||||
// Wait for beacon to be fully processed (needs to be separate prompt)
|
||||
time.Sleep(2 * time.Second)
|
||||
_ = t.NudgeSession(sessionName, session.PropulsionNudgeForRole("mayor", mayorDir)) // Non-fatal
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMayorStop(cmd *cobra.Command, args []string) error {
|
||||
t := tmux.NewTmux()
|
||||
|
||||
sessionName := getMayorSessionName()
|
||||
|
||||
// Check if session exists
|
||||
running, err := t.HasSession(sessionName)
|
||||
mgr, err := getMayorManager()
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking session: %w", err)
|
||||
}
|
||||
if !running {
|
||||
return errors.New("Mayor session is not running")
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Stopping Mayor session...")
|
||||
|
||||
// Try graceful shutdown first (best-effort interrupt)
|
||||
_ = t.SendKeysRaw(sessionName, "C-c")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Kill the session
|
||||
if err := t.KillSession(sessionName); err != nil {
|
||||
return fmt.Errorf("killing session: %w", err)
|
||||
if err := mgr.Stop(); err != nil {
|
||||
if err == mayor.ErrNotRunning {
|
||||
return fmt.Errorf("Mayor session is not running")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("%s Mayor session stopped.\n", style.Bold.Render("✓"))
|
||||
@@ -214,84 +137,68 @@ func runMayorStop(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
func runMayorAttach(cmd *cobra.Command, args []string) error {
|
||||
t := tmux.NewTmux()
|
||||
mgr, err := getMayorManager()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sessionName := getMayorSessionName()
|
||||
|
||||
// Check if session exists
|
||||
running, err := t.HasSession(sessionName)
|
||||
running, err := mgr.IsRunning()
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking session: %w", err)
|
||||
}
|
||||
if !running {
|
||||
// Auto-start if not running
|
||||
fmt.Println("Mayor session not running, starting...")
|
||||
if err := startMayorSession(t, sessionName, mayorAgentOverride); err != nil {
|
||||
if err := mgr.Start(mayorAgentOverride); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Use shared attach helper (smart: links if inside tmux, attaches if outside)
|
||||
return attachToTmuxSession(sessionName)
|
||||
return attachToTmuxSession(mgr.SessionName())
|
||||
}
|
||||
|
||||
func runMayorStatus(cmd *cobra.Command, args []string) error {
|
||||
t := tmux.NewTmux()
|
||||
|
||||
sessionName := getMayorSessionName()
|
||||
|
||||
running, err := t.HasSession(sessionName)
|
||||
mgr, err := getMayorManager()
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking session: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if running {
|
||||
// Get session info for more details
|
||||
info, err := t.GetSessionInfo(sessionName)
|
||||
if err == nil {
|
||||
status := "detached"
|
||||
if info.Attached {
|
||||
status = "attached"
|
||||
}
|
||||
info, err := mgr.Status()
|
||||
if err != nil {
|
||||
if err == mayor.ErrNotRunning {
|
||||
fmt.Printf("%s Mayor session is %s\n",
|
||||
style.Bold.Render("●"),
|
||||
style.Bold.Render("running"))
|
||||
fmt.Printf(" Status: %s\n", status)
|
||||
fmt.Printf(" Created: %s\n", info.Created)
|
||||
fmt.Printf("\nAttach with: %s\n", style.Dim.Render("gt mayor attach"))
|
||||
} else {
|
||||
fmt.Printf("%s Mayor session is %s\n",
|
||||
style.Bold.Render("●"),
|
||||
style.Bold.Render("running"))
|
||||
style.Dim.Render("○"),
|
||||
"not running")
|
||||
fmt.Printf("\nStart with: %s\n", style.Dim.Render("gt mayor start"))
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("%s Mayor session is %s\n",
|
||||
style.Dim.Render("○"),
|
||||
"not running")
|
||||
fmt.Printf("\nStart with: %s\n", style.Dim.Render("gt mayor start"))
|
||||
return fmt.Errorf("checking status: %w", err)
|
||||
}
|
||||
|
||||
status := "detached"
|
||||
if info.Attached {
|
||||
status = "attached"
|
||||
}
|
||||
fmt.Printf("%s Mayor session is %s\n",
|
||||
style.Bold.Render("●"),
|
||||
style.Bold.Render("running"))
|
||||
fmt.Printf(" Status: %s\n", status)
|
||||
fmt.Printf(" Created: %s\n", info.Created)
|
||||
fmt.Printf("\nAttach with: %s\n", style.Dim.Render("gt mayor attach"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMayorRestart(cmd *cobra.Command, args []string) error {
|
||||
t := tmux.NewTmux()
|
||||
|
||||
sessionName := getMayorSessionName()
|
||||
|
||||
running, err := t.HasSession(sessionName)
|
||||
mgr, err := getMayorManager()
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking session: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if running {
|
||||
// Stop the current session (best-effort interrupt before kill)
|
||||
fmt.Println("Stopping Mayor session...")
|
||||
_ = t.SendKeysRaw(sessionName, "C-c")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if err := t.KillSession(sessionName); err != nil {
|
||||
return fmt.Errorf("killing session: %w", err)
|
||||
}
|
||||
// Stop if running (ignore not-running error)
|
||||
if err := mgr.Stop(); err != nil && err != mayor.ErrNotRunning {
|
||||
return fmt.Errorf("stopping session: %w", err)
|
||||
}
|
||||
|
||||
// Start fresh
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/git"
|
||||
"github.com/steveyegge/gastown/internal/polecat"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
)
|
||||
@@ -361,7 +360,7 @@ func runPolecatList(cmd *cobra.Command, args []string) error {
|
||||
for _, r := range rigs {
|
||||
polecatGit := git.NewGit(r.Path)
|
||||
mgr := polecat.NewManager(r, polecatGit)
|
||||
sessMgr := session.NewManager(t, r)
|
||||
polecatMgr := polecat.NewSessionManager(t, r)
|
||||
|
||||
polecats, err := mgr.List()
|
||||
if err != nil {
|
||||
@@ -370,7 +369,7 @@ func runPolecatList(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
for _, p := range polecats {
|
||||
running, _ := sessMgr.IsRunning(p.Name)
|
||||
running, _ := polecatMgr.IsRunning(p.Name)
|
||||
allPolecats = append(allPolecats, PolecatListItem{
|
||||
Rig: r.Name,
|
||||
Name: p.Name,
|
||||
@@ -525,8 +524,8 @@ func runPolecatRemove(cmd *cobra.Command, args []string) error {
|
||||
for _, p := range toRemove {
|
||||
// Check if session is running
|
||||
if !polecatForce {
|
||||
sessMgr := session.NewManager(t, p.r)
|
||||
running, _ := sessMgr.IsRunning(p.polecatName)
|
||||
polecatMgr := polecat.NewSessionManager(t, p.r)
|
||||
running, _ := polecatMgr.IsRunning(p.polecatName)
|
||||
if running {
|
||||
removeErrors = append(removeErrors, fmt.Sprintf("%s/%s: session is running (stop first or use --force)", p.rigName, p.polecatName))
|
||||
continue
|
||||
@@ -682,11 +681,11 @@ func runPolecatStatus(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// Get session info
|
||||
t := tmux.NewTmux()
|
||||
sessMgr := session.NewManager(t, r)
|
||||
sessInfo, err := sessMgr.Status(polecatName)
|
||||
polecatMgr := polecat.NewSessionManager(t, r)
|
||||
sessInfo, err := polecatMgr.Status(polecatName)
|
||||
if err != nil {
|
||||
// Non-fatal - continue without session info
|
||||
sessInfo = &session.Info{
|
||||
sessInfo = &polecat.SessionInfo{
|
||||
Polecat: polecatName,
|
||||
Running: false,
|
||||
}
|
||||
@@ -1415,10 +1414,10 @@ func runPolecatNuke(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Step 1: Kill session (force mode - no graceful shutdown)
|
||||
sessMgr := session.NewManager(t, p.r)
|
||||
running, _ := sessMgr.IsRunning(p.polecatName)
|
||||
polecatMgr := polecat.NewSessionManager(t, p.r)
|
||||
running, _ := polecatMgr.IsRunning(p.polecatName)
|
||||
if running {
|
||||
if err := sessMgr.Stop(p.polecatName, true); err != nil {
|
||||
if err := polecatMgr.Stop(p.polecatName, true); err != nil {
|
||||
fmt.Printf(" %s session kill failed: %v\n", style.Warning.Render("⚠"), err)
|
||||
// Continue anyway - worktree removal will still work
|
||||
} else {
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/git"
|
||||
"github.com/steveyegge/gastown/internal/polecat"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
@@ -152,13 +151,13 @@ func SpawnPolecatForSling(rigName string, opts SlingSpawnOptions) (*SpawnedPolec
|
||||
|
||||
// Start session
|
||||
t := tmux.NewTmux()
|
||||
sessMgr := session.NewManager(t, r)
|
||||
polecatSessMgr := polecat.NewSessionManager(t, r)
|
||||
|
||||
// Check if already running
|
||||
running, _ := sessMgr.IsRunning(polecatName)
|
||||
running, _ := polecatSessMgr.IsRunning(polecatName)
|
||||
if !running {
|
||||
fmt.Printf("Starting session for %s/%s...\n", rigName, polecatName)
|
||||
startOpts := session.StartOptions{
|
||||
startOpts := polecat.SessionStartOptions{
|
||||
ClaudeConfigDir: claudeConfigDir,
|
||||
}
|
||||
if opts.Agent != "" {
|
||||
@@ -168,13 +167,13 @@ func SpawnPolecatForSling(rigName string, opts SlingSpawnOptions) (*SpawnedPolec
|
||||
}
|
||||
startOpts.Command = cmd
|
||||
}
|
||||
if err := sessMgr.Start(polecatName, startOpts); err != nil {
|
||||
if err := polecatSessMgr.Start(polecatName, startOpts); err != nil {
|
||||
return nil, fmt.Errorf("starting session: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get session name and pane
|
||||
sessionName := sessMgr.SessionName(polecatName)
|
||||
sessionName := polecatSessMgr.SessionName(polecatName)
|
||||
pane, err := getSessionPane(sessionName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting pane for %s: %w", sessionName, err)
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/polecat"
|
||||
"github.com/steveyegge/gastown/internal/refinery"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/wisp"
|
||||
@@ -958,11 +957,11 @@ func runRigShutdown(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// 1. Stop all polecat sessions
|
||||
t := tmux.NewTmux()
|
||||
sessMgr := session.NewManager(t, r)
|
||||
infos, err := sessMgr.List()
|
||||
polecatMgr := polecat.NewSessionManager(t, r)
|
||||
infos, err := polecatMgr.List()
|
||||
if err == nil && len(infos) > 0 {
|
||||
fmt.Printf(" Stopping %d polecat session(s)...\n", len(infos))
|
||||
if err := sessMgr.StopAll(rigShutdownForce); err != nil {
|
||||
if err := polecatMgr.StopAll(rigShutdownForce); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("polecat sessions: %v", err))
|
||||
}
|
||||
}
|
||||
@@ -1235,11 +1234,11 @@ func runRigStop(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// 1. Stop all polecat sessions
|
||||
t := tmux.NewTmux()
|
||||
sessMgr := session.NewManager(t, r)
|
||||
infos, err := sessMgr.List()
|
||||
polecatMgr := polecat.NewSessionManager(t, r)
|
||||
infos, err := polecatMgr.List()
|
||||
if err == nil && len(infos) > 0 {
|
||||
fmt.Printf(" Stopping %d polecat session(s)...\n", len(infos))
|
||||
if err := sessMgr.StopAll(rigStopForce); err != nil {
|
||||
if err := polecatMgr.StopAll(rigStopForce); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("polecat sessions: %v", err))
|
||||
}
|
||||
}
|
||||
@@ -1368,11 +1367,11 @@ func runRigRestart(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf(" Stopping...\n")
|
||||
|
||||
// 1. Stop all polecat sessions
|
||||
sessMgr := session.NewManager(t, r)
|
||||
infos, err := sessMgr.List()
|
||||
polecatMgr := polecat.NewSessionManager(t, r)
|
||||
infos, err := polecatMgr.List()
|
||||
if err == nil && len(infos) > 0 {
|
||||
fmt.Printf(" Stopping %d polecat session(s)...\n", len(infos))
|
||||
if err := sessMgr.StopAll(rigRestartForce); err != nil {
|
||||
if err := polecatMgr.StopAll(rigRestartForce); err != nil {
|
||||
stopErrors = append(stopErrors, fmt.Sprintf("polecat sessions: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/git"
|
||||
"github.com/steveyegge/gastown/internal/polecat"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/suggest"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
@@ -224,16 +224,16 @@ func parseAddress(addr string) (rigName, polecatName string, err error) {
|
||||
}
|
||||
|
||||
// getSessionManager creates a session manager for the given rig.
|
||||
func getSessionManager(rigName string) (*session.Manager, *rig.Rig, error) {
|
||||
func getSessionManager(rigName string) (*polecat.SessionManager, *rig.Rig, error) {
|
||||
_, r, err := getRig(rigName)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
t := tmux.NewTmux()
|
||||
mgr := session.NewManager(t, r)
|
||||
polecatMgr := polecat.NewSessionManager(t, r)
|
||||
|
||||
return mgr, r, nil
|
||||
return polecatMgr, r, nil
|
||||
}
|
||||
|
||||
func runSessionStart(cmd *cobra.Command, args []string) error {
|
||||
@@ -242,7 +242,7 @@ func runSessionStart(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
mgr, r, err := getSessionManager(rigName)
|
||||
polecatMgr, r, err := getSessionManager(rigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -261,12 +261,12 @@ func runSessionStart(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("%s", suggest.FormatSuggestion("Polecat", polecatName, suggestions, hint))
|
||||
}
|
||||
|
||||
opts := session.StartOptions{
|
||||
opts := polecat.SessionStartOptions{
|
||||
Issue: sessionIssue,
|
||||
}
|
||||
|
||||
fmt.Printf("Starting session for %s/%s...\n", rigName, polecatName)
|
||||
if err := mgr.Start(polecatName, opts); err != nil {
|
||||
if err := polecatMgr.Start(polecatName, opts); err != nil {
|
||||
return fmt.Errorf("starting session: %w", err)
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@ func runSessionStop(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
mgr, _, err := getSessionManager(rigName)
|
||||
polecatMgr, _, err := getSessionManager(rigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -300,7 +300,7 @@ func runSessionStop(cmd *cobra.Command, args []string) error {
|
||||
} else {
|
||||
fmt.Printf("Stopping session for %s/%s...\n", rigName, polecatName)
|
||||
}
|
||||
if err := mgr.Stop(polecatName, sessionForce); err != nil {
|
||||
if err := polecatMgr.Stop(polecatName, sessionForce); err != nil {
|
||||
return fmt.Errorf("stopping session: %w", err)
|
||||
}
|
||||
|
||||
@@ -326,13 +326,13 @@ func runSessionAttach(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
mgr, _, err := getSessionManager(rigName)
|
||||
polecatMgr, _, err := getSessionManager(rigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Attach (this replaces the process)
|
||||
return mgr.Attach(polecatName)
|
||||
return polecatMgr.Attach(polecatName)
|
||||
}
|
||||
|
||||
// SessionListItem represents a session in list output.
|
||||
@@ -381,8 +381,8 @@ func runSessionList(cmd *cobra.Command, args []string) error {
|
||||
var allSessions []SessionListItem
|
||||
|
||||
for _, r := range rigs {
|
||||
mgr := session.NewManager(t, r)
|
||||
infos, err := mgr.List()
|
||||
polecatMgr := polecat.NewSessionManager(t, r)
|
||||
infos, err := polecatMgr.List()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -428,7 +428,7 @@ func runSessionCapture(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
mgr, _, err := getSessionManager(rigName)
|
||||
polecatMgr, _, err := getSessionManager(rigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -446,7 +446,7 @@ func runSessionCapture(cmd *cobra.Command, args []string) error {
|
||||
lines = n
|
||||
}
|
||||
|
||||
output, err := mgr.Capture(polecatName, lines)
|
||||
output, err := polecatMgr.Capture(polecatName, lines)
|
||||
if err != nil {
|
||||
return fmt.Errorf("capturing output: %w", err)
|
||||
}
|
||||
@@ -475,12 +475,12 @@ func runSessionInject(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("no message provided (use -m or -f)")
|
||||
}
|
||||
|
||||
mgr, _, err := getSessionManager(rigName)
|
||||
polecatMgr, _, err := getSessionManager(rigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := mgr.Inject(polecatName, message); err != nil {
|
||||
if err := polecatMgr.Inject(polecatName, message); err != nil {
|
||||
return fmt.Errorf("injecting message: %w", err)
|
||||
}
|
||||
|
||||
@@ -495,13 +495,13 @@ func runSessionRestart(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
mgr, _, err := getSessionManager(rigName)
|
||||
polecatMgr, _, err := getSessionManager(rigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if running
|
||||
running, err := mgr.IsRunning(polecatName)
|
||||
running, err := polecatMgr.IsRunning(polecatName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking session: %w", err)
|
||||
}
|
||||
@@ -513,15 +513,15 @@ func runSessionRestart(cmd *cobra.Command, args []string) error {
|
||||
} else {
|
||||
fmt.Printf("Stopping session for %s/%s...\n", rigName, polecatName)
|
||||
}
|
||||
if err := mgr.Stop(polecatName, sessionForce); err != nil {
|
||||
if err := polecatMgr.Stop(polecatName, sessionForce); err != nil {
|
||||
return fmt.Errorf("stopping session: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Start fresh session
|
||||
fmt.Printf("Starting session for %s/%s...\n", rigName, polecatName)
|
||||
opts := session.StartOptions{}
|
||||
if err := mgr.Start(polecatName, opts); err != nil {
|
||||
opts := polecat.SessionStartOptions{}
|
||||
if err := polecatMgr.Start(polecatName, opts); err != nil {
|
||||
return fmt.Errorf("starting session: %w", err)
|
||||
}
|
||||
|
||||
@@ -537,13 +537,13 @@ func runSessionStatus(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
mgr, _, err := getSessionManager(rigName)
|
||||
polecatMgr, _, err := getSessionManager(rigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get session info
|
||||
info, err := mgr.Status(polecatName)
|
||||
info, err := polecatMgr.Status(polecatName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting status: %w", err)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
"github.com/steveyegge/gastown/internal/crew"
|
||||
"github.com/steveyegge/gastown/internal/deacon"
|
||||
"github.com/steveyegge/gastown/internal/git"
|
||||
"github.com/steveyegge/gastown/internal/mayor"
|
||||
"github.com/steveyegge/gastown/internal/polecat"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
@@ -160,7 +162,7 @@ func runStart(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf("Starting Gas Town from %s\n\n", style.Dim.Render(townRoot))
|
||||
|
||||
// Start core agents (Mayor and Deacon)
|
||||
if err := startCoreAgents(t, startAgentOverride); err != nil {
|
||||
if err := startCoreAgents(townRoot, startAgentOverride); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -186,33 +188,29 @@ func runStart(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// startCoreAgents starts Mayor and Deacon sessions.
|
||||
func startCoreAgents(t *tmux.Tmux, agentOverride string) error {
|
||||
// Get session names
|
||||
mayorSession := getMayorSessionName()
|
||||
deaconSession := getDeaconSessionName()
|
||||
|
||||
// startCoreAgents starts Mayor and Deacon sessions using the Manager pattern.
|
||||
func startCoreAgents(townRoot string, agentOverride string) error {
|
||||
// Start Mayor first (so Deacon sees it as up)
|
||||
mayorRunning, _ := t.HasSession(mayorSession)
|
||||
if mayorRunning {
|
||||
fmt.Printf(" %s Mayor already running\n", style.Dim.Render("○"))
|
||||
} else {
|
||||
fmt.Printf(" %s Starting Mayor...\n", style.Bold.Render("→"))
|
||||
if err := startMayorSession(t, mayorSession, agentOverride); err != nil {
|
||||
mayorMgr := mayor.NewManager(townRoot)
|
||||
if err := mayorMgr.Start(agentOverride); err != nil {
|
||||
if err == mayor.ErrAlreadyRunning {
|
||||
fmt.Printf(" %s Mayor already running\n", style.Dim.Render("○"))
|
||||
} else {
|
||||
return fmt.Errorf("starting Mayor: %w", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" %s Mayor started\n", style.Bold.Render("✓"))
|
||||
}
|
||||
|
||||
// Start Deacon (health monitor)
|
||||
deaconRunning, _ := t.HasSession(deaconSession)
|
||||
if deaconRunning {
|
||||
fmt.Printf(" %s Deacon already running\n", style.Dim.Render("○"))
|
||||
} else {
|
||||
fmt.Printf(" %s Starting Deacon...\n", style.Bold.Render("→"))
|
||||
if err := startDeaconSession(t, deaconSession, agentOverride); err != nil {
|
||||
deaconMgr := deacon.NewManager(townRoot)
|
||||
if err := deaconMgr.Start(); err != nil {
|
||||
if err == deacon.ErrAlreadyRunning {
|
||||
fmt.Printf(" %s Deacon already running\n", style.Dim.Render("○"))
|
||||
} else {
|
||||
return fmt.Errorf("starting Deacon: %w", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" %s Deacon started\n", style.Bold.Render("✓"))
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/events"
|
||||
"github.com/steveyegge/gastown/internal/git"
|
||||
"github.com/steveyegge/gastown/internal/polecat"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/townlog"
|
||||
@@ -111,8 +111,8 @@ func runStop(cmd *cobra.Command, args []string) error {
|
||||
stopped := 0
|
||||
|
||||
for _, r := range rigs {
|
||||
mgr := session.NewManager(t, r)
|
||||
infos, err := mgr.List()
|
||||
polecatMgr := polecat.NewSessionManager(t, r)
|
||||
infos, err := polecatMgr.List()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -125,10 +125,10 @@ func runStop(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Capture output before stopping (best effort)
|
||||
output, _ := mgr.Capture(info.Polecat, 50)
|
||||
output, _ := polecatMgr.Capture(info.Polecat, 50)
|
||||
|
||||
// Stop the session
|
||||
err := mgr.Stop(info.Polecat, force)
|
||||
err := polecatMgr.Stop(info.Polecat, force)
|
||||
if err != nil {
|
||||
result.Success = false
|
||||
result.Error = err.Error()
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/git"
|
||||
"github.com/steveyegge/gastown/internal/polecat"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/swarm"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
@@ -528,7 +527,7 @@ func spawnSwarmWorkersFromBeads(r *rig.Rig, townRoot string, swarmID string, wor
|
||||
Title string `json:"title"`
|
||||
}) error { //nolint:unparam // error return kept for future use
|
||||
t := tmux.NewTmux()
|
||||
sessMgr := session.NewManager(t, r)
|
||||
polecatSessMgr := polecat.NewSessionManager(t, r)
|
||||
polecatGit := git.NewGit(r.Path)
|
||||
polecatMgr := polecat.NewManager(r, polecatGit)
|
||||
|
||||
@@ -556,12 +555,12 @@ func spawnSwarmWorkersFromBeads(r *rig.Rig, townRoot string, swarmID string, wor
|
||||
}
|
||||
|
||||
// Check if already running
|
||||
running, _ := sessMgr.IsRunning(worker)
|
||||
running, _ := polecatSessMgr.IsRunning(worker)
|
||||
if running {
|
||||
fmt.Printf(" %s already running, injecting task...\n", worker)
|
||||
} else {
|
||||
fmt.Printf(" Starting %s...\n", worker)
|
||||
if err := sessMgr.Start(worker, session.StartOptions{}); err != nil {
|
||||
if err := polecatSessMgr.Start(worker, polecat.SessionStartOptions{}); err != nil {
|
||||
style.PrintWarning(" couldn't start %s: %v", worker, err)
|
||||
continue
|
||||
}
|
||||
@@ -572,7 +571,7 @@ func spawnSwarmWorkersFromBeads(r *rig.Rig, townRoot string, swarmID string, wor
|
||||
// Inject work assignment
|
||||
context := fmt.Sprintf("[SWARM] You are part of swarm %s.\n\nAssigned task: %s\nTitle: %s\n\nWork on this task. When complete, commit and signal DONE.",
|
||||
swarmID, task.ID, task.Title)
|
||||
if err := sessMgr.Inject(worker, context); err != nil {
|
||||
if err := polecatSessMgr.Inject(worker, context); err != nil {
|
||||
style.PrintWarning(" couldn't inject to %s: %v", worker, err)
|
||||
} else {
|
||||
fmt.Printf(" %s → %s ✓\n", worker, task.ID)
|
||||
|
||||
@@ -10,13 +10,14 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/claude"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
"github.com/steveyegge/gastown/internal/crew"
|
||||
"github.com/steveyegge/gastown/internal/daemon"
|
||||
"github.com/steveyegge/gastown/internal/deacon"
|
||||
"github.com/steveyegge/gastown/internal/events"
|
||||
"github.com/steveyegge/gastown/internal/mayor"
|
||||
"github.com/steveyegge/gastown/internal/polecat"
|
||||
"github.com/steveyegge/gastown/internal/refinery"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/witness"
|
||||
@@ -67,7 +68,6 @@ func runUp(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not in a Gas Town workspace: %w", err)
|
||||
}
|
||||
|
||||
t := tmux.NewTmux()
|
||||
allOK := true
|
||||
|
||||
// 1. Daemon (Go process)
|
||||
@@ -81,36 +81,35 @@ func runUp(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Get session names
|
||||
deaconSession := getDeaconSessionName()
|
||||
mayorSession := getMayorSessionName()
|
||||
|
||||
// 2. Deacon (Claude agent) - runs from townRoot/deacon/
|
||||
deaconDir := filepath.Join(townRoot, "deacon")
|
||||
if err := ensureSession(t, deaconSession, deaconDir, "deacon"); err != nil {
|
||||
printStatus("Deacon", false, err.Error())
|
||||
allOK = false
|
||||
// 2. Deacon (Claude agent)
|
||||
deaconMgr := deacon.NewManager(townRoot)
|
||||
if err := deaconMgr.Start(); err != nil {
|
||||
if err == deacon.ErrAlreadyRunning {
|
||||
printStatus("Deacon", true, deaconMgr.SessionName())
|
||||
} else {
|
||||
printStatus("Deacon", false, err.Error())
|
||||
allOK = false
|
||||
}
|
||||
} else {
|
||||
printStatus("Deacon", true, deaconSession)
|
||||
printStatus("Deacon", true, deaconMgr.SessionName())
|
||||
}
|
||||
|
||||
// 3. Mayor (Claude agent) - runs from townRoot/mayor/
|
||||
// IMPORTANT: Both settings.json and CLAUDE.md must be in ~/gt/mayor/, NOT ~/gt/
|
||||
// Files at town root would be inherited by ALL agents via directory traversal,
|
||||
// causing crew/polecat/etc to receive Mayor-specific context.
|
||||
mayorDir := filepath.Join(townRoot, "mayor")
|
||||
if err := ensureSession(t, mayorSession, mayorDir, "mayor"); err != nil {
|
||||
printStatus("Mayor", false, err.Error())
|
||||
allOK = false
|
||||
// 3. Mayor (Claude agent)
|
||||
mayorMgr := mayor.NewManager(townRoot)
|
||||
if err := mayorMgr.Start(""); err != nil {
|
||||
if err == mayor.ErrAlreadyRunning {
|
||||
printStatus("Mayor", true, mayorMgr.SessionName())
|
||||
} else {
|
||||
printStatus("Mayor", false, err.Error())
|
||||
allOK = false
|
||||
}
|
||||
} else {
|
||||
printStatus("Mayor", true, mayorSession)
|
||||
printStatus("Mayor", true, mayorMgr.SessionName())
|
||||
}
|
||||
|
||||
// 4. Witnesses (one per rig)
|
||||
rigs := discoverRigs(townRoot)
|
||||
for _, rigName := range rigs {
|
||||
sessionName := fmt.Sprintf("gt-%s-witness", rigName)
|
||||
|
||||
_, r, err := getRig(rigName)
|
||||
if err != nil {
|
||||
printStatus(fmt.Sprintf("Witness (%s)", rigName), false, err.Error())
|
||||
@@ -121,13 +120,13 @@ func runUp(cmd *cobra.Command, args []string) error {
|
||||
mgr := witness.NewManager(r)
|
||||
if err := mgr.Start(false); err != nil {
|
||||
if err == witness.ErrAlreadyRunning {
|
||||
printStatus(fmt.Sprintf("Witness (%s)", rigName), true, sessionName)
|
||||
printStatus(fmt.Sprintf("Witness (%s)", rigName), true, mgr.SessionName())
|
||||
} else {
|
||||
printStatus(fmt.Sprintf("Witness (%s)", rigName), false, err.Error())
|
||||
allOK = false
|
||||
}
|
||||
} else {
|
||||
printStatus(fmt.Sprintf("Witness (%s)", rigName), true, sessionName)
|
||||
printStatus(fmt.Sprintf("Witness (%s)", rigName), true, mgr.SessionName())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,22 +142,20 @@ func runUp(cmd *cobra.Command, args []string) error {
|
||||
mgr := refinery.NewManager(r)
|
||||
if err := mgr.Start(false); err != nil {
|
||||
if err == refinery.ErrAlreadyRunning {
|
||||
sessionName := fmt.Sprintf("gt-%s-refinery", rigName)
|
||||
printStatus(fmt.Sprintf("Refinery (%s)", rigName), true, sessionName)
|
||||
printStatus(fmt.Sprintf("Refinery (%s)", rigName), true, mgr.SessionName())
|
||||
} else {
|
||||
printStatus(fmt.Sprintf("Refinery (%s)", rigName), false, err.Error())
|
||||
allOK = false
|
||||
}
|
||||
} else {
|
||||
sessionName := fmt.Sprintf("gt-%s-refinery", rigName)
|
||||
printStatus(fmt.Sprintf("Refinery (%s)", rigName), true, sessionName)
|
||||
printStatus(fmt.Sprintf("Refinery (%s)", rigName), true, mgr.SessionName())
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Crew (if --restore)
|
||||
if upRestore {
|
||||
for _, rigName := range rigs {
|
||||
crewStarted, crewErrors := startCrewFromSettings(t, townRoot, rigName)
|
||||
crewStarted, crewErrors := startCrewFromSettings(townRoot, rigName)
|
||||
for _, name := range crewStarted {
|
||||
printStatus(fmt.Sprintf("Crew (%s/%s)", rigName, name), true, fmt.Sprintf("gt-%s-crew-%s", rigName, name))
|
||||
}
|
||||
@@ -170,7 +167,7 @@ func runUp(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// 7. Polecats with pinned work (if --restore)
|
||||
for _, rigName := range rigs {
|
||||
polecatsStarted, polecatErrors := startPolecatsWithWork(t, townRoot, rigName)
|
||||
polecatsStarted, polecatErrors := startPolecatsWithWork(townRoot, rigName)
|
||||
for _, name := range polecatsStarted {
|
||||
printStatus(fmt.Sprintf("Polecat (%s/%s)", rigName, name), true, fmt.Sprintf("gt-%s-polecat-%s", rigName, name))
|
||||
}
|
||||
@@ -252,79 +249,6 @@ func ensureDaemon(townRoot string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureSession starts a Claude session if not running.
|
||||
func ensureSession(t *tmux.Tmux, sessionName, workDir, role string) error {
|
||||
running, err := t.HasSession(sessionName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if running {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure Claude settings exist
|
||||
if err := claude.EnsureSettingsForRole(workDir, role); err != nil {
|
||||
return fmt.Errorf("ensuring Claude settings: %w", err)
|
||||
}
|
||||
|
||||
// Create session
|
||||
if err := t.NewSession(sessionName, workDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set environment (non-fatal: session works without these)
|
||||
_ = t.SetEnvironment(sessionName, "GT_ROLE", role)
|
||||
_ = t.SetEnvironment(sessionName, "BD_ACTOR", role)
|
||||
|
||||
// Apply theme based on role (non-fatal: theming failure doesn't affect operation)
|
||||
switch role {
|
||||
case "mayor":
|
||||
theme := tmux.MayorTheme()
|
||||
_ = t.ConfigureGasTownSession(sessionName, theme, "", "Mayor", "coordinator")
|
||||
case "deacon":
|
||||
theme := tmux.DeaconTheme()
|
||||
_ = t.ConfigureGasTownSession(sessionName, theme, "", "Deacon", "health-check")
|
||||
}
|
||||
|
||||
// Launch Claude
|
||||
// Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes
|
||||
var claudeCmd string
|
||||
runtimeCmd := config.GetRuntimeCommand("")
|
||||
if role == "deacon" {
|
||||
// Deacon uses respawn loop
|
||||
claudeCmd = `export GT_ROLE=deacon BD_ACTOR=deacon GIT_AUTHOR_NAME=deacon && while true; do echo "⛪ Starting Deacon session..."; ` + runtimeCmd + `; echo ""; echo "Deacon exited. Restarting in 2s... (Ctrl-C to stop)"; sleep 2; done`
|
||||
} else {
|
||||
claudeCmd = config.BuildAgentStartupCommand(role, role, "", "")
|
||||
}
|
||||
|
||||
if err := t.SendKeysDelayed(sessionName, claudeCmd, 200); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for Claude to start (non-fatal)
|
||||
// Note: Deacon respawn loop makes beacon tricky - Claude restarts multiple times
|
||||
// For non-respawn (mayor), inject beacon
|
||||
if role != "deacon" {
|
||||
if err := t.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil {
|
||||
// Non-fatal
|
||||
}
|
||||
|
||||
// Accept bypass permissions warning dialog if it appears.
|
||||
_ = t.AcceptBypassPermissionsWarning(sessionName)
|
||||
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
// Inject startup nudge for predecessor discovery via /resume
|
||||
_ = session.StartupNudge(t, sessionName, session.StartupNudgeConfig{
|
||||
Recipient: role,
|
||||
Sender: "human",
|
||||
Topic: "cold-start",
|
||||
}) // Non-fatal
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// discoverRigs finds all rigs in the town.
|
||||
func discoverRigs(townRoot string) []string {
|
||||
var rigs []string
|
||||
@@ -377,7 +301,7 @@ func discoverRigs(townRoot string) []string {
|
||||
|
||||
// startCrewFromSettings starts crew members based on rig settings.
|
||||
// Returns list of started crew names and map of errors.
|
||||
func startCrewFromSettings(t *tmux.Tmux, townRoot, rigName string) ([]string, map[string]error) {
|
||||
func startCrewFromSettings(townRoot, rigName string) ([]string, map[string]error) {
|
||||
started := []string{}
|
||||
errors := map[string]error{}
|
||||
|
||||
@@ -420,24 +344,14 @@ func startCrewFromSettings(t *tmux.Tmux, townRoot, rigName string) ([]string, ma
|
||||
// Parse startup preference and determine which crew to start
|
||||
toStart := parseCrewStartupPreference(settings.Crew.Startup, crewNames)
|
||||
|
||||
// Start each crew member
|
||||
// Start each crew member using Manager
|
||||
for _, crewName := range toStart {
|
||||
sessionName := fmt.Sprintf("gt-%s-crew-%s", rigName, crewName)
|
||||
|
||||
running, err := t.HasSession(sessionName)
|
||||
if err != nil {
|
||||
errors[crewName] = err
|
||||
continue
|
||||
}
|
||||
if running {
|
||||
started = append(started, crewName)
|
||||
continue
|
||||
}
|
||||
|
||||
// Start the crew member
|
||||
crewPath := filepath.Join(rigPath, "crew", crewName)
|
||||
if err := ensureCrewSession(t, sessionName, crewPath, rigName, crewName); err != nil {
|
||||
errors[crewName] = err
|
||||
if err := crewMgr.Start(crewName, crew.StartOptions{}); err != nil {
|
||||
if err == crew.ErrSessionRunning {
|
||||
started = append(started, crewName)
|
||||
} else {
|
||||
errors[crewName] = err
|
||||
}
|
||||
} else {
|
||||
started = append(started, crewName)
|
||||
}
|
||||
@@ -509,56 +423,9 @@ func parseCrewStartupPreference(pref string, available []string) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
// ensureCrewSession starts a crew session.
|
||||
func ensureCrewSession(t *tmux.Tmux, sessionName, crewPath, rigName, crewName string) error {
|
||||
// Create session in crew directory
|
||||
if err := t.NewSession(sessionName, crewPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set environment
|
||||
bdActor := fmt.Sprintf("%s/crew/%s", rigName, crewName)
|
||||
_ = t.SetEnvironment(sessionName, "GT_ROLE", "crew")
|
||||
_ = t.SetEnvironment(sessionName, "GT_RIG", rigName)
|
||||
_ = t.SetEnvironment(sessionName, "GT_CREW", crewName)
|
||||
_ = t.SetEnvironment(sessionName, "BD_ACTOR", bdActor)
|
||||
|
||||
// Apply theme (use rig-based theme)
|
||||
theme := tmux.AssignTheme(rigName)
|
||||
_ = t.ConfigureGasTownSession(sessionName, theme, "", "Crew", crewName)
|
||||
|
||||
// Launch Claude using runtime config
|
||||
// crewPath is like ~/gt/gastown/crew/max, so rig path is two dirs up
|
||||
rigPath := filepath.Dir(filepath.Dir(crewPath))
|
||||
claudeCmd := config.BuildCrewStartupCommand(rigName, crewName, rigPath, "")
|
||||
if err := t.SendKeysDelayed(sessionName, claudeCmd, 200); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for Claude to start (non-fatal)
|
||||
if err := t.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil {
|
||||
// Non-fatal
|
||||
}
|
||||
|
||||
// Accept bypass permissions warning dialog if it appears.
|
||||
_ = t.AcceptBypassPermissionsWarning(sessionName)
|
||||
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
// Inject startup nudge for predecessor discovery via /resume
|
||||
address := fmt.Sprintf("%s/crew/%s", rigName, crewName)
|
||||
_ = session.StartupNudge(t, sessionName, session.StartupNudgeConfig{
|
||||
Recipient: address,
|
||||
Sender: "human",
|
||||
Topic: "cold-start",
|
||||
}) // Non-fatal
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// startPolecatsWithWork starts polecats that have pinned beads (work attached).
|
||||
// Returns list of started polecat names and map of errors.
|
||||
func startPolecatsWithWork(t *tmux.Tmux, townRoot, rigName string) ([]string, map[string]error) {
|
||||
func startPolecatsWithWork(townRoot, rigName string) ([]string, map[string]error) {
|
||||
started := []string{}
|
||||
errors := map[string]error{}
|
||||
|
||||
@@ -572,6 +439,14 @@ func startPolecatsWithWork(t *tmux.Tmux, townRoot, rigName string) ([]string, ma
|
||||
return started, errors
|
||||
}
|
||||
|
||||
// Get polecat session manager
|
||||
_, r, err := getRig(rigName)
|
||||
if err != nil {
|
||||
return started, errors
|
||||
}
|
||||
t := tmux.NewTmux()
|
||||
polecatMgr := polecat.NewSessionManager(t, r)
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
@@ -593,22 +468,13 @@ func startPolecatsWithWork(t *tmux.Tmux, townRoot, rigName string) ([]string, ma
|
||||
continue
|
||||
}
|
||||
|
||||
// This polecat has work - start it
|
||||
sessionName := fmt.Sprintf("gt-%s-polecat-%s", rigName, polecatName)
|
||||
|
||||
running, err := t.HasSession(sessionName)
|
||||
if err != nil {
|
||||
errors[polecatName] = err
|
||||
continue
|
||||
}
|
||||
if running {
|
||||
started = append(started, polecatName)
|
||||
continue
|
||||
}
|
||||
|
||||
// Start the polecat
|
||||
if err := ensurePolecatSession(t, sessionName, polecatPath, rigName, polecatName); err != nil {
|
||||
errors[polecatName] = err
|
||||
// This polecat has work - start it using SessionManager
|
||||
if err := polecatMgr.Start(polecatName, polecat.SessionStartOptions{}); err != nil {
|
||||
if err == polecat.ErrSessionRunning {
|
||||
started = append(started, polecatName)
|
||||
} else {
|
||||
errors[polecatName] = err
|
||||
}
|
||||
} else {
|
||||
started = append(started, polecatName)
|
||||
}
|
||||
@@ -616,50 +482,3 @@ func startPolecatsWithWork(t *tmux.Tmux, townRoot, rigName string) ([]string, ma
|
||||
|
||||
return started, errors
|
||||
}
|
||||
|
||||
// ensurePolecatSession starts a polecat session.
|
||||
func ensurePolecatSession(t *tmux.Tmux, sessionName, polecatPath, rigName, polecatName string) error {
|
||||
// Create session in polecat directory
|
||||
if err := t.NewSession(sessionName, polecatPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set environment
|
||||
bdActor := fmt.Sprintf("%s/polecats/%s", rigName, polecatName)
|
||||
_ = t.SetEnvironment(sessionName, "GT_ROLE", "polecat")
|
||||
_ = t.SetEnvironment(sessionName, "GT_RIG", rigName)
|
||||
_ = t.SetEnvironment(sessionName, "GT_POLECAT", polecatName)
|
||||
_ = t.SetEnvironment(sessionName, "BD_ACTOR", bdActor)
|
||||
|
||||
// Apply theme (use rig-based theme)
|
||||
theme := tmux.AssignTheme(rigName)
|
||||
_ = t.ConfigureGasTownSession(sessionName, theme, "", "Polecat", polecatName)
|
||||
|
||||
// Launch Claude using runtime config
|
||||
// polecatPath is like ~/gt/gastown/polecats/toast, so rig path is two dirs up
|
||||
rigPath := filepath.Dir(filepath.Dir(polecatPath))
|
||||
claudeCmd := config.BuildPolecatStartupCommand(rigName, polecatName, rigPath, "")
|
||||
if err := t.SendKeysDelayed(sessionName, claudeCmd, 200); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for Claude to start (non-fatal)
|
||||
if err := t.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil {
|
||||
// Non-fatal
|
||||
}
|
||||
|
||||
// Accept bypass permissions warning dialog if it appears.
|
||||
_ = t.AcceptBypassPermissionsWarning(sessionName)
|
||||
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
// Inject startup nudge for predecessor discovery via /resume
|
||||
address := fmt.Sprintf("%s/polecats/%s", rigName, polecatName)
|
||||
_ = session.StartupNudge(t, sessionName, session.StartupNudgeConfig{
|
||||
Recipient: address,
|
||||
Sender: "witness",
|
||||
Topic: "dispatch",
|
||||
}) // Non-fatal
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
170
internal/deacon/manager.go
Normal file
170
internal/deacon/manager.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package deacon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/claude"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
)
|
||||
|
||||
// Common errors
|
||||
var (
|
||||
ErrNotRunning = errors.New("deacon not running")
|
||||
ErrAlreadyRunning = errors.New("deacon already running")
|
||||
)
|
||||
|
||||
// Manager handles deacon lifecycle operations.
|
||||
type Manager struct {
|
||||
townRoot string
|
||||
}
|
||||
|
||||
// NewManager creates a new deacon manager for a town.
|
||||
func NewManager(townRoot string) *Manager {
|
||||
return &Manager{
|
||||
townRoot: townRoot,
|
||||
}
|
||||
}
|
||||
|
||||
// SessionName returns the tmux session name for the deacon.
|
||||
// This is a package-level function for convenience.
|
||||
func SessionName() string {
|
||||
return session.DeaconSessionName()
|
||||
}
|
||||
|
||||
// SessionName returns the tmux session name for the deacon.
|
||||
func (m *Manager) SessionName() string {
|
||||
return SessionName()
|
||||
}
|
||||
|
||||
// deaconDir returns the working directory for the deacon.
|
||||
func (m *Manager) deaconDir() string {
|
||||
return filepath.Join(m.townRoot, "deacon")
|
||||
}
|
||||
|
||||
// Start starts the deacon session.
|
||||
// The deacon runs in a respawn loop for automatic recovery.
|
||||
func (m *Manager) Start() error {
|
||||
t := tmux.NewTmux()
|
||||
sessionID := m.SessionName()
|
||||
|
||||
// Check if session already exists
|
||||
running, _ := t.HasSession(sessionID)
|
||||
if running {
|
||||
// Session exists - check if Claude is actually running (healthy vs zombie)
|
||||
if t.IsClaudeRunning(sessionID) {
|
||||
return ErrAlreadyRunning
|
||||
}
|
||||
// Zombie - tmux alive but Claude dead. Kill and recreate.
|
||||
if err := t.KillSession(sessionID); err != nil {
|
||||
return fmt.Errorf("killing zombie session: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure deacon directory exists
|
||||
deaconDir := m.deaconDir()
|
||||
if err := os.MkdirAll(deaconDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating deacon directory: %w", err)
|
||||
}
|
||||
|
||||
// Ensure Claude settings exist
|
||||
if err := claude.EnsureSettingsForRole(deaconDir, "deacon"); err != nil {
|
||||
return fmt.Errorf("ensuring Claude settings: %w", err)
|
||||
}
|
||||
|
||||
// Create new tmux session
|
||||
if err := t.NewSession(sessionID, deaconDir); err != nil {
|
||||
return fmt.Errorf("creating tmux session: %w", err)
|
||||
}
|
||||
|
||||
// Set environment variables (non-fatal: session works without these)
|
||||
_ = t.SetEnvironment(sessionID, "GT_ROLE", "deacon")
|
||||
_ = t.SetEnvironment(sessionID, "BD_ACTOR", "deacon")
|
||||
|
||||
// Apply Deacon theming (non-fatal: theming failure doesn't affect operation)
|
||||
theme := tmux.DeaconTheme()
|
||||
_ = t.ConfigureGasTownSession(sessionID, theme, "", "Deacon", "health-check")
|
||||
|
||||
// Launch Claude in a respawn loop for automatic recovery
|
||||
// The respawn loop ensures the deacon restarts if Claude crashes
|
||||
runtimeCmd := config.GetRuntimeCommand("")
|
||||
respawnCmd := fmt.Sprintf(
|
||||
`export GT_ROLE=deacon BD_ACTOR=deacon GIT_AUTHOR_NAME=deacon && while true; do echo "⛪ Starting Deacon session..."; %s; echo ""; echo "Deacon exited. Restarting in 2s... (Ctrl-C to stop)"; sleep 2; done`,
|
||||
runtimeCmd,
|
||||
)
|
||||
|
||||
if err := t.SendKeysDelayed(sessionID, respawnCmd, 200); err != nil {
|
||||
_ = t.KillSession(sessionID) // best-effort cleanup
|
||||
return fmt.Errorf("starting Claude agent: %w", err)
|
||||
}
|
||||
|
||||
// Wait for Claude to start (non-fatal)
|
||||
// Note: Deacon respawn loop makes this tricky - Claude restarts multiple times
|
||||
if err := t.WaitForCommand(sessionID, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil {
|
||||
// Non-fatal - try to continue anyway
|
||||
}
|
||||
|
||||
// Accept bypass permissions warning dialog if it appears.
|
||||
_ = t.AcceptBypassPermissionsWarning(sessionID)
|
||||
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
// Note: Deacon doesn't get startup nudge due to respawn loop complexity
|
||||
// The deacon uses its own patrol pattern defined in its CLAUDE.md/prime
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the deacon session.
|
||||
func (m *Manager) Stop() error {
|
||||
t := tmux.NewTmux()
|
||||
sessionID := m.SessionName()
|
||||
|
||||
// Check if session exists
|
||||
running, err := t.HasSession(sessionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking session: %w", err)
|
||||
}
|
||||
if !running {
|
||||
return ErrNotRunning
|
||||
}
|
||||
|
||||
// Try graceful shutdown first (best-effort interrupt)
|
||||
_ = t.SendKeysRaw(sessionID, "C-c")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Kill the session
|
||||
if err := t.KillSession(sessionID); err != nil {
|
||||
return fmt.Errorf("killing session: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRunning checks if the deacon session is active.
|
||||
func (m *Manager) IsRunning() (bool, error) {
|
||||
t := tmux.NewTmux()
|
||||
return t.HasSession(m.SessionName())
|
||||
}
|
||||
|
||||
// Status returns information about the deacon session.
|
||||
func (m *Manager) Status() (*tmux.SessionInfo, error) {
|
||||
t := tmux.NewTmux()
|
||||
sessionID := m.SessionName()
|
||||
|
||||
running, err := t.HasSession(sessionID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("checking session: %w", err)
|
||||
}
|
||||
if !running {
|
||||
return nil, ErrNotRunning
|
||||
}
|
||||
|
||||
return t.GetSessionInfo(sessionID)
|
||||
}
|
||||
178
internal/mayor/manager.go
Normal file
178
internal/mayor/manager.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package mayor
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/claude"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
)
|
||||
|
||||
// Common errors
|
||||
var (
|
||||
ErrNotRunning = errors.New("mayor not running")
|
||||
ErrAlreadyRunning = errors.New("mayor already running")
|
||||
)
|
||||
|
||||
// Manager handles mayor lifecycle operations.
|
||||
type Manager struct {
|
||||
townRoot string
|
||||
}
|
||||
|
||||
// NewManager creates a new mayor manager for a town.
|
||||
func NewManager(townRoot string) *Manager {
|
||||
return &Manager{
|
||||
townRoot: townRoot,
|
||||
}
|
||||
}
|
||||
|
||||
// SessionName returns the tmux session name for the mayor.
|
||||
// This is a package-level function for convenience.
|
||||
func SessionName() string {
|
||||
return session.MayorSessionName()
|
||||
}
|
||||
|
||||
// SessionName returns the tmux session name for the mayor.
|
||||
func (m *Manager) SessionName() string {
|
||||
return SessionName()
|
||||
}
|
||||
|
||||
// mayorDir returns the working directory for the mayor.
|
||||
func (m *Manager) mayorDir() string {
|
||||
return filepath.Join(m.townRoot, "mayor")
|
||||
}
|
||||
|
||||
// Start starts the mayor session.
|
||||
// agentOverride optionally specifies a different agent alias to use.
|
||||
func (m *Manager) Start(agentOverride string) error {
|
||||
t := tmux.NewTmux()
|
||||
sessionID := m.SessionName()
|
||||
|
||||
// Check if session already exists
|
||||
running, _ := t.HasSession(sessionID)
|
||||
if running {
|
||||
// Session exists - check if Claude is actually running (healthy vs zombie)
|
||||
if t.IsClaudeRunning(sessionID) {
|
||||
return ErrAlreadyRunning
|
||||
}
|
||||
// Zombie - tmux alive but Claude dead. Kill and recreate.
|
||||
if err := t.KillSession(sessionID); err != nil {
|
||||
return fmt.Errorf("killing zombie session: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure mayor directory exists
|
||||
mayorDir := m.mayorDir()
|
||||
if err := os.MkdirAll(mayorDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating mayor directory: %w", err)
|
||||
}
|
||||
|
||||
// Ensure Claude settings exist
|
||||
if err := claude.EnsureSettingsForRole(mayorDir, "mayor"); err != nil {
|
||||
return fmt.Errorf("ensuring Claude settings: %w", err)
|
||||
}
|
||||
|
||||
// Create new tmux session
|
||||
if err := t.NewSession(sessionID, mayorDir); err != nil {
|
||||
return fmt.Errorf("creating tmux session: %w", err)
|
||||
}
|
||||
|
||||
// Set environment variables (non-fatal: session works without these)
|
||||
_ = t.SetEnvironment(sessionID, "GT_ROLE", "mayor")
|
||||
_ = t.SetEnvironment(sessionID, "BD_ACTOR", "mayor")
|
||||
|
||||
// Apply Mayor theming (non-fatal: theming failure doesn't affect operation)
|
||||
theme := tmux.MayorTheme()
|
||||
_ = t.ConfigureGasTownSession(sessionID, theme, "", "Mayor", "coordinator")
|
||||
|
||||
// Launch Claude - the startup hook handles 'gt prime' automatically
|
||||
// Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes
|
||||
startupCmd, err := config.BuildAgentStartupCommandWithAgentOverride("mayor", "mayor", "", "", agentOverride)
|
||||
if err != nil {
|
||||
_ = t.KillSession(sessionID) // best-effort cleanup
|
||||
return fmt.Errorf("building startup command: %w", err)
|
||||
}
|
||||
if err := t.SendKeysDelayed(sessionID, startupCmd, 200); err != nil {
|
||||
_ = t.KillSession(sessionID) // best-effort cleanup
|
||||
return fmt.Errorf("starting Claude agent: %w", err)
|
||||
}
|
||||
|
||||
// Wait for Claude to start (non-fatal)
|
||||
if err := t.WaitForCommand(sessionID, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil {
|
||||
// Non-fatal - try to continue anyway
|
||||
}
|
||||
|
||||
// Accept bypass permissions warning dialog if it appears.
|
||||
_ = t.AcceptBypassPermissionsWarning(sessionID)
|
||||
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
// Inject startup nudge for predecessor discovery via /resume
|
||||
_ = session.StartupNudge(t, sessionID, session.StartupNudgeConfig{
|
||||
Recipient: "mayor",
|
||||
Sender: "human",
|
||||
Topic: "cold-start",
|
||||
}) // Non-fatal
|
||||
|
||||
// GUPP: Gas Town Universal Propulsion Principle
|
||||
// Send the propulsion nudge to trigger autonomous coordination.
|
||||
// Wait for beacon to be fully processed (needs to be separate prompt)
|
||||
time.Sleep(2 * time.Second)
|
||||
_ = t.NudgeSession(sessionID, session.PropulsionNudgeForRole("mayor", mayorDir)) // Non-fatal
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the mayor session.
|
||||
func (m *Manager) Stop() error {
|
||||
t := tmux.NewTmux()
|
||||
sessionID := m.SessionName()
|
||||
|
||||
// Check if session exists
|
||||
running, err := t.HasSession(sessionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking session: %w", err)
|
||||
}
|
||||
if !running {
|
||||
return ErrNotRunning
|
||||
}
|
||||
|
||||
// Try graceful shutdown first (best-effort interrupt)
|
||||
_ = t.SendKeysRaw(sessionID, "C-c")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Kill the session
|
||||
if err := t.KillSession(sessionID); err != nil {
|
||||
return fmt.Errorf("killing session: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRunning checks if the mayor session is active.
|
||||
func (m *Manager) IsRunning() (bool, error) {
|
||||
t := tmux.NewTmux()
|
||||
return t.HasSession(m.SessionName())
|
||||
}
|
||||
|
||||
// Status returns information about the mayor session.
|
||||
func (m *Manager) Status() (*tmux.SessionInfo, error) {
|
||||
t := tmux.NewTmux()
|
||||
sessionID := m.SessionName()
|
||||
|
||||
running, err := t.HasSession(sessionID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("checking session: %w", err)
|
||||
}
|
||||
if !running {
|
||||
return nil, ErrNotRunning
|
||||
}
|
||||
|
||||
return t.GetSessionInfo(sessionID)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Package session provides polecat session lifecycle management.
|
||||
package session
|
||||
// Package polecat provides polecat workspace and session management.
|
||||
package polecat
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -14,41 +14,39 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
)
|
||||
|
||||
// debugSession logs non-fatal errors during session startup when GT_DEBUG_SESSION=1.
|
||||
// These errors are intentionally suppressed because they don't prevent session operation,
|
||||
// but logging them helps diagnose issues when needed.
|
||||
func debugSession(context string, err error) {
|
||||
if os.Getenv("GT_DEBUG_SESSION") != "" && err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[session-debug] %s: %v\n", context, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Common errors
|
||||
// Session errors
|
||||
var (
|
||||
ErrSessionRunning = errors.New("session already running")
|
||||
ErrSessionNotFound = errors.New("session not found")
|
||||
ErrPolecatNotFound = errors.New("polecat not found")
|
||||
)
|
||||
|
||||
// Manager handles polecat session lifecycle.
|
||||
type Manager struct {
|
||||
// SessionManager handles polecat session lifecycle.
|
||||
type SessionManager struct {
|
||||
tmux *tmux.Tmux
|
||||
rig *rig.Rig
|
||||
}
|
||||
|
||||
// NewManager creates a new session manager for a rig.
|
||||
func NewManager(t *tmux.Tmux, r *rig.Rig) *Manager {
|
||||
return &Manager{
|
||||
// NewSessionManager creates a new polecat session manager for a rig.
|
||||
func NewSessionManager(t *tmux.Tmux, r *rig.Rig) *SessionManager {
|
||||
return &SessionManager{
|
||||
tmux: t,
|
||||
rig: r,
|
||||
}
|
||||
}
|
||||
|
||||
// StartOptions configures session startup.
|
||||
type StartOptions struct {
|
||||
// SessionStartOptions configures polecat session startup.
|
||||
type SessionStartOptions struct {
|
||||
// WorkDir overrides the default working directory (polecat clone dir).
|
||||
WorkDir string
|
||||
|
||||
@@ -66,8 +64,8 @@ type StartOptions struct {
|
||||
ClaudeConfigDir string
|
||||
}
|
||||
|
||||
// Info contains information about a running session.
|
||||
type Info struct {
|
||||
// SessionInfo contains information about a running polecat session.
|
||||
type SessionInfo struct {
|
||||
// Polecat is the polecat name.
|
||||
Polecat string `json:"polecat"`
|
||||
|
||||
@@ -94,18 +92,17 @@ type Info struct {
|
||||
}
|
||||
|
||||
// SessionName generates the tmux session name for a polecat.
|
||||
func (m *Manager) SessionName(polecat string) string {
|
||||
func (m *SessionManager) SessionName(polecat string) string {
|
||||
return fmt.Sprintf("gt-%s-%s", m.rig.Name, polecat)
|
||||
}
|
||||
|
||||
// polecatDir returns the working directory for a polecat.
|
||||
func (m *Manager) polecatDir(polecat string) string {
|
||||
func (m *SessionManager) polecatDir(polecat string) string {
|
||||
return filepath.Join(m.rig.Path, "polecats", polecat)
|
||||
}
|
||||
|
||||
// hasPolecat checks if the polecat exists in this rig.
|
||||
func (m *Manager) hasPolecat(polecat string) bool {
|
||||
// Check filesystem directly to handle newly-created polecats
|
||||
func (m *SessionManager) hasPolecat(polecat string) bool {
|
||||
polecatPath := m.polecatDir(polecat)
|
||||
info, err := os.Stat(polecatPath)
|
||||
if err != nil {
|
||||
@@ -115,7 +112,7 @@ func (m *Manager) hasPolecat(polecat string) bool {
|
||||
}
|
||||
|
||||
// Start creates and starts a new session for a polecat.
|
||||
func (m *Manager) Start(polecat string, opts StartOptions) error {
|
||||
func (m *SessionManager) Start(polecat string, opts SessionStartOptions) error {
|
||||
if !m.hasPolecat(polecat) {
|
||||
return fmt.Errorf("%w: %s", ErrPolecatNotFound, polecat)
|
||||
}
|
||||
@@ -139,7 +136,6 @@ func (m *Manager) Start(polecat string, opts StartOptions) error {
|
||||
|
||||
// Ensure Claude settings exist in polecats/ (not polecats/<name>/) so we don't
|
||||
// write into the source repo. Claude walks up the tree to find settings.
|
||||
// All polecats share the same settings file.
|
||||
polecatsDir := filepath.Join(m.rig.Path, "polecats")
|
||||
if err := claude.EnsureSettingsForRole(polecatsDir, "polecat"); err != nil {
|
||||
return fmt.Errorf("ensuring Claude settings: %w", err)
|
||||
@@ -159,12 +155,8 @@ func (m *Manager) Start(polecat string, opts StartOptions) error {
|
||||
debugSession("SetEnvironment CLAUDE_CONFIG_DIR", m.tmux.SetEnvironment(sessionID, "CLAUDE_CONFIG_DIR", opts.ClaudeConfigDir))
|
||||
}
|
||||
|
||||
// CRITICAL: Set beads environment for worktree polecats (non-fatal: session works without)
|
||||
// Polecats need access to TOWN-level beads (parent of rig) for hooks and convoys.
|
||||
// Town beads use hq- prefix and store hooks, mail, and cross-rig coordination.
|
||||
// BEADS_NO_DAEMON=1 prevents daemon from committing to wrong branch.
|
||||
// Using town-level beads ensures gt prime and bd commands can find hooked work.
|
||||
townRoot := filepath.Dir(m.rig.Path) // Town root is parent of rig directory
|
||||
// Set beads environment for worktree polecats (non-fatal)
|
||||
townRoot := filepath.Dir(m.rig.Path)
|
||||
beadsDir := filepath.Join(townRoot, ".beads")
|
||||
debugSession("SetEnvironment BEADS_DIR", m.tmux.SetEnvironment(sessionID, "BEADS_DIR", beadsDir))
|
||||
debugSession("SetEnvironment BEADS_NO_DAEMON", m.tmux.SetEnvironment(sessionID, "BEADS_NO_DAEMON", "1"))
|
||||
@@ -174,12 +166,11 @@ func (m *Manager) Start(polecat string, opts StartOptions) error {
|
||||
if opts.Issue != "" {
|
||||
agentID := fmt.Sprintf("%s/polecats/%s", m.rig.Name, polecat)
|
||||
if err := m.hookIssue(opts.Issue, agentID, workDir); err != nil {
|
||||
// Non-fatal - warn but continue (session can still start)
|
||||
fmt.Printf("Warning: could not hook issue %s: %v\n", opts.Issue, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply theme (non-fatal: theming failure doesn't affect operation)
|
||||
// Apply theme (non-fatal)
|
||||
theme := tmux.AssignTheme(m.rig.Name)
|
||||
debugSession("ConfigureGasTownSession", m.tmux.ConfigureGasTownSession(sessionID, theme, m.rig.Name, polecat, "polecat"))
|
||||
|
||||
@@ -188,61 +179,43 @@ func (m *Manager) Start(polecat string, opts StartOptions) error {
|
||||
debugSession("SetPaneDiedHook", m.tmux.SetPaneDiedHook(sessionID, agentID))
|
||||
|
||||
// Send initial command with env vars exported inline
|
||||
// NOTE: tmux SetEnvironment only affects NEW panes, not the current shell.
|
||||
// We must export GT_ROLE, GT_RIG, GT_POLECAT inline for Claude to detect identity.
|
||||
command := opts.Command
|
||||
if command == "" {
|
||||
// Polecats run with full permissions - Gas Town is for grownups
|
||||
// Export env vars inline so Claude's role detection works
|
||||
command = config.BuildPolecatStartupCommand(m.rig.Name, polecat, m.rig.Path, "")
|
||||
}
|
||||
if err := m.tmux.SendKeys(sessionID, command); err != nil {
|
||||
return fmt.Errorf("sending command: %w", err)
|
||||
}
|
||||
|
||||
// Wait for Claude to start (non-fatal: session continues even if this times out)
|
||||
// Wait for Claude to start (non-fatal)
|
||||
debugSession("WaitForCommand", m.tmux.WaitForCommand(sessionID, constants.SupportedShells, constants.ClaudeStartTimeout))
|
||||
|
||||
// Accept bypass permissions warning dialog if it appears.
|
||||
// When Claude starts with --dangerously-skip-permissions, it shows a warning that
|
||||
// requires pressing Down to select "Yes, I accept" and Enter to confirm.
|
||||
// This is needed for automated polecat startup.
|
||||
// Accept bypass permissions warning dialog if it appears
|
||||
debugSession("AcceptBypassPermissionsWarning", m.tmux.AcceptBypassPermissionsWarning(sessionID))
|
||||
|
||||
// Wait for Claude to be fully ready at the prompt (not just started)
|
||||
// PRAGMATIC APPROACH: Use fixed delay rather than detection.
|
||||
// WaitForClaudeReady has false positives (detects > in various contexts).
|
||||
// Claude startup takes ~5-8 seconds on typical machines.
|
||||
// Reduced from 10s to 8s since AcceptBypassPermissionsWarning already adds ~1.2s.
|
||||
// Wait for Claude to be fully ready
|
||||
time.Sleep(8 * time.Second)
|
||||
|
||||
// Inject startup nudge for predecessor discovery via /resume
|
||||
// This becomes the session title in Claude Code's session picker
|
||||
address := fmt.Sprintf("%s/polecats/%s", m.rig.Name, polecat)
|
||||
debugSession("StartupNudge", StartupNudge(m.tmux, sessionID, StartupNudgeConfig{
|
||||
debugSession("StartupNudge", session.StartupNudge(m.tmux, sessionID, session.StartupNudgeConfig{
|
||||
Recipient: address,
|
||||
Sender: "witness",
|
||||
Topic: "assigned",
|
||||
MolID: opts.Issue,
|
||||
}))
|
||||
|
||||
// GUPP: Gas Town Universal Propulsion Principle
|
||||
// Send the propulsion nudge to trigger autonomous work execution.
|
||||
// The beacon alone is just metadata - this nudge is the actual instruction
|
||||
// that triggers Claude to check the hook and begin work.
|
||||
// Wait for beacon to be fully processed (needs to be separate prompt)
|
||||
// GUPP: Send propulsion nudge to trigger autonomous work execution
|
||||
time.Sleep(2 * time.Second)
|
||||
debugSession("NudgeSession PropulsionNudge", m.tmux.NudgeSession(sessionID, PropulsionNudge()))
|
||||
debugSession("NudgeSession PropulsionNudge", m.tmux.NudgeSession(sessionID, session.PropulsionNudge()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop terminates a polecat session.
|
||||
// If force is true, skips graceful shutdown and kills immediately.
|
||||
func (m *Manager) Stop(polecat string, force bool) error {
|
||||
func (m *SessionManager) Stop(polecat string, force bool) error {
|
||||
sessionID := m.SessionName(polecat)
|
||||
|
||||
// Check if session exists
|
||||
running, err := m.tmux.HasSession(sessionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking session: %w", err)
|
||||
@@ -251,23 +224,20 @@ func (m *Manager) Stop(polecat string, force bool) error {
|
||||
return ErrSessionNotFound
|
||||
}
|
||||
|
||||
// Sync beads before shutdown to preserve any changes
|
||||
// Run in the polecat's worktree directory
|
||||
// Sync beads before shutdown (non-fatal)
|
||||
if !force {
|
||||
polecatDir := m.polecatDir(polecat)
|
||||
if err := m.syncBeads(polecatDir); err != nil {
|
||||
// Non-fatal - log and continue with shutdown
|
||||
fmt.Printf("Warning: beads sync failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Try graceful shutdown first (unless forced, best-effort interrupt)
|
||||
// Try graceful shutdown first
|
||||
if !force {
|
||||
_ = m.tmux.SendKeysRaw(sessionID, "C-c")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Kill the session
|
||||
if err := m.tmux.KillSession(sessionID); err != nil {
|
||||
return fmt.Errorf("killing session: %w", err)
|
||||
}
|
||||
@@ -276,20 +246,20 @@ func (m *Manager) Stop(polecat string, force bool) error {
|
||||
}
|
||||
|
||||
// syncBeads runs bd sync in the given directory.
|
||||
func (m *Manager) syncBeads(workDir string) error {
|
||||
func (m *SessionManager) syncBeads(workDir string) error {
|
||||
cmd := exec.Command("bd", "sync")
|
||||
cmd.Dir = workDir
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// IsRunning checks if a polecat session is active.
|
||||
func (m *Manager) IsRunning(polecat string) (bool, error) {
|
||||
func (m *SessionManager) IsRunning(polecat string) (bool, error) {
|
||||
sessionID := m.SessionName(polecat)
|
||||
return m.tmux.HasSession(sessionID)
|
||||
}
|
||||
|
||||
// Status returns detailed status for a polecat session.
|
||||
func (m *Manager) Status(polecat string) (*Info, error) {
|
||||
func (m *SessionManager) Status(polecat string) (*SessionInfo, error) {
|
||||
sessionID := m.SessionName(polecat)
|
||||
|
||||
running, err := m.tmux.HasSession(sessionID)
|
||||
@@ -297,7 +267,7 @@ func (m *Manager) Status(polecat string) (*Info, error) {
|
||||
return nil, fmt.Errorf("checking session: %w", err)
|
||||
}
|
||||
|
||||
info := &Info{
|
||||
info := &SessionInfo{
|
||||
Polecat: polecat,
|
||||
SessionID: sessionID,
|
||||
Running: running,
|
||||
@@ -308,19 +278,15 @@ func (m *Manager) Status(polecat string) (*Info, error) {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// Get detailed session info
|
||||
tmuxInfo, err := m.tmux.GetSessionInfo(sessionID)
|
||||
if err != nil {
|
||||
// Non-fatal - return basic info
|
||||
return info, nil
|
||||
}
|
||||
|
||||
info.Attached = tmuxInfo.Attached
|
||||
info.Windows = tmuxInfo.Windows
|
||||
|
||||
// Parse created time from tmux format (e.g., "Thu Dec 19 10:30:00 2025")
|
||||
if tmuxInfo.Created != "" {
|
||||
// Try common tmux date formats
|
||||
formats := []string{
|
||||
"Mon Jan 2 15:04:05 2006",
|
||||
"Mon Jan _2 15:04:05 2006",
|
||||
@@ -335,7 +301,6 @@ func (m *Manager) Status(polecat string) (*Info, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse activity time (unix timestamp from tmux)
|
||||
if tmuxInfo.Activity != "" {
|
||||
var activityUnix int64
|
||||
if _, err := fmt.Sscanf(tmuxInfo.Activity, "%d", &activityUnix); err == nil && activityUnix > 0 {
|
||||
@@ -346,15 +311,15 @@ func (m *Manager) Status(polecat string) (*Info, error) {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// List returns information about all sessions for this rig.
|
||||
func (m *Manager) List() ([]Info, error) {
|
||||
// List returns information about all polecat sessions for this rig.
|
||||
func (m *SessionManager) List() ([]SessionInfo, error) {
|
||||
sessions, err := m.tmux.ListSessions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prefix := fmt.Sprintf("gt-%s-", m.rig.Name)
|
||||
var infos []Info
|
||||
var infos []SessionInfo
|
||||
|
||||
for _, sessionID := range sessions {
|
||||
if !strings.HasPrefix(sessionID, prefix) {
|
||||
@@ -362,7 +327,7 @@ func (m *Manager) List() ([]Info, error) {
|
||||
}
|
||||
|
||||
polecat := strings.TrimPrefix(sessionID, prefix)
|
||||
infos = append(infos, Info{
|
||||
infos = append(infos, SessionInfo{
|
||||
Polecat: polecat,
|
||||
SessionID: sessionID,
|
||||
Running: true,
|
||||
@@ -374,7 +339,7 @@ func (m *Manager) List() ([]Info, error) {
|
||||
}
|
||||
|
||||
// Attach attaches to a polecat session.
|
||||
func (m *Manager) Attach(polecat string) error {
|
||||
func (m *SessionManager) Attach(polecat string) error {
|
||||
sessionID := m.SessionName(polecat)
|
||||
|
||||
running, err := m.tmux.HasSession(sessionID)
|
||||
@@ -389,7 +354,7 @@ func (m *Manager) Attach(polecat string) error {
|
||||
}
|
||||
|
||||
// Capture returns the recent output from a polecat session.
|
||||
func (m *Manager) Capture(polecat string, lines int) (string, error) {
|
||||
func (m *SessionManager) Capture(polecat string, lines int) (string, error) {
|
||||
sessionID := m.SessionName(polecat)
|
||||
|
||||
running, err := m.tmux.HasSession(sessionID)
|
||||
@@ -404,9 +369,7 @@ func (m *Manager) Capture(polecat string, lines int) (string, error) {
|
||||
}
|
||||
|
||||
// CaptureSession returns the recent output from a session by raw session ID.
|
||||
// Use this for crew workers or other non-polecat sessions where the session
|
||||
// name doesn't follow the standard gt-{rig}-{polecat} pattern.
|
||||
func (m *Manager) CaptureSession(sessionID string, lines int) (string, error) {
|
||||
func (m *SessionManager) CaptureSession(sessionID string, lines int) (string, error) {
|
||||
running, err := m.tmux.HasSession(sessionID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("checking session: %w", err)
|
||||
@@ -419,8 +382,7 @@ func (m *Manager) CaptureSession(sessionID string, lines int) (string, error) {
|
||||
}
|
||||
|
||||
// Inject sends a message to a polecat session.
|
||||
// Uses a longer debounce delay for large messages to ensure paste completes.
|
||||
func (m *Manager) Inject(polecat, message string) error {
|
||||
func (m *SessionManager) Inject(polecat, message string) error {
|
||||
sessionID := m.SessionName(polecat)
|
||||
|
||||
running, err := m.tmux.HasSession(sessionID)
|
||||
@@ -431,19 +393,16 @@ func (m *Manager) Inject(polecat, message string) error {
|
||||
return ErrSessionNotFound
|
||||
}
|
||||
|
||||
// Use longer debounce for large messages (spawn context can be 1KB+)
|
||||
// Claude needs time to process paste before Enter is sent
|
||||
// Scale delay based on message size: 200ms base + 100ms per KB
|
||||
debounceMs := 200 + (len(message)/1024)*100
|
||||
if debounceMs > 1500 {
|
||||
debounceMs = 1500 // Cap at 1.5s for large pastes
|
||||
debounceMs = 1500
|
||||
}
|
||||
|
||||
return m.tmux.SendKeysDebounced(sessionID, message, debounceMs)
|
||||
}
|
||||
|
||||
// StopAll terminates all sessions for this rig.
|
||||
func (m *Manager) StopAll(force bool) error {
|
||||
// StopAll terminates all polecat sessions for this rig.
|
||||
func (m *SessionManager) StopAll(force bool) error {
|
||||
infos, err := m.List()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -460,10 +419,8 @@ func (m *Manager) StopAll(force bool) error {
|
||||
}
|
||||
|
||||
// hookIssue pins an issue to a polecat's hook using bd update.
|
||||
// This makes the work visible via 'gt hook' when the session starts.
|
||||
func (m *Manager) hookIssue(issueID, agentID, workDir string) error {
|
||||
// Use bd update to set status=hooked and assign to the polecat
|
||||
cmd := exec.Command("bd", "update", issueID, "--status=hooked", "--assignee="+agentID) //nolint:gosec // G204: bd is a trusted internal tool
|
||||
func (m *SessionManager) hookIssue(issueID, agentID, workDir string) error {
|
||||
cmd := exec.Command("bd", "update", issueID, "--status=hooked", "--assignee="+agentID) //nolint:gosec
|
||||
cmd.Dir = workDir
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
@@ -1,4 +1,4 @@
|
||||
package session
|
||||
package polecat
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -15,7 +15,7 @@ func TestSessionName(t *testing.T) {
|
||||
Name: "gastown",
|
||||
Polecats: []string{"Toast"},
|
||||
}
|
||||
m := NewManager(tmux.NewTmux(), r)
|
||||
m := NewSessionManager(tmux.NewTmux(), r)
|
||||
|
||||
name := m.SessionName("Toast")
|
||||
if name != "gt-gastown-Toast" {
|
||||
@@ -23,13 +23,13 @@ func TestSessionName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolecatDir(t *testing.T) {
|
||||
func TestSessionManagerPolecatDir(t *testing.T) {
|
||||
r := &rig.Rig{
|
||||
Name: "gastown",
|
||||
Path: "/home/user/ai/gastown",
|
||||
Polecats: []string{"Toast"},
|
||||
}
|
||||
m := NewManager(tmux.NewTmux(), r)
|
||||
m := NewSessionManager(tmux.NewTmux(), r)
|
||||
|
||||
dir := m.polecatDir("Toast")
|
||||
expected := "/home/user/ai/gastown/polecats/Toast"
|
||||
@@ -52,7 +52,7 @@ func TestHasPolecat(t *testing.T) {
|
||||
Path: root,
|
||||
Polecats: []string{"Toast", "Cheedo"},
|
||||
}
|
||||
m := NewManager(tmux.NewTmux(), r)
|
||||
m := NewSessionManager(tmux.NewTmux(), r)
|
||||
|
||||
if !m.hasPolecat("Toast") {
|
||||
t.Error("expected hasPolecat(Toast) = true")
|
||||
@@ -70,9 +70,9 @@ func TestStartPolecatNotFound(t *testing.T) {
|
||||
Name: "gastown",
|
||||
Polecats: []string{"Toast"},
|
||||
}
|
||||
m := NewManager(tmux.NewTmux(), r)
|
||||
m := NewSessionManager(tmux.NewTmux(), r)
|
||||
|
||||
err := m.Start("Unknown", StartOptions{})
|
||||
err := m.Start("Unknown", SessionStartOptions{})
|
||||
if err == nil {
|
||||
t.Error("expected error for unknown polecat")
|
||||
}
|
||||
@@ -83,7 +83,7 @@ func TestIsRunningNoSession(t *testing.T) {
|
||||
Name: "gastown",
|
||||
Polecats: []string{"Toast"},
|
||||
}
|
||||
m := NewManager(tmux.NewTmux(), r)
|
||||
m := NewSessionManager(tmux.NewTmux(), r)
|
||||
|
||||
running, err := m.IsRunning("Toast")
|
||||
if err != nil {
|
||||
@@ -94,12 +94,12 @@ func TestIsRunningNoSession(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListEmpty(t *testing.T) {
|
||||
func TestSessionManagerListEmpty(t *testing.T) {
|
||||
r := &rig.Rig{
|
||||
Name: "test-rig-unlikely-name",
|
||||
Polecats: []string{},
|
||||
}
|
||||
m := NewManager(tmux.NewTmux(), r)
|
||||
m := NewSessionManager(tmux.NewTmux(), r)
|
||||
|
||||
infos, err := m.List()
|
||||
if err != nil {
|
||||
@@ -115,7 +115,7 @@ func TestStopNotFound(t *testing.T) {
|
||||
Name: "test-rig",
|
||||
Polecats: []string{"Toast"},
|
||||
}
|
||||
m := NewManager(tmux.NewTmux(), r)
|
||||
m := NewSessionManager(tmux.NewTmux(), r)
|
||||
|
||||
err := m.Stop("Toast", false)
|
||||
if err != ErrSessionNotFound {
|
||||
@@ -128,7 +128,7 @@ func TestCaptureNotFound(t *testing.T) {
|
||||
Name: "test-rig",
|
||||
Polecats: []string{"Toast"},
|
||||
}
|
||||
m := NewManager(tmux.NewTmux(), r)
|
||||
m := NewSessionManager(tmux.NewTmux(), r)
|
||||
|
||||
_, err := m.Capture("Toast", 50)
|
||||
if err != ErrSessionNotFound {
|
||||
@@ -141,7 +141,7 @@ func TestInjectNotFound(t *testing.T) {
|
||||
Name: "test-rig",
|
||||
Polecats: []string{"Toast"},
|
||||
}
|
||||
m := NewManager(tmux.NewTmux(), r)
|
||||
m := NewSessionManager(tmux.NewTmux(), r)
|
||||
|
||||
err := m.Inject("Toast", "hello")
|
||||
if err != ErrSessionNotFound {
|
||||
@@ -58,8 +58,8 @@ func (m *Manager) stateFile() string {
|
||||
return filepath.Join(m.rig.Path, ".runtime", "refinery.json")
|
||||
}
|
||||
|
||||
// sessionName returns the tmux session name for this refinery.
|
||||
func (m *Manager) sessionName() string {
|
||||
// SessionName returns the tmux session name for this refinery.
|
||||
func (m *Manager) SessionName() string {
|
||||
return fmt.Sprintf("gt-%s-refinery", m.rig.Name)
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ func (m *Manager) Start(foreground bool) error {
|
||||
}
|
||||
|
||||
t := tmux.NewTmux()
|
||||
sessionID := m.sessionName()
|
||||
sessionID := m.SessionName()
|
||||
|
||||
if foreground {
|
||||
// In foreground mode, we're likely running inside the tmux session
|
||||
@@ -253,7 +253,7 @@ func (m *Manager) Stop() error {
|
||||
|
||||
// Check if tmux session exists
|
||||
t := tmux.NewTmux()
|
||||
sessionID := m.sessionName()
|
||||
sessionID := m.SessionName()
|
||||
sessionRunning, _ := t.HasSession(sessionID)
|
||||
|
||||
// If neither state nor session indicates running, it's not running
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/mail"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/polecat"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
)
|
||||
|
||||
@@ -59,12 +59,12 @@ func (m *Manager) ExecuteLanding(swarmID string, config LandingConfig) (*Landing
|
||||
|
||||
// Phase 1: Stop all polecat sessions
|
||||
t := tmux.NewTmux()
|
||||
sessMgr := session.NewManager(t, m.rig)
|
||||
polecatMgr := polecat.NewSessionManager(t, m.rig)
|
||||
|
||||
for _, worker := range swarm.Workers {
|
||||
running, _ := sessMgr.IsRunning(worker)
|
||||
running, _ := polecatMgr.IsRunning(worker)
|
||||
if running {
|
||||
err := sessMgr.Stop(worker, config.ForceKill)
|
||||
err := polecatMgr.Stop(worker, config.ForceKill)
|
||||
if err != nil {
|
||||
// Continue anyway
|
||||
} else {
|
||||
|
||||
@@ -59,8 +59,8 @@ func (m *Manager) saveState(w *Witness) error {
|
||||
return m.stateManager.Save(w)
|
||||
}
|
||||
|
||||
// sessionName returns the tmux session name for this witness.
|
||||
func (m *Manager) sessionName() string {
|
||||
// SessionName returns the tmux session name for this witness.
|
||||
func (m *Manager) SessionName() string {
|
||||
return fmt.Sprintf("gt-%s-witness", m.rig.Name)
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ func (m *Manager) Start(foreground bool) error {
|
||||
}
|
||||
|
||||
t := tmux.NewTmux()
|
||||
sessionID := m.sessionName()
|
||||
sessionID := m.SessionName()
|
||||
|
||||
if foreground {
|
||||
// Foreground mode is deprecated - patrol logic moved to mol-witness-patrol
|
||||
@@ -224,7 +224,7 @@ func (m *Manager) Stop() error {
|
||||
|
||||
// Check if tmux session exists
|
||||
t := tmux.NewTmux()
|
||||
sessionID := m.sessionName()
|
||||
sessionID := m.SessionName()
|
||||
sessionRunning, _ := t.HasSession(sessionID)
|
||||
|
||||
// If neither state nor session indicates running, it's not running
|
||||
|
||||
Reference in New Issue
Block a user