feat: allow witness restart agent override
This commit is contained in:
+2819
-2581
File diff suppressed because one or more lines are too long
+3
-3
@@ -759,7 +759,7 @@ func runRigBoot(cmd *cobra.Command, args []string) error {
|
|||||||
} else {
|
} else {
|
||||||
fmt.Printf(" Starting witness...\n")
|
fmt.Printf(" Starting witness...\n")
|
||||||
witMgr := witness.NewManager(r)
|
witMgr := witness.NewManager(r)
|
||||||
if err := witMgr.Start(false); err != nil {
|
if err := witMgr.Start(false, ""); err != nil {
|
||||||
if err == witness.ErrAlreadyRunning {
|
if err == witness.ErrAlreadyRunning {
|
||||||
skipped = append(skipped, "witness (already running)")
|
skipped = append(skipped, "witness (already running)")
|
||||||
} else {
|
} else {
|
||||||
@@ -839,7 +839,7 @@ func runRigStart(cmd *cobra.Command, args []string) error {
|
|||||||
} else {
|
} else {
|
||||||
fmt.Printf(" Starting witness...\n")
|
fmt.Printf(" Starting witness...\n")
|
||||||
witMgr := witness.NewManager(r)
|
witMgr := witness.NewManager(r)
|
||||||
if err := witMgr.Start(false); err != nil {
|
if err := witMgr.Start(false, ""); err != nil {
|
||||||
if err == witness.ErrAlreadyRunning {
|
if err == witness.ErrAlreadyRunning {
|
||||||
skipped = append(skipped, "witness")
|
skipped = append(skipped, "witness")
|
||||||
} else {
|
} else {
|
||||||
@@ -1418,7 +1418,7 @@ 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")
|
||||||
if err := witMgr.Start(false); err != nil {
|
if err := witMgr.Start(false, ""); err != nil {
|
||||||
if err == witness.ErrAlreadyRunning {
|
if err == witness.ErrAlreadyRunning {
|
||||||
skipped = append(skipped, "witness")
|
skipped = append(skipped, "witness")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ func startRigAgents(t *tmux.Tmux, townRoot string) {
|
|||||||
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 {
|
||||||
witMgr := witness.NewManager(r)
|
witMgr := witness.NewManager(r)
|
||||||
if err := witMgr.Start(false); err != nil {
|
if err := witMgr.Start(false, ""); err != nil {
|
||||||
if err == witness.ErrAlreadyRunning {
|
if err == witness.ErrAlreadyRunning {
|
||||||
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 {
|
||||||
|
|||||||
+1
-1
@@ -118,7 +118,7 @@ func runUp(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mgr := witness.NewManager(r)
|
mgr := witness.NewManager(r)
|
||||||
if err := mgr.Start(false); err != nil {
|
if err := mgr.Start(false, ""); err != nil {
|
||||||
if err == witness.ErrAlreadyRunning {
|
if err == witness.ErrAlreadyRunning {
|
||||||
printStatus(fmt.Sprintf("Witness (%s)", rigName), true, mgr.SessionName())
|
printStatus(fmt.Sprintf("Witness (%s)", rigName), true, mgr.SessionName())
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
witnessForeground bool
|
witnessForeground bool
|
||||||
witnessStatusJSON bool
|
witnessStatusJSON bool
|
||||||
|
witnessAgentOverride string
|
||||||
)
|
)
|
||||||
|
|
||||||
var witnessCmd = &cobra.Command{
|
var witnessCmd = &cobra.Command{
|
||||||
@@ -93,7 +94,8 @@ var witnessRestartCmd = &cobra.Command{
|
|||||||
Stops the current session (if running) and starts a fresh one.
|
Stops the current session (if running) and starts a fresh one.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
gt witness restart greenplace`,
|
gt witness restart greenplace
|
||||||
|
gt witness restart greenplace --agent codex`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: runWitnessRestart,
|
RunE: runWitnessRestart,
|
||||||
}
|
}
|
||||||
@@ -105,6 +107,9 @@ func init() {
|
|||||||
// Status flags
|
// Status flags
|
||||||
witnessStatusCmd.Flags().BoolVar(&witnessStatusJSON, "json", false, "Output as JSON")
|
witnessStatusCmd.Flags().BoolVar(&witnessStatusJSON, "json", false, "Output as JSON")
|
||||||
|
|
||||||
|
// Restart flags
|
||||||
|
witnessRestartCmd.Flags().StringVar(&witnessAgentOverride, "agent", "", "Agent alias to run the Witness with (overrides town default)")
|
||||||
|
|
||||||
// Add subcommands
|
// Add subcommands
|
||||||
witnessCmd.AddCommand(witnessStartCmd)
|
witnessCmd.AddCommand(witnessStartCmd)
|
||||||
witnessCmd.AddCommand(witnessStopCmd)
|
witnessCmd.AddCommand(witnessStopCmd)
|
||||||
@@ -136,7 +141,7 @@ func runWitnessStart(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
fmt.Printf("Starting witness for %s...\n", rigName)
|
fmt.Printf("Starting witness for %s...\n", rigName)
|
||||||
|
|
||||||
if err := mgr.Start(witnessForeground); err != nil {
|
if err := mgr.Start(witnessForeground, ""); err != nil {
|
||||||
if err == witness.ErrAlreadyRunning {
|
if err == witness.ErrAlreadyRunning {
|
||||||
fmt.Printf("%s Witness is already running\n", style.Dim.Render("⚠"))
|
fmt.Printf("%s Witness is already running\n", style.Dim.Render("⚠"))
|
||||||
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"))
|
||||||
@@ -289,7 +294,7 @@ 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)
|
||||||
if err := mgr.Start(false); err != nil && err != witness.ErrAlreadyRunning {
|
if err := mgr.Start(false, ""); err != nil && err != witness.ErrAlreadyRunning {
|
||||||
return err
|
return err
|
||||||
} else if err == nil {
|
} else if err == nil {
|
||||||
fmt.Printf("Started witness session for %s\n", rigName)
|
fmt.Printf("Started witness session for %s\n", rigName)
|
||||||
@@ -322,7 +327,7 @@ func runWitnessRestart(cmd *cobra.Command, args []string) error {
|
|||||||
_ = mgr.Stop()
|
_ = mgr.Stop()
|
||||||
|
|
||||||
// Start fresh
|
// Start fresh
|
||||||
if err := mgr.Start(false); err != nil {
|
if err := mgr.Start(false, witnessAgentOverride); err != nil {
|
||||||
return fmt.Errorf("starting witness: %w", err)
|
return fmt.Errorf("starting witness: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWitnessRestartAgentFlag(t *testing.T) {
|
||||||
|
flag := witnessRestartCmd.Flags().Lookup("agent")
|
||||||
|
if flag == nil {
|
||||||
|
t.Fatal("expected witness restart to define --agent flag")
|
||||||
|
}
|
||||||
|
if flag.DefValue != "" {
|
||||||
|
t.Errorf("expected default agent override to be empty, got %q", flag.DefValue)
|
||||||
|
}
|
||||||
|
if !strings.Contains(flag.Usage, "overrides town default") {
|
||||||
|
t.Errorf("expected --agent usage to mention overrides town default, got %q", flag.Usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -396,7 +396,7 @@ func (d *Daemon) ensureWitnessRunning(rigName string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Manager.Start() handles: zombie detection, session creation, env vars, theming,
|
// Manager.Start() handles: zombie detection, session creation, env vars, theming,
|
||||||
// WaitForClaudeReady, and crucially - startup/propulsion nudges (GUPP).
|
// startup readiness waits, and crucially - startup/propulsion nudges (GUPP).
|
||||||
// It returns ErrAlreadyRunning if Claude is already running in tmux.
|
// It returns ErrAlreadyRunning if Claude is already running in tmux.
|
||||||
r := &rig.Rig{
|
r := &rig.Rig{
|
||||||
Name: rigName,
|
Name: rigName,
|
||||||
@@ -404,7 +404,7 @@ func (d *Daemon) ensureWitnessRunning(rigName string) {
|
|||||||
}
|
}
|
||||||
mgr := witness.NewManager(r)
|
mgr := witness.NewManager(r)
|
||||||
|
|
||||||
if err := mgr.Start(false); err != nil {
|
if err := mgr.Start(false, ""); err != nil {
|
||||||
if err == witness.ErrAlreadyRunning {
|
if err == witness.ErrAlreadyRunning {
|
||||||
// Already running - nothing to do
|
// Already running - nothing to do
|
||||||
return
|
return
|
||||||
|
|||||||
+26
-12
@@ -99,7 +99,8 @@ func (m *Manager) witnessDir() string {
|
|||||||
// Start starts the witness.
|
// Start starts the witness.
|
||||||
// If foreground is true, only updates state (no tmux session - deprecated).
|
// If foreground is true, only updates state (no tmux session - deprecated).
|
||||||
// Otherwise, spawns a Claude agent in a tmux session.
|
// Otherwise, spawns a Claude agent in a tmux session.
|
||||||
func (m *Manager) Start(foreground bool) error {
|
// agentOverride optionally specifies a different agent alias to use.
|
||||||
|
func (m *Manager) Start(foreground bool, agentOverride string) error {
|
||||||
w, err := m.loadState()
|
w, err := m.loadState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -152,15 +153,8 @@ func (m *Manager) Start(foreground bool) error {
|
|||||||
return fmt.Errorf("ensuring Claude settings: %w", err)
|
return fmt.Errorf("ensuring Claude settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build startup command first
|
// Create new tmux session
|
||||||
// Pass m.rig.Path so rig agent settings are honored (not town-level defaults)
|
if err := t.NewSession(sessionID, witnessDir); err != nil {
|
||||||
bdActor := fmt.Sprintf("%s/witness", m.rig.Name)
|
|
||||||
command := config.BuildAgentStartupCommand("witness", bdActor, m.rig.Path, "")
|
|
||||||
runtimeConfig := config.LoadRuntimeConfig(m.rig.Path)
|
|
||||||
|
|
||||||
// Create session with command directly to avoid send-keys race condition.
|
|
||||||
// See: https://github.com/anthropics/gastown/issues/280
|
|
||||||
if err := t.NewSessionWithCommand(sessionID, witnessDir, command); err != nil {
|
|
||||||
return fmt.Errorf("creating tmux session: %w", err)
|
return fmt.Errorf("creating tmux session: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,8 +186,28 @@ func (m *Manager) Start(foreground bool) error {
|
|||||||
return fmt.Errorf("saving state: %w", err)
|
return fmt.Errorf("saving state: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for runtime to start and show its prompt (non-fatal)
|
// Launch Claude directly (no shell respawn loop)
|
||||||
if err := t.WaitForRuntimeReady(sessionID, runtimeConfig, constants.ClaudeStartTimeout); err != nil {
|
// 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
|
||||||
|
// Pass m.rig.Path so rig agent settings are honored (not town-level defaults)
|
||||||
|
command, err := config.BuildAgentStartupCommandWithAgentOverride("witness", bdActor, m.rig.Path, "", agentOverride)
|
||||||
|
if err != nil {
|
||||||
|
_ = t.KillSession(sessionID)
|
||||||
|
return fmt.Errorf("building startup command: %w", err)
|
||||||
|
}
|
||||||
|
// Wait for shell to be ready before sending keys (prevents "can't find pane" under load)
|
||||||
|
if err := t.WaitForShellReady(sessionID, 5*time.Second); err != nil {
|
||||||
|
_ = t.KillSession(sessionID)
|
||||||
|
return fmt.Errorf("waiting for shell: %w", err)
|
||||||
|
}
|
||||||
|
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 (non-fatal).
|
||||||
|
if err := t.WaitForCommand(sessionID, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil {
|
||||||
// Non-fatal - try to continue anyway
|
// Non-fatal - try to continue anyway
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user