bd sync: merge queue swarm launched, molecules complete
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
@@ -504,19 +505,24 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
|
||||
_ = t.SetEnvironment(sessionID, "GT_RIG", r.Name)
|
||||
_ = t.SetEnvironment(sessionID, "GT_CREW", name)
|
||||
|
||||
// Apply theme
|
||||
theme := tmux.AssignTheme(r.Name)
|
||||
_ = t.ConfigureGasTownSession(sessionID, theme, r.Name, name, "crew")
|
||||
// Wait for shell to be ready after session creation
|
||||
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 like Mayor)
|
||||
// Use SendKeysDelayed to allow shell initialization after NewSession
|
||||
if err := t.SendKeysDelayed(sessionID, "claude --dangerously-skip-permissions", 200); err != nil {
|
||||
if err := t.SendKeys(sessionID, "claude --dangerously-skip-permissions"); err != nil {
|
||||
return fmt.Errorf("starting claude: %w", err)
|
||||
}
|
||||
|
||||
// Wait a moment for Claude to initialize, then prime it
|
||||
// We send gt prime after a short delay to ensure Claude is ready
|
||||
if err := t.SendKeysDelayed(sessionID, "gt prime", 2000); err != nil {
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -525,15 +531,20 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
|
||||
style.Bold.Render("✓"), r.Name, name)
|
||||
} else {
|
||||
// Session exists - check if Claude is still running
|
||||
paneCmd, err := t.GetPaneCommand(sessionID)
|
||||
if err == nil && isShellCommand(paneCmd) {
|
||||
// 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
|
||||
fmt.Printf("Claude exited, restarting...\n")
|
||||
if err := t.SendKeys(sessionID, "claude --dangerously-skip-permissions"); err != nil {
|
||||
return fmt.Errorf("restarting claude: %w", err)
|
||||
}
|
||||
// Prime after restart
|
||||
if err := t.SendKeysDelayed(sessionID, "gt prime", 2000); err != nil {
|
||||
// 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)
|
||||
}
|
||||
if err := t.SendKeys(sessionID, "gt prime"); err != nil {
|
||||
fmt.Printf("Warning: Could not send prime command: %v\n", err)
|
||||
}
|
||||
}
|
||||
@@ -730,13 +741,13 @@ func runCrewRefresh(cmd *cobra.Command, args []string) error {
|
||||
_ = t.SetEnvironment(sessionID, "GT_RIG", r.Name)
|
||||
_ = t.SetEnvironment(sessionID, "GT_CREW", name)
|
||||
|
||||
// Apply theme
|
||||
theme := tmux.AssignTheme(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
|
||||
// Use SendKeysDelayed to allow shell initialization after NewSession
|
||||
if err := t.SendKeysDelayed(sessionID, "claude", 200); err != nil {
|
||||
// Start claude (refresh uses regular permissions, reads handoff mail)
|
||||
if err := t.SendKeys(sessionID, "claude"); err != nil {
|
||||
return fmt.Errorf("starting claude: %w", err)
|
||||
}
|
||||
|
||||
@@ -784,18 +795,22 @@ func runCrewRestart(cmd *cobra.Command, args []string) error {
|
||||
t.SetEnvironment(sessionID, "GT_RIG", r.Name)
|
||||
t.SetEnvironment(sessionID, "GT_CREW", name)
|
||||
|
||||
// Apply theme
|
||||
theme := tmux.AssignTheme(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)
|
||||
// Use SendKeysDelayed to allow shell initialization after NewSession
|
||||
if err := t.SendKeysDelayed(sessionID, "claude --dangerously-skip-permissions", 200); err != nil {
|
||||
if err := t.SendKeys(sessionID, "claude --dangerously-skip-permissions"); err != nil {
|
||||
return fmt.Errorf("starting claude: %w", err)
|
||||
}
|
||||
|
||||
// Wait for Claude to initialize, then prime it
|
||||
if err := t.SendKeysDelayed(sessionID, "gt prime", 2000); err != nil {
|
||||
// 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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -243,6 +243,89 @@ Run: bd mail inbox
|
||||
return t.SendKeys(session, banner)
|
||||
}
|
||||
|
||||
// IsClaudeRunning checks if Claude appears to be running in the session.
|
||||
// It checks both the pane command (node) and pane content for Claude UI markers.
|
||||
func (t *Tmux) IsClaudeRunning(session string) bool {
|
||||
// First check: pane command should be node (Claude is a node app)
|
||||
cmd, err := t.GetPaneCommand(session)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if cmd == "node" {
|
||||
return true
|
||||
}
|
||||
|
||||
// If we see a shell, check pane content for Claude UI markers
|
||||
// This helps detect if user is in a subshell spawned FROM Claude
|
||||
content, err := t.CapturePane(session, 30)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Look for Claude's distinctive UI markers
|
||||
claudeMarkers := []string{
|
||||
"⏺", // Claude's bullet point
|
||||
"⎿", // Claude's tree continuation
|
||||
"─", // Claude's box drawing
|
||||
"╭", // Claude's rounded corners
|
||||
}
|
||||
for _, marker := range claudeMarkers {
|
||||
if strings.Contains(content, marker) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// WaitForCommand polls until the pane is NOT running one of the excluded commands.
|
||||
// Useful for waiting until a shell has started a new process (e.g., claude).
|
||||
// Returns nil when a non-excluded command is detected, or error on timeout.
|
||||
func (t *Tmux) WaitForCommand(session string, excludeCommands []string, timeout time.Duration) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
cmd, err := t.GetPaneCommand(session)
|
||||
if err != nil {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
// Check if current command is NOT in the exclude list
|
||||
excluded := false
|
||||
for _, exc := range excludeCommands {
|
||||
if cmd == exc {
|
||||
excluded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !excluded {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
return fmt.Errorf("timeout waiting for command (still running excluded command)")
|
||||
}
|
||||
|
||||
// WaitForShellReady polls until the pane is running a shell command.
|
||||
// Useful for waiting until a process has exited and returned to shell.
|
||||
func (t *Tmux) WaitForShellReady(session string, timeout time.Duration) error {
|
||||
shells := []string{"bash", "zsh", "sh", "fish", "tcsh", "ksh"}
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
cmd, err := t.GetPaneCommand(session)
|
||||
if err != nil {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
for _, shell := range shells {
|
||||
if cmd == shell {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
return fmt.Errorf("timeout waiting for shell")
|
||||
}
|
||||
|
||||
// GetSessionInfo returns detailed information about a session.
|
||||
func (t *Tmux) GetSessionInfo(name string) (*SessionInfo, error) {
|
||||
format := "#{session_name}|#{session_windows}|#{session_created_string}|#{session_attached}"
|
||||
|
||||
Reference in New Issue
Block a user