bd sync: merge queue swarm launched, molecules complete

This commit is contained in:
Steve Yegge
2025-12-19 14:47:08 -08:00
parent f37cae33df
commit 8b853ac4d2
3 changed files with 415 additions and 317 deletions

View File

@@ -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)
}

View File

@@ -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}"