diff --git a/internal/cmd/crew_at.go b/internal/cmd/crew_at.go index ab12ffe2..ae5e99a5 100644 --- a/internal/cmd/crew_at.go +++ b/internal/cmd/crew_at.go @@ -111,26 +111,20 @@ func runCrewAt(cmd *cobra.Command, args []string) error { return fmt.Errorf("waiting for shell: %w", err) } - // Start claude with skip permissions (crew workers are trusted like Mayor) - if err := t.SendKeys(sessionID, "claude --dangerously-skip-permissions"); err != nil { + // Get pane ID for respawn + paneID, err := t.GetPaneID(sessionID) + if err != nil { + 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 + claudeCmd := `claude --dangerously-skip-permissions "gt prime"` + if err := t.RespawnPane(paneID, claudeCmd); err != nil { return fmt.Errorf("starting claude: %w", err) } - // Wait for Claude to start (pane command changes from shell to node) - 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) - - // Send gt prime to initialize context - 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) - } - fmt.Printf("%s Created session for %s/%s\n", style.Bold.Render("✓"), r.Name, name) } else { @@ -138,28 +132,21 @@ func runCrewAt(cmd *cobra.Command, args []string) error { // Uses both pane command check and UI marker detection to avoid // restarting when user is in a subshell spawned from Claude if !t.IsClaudeRunning(sessionID) { - // Claude has exited, restart it + // Claude has exited, restart it using respawn-pane fmt.Printf("Claude exited, restarting...\n") - if err := t.SendKeys(sessionID, "claude --dangerously-skip-permissions"); err != nil { + + // Get pane ID for respawn + paneID, err := t.GetPaneID(sessionID) + if err != nil { + 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 + claudeCmd := `claude --dangerously-skip-permissions "gt prime"` + if err := t.RespawnPane(paneID, claudeCmd); err != nil { return fmt.Errorf("restarting claude: %w", err) } - // Wait for Claude to start, then prime - 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 { - 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 - time.Sleep(5 * time.Second) - crewPrompt := "Run gt prime. Check your mail and in-progress issues. 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) - } } } diff --git a/internal/tmux/tmux.go b/internal/tmux/tmux.go index 7c7e8628..d48f2527 100644 --- a/internal/tmux/tmux.go +++ b/internal/tmux/tmux.go @@ -236,6 +236,20 @@ func (t *Tmux) GetPaneCommand(session string) (string, error) { return strings.TrimSpace(out), nil } +// GetPaneID returns the pane identifier for a session's first pane. +// Returns a pane ID like "%0" that can be used with RespawnPane. +func (t *Tmux) GetPaneID(session string) (string, error) { + out, err := t.run("list-panes", "-t", session, "-F", "#{pane_id}") + if err != nil { + return "", err + } + lines := strings.Split(out, "\n") + if len(lines) == 0 || lines[0] == "" { + return "", fmt.Errorf("no panes found in session %s", session) + } + return lines[0], nil +} + // GetPaneWorkDir returns the current working directory of a pane. func (t *Tmux) GetPaneWorkDir(session string) (string, error) { out, err := t.run("list-panes", "-t", session, "-F", "#{pane_current_path}")