Unify agent startup with GUPP propulsion nudge
Witness and Refinery startup was duplicated across cmd/witness.go, cmd/up.go, cmd/rig.go, and daemon.go. Worse, not all code paths sent the propulsion nudge (GUPP - Gas Town Universal Propulsion Principle). Now unified in Manager.Start() which handles everything including nudges. Changes: - witness/manager.go: Full rewrite with session creation, env vars, theming, WaitForClaudeReady, startup nudge, and propulsion nudge (GUPP) - refinery/manager.go: Add propulsion nudge sequence after Claude startup - cmd/witness.go: Simplify to just call mgr.Start(), remove ensureWitnessSession - cmd/rig.go: Use witness.Manager.Start() instead of inline session creation - cmd/start.go: Use witness.Manager.Start() - cmd/up.go: Use witness.Manager.Start(), remove ensureWitness(), add EnsureSettingsForRole in ensureSession() - daemon.go: Use witness.Manager.Start() and refinery.Manager.Start() for unified startup with proper nudges This ensures all agent startup paths (gt witness start, gt rig boot, gt up, daemon restarts) consistently apply GUPP propulsion nudges. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -737,15 +737,14 @@ func runRigBoot(cmd *cobra.Command, args []string) error {
|
|||||||
skipped = append(skipped, "witness (already running)")
|
skipped = append(skipped, "witness (already running)")
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" Starting witness...\n")
|
fmt.Printf(" Starting witness...\n")
|
||||||
// Use ensureWitnessSession to create tmux session (same as gt witness start)
|
witMgr := witness.NewManager(r)
|
||||||
created, err := ensureWitnessSession(rigName, r)
|
if err := witMgr.Start(false); err != nil {
|
||||||
if err != nil {
|
if err == witness.ErrAlreadyRunning {
|
||||||
return fmt.Errorf("starting witness: %w", err)
|
skipped = append(skipped, "witness (already running)")
|
||||||
}
|
} else {
|
||||||
if created {
|
return fmt.Errorf("starting witness: %w", err)
|
||||||
// Update manager state to reflect running session
|
}
|
||||||
witMgr := witness.NewManager(r)
|
} else {
|
||||||
_ = witMgr.Start() // non-fatal: state file update
|
|
||||||
started = append(started, "witness")
|
started = append(started, "witness")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -818,13 +817,15 @@ func runRigStart(cmd *cobra.Command, args []string) error {
|
|||||||
skipped = append(skipped, "witness")
|
skipped = append(skipped, "witness")
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" Starting witness...\n")
|
fmt.Printf(" Starting witness...\n")
|
||||||
created, err := ensureWitnessSession(rigName, r)
|
witMgr := witness.NewManager(r)
|
||||||
if err != nil {
|
if err := witMgr.Start(false); err != nil {
|
||||||
fmt.Printf(" %s Failed to start witness: %v\n", style.Warning.Render("⚠"), err)
|
if err == witness.ErrAlreadyRunning {
|
||||||
hasError = true
|
skipped = append(skipped, "witness")
|
||||||
} else if created {
|
} else {
|
||||||
witMgr := witness.NewManager(r)
|
fmt.Printf(" %s Failed to start witness: %v\n", style.Warning.Render("⚠"), err)
|
||||||
_ = witMgr.Start()
|
hasError = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
started = append(started, "witness")
|
started = append(started, "witness")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1385,12 +1386,14 @@ func runRigRestart(cmd *cobra.Command, args []string) error {
|
|||||||
skipped = append(skipped, "witness")
|
skipped = append(skipped, "witness")
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" Starting witness...\n")
|
fmt.Printf(" Starting witness...\n")
|
||||||
created, err := ensureWitnessSession(rigName, r)
|
if err := witMgr.Start(false); err != nil {
|
||||||
if err != nil {
|
if err == witness.ErrAlreadyRunning {
|
||||||
fmt.Printf(" %s Failed to start witness: %v\n", style.Warning.Render("⚠"), err)
|
skipped = append(skipped, "witness")
|
||||||
startErrors = append(startErrors, fmt.Sprintf("witness: %v", err))
|
} else {
|
||||||
} else if created {
|
fmt.Printf(" %s Failed to start witness: %v\n", style.Warning.Render("⚠"), err)
|
||||||
_ = witMgr.Start()
|
startErrors = append(startErrors, fmt.Sprintf("witness: %v", err))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
started = append(started, "witness")
|
started = append(started, "witness")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/steveyegge/gastown/internal/session"
|
"github.com/steveyegge/gastown/internal/session"
|
||||||
"github.com/steveyegge/gastown/internal/style"
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
"github.com/steveyegge/gastown/internal/tmux"
|
"github.com/steveyegge/gastown/internal/tmux"
|
||||||
|
"github.com/steveyegge/gastown/internal/witness"
|
||||||
"github.com/steveyegge/gastown/internal/workspace"
|
"github.com/steveyegge/gastown/internal/workspace"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -225,10 +226,14 @@ func startRigAgents(t *tmux.Tmux, townRoot string) {
|
|||||||
if witnessRunning {
|
if witnessRunning {
|
||||||
fmt.Printf(" %s %s witness already running\n", style.Dim.Render("○"), r.Name)
|
fmt.Printf(" %s %s witness already running\n", style.Dim.Render("○"), r.Name)
|
||||||
} else {
|
} else {
|
||||||
created, err := ensureWitnessSession(r.Name, r)
|
witMgr := witness.NewManager(r)
|
||||||
if err != nil {
|
if err := witMgr.Start(false); err != nil {
|
||||||
fmt.Printf(" %s %s witness failed: %v\n", style.Dim.Render("○"), r.Name, err)
|
if err == witness.ErrAlreadyRunning {
|
||||||
} else if created {
|
fmt.Printf(" %s %s witness already running\n", style.Dim.Render("○"), r.Name)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" %s %s witness failed: %v\n", style.Dim.Render("○"), r.Name, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
fmt.Printf(" %s %s witness started\n", style.Bold.Render("✓"), r.Name)
|
fmt.Printf(" %s %s witness started\n", style.Bold.Render("✓"), r.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/steveyegge/gastown/internal/beads"
|
"github.com/steveyegge/gastown/internal/beads"
|
||||||
|
"github.com/steveyegge/gastown/internal/claude"
|
||||||
"github.com/steveyegge/gastown/internal/config"
|
"github.com/steveyegge/gastown/internal/config"
|
||||||
"github.com/steveyegge/gastown/internal/constants"
|
"github.com/steveyegge/gastown/internal/constants"
|
||||||
"github.com/steveyegge/gastown/internal/daemon"
|
"github.com/steveyegge/gastown/internal/daemon"
|
||||||
@@ -18,6 +19,7 @@ import (
|
|||||||
"github.com/steveyegge/gastown/internal/session"
|
"github.com/steveyegge/gastown/internal/session"
|
||||||
"github.com/steveyegge/gastown/internal/style"
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
"github.com/steveyegge/gastown/internal/tmux"
|
"github.com/steveyegge/gastown/internal/tmux"
|
||||||
|
"github.com/steveyegge/gastown/internal/witness"
|
||||||
"github.com/steveyegge/gastown/internal/workspace"
|
"github.com/steveyegge/gastown/internal/workspace"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -103,11 +105,22 @@ func runUp(cmd *cobra.Command, args []string) error {
|
|||||||
rigs := discoverRigs(townRoot)
|
rigs := discoverRigs(townRoot)
|
||||||
for _, rigName := range rigs {
|
for _, rigName := range rigs {
|
||||||
sessionName := fmt.Sprintf("gt-%s-witness", rigName)
|
sessionName := fmt.Sprintf("gt-%s-witness", rigName)
|
||||||
rigPath := filepath.Join(townRoot, rigName)
|
|
||||||
|
|
||||||
if err := ensureWitness(t, sessionName, rigPath, rigName); err != nil {
|
_, r, err := getRig(rigName)
|
||||||
|
if err != nil {
|
||||||
printStatus(fmt.Sprintf("Witness (%s)", rigName), false, err.Error())
|
printStatus(fmt.Sprintf("Witness (%s)", rigName), false, err.Error())
|
||||||
allOK = false
|
allOK = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr := witness.NewManager(r)
|
||||||
|
if err := mgr.Start(false); err != nil {
|
||||||
|
if err == witness.ErrAlreadyRunning {
|
||||||
|
printStatus(fmt.Sprintf("Witness (%s)", rigName), true, sessionName)
|
||||||
|
} else {
|
||||||
|
printStatus(fmt.Sprintf("Witness (%s)", rigName), false, err.Error())
|
||||||
|
allOK = false
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
printStatus(fmt.Sprintf("Witness (%s)", rigName), true, sessionName)
|
printStatus(fmt.Sprintf("Witness (%s)", rigName), true, sessionName)
|
||||||
}
|
}
|
||||||
@@ -244,6 +257,11 @@ func ensureSession(t *tmux.Tmux, sessionName, workDir, role string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure Claude settings exist
|
||||||
|
if err := claude.EnsureSettingsForRole(workDir, role); err != nil {
|
||||||
|
return fmt.Errorf("ensuring Claude settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Create session
|
// Create session
|
||||||
if err := t.NewSession(sessionName, workDir); err != nil {
|
if err := t.NewSession(sessionName, workDir); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -302,59 +320,6 @@ func ensureSession(t *tmux.Tmux, sessionName, workDir, role string) error {
|
|||||||
return nil
|
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 Claude using runtime config
|
|
||||||
// Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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/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.
|
// discoverRigs finds all rigs in the town.
|
||||||
func discoverRigs(townRoot string) []string {
|
func discoverRigs(townRoot string) []string {
|
||||||
var rigs []string
|
var rigs []string
|
||||||
|
|||||||
@@ -5,15 +5,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/steveyegge/gastown/internal/claude"
|
|
||||||
"github.com/steveyegge/gastown/internal/config"
|
|
||||||
"github.com/steveyegge/gastown/internal/constants"
|
|
||||||
"github.com/steveyegge/gastown/internal/rig"
|
"github.com/steveyegge/gastown/internal/rig"
|
||||||
"github.com/steveyegge/gastown/internal/session"
|
|
||||||
"github.com/steveyegge/gastown/internal/style"
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
"github.com/steveyegge/gastown/internal/tmux"
|
"github.com/steveyegge/gastown/internal/tmux"
|
||||||
"github.com/steveyegge/gastown/internal/witness"
|
"github.com/steveyegge/gastown/internal/witness"
|
||||||
@@ -136,42 +130,28 @@ func getWitnessManager(rigName string) (*witness.Manager, *rig.Rig, error) {
|
|||||||
func runWitnessStart(cmd *cobra.Command, args []string) error {
|
func runWitnessStart(cmd *cobra.Command, args []string) error {
|
||||||
rigName := args[0]
|
rigName := args[0]
|
||||||
|
|
||||||
mgr, r, err := getWitnessManager(rigName)
|
mgr, _, err := getWitnessManager(rigName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Starting witness for %s...\n", rigName)
|
fmt.Printf("Starting witness for %s...\n", rigName)
|
||||||
|
|
||||||
if witnessForeground {
|
if err := mgr.Start(witnessForeground); err != nil {
|
||||||
// Foreground mode is no longer supported - patrol logic moved to mol-witness-patrol
|
if err == witness.ErrAlreadyRunning {
|
||||||
if err := mgr.Start(); err != nil {
|
fmt.Printf("%s Witness is already running\n", style.Dim.Render("⚠"))
|
||||||
if err == witness.ErrAlreadyRunning {
|
fmt.Printf(" %s\n", style.Dim.Render("Use 'gt witness attach' to connect"))
|
||||||
fmt.Printf("%s Witness is already running\n", style.Dim.Render("⚠"))
|
return nil
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("starting witness: %w", err)
|
|
||||||
}
|
}
|
||||||
|
return fmt.Errorf("starting witness: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if witnessForeground {
|
||||||
fmt.Printf("%s Note: Foreground mode no longer runs patrol loop\n", style.Dim.Render("⚠"))
|
fmt.Printf("%s Note: Foreground mode no longer runs patrol loop\n", style.Dim.Render("⚠"))
|
||||||
fmt.Printf(" %s\n", style.Dim.Render("Patrol logic is now handled by mol-witness-patrol molecule"))
|
fmt.Printf(" %s\n", style.Dim.Render("Patrol logic is now handled by mol-witness-patrol molecule"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Background mode: create tmux session with Claude
|
|
||||||
created, err := ensureWitnessSession(rigName, r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !created {
|
|
||||||
fmt.Printf("%s Witness session already running\n", style.Dim.Render("⚠"))
|
|
||||||
fmt.Printf(" %s\n", style.Dim.Render("Use 'gt witness attach' to connect"))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update manager state to reflect running session (non-fatal: state file update)
|
|
||||||
_ = mgr.Start()
|
|
||||||
|
|
||||||
fmt.Printf("%s Witness started for %s\n", style.Bold.Render("✓"), rigName)
|
fmt.Printf("%s Witness started for %s\n", style.Bold.Render("✓"), rigName)
|
||||||
fmt.Printf(" %s\n", style.Dim.Render("Use 'gt witness attach' to connect"))
|
fmt.Printf(" %s\n", style.Dim.Render("Use 'gt witness attach' to connect"))
|
||||||
fmt.Printf(" %s\n", style.Dim.Render("Use 'gt witness status' to check progress"))
|
fmt.Printf(" %s\n", style.Dim.Render("Use 'gt witness status' to check progress"))
|
||||||
@@ -283,95 +263,6 @@ func witnessSessionName(rigName string) string {
|
|||||||
return fmt.Sprintf("gt-%s-witness", rigName)
|
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 (and is healthy).
|
|
||||||
// Implements 'ensure' semantics: if session exists but Claude is dead (zombie), kills and recreates.
|
|
||||||
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 {
|
|
||||||
// Session exists - check if Claude is actually running (healthy vs zombie)
|
|
||||||
if t.IsClaudeRunning(sessionName) {
|
|
||||||
// Healthy - Claude is running
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
// Zombie - tmux alive but Claude dead. Kill and recreate.
|
|
||||||
fmt.Printf("%s Detected zombie session (tmux alive, Claude dead). Recreating...\n", style.Dim.Render("⚠"))
|
|
||||||
if err := t.KillSession(sessionName); err != nil {
|
|
||||||
return false, fmt.Errorf("killing zombie session: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
if err := claude.EnsureSettingsForRole(witnessDir, "witness"); err != nil {
|
|
||||||
return false, fmt.Errorf("ensuring Claude 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)
|
|
||||||
|
|
||||||
// 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 {
|
func runWitnessAttach(cmd *cobra.Command, args []string) error {
|
||||||
rigName := ""
|
rigName := ""
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
@@ -390,8 +281,8 @@ func runWitnessAttach(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify rig exists
|
// Verify rig exists and get manager
|
||||||
_, r, err := getWitnessManager(rigName)
|
mgr, _, err := getWitnessManager(rigName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -399,12 +290,9 @@ func runWitnessAttach(cmd *cobra.Command, args []string) error {
|
|||||||
sessionName := witnessSessionName(rigName)
|
sessionName := witnessSessionName(rigName)
|
||||||
|
|
||||||
// Ensure session exists (creates if needed)
|
// Ensure session exists (creates if needed)
|
||||||
created, err := ensureWitnessSession(rigName, r)
|
if err := mgr.Start(false); err != nil && err != witness.ErrAlreadyRunning {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
} else if err == nil {
|
||||||
|
|
||||||
if created {
|
|
||||||
fmt.Printf("Started witness session for %s\n", rigName)
|
fmt.Printf("Started witness session for %s\n", rigName)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,36 +312,21 @@ func runWitnessAttach(cmd *cobra.Command, args []string) error {
|
|||||||
func runWitnessRestart(cmd *cobra.Command, args []string) error {
|
func runWitnessRestart(cmd *cobra.Command, args []string) error {
|
||||||
rigName := args[0]
|
rigName := args[0]
|
||||||
|
|
||||||
mgr, r, err := getWitnessManager(rigName)
|
mgr, _, err := getWitnessManager(rigName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Restarting witness for %s...\n", rigName)
|
fmt.Printf("Restarting witness for %s...\n", rigName)
|
||||||
|
|
||||||
// Kill tmux session if it exists
|
// Stop existing session (non-fatal: may not be running)
|
||||||
t := tmux.NewTmux()
|
|
||||||
sessionName := witnessSessionName(rigName)
|
|
||||||
running, _ := t.HasSession(sessionName)
|
|
||||||
if running {
|
|
||||||
if err := t.KillSession(sessionName); err != nil {
|
|
||||||
style.PrintWarning("failed to kill session: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update state file to stopped (non-fatal: state file update)
|
|
||||||
_ = mgr.Stop()
|
_ = mgr.Stop()
|
||||||
|
|
||||||
// Start fresh
|
// Start fresh
|
||||||
created, err := ensureWitnessSession(rigName, r)
|
if err := mgr.Start(false); err != nil {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("starting witness: %w", err)
|
return fmt.Errorf("starting witness: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if created {
|
|
||||||
_ = mgr.Start() // non-fatal: state file update
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("%s Witness restarted for %s\n", style.Bold.Render("✓"), rigName)
|
fmt.Printf("%s Witness restarted for %s\n", style.Bold.Render("✓"), rigName)
|
||||||
fmt.Printf(" %s\n", style.Dim.Render("Use 'gt witness attach' to connect"))
|
fmt.Printf(" %s\n", style.Dim.Render("Use 'gt witness attach' to connect"))
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -21,8 +21,11 @@ import (
|
|||||||
"github.com/steveyegge/gastown/internal/deacon"
|
"github.com/steveyegge/gastown/internal/deacon"
|
||||||
"github.com/steveyegge/gastown/internal/feed"
|
"github.com/steveyegge/gastown/internal/feed"
|
||||||
"github.com/steveyegge/gastown/internal/polecat"
|
"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/session"
|
||||||
"github.com/steveyegge/gastown/internal/tmux"
|
"github.com/steveyegge/gastown/internal/tmux"
|
||||||
|
"github.com/steveyegge/gastown/internal/witness"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Daemon is the town-level background service.
|
// Daemon is the town-level background service.
|
||||||
@@ -444,53 +447,28 @@ func (d *Daemon) ensureWitnessRunning(rigName string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agent bead check failed or state is not running/working.
|
// Agent not running (or bead not found) - use Manager.Start() for unified startup
|
||||||
// FALLBACK: Check if tmux session is actually healthy before attempting restart.
|
// Manager.Start() handles: zombie detection, session creation, env vars, theming,
|
||||||
// This prevents killing healthy sessions when bead state is stale or unreadable.
|
// WaitForClaudeReady, and crucially - startup/propulsion nudges (GUPP)
|
||||||
// Skip this check if agent was marked dead (we already handled that above).
|
|
||||||
if beadState != "dead" {
|
|
||||||
hasSession, sessionErr := d.tmux.HasSession(sessionName)
|
|
||||||
if sessionErr == nil && hasSession {
|
|
||||||
// Session exists - check if Claude is actually running in it
|
|
||||||
if d.tmux.IsClaudeRunning(sessionName) {
|
|
||||||
// Session is healthy - don't restart it
|
|
||||||
// The bead state may be stale; agent will update it on next activity
|
|
||||||
d.logger.Printf("Witness for %s session healthy (Claude running), skipping restart despite stale bead", rigName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Agent not running (or bead not found) AND session is not healthy - start it
|
|
||||||
d.logger.Printf("Witness for %s not running per agent bead, starting...", rigName)
|
d.logger.Printf("Witness for %s not running per agent bead, starting...", rigName)
|
||||||
|
|
||||||
// Create session in witness directory
|
r := &rig.Rig{
|
||||||
// Use EnsureSessionFresh to handle zombie sessions that exist but have dead Claude
|
Name: rigName,
|
||||||
witnessDir := filepath.Join(d.config.TownRoot, rigName, "witness")
|
Path: filepath.Join(d.config.TownRoot, rigName),
|
||||||
if err := d.tmux.EnsureSessionFresh(sessionName, witnessDir); err != nil {
|
}
|
||||||
d.logger.Printf("Error creating witness session for %s: %v", rigName, err)
|
mgr := witness.NewManager(r)
|
||||||
|
|
||||||
|
if err := mgr.Start(false); err != nil {
|
||||||
|
if err == witness.ErrAlreadyRunning {
|
||||||
|
// Session is healthy (Claude running) - bead state was stale
|
||||||
|
d.logger.Printf("Witness for %s session healthy (Claude running), skipping restart despite stale bead", rigName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.logger.Printf("Error starting witness for %s: %v", rigName, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set environment
|
d.logger.Printf("Witness session for %s started successfully (with nudges)", rigName)
|
||||||
_ = d.tmux.SetEnvironment(sessionName, "GT_ROLE", "witness")
|
|
||||||
_ = d.tmux.SetEnvironment(sessionName, "GT_RIG", rigName)
|
|
||||||
_ = d.tmux.SetEnvironment(sessionName, "BD_ACTOR", rigName+"-witness")
|
|
||||||
|
|
||||||
// Launch Claude
|
|
||||||
bdActor := fmt.Sprintf("%s/witness", rigName)
|
|
||||||
envVars := map[string]string{
|
|
||||||
"GT_ROLE": "witness",
|
|
||||||
"GT_RIG": rigName,
|
|
||||||
"BD_ACTOR": bdActor,
|
|
||||||
"GIT_AUTHOR_NAME": bdActor,
|
|
||||||
}
|
|
||||||
if err := d.tmux.SendKeys(sessionName, config.BuildStartupCommand(envVars, "", "")); err != nil {
|
|
||||||
d.logger.Printf("Error launching Claude in witness session for %s: %v", rigName, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
d.logger.Printf("Witness session for %s started successfully", rigName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensureRefineriesRunning ensures refineries are running for all rigs.
|
// ensureRefineriesRunning ensures refineries are running for all rigs.
|
||||||
@@ -531,76 +509,28 @@ func (d *Daemon) ensureRefineryRunning(rigName string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agent bead check failed or state is not running/working.
|
// Agent not running (or bead not found) - use Manager.Start() for unified startup
|
||||||
// FALLBACK: Check if tmux session is actually healthy before attempting restart.
|
// Manager.Start() handles: zombie detection, session creation, env vars, theming,
|
||||||
// This prevents killing healthy sessions when bead state is stale or unreadable.
|
// WaitForClaudeReady, and crucially - startup/propulsion nudges (GUPP)
|
||||||
// Skip this check if agent was marked dead (we already handled that above).
|
|
||||||
if beadState != "dead" {
|
|
||||||
hasSession, sessionErr := d.tmux.HasSession(sessionName)
|
|
||||||
if sessionErr == nil && hasSession {
|
|
||||||
// Session exists - check if Claude is actually running in it
|
|
||||||
if d.tmux.IsClaudeRunning(sessionName) {
|
|
||||||
// Session is healthy - don't restart it
|
|
||||||
// The bead state may be stale; agent will update it on next activity
|
|
||||||
d.logger.Printf("Refinery for %s session healthy (Claude running), skipping restart despite stale bead", rigName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Agent not running (or bead not found) AND session is not healthy - start it
|
|
||||||
d.logger.Printf("Refinery for %s not running per agent bead, starting...", rigName)
|
d.logger.Printf("Refinery for %s not running per agent bead, starting...", rigName)
|
||||||
|
|
||||||
// Determine working directory
|
r := &rig.Rig{
|
||||||
rigPath := filepath.Join(d.config.TownRoot, rigName)
|
Name: rigName,
|
||||||
refineryDir := filepath.Join(rigPath, "refinery", "rig")
|
Path: filepath.Join(d.config.TownRoot, rigName),
|
||||||
if _, err := os.Stat(refineryDir); os.IsNotExist(err) {
|
|
||||||
// Fall back to rig path if refinery/rig doesn't exist
|
|
||||||
refineryDir = rigPath
|
|
||||||
}
|
}
|
||||||
|
mgr := refinery.NewManager(r)
|
||||||
|
|
||||||
// Create session in refinery directory
|
if err := mgr.Start(false); err != nil {
|
||||||
// Use EnsureSessionFresh to handle zombie sessions that exist but have dead Claude
|
if err == refinery.ErrAlreadyRunning {
|
||||||
if err := d.tmux.EnsureSessionFresh(sessionName, refineryDir); err != nil {
|
// Session is healthy (Claude running) - bead state was stale
|
||||||
d.logger.Printf("Error creating refinery session for %s: %v", rigName, err)
|
d.logger.Printf("Refinery for %s session healthy (Claude running), skipping restart despite stale bead", rigName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.logger.Printf("Error starting refinery for %s: %v", rigName, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set environment
|
d.logger.Printf("Refinery session for %s started successfully (with nudges)", rigName)
|
||||||
bdActor := fmt.Sprintf("%s/refinery", rigName)
|
|
||||||
_ = d.tmux.SetEnvironment(sessionName, "GT_ROLE", "refinery")
|
|
||||||
_ = d.tmux.SetEnvironment(sessionName, "GT_RIG", rigName)
|
|
||||||
_ = d.tmux.SetEnvironment(sessionName, "BD_ACTOR", bdActor)
|
|
||||||
|
|
||||||
// Set beads environment
|
|
||||||
beadsDir := filepath.Join(rigPath, "mayor", "rig", ".beads")
|
|
||||||
_ = d.tmux.SetEnvironment(sessionName, "BEADS_DIR", beadsDir)
|
|
||||||
_ = d.tmux.SetEnvironment(sessionName, "BEADS_NO_DAEMON", "1")
|
|
||||||
_ = d.tmux.SetEnvironment(sessionName, "BEADS_AGENT_NAME", bdActor)
|
|
||||||
|
|
||||||
// Apply theming (non-fatal)
|
|
||||||
theme := tmux.AssignTheme(rigName)
|
|
||||||
_ = d.tmux.ConfigureGasTownSession(sessionName, theme, rigName, "refinery", "refinery")
|
|
||||||
|
|
||||||
// Launch Claude with environment exported inline
|
|
||||||
envVars := map[string]string{
|
|
||||||
"GT_ROLE": "refinery",
|
|
||||||
"GT_RIG": rigName,
|
|
||||||
"BD_ACTOR": bdActor,
|
|
||||||
"GIT_AUTHOR_NAME": bdActor,
|
|
||||||
}
|
|
||||||
if err := d.tmux.SendKeys(sessionName, config.BuildStartupCommand(envVars, "", "")); err != nil {
|
|
||||||
d.logger.Printf("Error launching Claude in refinery session for %s: %v", rigName, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for Claude to start, then accept bypass permissions warning if it appears.
|
|
||||||
if err := d.tmux.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil {
|
|
||||||
// Non-fatal - Claude might still start
|
|
||||||
}
|
|
||||||
_ = d.tmux.AcceptBypassPermissionsWarning(sessionName)
|
|
||||||
|
|
||||||
d.logger.Printf("Refinery session for %s started successfully", rigName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getKnownRigs returns list of registered rig names.
|
// getKnownRigs returns list of registered rig names.
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ import (
|
|||||||
"github.com/steveyegge/gastown/internal/beads"
|
"github.com/steveyegge/gastown/internal/beads"
|
||||||
"github.com/steveyegge/gastown/internal/claude"
|
"github.com/steveyegge/gastown/internal/claude"
|
||||||
"github.com/steveyegge/gastown/internal/config"
|
"github.com/steveyegge/gastown/internal/config"
|
||||||
|
"github.com/steveyegge/gastown/internal/constants"
|
||||||
"github.com/steveyegge/gastown/internal/events"
|
"github.com/steveyegge/gastown/internal/events"
|
||||||
"github.com/steveyegge/gastown/internal/mail"
|
"github.com/steveyegge/gastown/internal/mail"
|
||||||
"github.com/steveyegge/gastown/internal/mrqueue"
|
"github.com/steveyegge/gastown/internal/mrqueue"
|
||||||
"github.com/steveyegge/gastown/internal/rig"
|
"github.com/steveyegge/gastown/internal/rig"
|
||||||
|
"github.com/steveyegge/gastown/internal/session"
|
||||||
"github.com/steveyegge/gastown/internal/tmux"
|
"github.com/steveyegge/gastown/internal/tmux"
|
||||||
"github.com/steveyegge/gastown/internal/util"
|
"github.com/steveyegge/gastown/internal/util"
|
||||||
)
|
)
|
||||||
@@ -209,6 +211,31 @@ func (m *Manager) Start(foreground bool) error {
|
|||||||
return fmt.Errorf("starting Claude agent: %w", err)
|
return fmt.Errorf("starting Claude agent: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for Claude to start and show its prompt (non-fatal)
|
||||||
|
// WaitForClaudeReady waits for "> " prompt, more reliable than just checking node is running
|
||||||
|
if err := t.WaitForClaudeReady(sessionID, 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
|
||||||
|
address := fmt.Sprintf("%s/refinery", m.rig.Name)
|
||||||
|
_ = session.StartupNudge(t, sessionID, 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(sessionID, session.PropulsionNudgeForRole("refinery", refineryRigDir)) // Non-fatal
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,18 @@ package witness
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveyegge/gastown/internal/agent"
|
"github.com/steveyegge/gastown/internal/agent"
|
||||||
|
"github.com/steveyegge/gastown/internal/claude"
|
||||||
|
"github.com/steveyegge/gastown/internal/config"
|
||||||
|
"github.com/steveyegge/gastown/internal/constants"
|
||||||
"github.com/steveyegge/gastown/internal/rig"
|
"github.com/steveyegge/gastown/internal/rig"
|
||||||
|
"github.com/steveyegge/gastown/internal/session"
|
||||||
|
"github.com/steveyegge/gastown/internal/tmux"
|
||||||
"github.com/steveyegge/gastown/internal/util"
|
"github.com/steveyegge/gastown/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -67,25 +74,143 @@ func (m *Manager) Status() (*Witness, error) {
|
|||||||
return w, nil
|
return w, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start starts the witness (marks it as running).
|
// sessionName returns the tmux session name for this witness.
|
||||||
// Patrol logic is now handled by mol-witness-patrol molecule executed by Claude.
|
func (m *Manager) sessionName() string {
|
||||||
func (m *Manager) Start() error {
|
return fmt.Sprintf("gt-%s-witness", m.rig.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// witnessDir returns the working directory for the witness.
|
||||||
|
// Prefers witness/rig/, falls back to witness/, then rig root.
|
||||||
|
func (m *Manager) witnessDir() string {
|
||||||
|
witnessRigDir := filepath.Join(m.rig.Path, "witness", "rig")
|
||||||
|
if _, err := os.Stat(witnessRigDir); err == nil {
|
||||||
|
return witnessRigDir
|
||||||
|
}
|
||||||
|
|
||||||
|
witnessDir := filepath.Join(m.rig.Path, "witness")
|
||||||
|
if _, err := os.Stat(witnessDir); err == nil {
|
||||||
|
return witnessDir
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.rig.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the witness.
|
||||||
|
// If foreground is true, only updates state (no tmux session - deprecated).
|
||||||
|
// Otherwise, spawns a Claude agent in a tmux session.
|
||||||
|
func (m *Manager) Start(foreground bool) error {
|
||||||
w, err := m.loadState()
|
w, err := m.loadState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t := tmux.NewTmux()
|
||||||
|
sessionID := m.sessionName()
|
||||||
|
|
||||||
|
if foreground {
|
||||||
|
// Foreground mode is deprecated - patrol logic moved to mol-witness-patrol
|
||||||
|
if w.State == StateRunning && w.PID > 0 && util.ProcessExists(w.PID) {
|
||||||
|
return ErrAlreadyRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
w.State = StateRunning
|
||||||
|
w.StartedAt = &now
|
||||||
|
w.PID = os.Getpid()
|
||||||
|
w.MonitoredPolecats = m.rig.Polecats
|
||||||
|
|
||||||
|
return m.saveState(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background mode: 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) {
|
||||||
|
// Healthy - Claude is running
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check via PID for backwards compatibility
|
||||||
if w.State == StateRunning && w.PID > 0 && util.ProcessExists(w.PID) {
|
if w.State == StateRunning && w.PID > 0 && util.ProcessExists(w.PID) {
|
||||||
return ErrAlreadyRunning
|
return ErrAlreadyRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Working directory
|
||||||
|
witnessDir := m.witnessDir()
|
||||||
|
|
||||||
|
// Ensure Claude settings exist (autonomous role needs mail in SessionStart)
|
||||||
|
if err := claude.EnsureSettingsForRole(witnessDir, "witness"); err != nil {
|
||||||
|
return fmt.Errorf("ensuring Claude settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new tmux session
|
||||||
|
if err := t.NewSession(sessionID, witnessDir); err != nil {
|
||||||
|
return fmt.Errorf("creating tmux session: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set environment variables (non-fatal: session works without these)
|
||||||
|
bdActor := fmt.Sprintf("%s/witness", m.rig.Name)
|
||||||
|
_ = t.SetEnvironment(sessionID, "GT_ROLE", "witness")
|
||||||
|
_ = t.SetEnvironment(sessionID, "GT_RIG", m.rig.Name)
|
||||||
|
_ = t.SetEnvironment(sessionID, "BD_ACTOR", bdActor)
|
||||||
|
|
||||||
|
// Apply Gas Town theming (non-fatal: theming failure doesn't affect operation)
|
||||||
|
theme := tmux.AssignTheme(m.rig.Name)
|
||||||
|
_ = t.ConfigureGasTownSession(sessionID, theme, m.rig.Name, "witness", "witness")
|
||||||
|
|
||||||
|
// Update state to running
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
w.State = StateRunning
|
w.State = StateRunning
|
||||||
w.StartedAt = &now
|
w.StartedAt = &now
|
||||||
w.PID = os.Getpid()
|
w.PID = 0 // Claude agent doesn't have a PID we track
|
||||||
w.MonitoredPolecats = m.rig.Polecats
|
w.MonitoredPolecats = m.rig.Polecats
|
||||||
|
if err := m.saveState(w); err != nil {
|
||||||
|
_ = t.KillSession(sessionID) // best-effort cleanup on state save failure
|
||||||
|
return fmt.Errorf("saving state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return m.saveState(w)
|
// 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
|
||||||
|
command := config.BuildAgentStartupCommand("witness", bdActor, "", "")
|
||||||
|
if err := t.SendKeys(sessionID, command); err != nil {
|
||||||
|
_ = t.KillSession(sessionID) // best-effort cleanup
|
||||||
|
return fmt.Errorf("starting Claude agent: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for Claude to start and show its prompt (non-fatal)
|
||||||
|
// WaitForClaudeReady waits for "> " prompt, more reliable than just checking node is running
|
||||||
|
if err := t.WaitForClaudeReady(sessionID, 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
|
||||||
|
address := fmt.Sprintf("%s/witness", m.rig.Name)
|
||||||
|
_ = session.StartupNudge(t, sessionID, 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(sessionID, session.PropulsionNudgeForRole("witness", witnessDir)) // Non-fatal
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop stops the witness.
|
// Stop stops the witness.
|
||||||
@@ -95,12 +220,23 @@ func (m *Manager) Stop() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if w.State != StateRunning {
|
// Check if tmux session exists
|
||||||
|
t := tmux.NewTmux()
|
||||||
|
sessionID := m.sessionName()
|
||||||
|
sessionRunning, _ := t.HasSession(sessionID)
|
||||||
|
|
||||||
|
// If neither state nor session indicates running, it's not running
|
||||||
|
if w.State != StateRunning && !sessionRunning {
|
||||||
return ErrNotRunning
|
return ErrNotRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have a PID, try to stop it gracefully
|
// Kill tmux session if it exists (best-effort: may already be dead)
|
||||||
if w.PID > 0 && w.PID != os.Getpid() {
|
if sessionRunning {
|
||||||
|
_ = t.KillSession(sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a PID and it's a different process, try to stop it gracefully
|
||||||
|
if w.PID > 0 && w.PID != os.Getpid() && util.ProcessExists(w.PID) {
|
||||||
// Send SIGTERM (best-effort graceful stop)
|
// Send SIGTERM (best-effort graceful stop)
|
||||||
if proc, err := os.FindProcess(w.PID); err == nil {
|
if proc, err := os.FindProcess(w.PID); err == nil {
|
||||||
_ = proc.Signal(os.Interrupt)
|
_ = proc.Signal(os.Interrupt)
|
||||||
@@ -112,4 +248,3 @@ func (m *Manager) Stop() error {
|
|||||||
|
|
||||||
return m.saveState(w)
|
return m.saveState(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user