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:
mayor
2026-01-06 01:28:16 -08:00
committed by julianknutsen
parent f6f6acdb2d
commit 31a32c084b
7 changed files with 277 additions and 339 deletions

View File

@@ -21,8 +21,11 @@ import (
"github.com/steveyegge/gastown/internal/deacon"
"github.com/steveyegge/gastown/internal/feed"
"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/tmux"
"github.com/steveyegge/gastown/internal/witness"
)
// 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.
// FALLBACK: Check if tmux session is actually healthy before attempting restart.
// This prevents killing healthy sessions when bead state is stale or unreadable.
// 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
// Agent not running (or bead not found) - use Manager.Start() for unified startup
// Manager.Start() handles: zombie detection, session creation, env vars, theming,
// WaitForClaudeReady, and crucially - startup/propulsion nudges (GUPP)
d.logger.Printf("Witness for %s not running per agent bead, starting...", rigName)
// Create session in witness directory
// Use EnsureSessionFresh to handle zombie sessions that exist but have dead Claude
witnessDir := filepath.Join(d.config.TownRoot, rigName, "witness")
if err := d.tmux.EnsureSessionFresh(sessionName, witnessDir); err != nil {
d.logger.Printf("Error creating witness session for %s: %v", rigName, err)
r := &rig.Rig{
Name: rigName,
Path: filepath.Join(d.config.TownRoot, rigName),
}
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
}
// Set environment
_ = 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)
d.logger.Printf("Witness session for %s started successfully (with nudges)", rigName)
}
// 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.
// FALLBACK: Check if tmux session is actually healthy before attempting restart.
// This prevents killing healthy sessions when bead state is stale or unreadable.
// 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
// Agent not running (or bead not found) - use Manager.Start() for unified startup
// Manager.Start() handles: zombie detection, session creation, env vars, theming,
// WaitForClaudeReady, and crucially - startup/propulsion nudges (GUPP)
d.logger.Printf("Refinery for %s not running per agent bead, starting...", rigName)
// Determine working directory
rigPath := filepath.Join(d.config.TownRoot, rigName)
refineryDir := filepath.Join(rigPath, "refinery", "rig")
if _, err := os.Stat(refineryDir); os.IsNotExist(err) {
// Fall back to rig path if refinery/rig doesn't exist
refineryDir = rigPath
r := &rig.Rig{
Name: rigName,
Path: filepath.Join(d.config.TownRoot, rigName),
}
mgr := refinery.NewManager(r)
// Create session in refinery directory
// Use EnsureSessionFresh to handle zombie sessions that exist but have dead Claude
if err := d.tmux.EnsureSessionFresh(sessionName, refineryDir); err != nil {
d.logger.Printf("Error creating refinery session for %s: %v", rigName, err)
if err := mgr.Start(false); err != nil {
if err == refinery.ErrAlreadyRunning {
// Session is healthy (Claude running) - bead state was stale
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
}
// Set environment
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)
d.logger.Printf("Refinery session for %s started successfully (with nudges)", rigName)
}
// getKnownRigs returns list of registered rig names.