Files
gastown/internal/cmd/crew_lifecycle.go
Steve Yegge 5560b64083 crew restart/at: use NudgeSession with longer delay for resume prompt
The resume prompt wasn't getting its Enter key reliably. Changed from
SendKeysDelayedDebounced (3s delay, 300ms debounce) to NudgeSession
(5s delay, 500ms debounce) which is the battle-tested method for
messaging Claude sessions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 16:08:05 -08:00

222 lines
6.4 KiB
Go

package cmd
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/crew"
"github.com/steveyegge/gastown/internal/mail"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/tmux"
)
func runCrewRemove(cmd *cobra.Command, args []string) error {
name := args[0]
crewMgr, r, err := getCrewManager(crewRig)
if err != nil {
return err
}
// Check for running session (unless forced)
if !crewForce {
t := tmux.NewTmux()
sessionID := crewSessionName(r.Name, name)
hasSession, _ := t.HasSession(sessionID)
if hasSession {
return fmt.Errorf("session '%s' is running (use --force to kill and remove)", sessionID)
}
}
// Kill session if it exists
t := tmux.NewTmux()
sessionID := crewSessionName(r.Name, name)
if hasSession, _ := t.HasSession(sessionID); hasSession {
if err := t.KillSession(sessionID); err != nil {
return fmt.Errorf("killing session: %w", err)
}
fmt.Printf("Killed session %s\n", sessionID)
}
// Remove the crew workspace
if err := crewMgr.Remove(name, crewForce); err != nil {
if err == crew.ErrCrewNotFound {
return fmt.Errorf("crew workspace '%s' not found", name)
}
if err == crew.ErrHasChanges {
return fmt.Errorf("crew workspace has uncommitted changes (use --force to remove anyway)")
}
return fmt.Errorf("removing crew workspace: %w", err)
}
fmt.Printf("%s Removed crew workspace: %s/%s\n",
style.Bold.Render("✓"), r.Name, name)
return nil
}
func runCrewRefresh(cmd *cobra.Command, args []string) error {
name := args[0]
crewMgr, r, err := getCrewManager(crewRig)
if err != nil {
return err
}
// Get the crew worker
worker, err := crewMgr.Get(name)
if err != nil {
if err == crew.ErrCrewNotFound {
return fmt.Errorf("crew workspace '%s' not found", name)
}
return fmt.Errorf("getting crew worker: %w", err)
}
t := tmux.NewTmux()
sessionID := crewSessionName(r.Name, name)
// Check if session exists
hasSession, _ := t.HasSession(sessionID)
// Create handoff message
handoffMsg := crewMessage
if handoffMsg == "" {
handoffMsg = fmt.Sprintf("Context refresh for %s. Check mail and beads for current work state.", name)
}
// Send handoff mail to self
mailDir := filepath.Join(worker.ClonePath, "mail")
if _, err := os.Stat(mailDir); os.IsNotExist(err) {
if err := os.MkdirAll(mailDir, 0755); err != nil {
return fmt.Errorf("creating mail dir: %w", err)
}
}
// Create and send mail
mailbox := mail.NewMailbox(mailDir)
msg := &mail.Message{
From: fmt.Sprintf("%s/%s", r.Name, name),
To: fmt.Sprintf("%s/%s", r.Name, name),
Subject: "🤝 HANDOFF: Context Refresh",
Body: handoffMsg,
}
if err := mailbox.Append(msg); err != nil {
return fmt.Errorf("sending handoff mail: %w", err)
}
fmt.Printf("Sent handoff mail to %s/%s\n", r.Name, name)
// Kill existing session if running
if hasSession {
if err := t.KillSession(sessionID); err != nil {
return fmt.Errorf("killing old session: %w", err)
}
fmt.Printf("Killed old session %s\n", sessionID)
}
// Start new session
if err := t.NewSession(sessionID, worker.ClonePath); err != nil {
return fmt.Errorf("creating session: %w", err)
}
// Set environment
_ = t.SetEnvironment(sessionID, "GT_RIG", r.Name)
_ = t.SetEnvironment(sessionID, "GT_CREW", name)
// Wait for shell to be ready
if err := t.WaitForShellReady(sessionID, 5*time.Second); err != nil {
return fmt.Errorf("waiting for shell: %w", err)
}
// Start claude (refresh uses regular permissions, reads handoff mail)
if err := t.SendKeys(sessionID, "claude"); err != nil {
return fmt.Errorf("starting claude: %w", err)
}
fmt.Printf("%s Refreshed crew workspace: %s/%s\n",
style.Bold.Render("✓"), r.Name, name)
fmt.Printf("Attach with: %s\n", style.Dim.Render(fmt.Sprintf("gt crew at %s", name)))
return nil
}
func runCrewRestart(cmd *cobra.Command, args []string) error {
name := args[0]
crewMgr, r, err := getCrewManager(crewRig)
if err != nil {
return err
}
// Get the crew worker
worker, err := crewMgr.Get(name)
if err != nil {
if err == crew.ErrCrewNotFound {
return fmt.Errorf("crew workspace '%s' not found", name)
}
return fmt.Errorf("getting crew worker: %w", err)
}
t := tmux.NewTmux()
sessionID := crewSessionName(r.Name, name)
// Kill existing session if running
if hasSession, _ := t.HasSession(sessionID); hasSession {
if err := t.KillSession(sessionID); err != nil {
return fmt.Errorf("killing old session: %w", err)
}
fmt.Printf("Killed session %s\n", sessionID)
}
// Start new session
if err := t.NewSession(sessionID, worker.ClonePath); err != nil {
return fmt.Errorf("creating session: %w", err)
}
// Set environment
t.SetEnvironment(sessionID, "GT_RIG", r.Name)
t.SetEnvironment(sessionID, "GT_CREW", name)
// Apply rig-based theming (uses config if set, falls back to hash)
theme := getThemeForRig(r.Name)
_ = t.ConfigureGasTownSession(sessionID, theme, r.Name, name, "crew")
// Wait for shell to be ready
if err := t.WaitForShellReady(sessionID, 5*time.Second); err != nil {
return fmt.Errorf("waiting for shell: %w", err)
}
// Start claude with skip permissions (crew workers are trusted)
if err := t.SendKeys(sessionID, "claude --dangerously-skip-permissions"); err != nil {
return fmt.Errorf("starting claude: %w", err)
}
// Wait for Claude to start, then prime it
shells := []string{"bash", "zsh", "sh", "fish", "tcsh", "ksh"}
if err := t.WaitForCommand(sessionID, shells, 15*time.Second); err != nil {
fmt.Printf("Warning: Timeout waiting for Claude to start: %v\n", err)
}
// Give Claude time to initialize after process starts
time.Sleep(500 * time.Millisecond)
if err := t.SendKeys(sessionID, "gt prime"); err != nil {
// Non-fatal: Claude started but priming failed
fmt.Printf("Warning: Could not send prime command: %v\n", err)
}
// Send crew resume prompt after prime completes
// Use NudgeSession (the canonical way to message Claude) with longer pre-delay
// to ensure gt prime has finished processing
time.Sleep(5 * time.Second)
crewPrompt := "Read your mail, act on anything urgent, else await instructions."
if err := t.NudgeSession(sessionID, crewPrompt); err != nil {
fmt.Printf("Warning: Could not send resume prompt: %v\n", err)
}
fmt.Printf("%s Restarted crew workspace: %s/%s\n",
style.Bold.Render("✓"), r.Name, name)
fmt.Printf("Attach with: %s\n", style.Dim.Render(fmt.Sprintf("gt crew at %s", name)))
return nil
}