codex
This commit is contained in:
committed by
Cameron Palmer
parent
f4cbcb4ce9
commit
38adfa4d8b
@@ -73,7 +73,7 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resolve account for Claude config
|
||||
// Resolve account for runtime config
|
||||
townRoot, err := workspace.FindFromCwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding town root: %w", err)
|
||||
@@ -87,6 +87,8 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf("Using account: %s\n", accountHandle)
|
||||
}
|
||||
|
||||
runtimeConfig := config.LoadRuntimeConfig(r.Path)
|
||||
|
||||
// Check if session exists
|
||||
t := tmux.NewTmux()
|
||||
sessionID := crewSessionName(r.Name, name)
|
||||
@@ -95,15 +97,15 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("checking session: %w", err)
|
||||
}
|
||||
|
||||
// Before creating a new session, check if there's already a Claude session
|
||||
// Before creating a new session, check if there's already a runtime session
|
||||
// running in this crew's directory (might have been started manually or via
|
||||
// a different mechanism)
|
||||
if !hasSession {
|
||||
existingSessions, err := t.FindSessionByWorkDir(worker.ClonePath, true)
|
||||
existingSessions, err := t.FindSessionByWorkDir(worker.ClonePath, runtimeConfig.Tmux.ProcessNames)
|
||||
if err == nil && len(existingSessions) > 0 {
|
||||
// Found an existing session with an agent running in this directory
|
||||
// Found an existing session with runtime running in this directory
|
||||
existingSession := existingSessions[0]
|
||||
fmt.Printf("%s Found existing agent session '%s' in crew directory\n",
|
||||
fmt.Printf("%s Found existing runtime session '%s' in crew directory\n",
|
||||
style.Warning.Render("⚠"),
|
||||
existingSession)
|
||||
fmt.Printf(" Attaching to existing session instead of creating a new one\n")
|
||||
@@ -137,9 +139,9 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
|
||||
_ = t.SetEnvironment(sessionID, "GT_RIG", r.Name)
|
||||
_ = t.SetEnvironment(sessionID, "GT_CREW", name)
|
||||
|
||||
// Set CLAUDE_CONFIG_DIR for account selection (non-fatal)
|
||||
if claudeConfigDir != "" {
|
||||
_ = t.SetEnvironment(sessionID, "CLAUDE_CONFIG_DIR", claudeConfigDir)
|
||||
// Set runtime config dir for account selection (non-fatal)
|
||||
if runtimeConfig.Session != nil && runtimeConfig.Session.ConfigDirEnv != "" && claudeConfigDir != "" {
|
||||
_ = t.SetEnvironment(sessionID, runtimeConfig.Session.ConfigDirEnv, claudeConfigDir)
|
||||
}
|
||||
|
||||
// Apply rig-based theming (non-fatal: theming failure doesn't affect operation)
|
||||
@@ -158,31 +160,35 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("getting pane ID: %w", err)
|
||||
}
|
||||
|
||||
// Use respawn-pane to replace shell with Claude directly
|
||||
// This gives cleaner lifecycle: Claude exits → session ends (no intermediate shell)
|
||||
// Pass "gt prime" as initial prompt so Claude loads context immediately
|
||||
// Use respawn-pane to replace shell with runtime directly
|
||||
// This gives cleaner lifecycle: runtime exits → session ends (no intermediate shell)
|
||||
// Pass "gt prime" as initial prompt if supported
|
||||
// Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes
|
||||
startupCmd, err := config.BuildCrewStartupCommandWithAgentOverride(r.Name, name, r.Path, "gt prime", crewAgentOverride)
|
||||
if err != nil {
|
||||
return fmt.Errorf("building startup command: %w", err)
|
||||
}
|
||||
// Prepend config dir env if available
|
||||
if runtimeConfig.Session != nil && runtimeConfig.Session.ConfigDirEnv != "" && claudeConfigDir != "" {
|
||||
startupCmd = config.PrependEnv(startupCmd, map[string]string{runtimeConfig.Session.ConfigDirEnv: claudeConfigDir})
|
||||
}
|
||||
if err := t.RespawnPane(paneID, startupCmd); err != nil {
|
||||
return fmt.Errorf("starting claude: %w", err)
|
||||
return fmt.Errorf("starting runtime: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Created session for %s/%s\n",
|
||||
style.Bold.Render("✓"), r.Name, name)
|
||||
} else {
|
||||
// Session exists - check if Claude is still running
|
||||
// Session exists - check if runtime is still running
|
||||
// Uses both pane command check and UI marker detection to avoid
|
||||
// restarting when user is in a subshell spawned from Claude
|
||||
// restarting when user is in a subshell spawned from the runtime
|
||||
agentCfg, _, err := config.ResolveAgentConfigWithOverride(townRoot, r.Path, crewAgentOverride)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving agent: %w", err)
|
||||
}
|
||||
if !t.IsAgentRunning(sessionID, config.ExpectedPaneCommands(agentCfg)...) {
|
||||
// Claude has exited, restart it using respawn-pane
|
||||
fmt.Printf("Claude exited, restarting...\n")
|
||||
// Runtime has exited, restart it using respawn-pane
|
||||
fmt.Printf("Runtime exited, restarting...\n")
|
||||
|
||||
// Get pane ID for respawn
|
||||
paneID, err := t.GetPaneID(sessionID)
|
||||
@@ -190,15 +196,19 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("getting pane ID: %w", err)
|
||||
}
|
||||
|
||||
// Use respawn-pane to replace shell with Claude directly
|
||||
// Pass "gt prime" as initial prompt so Claude loads context immediately
|
||||
// Use respawn-pane to replace shell with runtime directly
|
||||
// Pass "gt prime" as initial prompt if supported
|
||||
// Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes
|
||||
startupCmd, err := config.BuildCrewStartupCommandWithAgentOverride(r.Name, name, r.Path, "gt prime", crewAgentOverride)
|
||||
if err != nil {
|
||||
return fmt.Errorf("building startup command: %w", err)
|
||||
}
|
||||
// Prepend config dir env if available
|
||||
if runtimeConfig.Session != nil && runtimeConfig.Session.ConfigDirEnv != "" && claudeConfigDir != "" {
|
||||
startupCmd = config.PrependEnv(startupCmd, map[string]string{runtimeConfig.Session.ConfigDirEnv: claudeConfigDir})
|
||||
}
|
||||
if err := t.RespawnPane(paneID, startupCmd); err != nil {
|
||||
return fmt.Errorf("restarting claude: %w", err)
|
||||
return fmt.Errorf("restarting runtime: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ func detectCrewFromCwd() (*crewDetection, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// isShellCommand checks if the command is a shell (meaning Claude has exited).
|
||||
// isShellCommand checks if the command is a shell (meaning the runtime has exited).
|
||||
func isShellCommand(cmd string) bool {
|
||||
shells := constants.SupportedShells
|
||||
for _, shell := range shells {
|
||||
@@ -170,6 +170,29 @@ func execAgent(cfg *config.RuntimeConfig, prompt string) error {
|
||||
return syscall.Exec(agentPath, args, os.Environ())
|
||||
}
|
||||
|
||||
// execRuntime execs the runtime CLI, replacing the current process.
|
||||
// Used when we're already in the target session and just need to start the runtime.
|
||||
// If prompt is provided, it's passed according to the runtime's prompt mode.
|
||||
func execRuntime(prompt, rigPath, configDir string) error {
|
||||
runtimeConfig := config.LoadRuntimeConfig(rigPath)
|
||||
args := runtimeConfig.BuildArgsWithPrompt(prompt)
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("runtime command not configured")
|
||||
}
|
||||
|
||||
binPath, err := exec.LookPath(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("runtime command not found: %w", err)
|
||||
}
|
||||
|
||||
env := os.Environ()
|
||||
if runtimeConfig.Session != nil && runtimeConfig.Session.ConfigDirEnv != "" && configDir != "" {
|
||||
env = append(env, fmt.Sprintf("%s=%s", runtimeConfig.Session.ConfigDirEnv, configDir))
|
||||
}
|
||||
|
||||
return syscall.Exec(binPath, args, env)
|
||||
}
|
||||
|
||||
// isInTmuxSession checks if we're currently inside the target tmux session.
|
||||
func isInTmuxSession(targetSession string) bool {
|
||||
// TMUX env var format: /tmp/tmux-501/default,12345,0
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
"github.com/steveyegge/gastown/internal/crew"
|
||||
"github.com/steveyegge/gastown/internal/mail"
|
||||
"github.com/steveyegge/gastown/internal/runtime"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/townlog"
|
||||
@@ -163,7 +164,7 @@ func runCrewRemove(cmd *cobra.Command, args []string) error {
|
||||
} else {
|
||||
// Default: CLOSE the agent bead (preserves CV history)
|
||||
closeArgs := []string{"close", agentBeadID, "--reason=Crew workspace removed"}
|
||||
if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" {
|
||||
if sessionID := runtime.SessionIDFromEnv(); sessionID != "" {
|
||||
closeArgs = append(closeArgs, "--session="+sessionID)
|
||||
}
|
||||
closeCmd := exec.Command("bd", closeArgs...)
|
||||
@@ -236,9 +237,9 @@ func runCrewRefresh(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// Use manager's Start() with refresh options
|
||||
err = crewMgr.Start(name, crew.StartOptions{
|
||||
KillExisting: true, // Kill old session if running
|
||||
Topic: "refresh", // Startup nudge topic
|
||||
Interactive: true, // No --dangerously-skip-permissions
|
||||
KillExisting: true, // Kill old session if running
|
||||
Topic: "refresh", // Startup nudge topic
|
||||
Interactive: true, // No --dangerously-skip-permissions
|
||||
AgentOverride: crewAgentOverride,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -347,8 +348,8 @@ func runCrewRestart(cmd *cobra.Command, args []string) error {
|
||||
// Use manager's Start() with restart options
|
||||
// Start() will create workspace if needed (idempotent)
|
||||
err = crewMgr.Start(name, crew.StartOptions{
|
||||
KillExisting: true, // Kill old session if running
|
||||
Topic: "restart", // Startup nudge topic
|
||||
KillExisting: true, // Kill old session if running
|
||||
Topic: "restart", // Startup nudge topic
|
||||
AgentOverride: crewAgentOverride,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -427,8 +428,8 @@ func runCrewRestartAll() error {
|
||||
|
||||
// Use manager's Start() with restart options
|
||||
err = crewMgr.Start(agent.AgentName, crew.StartOptions{
|
||||
KillExisting: true, // Kill old session if running
|
||||
Topic: "restart", // Startup nudge topic
|
||||
KillExisting: true, // Kill old session if running
|
||||
Topic: "restart", // Startup nudge topic
|
||||
AgentOverride: crewAgentOverride,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
"github.com/steveyegge/gastown/internal/deacon"
|
||||
"github.com/steveyegge/gastown/internal/polecat"
|
||||
"github.com/steveyegge/gastown/internal/runtime"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
@@ -112,7 +113,7 @@ var deaconTriggerPendingCmd = &cobra.Command{
|
||||
|
||||
⚠️ BOOTSTRAP MODE ONLY - Uses regex detection (ZFC violation acceptable).
|
||||
|
||||
This command uses WaitForClaudeReady (regex) to detect when Claude is ready.
|
||||
This command uses WaitForRuntimeReady (regex) to detect when the runtime is ready.
|
||||
This is appropriate for daemon bootstrap when no AI is available.
|
||||
|
||||
In steady-state, the Deacon should use AI-based observation instead:
|
||||
@@ -383,6 +384,9 @@ func startDeaconSession(t *tmux.Tmux, sessionName, agentOverride string) error {
|
||||
}
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
runtimeConfig := config.LoadRuntimeConfig("")
|
||||
_ = runtime.RunStartupFallback(t, sessionName, "deacon", runtimeConfig)
|
||||
|
||||
// Inject startup nudge for predecessor discovery via /resume
|
||||
_ = session.StartupNudge(t, sessionName, session.StartupNudgeConfig{
|
||||
Recipient: "deacon",
|
||||
|
||||
@@ -357,9 +357,13 @@ func buildRestartCommand(sessionName string) (string, error) {
|
||||
// Build environment exports - role vars first, then Claude vars
|
||||
var exports []string
|
||||
if gtRole != "" {
|
||||
exports = append(exports, fmt.Sprintf("GT_ROLE=%s", gtRole))
|
||||
exports = append(exports, fmt.Sprintf("BD_ACTOR=%s", gtRole))
|
||||
exports = append(exports, fmt.Sprintf("GIT_AUTHOR_NAME=%s", gtRole))
|
||||
runtimeConfig := config.LoadRuntimeConfig("")
|
||||
exports = append(exports, "GT_ROLE="+gtRole)
|
||||
exports = append(exports, "BD_ACTOR="+gtRole)
|
||||
exports = append(exports, "GIT_AUTHOR_NAME="+gtRole)
|
||||
if runtimeConfig.Session != nil && runtimeConfig.Session.SessionIDEnv != "" {
|
||||
exports = append(exports, "GT_SESSION_ID_ENV="+runtimeConfig.Session.SessionIDEnv)
|
||||
}
|
||||
}
|
||||
|
||||
// Add Claude-related env vars from current environment
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/events"
|
||||
"github.com/steveyegge/gastown/internal/runtime"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
)
|
||||
|
||||
@@ -172,7 +173,7 @@ func runHook(_ *cobra.Command, args []string) error {
|
||||
// Close completed molecule bead (use bd close --force for pinned)
|
||||
closeArgs := []string{"close", existing.ID, "--force",
|
||||
"--reason=Auto-replaced by gt hook (molecule complete)"}
|
||||
if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" {
|
||||
if sessionID := runtime.SessionIDFromEnv(); sessionID != "" {
|
||||
closeArgs = append(closeArgs, "--session="+sessionID)
|
||||
}
|
||||
closeCmd := exec.Command("bd", closeArgs...)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/git"
|
||||
"github.com/steveyegge/gastown/internal/polecat"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/runtime"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
)
|
||||
@@ -89,7 +90,6 @@ Examples:
|
||||
RunE: runPolecatRemove,
|
||||
}
|
||||
|
||||
|
||||
var polecatSyncCmd = &cobra.Command{
|
||||
Use: "sync <rig>/<polecat>",
|
||||
Short: "Sync beads for a polecat",
|
||||
@@ -129,15 +129,15 @@ Examples:
|
||||
}
|
||||
|
||||
var (
|
||||
polecatSyncAll bool
|
||||
polecatSyncFromMain bool
|
||||
polecatStatusJSON bool
|
||||
polecatGitStateJSON bool
|
||||
polecatGCDryRun bool
|
||||
polecatNukeAll bool
|
||||
polecatNukeDryRun bool
|
||||
polecatNukeForce bool
|
||||
polecatCheckRecoveryJSON bool
|
||||
polecatSyncAll bool
|
||||
polecatSyncFromMain bool
|
||||
polecatStatusJSON bool
|
||||
polecatGitStateJSON bool
|
||||
polecatGCDryRun bool
|
||||
polecatNukeAll bool
|
||||
polecatNukeDryRun bool
|
||||
polecatNukeForce bool
|
||||
polecatCheckRecoveryJSON bool
|
||||
)
|
||||
|
||||
var polecatGCCmd = &cobra.Command{
|
||||
@@ -975,7 +975,7 @@ type RecoveryStatus struct {
|
||||
NeedsRecovery bool `json:"needs_recovery"`
|
||||
Verdict string `json:"verdict"` // SAFE_TO_NUKE or NEEDS_RECOVERY
|
||||
Branch string `json:"branch,omitempty"`
|
||||
Issue string `json:"issue,omitempty"`
|
||||
Issue string `json:"issue,omitempty"`
|
||||
}
|
||||
|
||||
func runPolecatCheckRecovery(cmd *cobra.Command, args []string) error {
|
||||
@@ -1477,7 +1477,7 @@ func runPolecatNuke(cmd *cobra.Command, args []string) error {
|
||||
// Step 5: Close agent bead (if exists)
|
||||
agentBeadID := beads.PolecatBeadID(p.rigName, p.polecatName)
|
||||
closeArgs := []string{"close", agentBeadID, "--reason=nuked"}
|
||||
if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" {
|
||||
if sessionID := runtime.SessionIDFromEnv(); sessionID != "" {
|
||||
closeArgs = append(closeArgs, "--session="+sessionID)
|
||||
}
|
||||
closeCmd := exec.Command("bd", closeArgs...)
|
||||
|
||||
@@ -139,7 +139,7 @@ func SpawnPolecatForSling(rigName string, opts SlingSpawnOptions) (*SpawnedPolec
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Resolve account for Claude config
|
||||
// Resolve account for runtime config
|
||||
accountsPath := constants.MayorAccountsPath(townRoot)
|
||||
claudeConfigDir, accountHandle, err := config.ResolveAccountConfigDir(accountsPath, opts.Account)
|
||||
if err != nil {
|
||||
@@ -158,7 +158,7 @@ func SpawnPolecatForSling(rigName string, opts SlingSpawnOptions) (*SpawnedPolec
|
||||
if !running {
|
||||
fmt.Printf("Starting session for %s/%s...\n", rigName, polecatName)
|
||||
startOpts := polecat.SessionStartOptions{
|
||||
ClaudeConfigDir: claudeConfigDir,
|
||||
RuntimeConfigDir: claudeConfigDir,
|
||||
}
|
||||
if opts.Agent != "" {
|
||||
cmd, err := config.BuildPolecatStartupCommandWithAgentOverride(rigName, polecatName, r.Path, "", opts.Agent)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/events"
|
||||
"github.com/steveyegge/gastown/internal/lock"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/runtime"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/templates"
|
||||
@@ -1499,22 +1500,17 @@ func outputSessionMetadata(ctx RoleContext) {
|
||||
// resolveSessionIDForPrime finds the session ID from available sources.
|
||||
// Priority: GT_SESSION_ID env, CLAUDE_SESSION_ID env, persisted file, fallback.
|
||||
func resolveSessionIDForPrime(actor string) string {
|
||||
// 1. GT_SESSION_ID (new canonical)
|
||||
if id := os.Getenv("GT_SESSION_ID"); id != "" {
|
||||
// 1. Try runtime's session ID lookup (checks GT_SESSION_ID_ENV, then CLAUDE_SESSION_ID)
|
||||
if id := runtime.SessionIDFromEnv(); id != "" {
|
||||
return id
|
||||
}
|
||||
|
||||
// 2. CLAUDE_SESSION_ID (legacy/Claude Code)
|
||||
if id := os.Getenv("CLAUDE_SESSION_ID"); id != "" {
|
||||
return id
|
||||
}
|
||||
|
||||
// 3. Persisted session file (from gt prime --hook)
|
||||
// 2. Persisted session file (from gt prime --hook)
|
||||
if id := ReadPersistedSessionID(); id != "" {
|
||||
return id
|
||||
}
|
||||
|
||||
// 4. Fallback to generated identifier
|
||||
// 3. Fallback to generated identifier
|
||||
return fmt.Sprintf("%s-%d", actor, os.Getpid())
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/polecat"
|
||||
"github.com/steveyegge/gastown/internal/refinery"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/runtime"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/witness"
|
||||
@@ -317,6 +319,86 @@ func discoverAllRigs(townRoot string) ([]*rig.Rig, error) {
|
||||
return rigMgr.DiscoverRigs()
|
||||
}
|
||||
|
||||
// ensureRefinerySession creates a refinery tmux session if it doesn't exist.
|
||||
// Returns true if a new session was created, false if it already existed.
|
||||
func ensureRefinerySession(rigName string, r *rig.Rig) (bool, error) {
|
||||
t := tmux.NewTmux()
|
||||
sessionName := fmt.Sprintf("gt-%s-refinery", rigName)
|
||||
|
||||
// Check if session already exists
|
||||
running, err := t.HasSession(sessionName)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("checking session: %w", err)
|
||||
}
|
||||
|
||||
if running {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Working directory is the refinery's rig clone
|
||||
refineryRigDir := filepath.Join(r.Path, "refinery", "rig")
|
||||
if _, err := os.Stat(refineryRigDir); os.IsNotExist(err) {
|
||||
// Fall back to rig path if refinery/rig doesn't exist
|
||||
refineryRigDir = r.Path
|
||||
}
|
||||
|
||||
// Ensure runtime settings exist (autonomous role needs mail in SessionStart)
|
||||
runtimeConfig := config.LoadRuntimeConfig(r.Path)
|
||||
if err := runtime.EnsureSettingsForRole(refineryRigDir, "refinery", runtimeConfig); err != nil {
|
||||
return false, fmt.Errorf("ensuring runtime settings: %w", err)
|
||||
}
|
||||
|
||||
// Create new tmux session
|
||||
if err := t.NewSession(sessionName, refineryRigDir); err != nil {
|
||||
return false, fmt.Errorf("creating session: %w", err)
|
||||
}
|
||||
|
||||
// Set environment
|
||||
bdActor := fmt.Sprintf("%s/refinery", rigName)
|
||||
_ = t.SetEnvironment(sessionName, "GT_ROLE", "refinery")
|
||||
_ = t.SetEnvironment(sessionName, "GT_RIG", rigName)
|
||||
_ = t.SetEnvironment(sessionName, "BD_ACTOR", bdActor)
|
||||
|
||||
// Set beads environment
|
||||
beadsDir := filepath.Join(r.Path, "mayor", "rig", ".beads")
|
||||
_ = t.SetEnvironment(sessionName, "BEADS_DIR", beadsDir)
|
||||
_ = t.SetEnvironment(sessionName, "BEADS_NO_DAEMON", "1")
|
||||
_ = t.SetEnvironment(sessionName, "BEADS_AGENT_NAME", fmt.Sprintf("%s/refinery", rigName))
|
||||
|
||||
// Apply Gas Town theming (non-fatal: theming failure doesn't affect operation)
|
||||
theme := tmux.AssignTheme(rigName)
|
||||
_ = t.ConfigureGasTownSession(sessionName, theme, rigName, "refinery", "refinery")
|
||||
|
||||
// Launch Claude directly (no respawn loop - daemon handles restart)
|
||||
// Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes
|
||||
if err := t.SendKeys(sessionName, config.BuildAgentStartupCommand("refinery", bdActor, "", "")); err != nil {
|
||||
return false, 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)
|
||||
_ = runtime.RunStartupFallback(t, sessionName, "refinery", runtimeConfig)
|
||||
|
||||
// Inject startup nudge for predecessor discovery via /resume
|
||||
address := fmt.Sprintf("%s/refinery", rigName)
|
||||
_ = session.StartupNudge(t, sessionName, session.StartupNudgeConfig{
|
||||
Recipient: address,
|
||||
Sender: "deacon",
|
||||
Topic: "patrol",
|
||||
}) // Non-fatal
|
||||
|
||||
// GUPP: Gas Town Universal Propulsion Principle
|
||||
// Send the propulsion nudge to trigger autonomous patrol execution.
|
||||
// Wait for beacon to be fully processed (needs to be separate prompt)
|
||||
time.Sleep(2 * time.Second)
|
||||
_ = t.NudgeSession(sessionName, session.PropulsionNudgeForRole("refinery", refineryRigDir)) // Non-fatal
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func runShutdown(cmd *cobra.Command, args []string) error {
|
||||
t := tmux.NewTmux()
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/git"
|
||||
"github.com/steveyegge/gastown/internal/polecat"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/runtime"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/swarm"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
@@ -808,7 +809,7 @@ func runSwarmLand(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// Close the swarm epic in beads
|
||||
closeArgs := []string{"close", swarmID, "--reason", "Swarm landed to main"}
|
||||
if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" {
|
||||
if sessionID := runtime.SessionIDFromEnv(); sessionID != "" {
|
||||
closeArgs = append(closeArgs, "--session="+sessionID)
|
||||
}
|
||||
closeCmd := exec.Command("bd", closeArgs...)
|
||||
@@ -867,7 +868,7 @@ func runSwarmCancel(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// Close the swarm epic in beads with canceled reason
|
||||
closeArgs := []string{"close", swarmID, "--reason", "Swarm canceled"}
|
||||
if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" {
|
||||
if sessionID := runtime.SessionIDFromEnv(); sessionID != "" {
|
||||
closeArgs = append(closeArgs, "--session="+sessionID)
|
||||
}
|
||||
closeCmd := exec.Command("bd", closeArgs...)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/formula"
|
||||
"github.com/steveyegge/gastown/internal/runtime"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
@@ -322,7 +323,7 @@ func runSynthesisClose(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// Close the convoy
|
||||
closeArgs := []string{"close", convoyID, "--reason=synthesis complete"}
|
||||
if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" {
|
||||
if sessionID := runtime.SessionIDFromEnv(); sessionID != "" {
|
||||
closeArgs = append(closeArgs, "--session="+sessionID)
|
||||
}
|
||||
closeCmd := exec.Command("bd", closeArgs...)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"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"
|
||||
@@ -18,6 +19,8 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/mayor"
|
||||
"github.com/steveyegge/gastown/internal/polecat"
|
||||
"github.com/steveyegge/gastown/internal/refinery"
|
||||
"github.com/steveyegge/gastown/internal/runtime"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/witness"
|
||||
@@ -249,6 +252,127 @@ 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
|
||||
}
|
||||
|
||||
// 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 runtime
|
||||
// Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes
|
||||
var claudeCmd string
|
||||
runtimeCmd := config.GetRuntimeCommand("")
|
||||
runtimeConfig := config.LoadRuntimeConfig("")
|
||||
if role == "deacon" {
|
||||
// Deacon uses respawn loop
|
||||
prefix := "GT_ROLE=deacon BD_ACTOR=deacon GIT_AUTHOR_NAME=deacon"
|
||||
if runtimeConfig.Session != nil && runtimeConfig.Session.SessionIDEnv != "" {
|
||||
prefix = prefix + " GT_SESSION_ID_ENV=" + runtimeConfig.Session.SessionIDEnv
|
||||
}
|
||||
claudeCmd = `export ` + prefix + ` && 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
|
||||
}
|
||||
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
|
||||
_ = runtime.RunStartupFallback(t, sessionName, role, runtimeConfig)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureWitness starts a witness session for a rig.
|
||||
func ensureWitness(t *tmux.Tmux, sessionName, rigPath, rigName string) error {
|
||||
running, err := t.HasSession(sessionName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if running {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create session in rig directory
|
||||
if err := t.NewSession(sessionName, rigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set environment (non-fatal: session works without these)
|
||||
bdActor := fmt.Sprintf("%s/witness", rigName)
|
||||
_ = t.SetEnvironment(sessionName, "GT_ROLE", "witness")
|
||||
_ = t.SetEnvironment(sessionName, "GT_RIG", rigName)
|
||||
_ = t.SetEnvironment(sessionName, "BD_ACTOR", bdActor)
|
||||
|
||||
// Apply theme (non-fatal: theming failure doesn't affect operation)
|
||||
theme := tmux.AssignTheme(rigName)
|
||||
_ = t.ConfigureGasTownSession(sessionName, theme, "", "Witness", rigName)
|
||||
|
||||
// Launch runtime using runtime config
|
||||
// Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes
|
||||
runtimeConfig := config.LoadRuntimeConfig(rigPath)
|
||||
claudeCmd := config.BuildAgentStartupCommand("witness", bdActor, 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
|
||||
}
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
_ = runtime.RunStartupFallback(t, sessionName, "witness", runtimeConfig)
|
||||
|
||||
// Inject startup nudge for predecessor discovery via /resume
|
||||
address := fmt.Sprintf("%s/witness", rigName)
|
||||
_ = session.StartupNudge(t, sessionName, session.StartupNudgeConfig{
|
||||
Recipient: address,
|
||||
Sender: "deacon",
|
||||
Topic: "patrol",
|
||||
}) // Non-fatal
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// discoverRigs finds all rigs in the town.
|
||||
func discoverRigs(townRoot string) []string {
|
||||
var rigs []string
|
||||
|
||||
@@ -5,8 +5,15 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/runtime"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/witness"
|
||||
@@ -262,6 +269,87 @@ func witnessSessionName(rigName string) string {
|
||||
return fmt.Sprintf("gt-%s-witness", rigName)
|
||||
}
|
||||
|
||||
// ensureWitnessSession creates a witness tmux session if it doesn't exist.
|
||||
// Returns true if a new session was created, false if it already existed.
|
||||
func ensureWitnessSession(rigName string, r *rig.Rig) (bool, error) {
|
||||
t := tmux.NewTmux()
|
||||
sessionName := witnessSessionName(rigName)
|
||||
|
||||
// Check if session already exists
|
||||
running, err := t.HasSession(sessionName)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("checking session: %w", err)
|
||||
}
|
||||
|
||||
if running {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Working directory is the witness's rig clone (if it exists) or witness dir
|
||||
// This ensures gt prime detects the Witness role correctly
|
||||
witnessDir := filepath.Join(r.Path, "witness", "rig")
|
||||
if _, err := os.Stat(witnessDir); os.IsNotExist(err) {
|
||||
// Try witness/ without rig subdirectory
|
||||
witnessDir = filepath.Join(r.Path, "witness")
|
||||
if _, err := os.Stat(witnessDir); os.IsNotExist(err) {
|
||||
// Fall back to rig path (shouldn't happen in normal setup)
|
||||
witnessDir = r.Path
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure Claude settings exist (autonomous role needs mail in SessionStart)
|
||||
runtimeConfig := config.LoadRuntimeConfig(r.Path)
|
||||
if err := runtime.EnsureSettingsForRole(witnessDir, "witness", runtimeConfig); err != nil {
|
||||
return false, fmt.Errorf("ensuring runtime settings: %w", err)
|
||||
}
|
||||
|
||||
// Create new tmux session
|
||||
if err := t.NewSession(sessionName, witnessDir); err != nil {
|
||||
return false, fmt.Errorf("creating session: %w", err)
|
||||
}
|
||||
|
||||
// Set environment
|
||||
bdActor := fmt.Sprintf("%s/witness", rigName)
|
||||
t.SetEnvironment(sessionName, "GT_ROLE", "witness")
|
||||
t.SetEnvironment(sessionName, "GT_RIG", rigName)
|
||||
t.SetEnvironment(sessionName, "BD_ACTOR", bdActor)
|
||||
|
||||
// Apply Gas Town theming (non-fatal: theming failure doesn't affect operation)
|
||||
theme := tmux.AssignTheme(rigName)
|
||||
_ = t.ConfigureGasTownSession(sessionName, theme, rigName, "witness", "witness")
|
||||
|
||||
// Launch Claude directly (no shell respawn loop)
|
||||
// Restarts are handled by daemon via LIFECYCLE mail or deacon health-scan
|
||||
// NOTE: No gt prime injection needed - SessionStart hook handles it automatically
|
||||
// Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes
|
||||
if err := t.SendKeys(sessionName, config.BuildAgentStartupCommand("witness", bdActor, "", "")); err != nil {
|
||||
return false, 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)
|
||||
_ = runtime.RunStartupFallback(t, sessionName, "witness", runtimeConfig)
|
||||
|
||||
// Inject startup nudge for predecessor discovery via /resume
|
||||
address := fmt.Sprintf("%s/witness", rigName)
|
||||
_ = session.StartupNudge(t, sessionName, session.StartupNudgeConfig{
|
||||
Recipient: address,
|
||||
Sender: "deacon",
|
||||
Topic: "patrol",
|
||||
}) // Non-fatal
|
||||
|
||||
// GUPP: Gas Town Universal Propulsion Principle
|
||||
// Send the propulsion nudge to trigger autonomous patrol execution.
|
||||
// Wait for beacon to be fully processed (needs to be separate prompt)
|
||||
time.Sleep(2 * time.Second)
|
||||
_ = t.NudgeSession(sessionName, session.PropulsionNudgeForRole("witness", witnessDir)) // Non-fatal
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func runWitnessAttach(cmd *cobra.Command, args []string) error {
|
||||
rigName := ""
|
||||
if len(args) > 0 {
|
||||
|
||||
Reference in New Issue
Block a user