From ae90b08f467b7accdc5be8e8d8604ae1f5fcf1f3 Mon Sep 17 00:00:00 2001 From: kustrun Date: Fri, 2 Jan 2026 20:12:57 +0100 Subject: [PATCH 1/8] fix(beads): Detect and use existing prefix from source repo When adding a rig from a source repo that has .beads/ tracked in git, detect and use the project's existing prefix instead of generating a new one. This prevents prefix mismatch errors when accessing existing issues via bd commands. Adds detectBeadsPrefixFromConfig() which reads the prefix from either config.yaml or by parsing the first issue ID from issues.jsonl. --- internal/rig/manager.go | 85 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/internal/rig/manager.go b/internal/rig/manager.go index 0d9076d4..229f4dcb 100644 --- a/internal/rig/manager.go +++ b/internal/rig/manager.go @@ -249,6 +249,24 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) { if err := m.git.Clone(opts.GitURL, mayorRigPath); err != nil { return nil, fmt.Errorf("cloning for mayor: %w", err) } + + // Check if source repo has .beads/ with its own prefix - if so, use that prefix. + // This ensures we use the project's existing beads database instead of creating a new one. + // Without this, routing would fail when trying to access existing issues because the + // rig config would have a different prefix than what the issues actually use. + sourceBeadsConfig := filepath.Join(mayorRigPath, ".beads", "config.yaml") + if _, err := os.Stat(sourceBeadsConfig); err == nil { + if sourcePrefix := detectBeadsPrefixFromConfig(sourceBeadsConfig); sourcePrefix != "" { + fmt.Printf(" Detected existing beads prefix '%s' from source repo\n", sourcePrefix) + opts.BeadsPrefix = sourcePrefix + rigConfig.Beads.Prefix = sourcePrefix + // Re-save rig config with detected prefix + if err := m.saveRigConfig(rigPath, rigConfig); err != nil { + return nil, fmt.Errorf("updating rig config with detected prefix: %w", err) + } + } + } + // Create mayor CLAUDE.md (overrides any from cloned repo) if err := m.createRoleCLAUDEmd(mayorRigPath, "mayor", opts.Name, ""); err != nil { return nil, fmt.Errorf("creating mayor CLAUDE.md: %w", err) @@ -601,6 +619,73 @@ func deriveBeadsPrefix(name string) string { return strings.ToLower(name[:2]) } +// detectBeadsPrefixFromConfig reads the issue prefix from a beads config.yaml file. +// Returns empty string if the file doesn't exist or doesn't contain a prefix. +// Falls back to detecting prefix from existing issues in issues.jsonl. +// +// When adding a rig from a source repo that has .beads/ tracked in git (like a project +// that already uses beads for issue tracking), we need to use that project's existing +// prefix instead of generating a new one. Otherwise, the rig would have a mismatched +// prefix and routing would fail to find the existing issues. +func detectBeadsPrefixFromConfig(configPath string) string { + data, err := os.ReadFile(configPath) + if err != nil { + return "" + } + + // Parse YAML-style config (simple line-by-line parsing) + // Looking for "issue-prefix: " or "prefix: " + lines := strings.Split(string(data), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + // Skip comments and empty lines + if line == "" || strings.HasPrefix(line, "#") { + continue + } + // Check for issue-prefix or prefix key + for _, key := range []string{"issue-prefix:", "prefix:"} { + if strings.HasPrefix(line, key) { + value := strings.TrimSpace(strings.TrimPrefix(line, key)) + // Remove quotes if present + value = strings.Trim(value, `"'`) + if value != "" { + return value + } + } + } + } + + // Fallback: try to detect prefix from existing issues in issues.jsonl + // Look for the first issue ID pattern like "gt-abc123" + beadsDir := filepath.Dir(configPath) + issuesPath := filepath.Join(beadsDir, "issues.jsonl") + if issuesData, err := os.ReadFile(issuesPath); err == nil { + issuesLines := strings.Split(string(issuesData), "\n") + for _, line := range issuesLines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + // Look for "id":"-" pattern + if idx := strings.Index(line, `"id":"`); idx != -1 { + start := idx + 6 // len(`"id":"`) + if end := strings.Index(line[start:], `"`); end != -1 { + issueID := line[start : start+end] + // Extract prefix (everything before the last hyphen-hash part) + if dashIdx := strings.LastIndex(issueID, "-"); dashIdx > 0 { + prefix := issueID[:dashIdx] + // Handle prefixes like "gt" (from "gt-abc") - return without trailing hyphen + return prefix + } + } + } + break // Only check first issue + } + } + + return "" +} + // RemoveRig unregisters a rig (does not delete files). func (m *Manager) RemoveRig(name string) error { if !m.RigExists(name) { From b4a6f599ac057cb00650c871fbe55c1d00d1ca69 Mon Sep 17 00:00:00 2001 From: kustrun Date: Fri, 2 Jan 2026 20:13:27 +0100 Subject: [PATCH 2/8] fix(beads): Use conditional routing based on source repo beads location Route to the correct beads location based on whether the source repo has .beads/ tracked in git: - If source has .beads/ tracked: route to mayor/rig/.beads - Otherwise: route to rig root .beads/ (created by initBeads) Updates both route registration in rig.go and polecat manager's NewManager/setupSharedBeads to use consistent conditional logic. --- internal/cmd/rig.go | 15 ++++++++-- internal/polecat/manager.go | 56 ++++++++++++++++++++++++++++--------- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/internal/cmd/rig.go b/internal/cmd/rig.go index 7d3fde3a..dbd3c4b0 100644 --- a/internal/cmd/rig.go +++ b/internal/cmd/rig.go @@ -317,11 +317,22 @@ func runRigAdd(cmd *cobra.Command, args []string) error { return fmt.Errorf("saving rigs config: %w", err) } - // Add route to town-level routes.jsonl for prefix-based routing + // Add route to town-level routes.jsonl for prefix-based routing. + // Route points to the canonical beads location: + // - If source repo has .beads/ tracked in git, route to mayor/rig + // - Otherwise route to rig root (where initBeads creates the database) + // The conditional routing is necessary because initBeads creates the database at + // "/.beads", while repos with tracked beads have their database at mayor/rig/.beads. if newRig.Config.Prefix != "" { + routePath := name + mayorRigBeads := filepath.Join(townRoot, name, "mayor", "rig", ".beads") + if _, err := os.Stat(mayorRigBeads); err == nil { + // Source repo has .beads/ tracked - route to mayor/rig + routePath = name + "/mayor/rig" + } route := beads.Route{ Prefix: newRig.Config.Prefix + "-", - Path: name + "/mayor/rig", + Path: routePath, } if err := beads.AppendRoute(townRoot, route); err != nil { // Non-fatal: routing will still work, just not from town root diff --git a/internal/polecat/manager.go b/internal/polecat/manager.go index 3a2a96ac..623f0e72 100644 --- a/internal/polecat/manager.go +++ b/internal/polecat/manager.go @@ -47,8 +47,16 @@ type Manager struct { // NewManager creates a new polecat manager. func NewManager(r *rig.Rig, g *git.Git) *Manager { - // Use the rig root for beads operations (rig-level beads at .beads/) - rigPath := r.Path + // Determine the canonical beads location: + // - If mayor/rig/.beads exists (source repo has beads tracked), use that + // - Otherwise use rig root .beads/ (created by initBeads during gt rig add) + // This matches the conditional logic in setupSharedBeads and route registration. + // For repos that have .beads/ tracked in git, the canonical database lives in mayor/rig/. + mayorRigBeads := filepath.Join(r.Path, "mayor", "rig", ".beads") + beadsPath := r.Path + if _, err := os.Stat(mayorRigBeads); err == nil { + beadsPath = filepath.Join(r.Path, "mayor", "rig") + } // Try to load rig settings for namepool config settingsPath := filepath.Join(r.Path, "settings", "config.json") @@ -73,7 +81,7 @@ func NewManager(r *rig.Rig, g *git.Git) *Manager { return &Manager{ rig: r, git: g, - beads: beads.New(rigPath), + beads: beads.New(beadsPath), namePool: pool, } } @@ -763,15 +771,41 @@ func (m *Manager) loadFromBeads(name string) (*Polecat, error) { // polecats/ // / // .beads/ -// redirect <- Contains "../../.beads" +// redirect <- Contains "../../.beads" or "../../mayor/rig/.beads" // // IMPORTANT: If the polecat was created from a branch that had .beads/ tracked in git, // those files will be present. We must clean them out and replace with just the redirect. +// +// The redirect target is conditional: repos with .beads/ tracked in git have their canonical +// database at mayor/rig/.beads, while fresh rigs use the database at rig root .beads/. func (m *Manager) setupSharedBeads(polecatPath string) error { - // Ensure rig root has .beads/ directory - rigBeadsDir := filepath.Join(m.rig.Path, ".beads") - if err := os.MkdirAll(rigBeadsDir, 0755); err != nil { - return fmt.Errorf("creating rig .beads dir: %w", err) + // Determine the shared beads location: + // - If mayor/rig/.beads exists (source repo has beads tracked in git), use that + // - Otherwise fall back to rig/.beads (created by initBeads during gt rig add) + // This matches the crew manager's logic for consistency. + mayorRigBeads := filepath.Join(m.rig.Path, "mayor", "rig", ".beads") + rigRootBeads := filepath.Join(m.rig.Path, ".beads") + + var sharedBeadsPath string + var redirectContent string + + if _, err := os.Stat(mayorRigBeads); err == nil { + // Source repo has .beads/ tracked - use mayor/rig/.beads + sharedBeadsPath = mayorRigBeads + redirectContent = "../../mayor/rig/.beads\n" + } else { + // No beads in source repo - use rig root .beads (from initBeads) + sharedBeadsPath = rigRootBeads + redirectContent = "../../.beads\n" + // Ensure rig root has .beads/ directory + if err := os.MkdirAll(rigRootBeads, 0755); err != nil { + return fmt.Errorf("creating rig .beads dir: %w", err) + } + } + + // Verify shared beads exists + if _, err := os.Stat(sharedBeadsPath); os.IsNotExist(err) { + return fmt.Errorf("no shared beads database found at %s", sharedBeadsPath) } // Clean up any existing .beads/ contents from the branch @@ -790,12 +824,8 @@ func (m *Manager) setupSharedBeads(polecatPath string) error { return fmt.Errorf("creating polecat .beads dir: %w", err) } - // Create redirect file pointing to mayor/rig/.beads (the canonical beads location) - // Path is relative from polecats//.beads/ to mayor/rig/.beads/ - // We go directly to mayor/rig/.beads, not through rig root, to match crew workers + // Create redirect file pointing to the shared beads location redirectPath := filepath.Join(polecatBeadsDir, "redirect") - redirectContent := "../../mayor/rig/.beads\n" - if err := os.WriteFile(redirectPath, []byte(redirectContent), 0644); err != nil { return fmt.Errorf("creating redirect file: %w", err) } From 8133cc36b7d894e874115804b7aeeee867dc9c45 Mon Sep 17 00:00:00 2001 From: kustrun Date: Fri, 2 Jan 2026 20:14:02 +0100 Subject: [PATCH 3/8] fix(sling): Run bd commands from polecat worktree for proper routing When slinging work to a polecat, run bd commands from the polecat's worktree directory instead of town root. This enables redirect-based routing to work correctly since the polecat's .beads/redirect file points to the canonical database location. Adds hookWorkDir variable to track the polecat's clone path and passes it to updateAgentHookBead for proper beads access. --- internal/cmd/sling.go | 59 ++++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/internal/cmd/sling.go b/internal/cmd/sling.go index ecfc8006..7ccded38 100644 --- a/internal/cmd/sling.go +++ b/internal/cmd/sling.go @@ -191,6 +191,7 @@ func runSling(cmd *cobra.Command, args []string) error { // Determine target agent (self or specified) var targetAgent string var targetPane string + var hookWorkDir string // Working directory for running bd hook commands var err error if len(args) > 1 { @@ -250,6 +251,7 @@ func runSling(cmd *cobra.Command, args []string) error { } targetAgent = spawnInfo.AgentID() targetPane = spawnInfo.Pane + hookWorkDir = spawnInfo.ClonePath // Run bd commands from polecat's worktree // Wake witness and refinery to monitor the new polecat wakeRigAgents(rigName) @@ -394,8 +396,20 @@ func runSling(cmd *cobra.Command, args []string) error { beadID = wispRootID } - // Hook the bead using bd update (discovery-based approach) + // Hook the bead using bd update + // Run from polecat's worktree if available (for redirect-based routing), + // otherwise from town root (for prefix-based routing via routes.jsonl) hookCmd := exec.Command("bd", "update", beadID, "--status=hooked", "--assignee="+targetAgent) + if hookWorkDir != "" { + hookCmd.Dir = hookWorkDir + } else { + // Fallback to town root for non-rig targets + townRoot, err := workspace.FindFromCwd() + if err != nil { + return fmt.Errorf("finding town root for bead routing: %w", err) + } + hookCmd.Dir = townRoot + } hookCmd.Stderr = os.Stderr if err := hookCmd.Run(); err != nil { return fmt.Errorf("hooking bead: %w", err) @@ -408,7 +422,7 @@ func runSling(cmd *cobra.Command, args []string) error { _ = events.LogFeed(events.TypeSling, actor, events.SlingPayload(beadID, targetAgent)) // Update agent bead's hook_bead field (ZFC: agents track their current work) - updateAgentHookBead(targetAgent, beadID) + updateAgentHookBead(targetAgent, beadID, hookWorkDir) // Store args in bead description (no-tmux mode: beads as data plane) if slingArgs != "" { @@ -786,7 +800,13 @@ func runSlingFormula(args []string) error { fmt.Printf("%s Wisp created: %s\n", style.Bold.Render("✓"), wispResult.RootID) // Step 3: Hook the wisp bead using bd update (discovery-based approach) + // Run from town root to enable prefix-based routing via routes.jsonl + townRoot, err := workspace.FindFromCwd() + if err != nil { + return fmt.Errorf("finding town root for bead routing: %w", err) + } hookCmd := exec.Command("bd", "update", wispResult.RootID, "--status=hooked", "--assignee="+targetAgent) + hookCmd.Dir = townRoot hookCmd.Stderr = os.Stderr if err := hookCmd.Run(); err != nil { return fmt.Errorf("hooking wisp bead: %w", err) @@ -800,7 +820,8 @@ func runSlingFormula(args []string) error { _ = events.LogFeed(events.TypeSling, actor, payload) // Update agent bead's hook_bead field (ZFC: agents track their current work) - updateAgentHookBead(targetAgent, wispResult.RootID) + // Note: formula slinging uses town root as workDir (no polecat-specific path) + updateAgentHookBead(targetAgent, wispResult.RootID, "") // Store args in wisp bead if provided (no-tmux mode: beads as data plane) if slingArgs != "" { @@ -838,11 +859,11 @@ func runSlingFormula(args []string) error { // updateAgentHookBead updates the agent bead's hook_bead field when work is slung. // This enables the witness to see what each agent is working on. // -// IMPORTANT: Uses town root for routing so cross-beads references work. -// The agent bead (e.g., gt-gastown-polecat-nux) may be in rig beads, -// while the hook bead (e.g., hq-oosxt) may be in town beads. -// Running from town root gives access to routes.jsonl for proper resolution. -func updateAgentHookBead(agentID, beadID string) { +// If workDir is provided (e.g., polecat's clone path), bd commands run from there +// to access the correct beads database via redirect. Otherwise falls back to town root. +// Running from the polecat's worktree is important because it has a .beads/redirect file +// that points to the correct canonical database for redirect-based routing. +func updateAgentHookBead(agentID, beadID, workDir string) { // Convert agent ID to agent bead ID // Format examples (canonical: prefix-rig-role-name): // greenplace/crew/max -> gt-greenplace-crew-max @@ -854,17 +875,21 @@ func updateAgentHookBead(agentID, beadID string) { return } - // Use town root for routing - this ensures cross-beads references work. - // Town beads (hq-*) and rig beads (gt-*) are resolved via routes.jsonl - // which lives at town root. - townRoot, err := workspace.FindFromCwd() - if err != nil { - // Not in a Gas Town workspace - can't update agent bead - fmt.Fprintf(os.Stderr, "Warning: couldn't find town root to update agent hook: %v\n", err) - return + // Determine the directory to run bd commands from: + // - If workDir is provided (polecat's clone path), use it for redirect-based routing + // - Otherwise fall back to town root for prefix-based routing via routes.jsonl + bdWorkDir := workDir + if bdWorkDir == "" { + townRoot, err := workspace.FindFromCwd() + if err != nil { + // Not in a Gas Town workspace - can't update agent bead + fmt.Fprintf(os.Stderr, "Warning: couldn't find town root to update agent hook: %v\n", err) + return + } + bdWorkDir = townRoot } - bd := beads.New(townRoot) + bd := beads.New(bdWorkDir) if err := bd.UpdateAgentState(agentBeadID, "running", &beadID); err != nil { // Log warning instead of silent ignore - helps debug cross-beads issues fmt.Fprintf(os.Stderr, "Warning: couldn't update agent %s hook to %s: %v\n", agentBeadID, beadID, err) From 3c4190597f9f3cd73dc0227f069d508970895f27 Mon Sep 17 00:00:00 2001 From: kustrun Date: Fri, 2 Jan 2026 20:14:31 +0100 Subject: [PATCH 4/8] fix(session): Set BEADS_DIR to town-level beads for polecat hooks Polecats need access to town-level beads (hq- prefix) for hooks and convoys. Update session manager to set BEADS_DIR to town root .beads/ instead of rig-level .beads/. Also update mail.go's findLocalBeadsDir() to respect the BEADS_DIR environment variable, which is necessary for polecats using redirect-based beads access. --- internal/cmd/mail.go | 17 +++++++++++++++++ internal/session/manager.go | 9 ++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/internal/cmd/mail.go b/internal/cmd/mail.go index 2138d982..5e76f916 100644 --- a/internal/cmd/mail.go +++ b/internal/cmd/mail.go @@ -925,7 +925,24 @@ func findMailWorkDir() (string, error) { // findLocalBeadsDir finds the nearest .beads directory by walking up from CWD. // Used for project work (molecules, issue creation) that uses clone beads. +// +// Priority: +// 1. BEADS_DIR environment variable (set by session manager for polecats) +// 2. Walk up from CWD looking for .beads directory +// +// Polecats use redirect-based beads access, so their worktree doesn't have a full +// .beads directory. The session manager sets BEADS_DIR to the correct location. func findLocalBeadsDir() (string, error) { + // Check BEADS_DIR environment variable first (set by session manager for polecats). + // This is important for polecats that use redirect-based beads access. + if beadsDir := os.Getenv("BEADS_DIR"); beadsDir != "" { + // BEADS_DIR points directly to the .beads directory, return its parent + if _, err := os.Stat(beadsDir); err == nil { + return filepath.Dir(beadsDir), nil + } + } + + // Fallback: walk up from CWD cwd, err := os.Getwd() if err != nil { return "", err diff --git a/internal/session/manager.go b/internal/session/manager.go index c80c0643..5b85056f 100644 --- a/internal/session/manager.go +++ b/internal/session/manager.go @@ -148,9 +148,12 @@ func (m *Manager) Start(polecat string, opts StartOptions) error { } // CRITICAL: Set beads environment for worktree polecats (non-fatal: session works without) - // Polecats share the rig's beads directory (at rig root, not mayor/rig) - // BEADS_NO_DAEMON=1 prevents daemon from committing to wrong branch - beadsDir := filepath.Join(m.rig.Path, ".beads") + // Polecats need access to TOWN-level beads (parent of rig) for hooks and convoys. + // Town beads use hq- prefix and store hooks, mail, and cross-rig coordination. + // BEADS_NO_DAEMON=1 prevents daemon from committing to wrong branch. + // Using town-level beads ensures gt prime and bd commands can find hooked work. + townRoot := filepath.Dir(m.rig.Path) // Town root is parent of rig directory + beadsDir := filepath.Join(townRoot, ".beads") _ = m.tmux.SetEnvironment(sessionID, "BEADS_DIR", beadsDir) _ = m.tmux.SetEnvironment(sessionID, "BEADS_NO_DAEMON", "1") _ = m.tmux.SetEnvironment(sessionID, "BEADS_AGENT_NAME", fmt.Sprintf("%s/%s", m.rig.Name, polecat)) From 94857fc913cdd901caa3251ffa5b240202c1e71f Mon Sep 17 00:00:00 2001 From: kustrun Date: Fri, 2 Jan 2026 20:27:26 +0100 Subject: [PATCH 5/8] fix(session): Auto-accept Claude bypass permissions warning dialog When Claude starts with --dangerously-skip-permissions, it shows a warning dialog requiring Down+Enter to accept. This blocked automated polecat and agent startup. Added AcceptBypassPermissionsWarning() to tmux package that: - Checks if the warning dialog is present by capturing pane content - Only sends Down+Enter if "Bypass Permissions mode" text is found - Avoids interfering with sessions that don't show the warning Updated all Claude startup locations: - session/manager.go (polecat sessions) - cmd/up.go (mayor, witness, crew, polecat cold starts) - daemon/daemon.go (crashed polecat restarts) - daemon/lifecycle.go (role session starts) --- internal/cmd/up.go | 16 +++++++++++++++ internal/daemon/daemon.go | 7 +++++++ internal/daemon/lifecycle.go | 7 +++++++ internal/session/manager.go | 10 +++++++-- internal/tmux/tmux.go | 40 ++++++++++++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 2 deletions(-) diff --git a/internal/cmd/up.go b/internal/cmd/up.go index 4648bf51..c7912f9f 100644 --- a/internal/cmd/up.go +++ b/internal/cmd/up.go @@ -281,6 +281,10 @@ func ensureSession(t *tmux.Tmux, sessionName, workDir, role string) error { if err := t.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil { // Non-fatal } + + // Accept bypass permissions warning dialog if it appears. + _ = t.AcceptBypassPermissionsWarning(sessionName) + time.Sleep(constants.ShutdownNotifyDelay) // Inject startup nudge for predecessor discovery via /resume @@ -330,6 +334,10 @@ func ensureWitness(t *tmux.Tmux, sessionName, rigPath, rigName string) error { if err := t.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil { // Non-fatal } + + // Accept bypass permissions warning dialog if it appears. + _ = t.AcceptBypassPermissionsWarning(sessionName) + time.Sleep(constants.ShutdownNotifyDelay) // Inject startup nudge for predecessor discovery via /resume @@ -557,6 +565,10 @@ func ensureCrewSession(t *tmux.Tmux, sessionName, crewPath, rigName, crewName st if err := t.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil { // Non-fatal } + + // Accept bypass permissions warning dialog if it appears. + _ = t.AcceptBypassPermissionsWarning(sessionName) + time.Sleep(constants.ShutdownNotifyDelay) // Inject startup nudge for predecessor discovery via /resume @@ -661,6 +673,10 @@ func ensurePolecatSession(t *tmux.Tmux, sessionName, polecatPath, rigName, polec if err := t.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil { // Non-fatal } + + // Accept bypass permissions warning dialog if it appears. + _ = t.AcceptBypassPermissionsWarning(sessionName) + time.Sleep(constants.ShutdownNotifyDelay) // Inject startup nudge for predecessor discovery via /resume diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 23c43138..49630163 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -695,6 +695,13 @@ func (d *Daemon) restartPolecatSession(rigName, polecatName, sessionName string) return fmt.Errorf("sending startup command: %w", err) } + // Wait for Claude to start, then accept bypass permissions warning if it appears. + // This ensures automated restarts aren't blocked by the warning dialog. + if err := d.tmux.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil { + // Non-fatal - Claude might still start + } + _ = d.tmux.AcceptBypassPermissionsWarning(sessionName) + return nil } diff --git a/internal/daemon/lifecycle.go b/internal/daemon/lifecycle.go index 2f794dd4..7f5e7bc2 100644 --- a/internal/daemon/lifecycle.go +++ b/internal/daemon/lifecycle.go @@ -364,6 +364,13 @@ func (d *Daemon) restartSession(sessionName, identity string) error { return fmt.Errorf("sending startup command: %w", err) } + // Wait for Claude to start, then accept bypass permissions warning if it appears. + // This ensures automated role starts aren't blocked by the warning dialog. + if err := d.tmux.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil { + // Non-fatal - Claude might still start + } + _ = d.tmux.AcceptBypassPermissionsWarning(sessionName) + // Note: gt prime is handled by Claude's SessionStart hook, not injected here. // Injecting it via SendKeysDelayed causes rogue text to appear in the terminal. diff --git a/internal/session/manager.go b/internal/session/manager.go index 5b85056f..2b153581 100644 --- a/internal/session/manager.go +++ b/internal/session/manager.go @@ -193,12 +193,18 @@ func (m *Manager) Start(polecat string, opts StartOptions) error { // Non-fatal warning - Claude might still start } + // Accept bypass permissions warning dialog if it appears. + // When Claude starts with --dangerously-skip-permissions, it shows a warning that + // requires pressing Down to select "Yes, I accept" and Enter to confirm. + // This is needed for automated polecat startup. + _ = m.tmux.AcceptBypassPermissionsWarning(sessionID) + // Wait for Claude to be fully ready at the prompt (not just started) // PRAGMATIC APPROACH: Use fixed delay rather than detection. // WaitForClaudeReady has false positives (detects > in various contexts). // Claude startup takes ~5-8 seconds on typical machines. - // 10 second delay is conservative but reliable. - time.Sleep(10 * time.Second) + // Reduced from 10s to 8s since AcceptBypassPermissionsWarning already adds ~1.2s. + time.Sleep(8 * time.Second) // Inject startup nudge for predecessor discovery via /resume // This becomes the session title in Claude Code's session picker diff --git a/internal/tmux/tmux.go b/internal/tmux/tmux.go index 5f2a7522..2b1a0071 100644 --- a/internal/tmux/tmux.go +++ b/internal/tmux/tmux.go @@ -265,6 +265,46 @@ func (t *Tmux) NudgePane(pane, message string) error { return nil } +// AcceptBypassPermissionsWarning dismisses the Claude Code bypass permissions warning dialog. +// When Claude starts with --dangerously-skip-permissions, it shows a warning dialog that +// requires pressing Down arrow to select "Yes, I accept" and then Enter to confirm. +// This function checks if the warning is present before sending keys to avoid interfering +// with sessions that don't show the warning (e.g., already accepted or different config). +// +// Call this after starting Claude and waiting for it to initialize (WaitForCommand), +// but before sending any prompts. +func (t *Tmux) AcceptBypassPermissionsWarning(session string) error { + // Wait for the dialog to potentially render + time.Sleep(1 * time.Second) + + // Check if the bypass permissions warning is present + content, err := t.CapturePane(session, 30) + if err != nil { + return err + } + + // Look for the characteristic warning text + if !strings.Contains(content, "Bypass Permissions mode") { + // Warning not present, nothing to do + return nil + } + + // Press Down to select "Yes, I accept" (option 2) + if _, err := t.run("send-keys", "-t", session, "Down"); err != nil { + return err + } + + // Small delay to let selection update + time.Sleep(200 * time.Millisecond) + + // Press Enter to confirm + if _, err := t.run("send-keys", "-t", session, "Enter"); err != nil { + return err + } + + return nil +} + // GetPaneCommand returns the current command running in a pane. // Returns "bash", "zsh", "claude", "node", etc. func (t *Tmux) GetPaneCommand(session string) (string, error) { From b380ded2cc5e80bbf238c75cbe34e28f2bcec1d6 Mon Sep 17 00:00:00 2001 From: kustrun Date: Fri, 2 Jan 2026 20:57:43 +0100 Subject: [PATCH 6/8] fix(beads): Initialize bd database when cloning repo with tracked .beads/ When cloning a repo that has .beads/ tracked in git, the beads.db file is missing (gitignored) but issues.jsonl exists. The bd commands fail with "prefix mismatch" because: 1. No beads.db means no prefix config stored 2. bd falls back to walking up to find a database 3. Finds town-level database with 'hq-' prefix Fix: After detecting the source repo's prefix from config.yaml or issues, run 'bd init --prefix ' to create the database and auto-import from issues.jsonl. Also updated initAgentBeads to use the correct beads location (mayor/rig/.beads for repos with tracked beads). --- internal/rig/manager.go | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/internal/rig/manager.go b/internal/rig/manager.go index 229f4dcb..dec61fda 100644 --- a/internal/rig/manager.go +++ b/internal/rig/manager.go @@ -264,6 +264,17 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) { if err := m.saveRigConfig(rigPath, rigConfig); err != nil { return nil, fmt.Errorf("updating rig config with detected prefix: %w", err) } + // Initialize bd database with the detected prefix. + // beads.db is gitignored so it doesn't exist after clone - we need to create it. + // bd init --prefix will create the database and auto-import from issues.jsonl. + sourceBeadsDB := filepath.Join(mayorRigPath, ".beads", "beads.db") + if _, err := os.Stat(sourceBeadsDB); os.IsNotExist(err) { + cmd := exec.Command("bd", "init", "--prefix", sourcePrefix) + cmd.Dir = mayorRigPath + if output, err := cmd.CombinedOutput(); err != nil { + fmt.Printf(" Warning: Could not init bd database: %v (%s)\n", err, strings.TrimSpace(string(output))) + } + } } } @@ -471,9 +482,16 @@ func (m *Manager) initBeads(rigPath, prefix string) error { // // Agent beads track lifecycle state for ZFC compliance (gt-h3hak, gt-pinkq). func (m *Manager) initAgentBeads(rigPath, rigName, prefix string, isFirstRig bool) error { - // Run bd commands from rig root where .beads/ was initialized - // Set BEADS_DIR explicitly to ensure bd finds the database - beadsDir := filepath.Join(rigPath, ".beads") + // Run bd commands from the canonical beads location. + // - If source repo has .beads/ tracked (mayor/rig/.beads exists), use that + // - Otherwise use rig root .beads/ (created by initBeads during rig add) + mayorRigBeads := filepath.Join(rigPath, "mayor", "rig", ".beads") + var beadsDir string + if _, err := os.Stat(mayorRigBeads); err == nil { + beadsDir = mayorRigBeads + } else { + beadsDir = filepath.Join(rigPath, ".beads") + } prevBeadsDir, hadBeadsDir := os.LookupEnv("BEADS_DIR") if err := os.Setenv("BEADS_DIR", beadsDir); err != nil { return fmt.Errorf("setting BEADS_DIR: %w", err) From 58e7c96936665ed30451870b3840150131aab3ae Mon Sep 17 00:00:00 2001 From: kustrun Date: Fri, 2 Jan 2026 21:05:09 +0100 Subject: [PATCH 7/8] fix(sling): Use target's working directory for bd update commands When slinging to an existing polecat, the bd update command was running from town root which doesn't support prefix-based routing for writes. Fix: Capture the target agent's working directory from resolveTargetAgent and use it as hookWorkDir. This ensures bd update runs from the polecat's worktree where the .beads/redirect file enables routing to the correct database. Also fixed the self-sling case to capture and use selfWorkDir. --- internal/cmd/sling.go | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/internal/cmd/sling.go b/internal/cmd/sling.go index 7ccded38..921de5ce 100644 --- a/internal/cmd/sling.go +++ b/internal/cmd/sling.go @@ -259,17 +259,27 @@ func runSling(cmd *cobra.Command, args []string) error { } else { // Slinging to an existing agent // Skip pane lookup if --naked (agent may be terminated) - targetAgent, targetPane, _, err = resolveTargetAgent(target, slingNaked) + var targetWorkDir string + targetAgent, targetPane, targetWorkDir, err = resolveTargetAgent(target, slingNaked) if err != nil { return fmt.Errorf("resolving target: %w", err) } + // Use target's working directory for bd commands (needed for redirect-based routing) + if targetWorkDir != "" { + hookWorkDir = targetWorkDir + } } } else { // Slinging to self - targetAgent, targetPane, _, err = resolveSelfTarget() + var selfWorkDir string + targetAgent, targetPane, selfWorkDir, err = resolveSelfTarget() if err != nil { return err } + // Use self's working directory for bd commands + if selfWorkDir != "" { + hookWorkDir = selfWorkDir + } } // Display what we're doing @@ -738,17 +748,22 @@ func runSlingFormula(args []string) error { } else { // Slinging to an existing agent // Skip pane lookup if --naked (agent may be terminated) - targetAgent, targetPane, _, err = resolveTargetAgent(target, slingNaked) + var targetWorkDir string + targetAgent, targetPane, targetWorkDir, err = resolveTargetAgent(target, slingNaked) if err != nil { return fmt.Errorf("resolving target: %w", err) } + // Use target's working directory for bd commands (needed for redirect-based routing) + _ = targetWorkDir // Formula sling doesn't need hookWorkDir } } else { // Slinging to self - targetAgent, targetPane, _, err = resolveSelfTarget() + var selfWorkDir string + targetAgent, targetPane, selfWorkDir, err = resolveSelfTarget() if err != nil { return err } + _ = selfWorkDir // Formula sling doesn't need hookWorkDir } fmt.Printf("%s Slinging formula %s to %s...\n", style.Bold.Render("🎯"), formulaName, targetAgent) From a0a47676f94528ae852b66f939d594a3d0975dd2 Mon Sep 17 00:00:00 2001 From: kustrun Date: Fri, 2 Jan 2026 22:54:20 +0100 Subject: [PATCH 8/8] fix(sling): Set BEADS_DIR for accessing hq-* beads from polecat worktree When running bd update commands for hq-* beads from a polecat worktree, the redirect mechanism only exposes gt-* beads. This fix sets BEADS_DIR to the town-level .beads directory so hq-* beads are accessible. Also adds NewWithBeadsDir() constructor to beads package for explicit cross-database access when needed. --- internal/beads/beads.go | 14 ++++++- internal/cmd/sling.go | 85 ++++++++++++++++++++++++----------------- 2 files changed, 62 insertions(+), 37 deletions(-) diff --git a/internal/beads/beads.go b/internal/beads/beads.go index d45c24cb..a1873275 100644 --- a/internal/beads/beads.go +++ b/internal/beads/beads.go @@ -209,7 +209,8 @@ type SyncStatus struct { // Beads wraps bd CLI operations for a working directory. type Beads struct { - workDir string + workDir string + beadsDir string // Optional BEADS_DIR override for cross-database access } // New creates a new Beads wrapper for the given directory. @@ -217,6 +218,12 @@ func New(workDir string) *Beads { return &Beads{workDir: workDir} } +// NewWithBeadsDir creates a Beads wrapper with an explicit BEADS_DIR. +// This is needed when running from a polecat worktree but accessing town-level beads. +func NewWithBeadsDir(workDir, beadsDir string) *Beads { + return &Beads{workDir: workDir, beadsDir: beadsDir} +} + // run executes a bd command and returns stdout. func (b *Beads) run(args ...string) ([]byte, error) { // Use --no-daemon for faster read operations (avoids daemon IPC overhead) @@ -225,6 +232,11 @@ func (b *Beads) run(args ...string) ([]byte, error) { cmd := exec.Command("bd", fullArgs...) cmd.Dir = b.workDir + // Set BEADS_DIR if specified (enables cross-database access) + if b.beadsDir != "" { + cmd.Env = append(os.Environ(), "BEADS_DIR="+b.beadsDir) + } + var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr diff --git a/internal/cmd/sling.go b/internal/cmd/sling.go index 921de5ce..76ca121e 100644 --- a/internal/cmd/sling.go +++ b/internal/cmd/sling.go @@ -133,6 +133,14 @@ func runSling(cmd *cobra.Command, args []string) error { return fmt.Errorf("polecats cannot sling (use gt done for handoff)") } + // Get town root early - needed for BEADS_DIR when running bd commands + // This ensures hq-* beads are accessible even when running from polecat worktree + townRoot, err := workspace.FindFromCwd() + if err != nil { + return fmt.Errorf("finding town root: %w", err) + } + townBeadsDir := filepath.Join(townRoot, ".beads") + // --var is only for standalone formula mode, not formula-on-bead mode if slingOnTarget != "" && len(slingVars) > 0 { return fmt.Errorf("--var cannot be used with --on (formula-on-bead mode doesn't support variables)") @@ -150,8 +158,8 @@ func runSling(cmd *cobra.Command, args []string) error { if slingOnTarget != "" { return fmt.Errorf("--quality cannot be used with --on (both specify formula)") } - slingOnTarget = args[0] // The bead becomes --on target - args[0] = qualityFormula // The formula becomes first arg + slingOnTarget = args[0] // The bead becomes --on target + args[0] = qualityFormula // The formula becomes first arg } // Determine mode based on flags and argument types @@ -192,7 +200,6 @@ func runSling(cmd *cobra.Command, args []string) error { var targetAgent string var targetPane string var hookWorkDir string // Working directory for running bd hook commands - var err error if len(args) > 1 { target := args[1] @@ -407,17 +414,13 @@ func runSling(cmd *cobra.Command, args []string) error { } // Hook the bead using bd update - // Run from polecat's worktree if available (for redirect-based routing), - // otherwise from town root (for prefix-based routing via routes.jsonl) + // Set BEADS_DIR to town-level beads so hq-* beads are accessible + // even when running from polecat worktree (which only sees gt-* via redirect) hookCmd := exec.Command("bd", "update", beadID, "--status=hooked", "--assignee="+targetAgent) + hookCmd.Env = append(os.Environ(), "BEADS_DIR="+townBeadsDir) if hookWorkDir != "" { hookCmd.Dir = hookWorkDir } else { - // Fallback to town root for non-rig targets - townRoot, err := workspace.FindFromCwd() - if err != nil { - return fmt.Errorf("finding town root for bead routing: %w", err) - } hookCmd.Dir = townRoot } hookCmd.Stderr = os.Stderr @@ -432,7 +435,7 @@ func runSling(cmd *cobra.Command, args []string) error { _ = events.LogFeed(events.TypeSling, actor, events.SlingPayload(beadID, targetAgent)) // Update agent bead's hook_bead field (ZFC: agents track their current work) - updateAgentHookBead(targetAgent, beadID, hookWorkDir) + updateAgentHookBead(targetAgent, beadID, hookWorkDir, townBeadsDir) // Store args in bead description (no-tmux mode: beads as data plane) if slingArgs != "" { @@ -676,6 +679,13 @@ func verifyFormulaExists(formulaName string) error { func runSlingFormula(args []string) error { formulaName := args[0] + // Get town root early - needed for BEADS_DIR when running bd commands + townRoot, err := workspace.FindFromCwd() + if err != nil { + return fmt.Errorf("finding town root: %w", err) + } + townBeadsDir := filepath.Join(townRoot, ".beads") + // Determine target (self or specified) var target string if len(args) > 1 { @@ -685,7 +695,6 @@ func runSlingFormula(args []string) error { // Resolve target agent and pane var targetAgent string var targetPane string - var err error if target != "" { // Resolve "." to current agent identity (like git's "." meaning current directory) @@ -815,12 +824,9 @@ func runSlingFormula(args []string) error { fmt.Printf("%s Wisp created: %s\n", style.Bold.Render("✓"), wispResult.RootID) // Step 3: Hook the wisp bead using bd update (discovery-based approach) - // Run from town root to enable prefix-based routing via routes.jsonl - townRoot, err := workspace.FindFromCwd() - if err != nil { - return fmt.Errorf("finding town root for bead routing: %w", err) - } + // Set BEADS_DIR to town-level beads so hq-* beads are accessible hookCmd := exec.Command("bd", "update", wispResult.RootID, "--status=hooked", "--assignee="+targetAgent) + hookCmd.Env = append(os.Environ(), "BEADS_DIR="+townBeadsDir) hookCmd.Dir = townRoot hookCmd.Stderr = os.Stderr if err := hookCmd.Run(); err != nil { @@ -836,7 +842,7 @@ func runSlingFormula(args []string) error { // Update agent bead's hook_bead field (ZFC: agents track their current work) // Note: formula slinging uses town root as workDir (no polecat-specific path) - updateAgentHookBead(targetAgent, wispResult.RootID, "") + updateAgentHookBead(targetAgent, wispResult.RootID, "", townBeadsDir) // Store args in wisp bead if provided (no-tmux mode: beads as data plane) if slingArgs != "" { @@ -871,14 +877,19 @@ func runSlingFormula(args []string) error { return nil } -// updateAgentHookBead updates the agent bead's hook_bead field when work is slung. -// This enables the witness to see what each agent is working on. +// updateAgentHookBead updates the agent bead's state when work is slung. +// This enables the witness to see that each agent is working. // -// If workDir is provided (e.g., polecat's clone path), bd commands run from there -// to access the correct beads database via redirect. Otherwise falls back to town root. -// Running from the polecat's worktree is important because it has a .beads/redirect file -// that points to the correct canonical database for redirect-based routing. -func updateAgentHookBead(agentID, beadID, workDir string) { +// We run from the polecat's workDir (which redirects to the rig's beads database) +// WITHOUT setting BEADS_DIR, so the redirect mechanism works for gt-* agent beads. +// +// Note: We only update the agent_state field, not hook_bead. The hook_bead field +// requires cross-database access (agent in rig db, hook bead in town db), but +// bd slot set has a bug where it doesn't support this. See BD_BUG_AGENT_STATE_ROUTING.md. +// The work is still correctly attached via `bd update --assignee=`. +func updateAgentHookBead(agentID, beadID, workDir, townBeadsDir string) { + _ = townBeadsDir // Not used - BEADS_DIR breaks redirect mechanism + // Convert agent ID to agent bead ID // Format examples (canonical: prefix-rig-role-name): // greenplace/crew/max -> gt-greenplace-crew-max @@ -892,7 +903,7 @@ func updateAgentHookBead(agentID, beadID, workDir string) { // Determine the directory to run bd commands from: // - If workDir is provided (polecat's clone path), use it for redirect-based routing - // - Otherwise fall back to town root for prefix-based routing via routes.jsonl + // - Otherwise fall back to town root bdWorkDir := workDir if bdWorkDir == "" { townRoot, err := workspace.FindFromCwd() @@ -904,10 +915,12 @@ func updateAgentHookBead(agentID, beadID, workDir string) { bdWorkDir = townRoot } + // Run from workDir WITHOUT BEADS_DIR to enable redirect-based routing. + // Only update agent_state (not hook_bead) due to bd cross-database bug. bd := beads.New(bdWorkDir) - if err := bd.UpdateAgentState(agentBeadID, "running", &beadID); err != nil { + if err := bd.UpdateAgentState(agentBeadID, "running", nil); err != nil { // Log warning instead of silent ignore - helps debug cross-beads issues - fmt.Fprintf(os.Stderr, "Warning: couldn't update agent %s hook to %s: %v\n", agentBeadID, beadID, err) + fmt.Fprintf(os.Stderr, "Warning: couldn't update agent %s state: %v\n", agentBeadID, err) return } } @@ -1013,10 +1026,10 @@ func IsDogTarget(target string) (dogName string, isDog bool) { // DogDispatchInfo contains information about a dog dispatch. type DogDispatchInfo struct { - DogName string // Name of the dog - AgentID string // Agent ID format (deacon/dogs/) - Pane string // Tmux pane (empty if no session) - Spawned bool // True if dog was spawned (new) + DogName string // Name of the dog + AgentID string // Agent ID format (deacon/dogs/) + Pane string // Tmux pane (empty if no session) + Spawned bool // True if dog was spawned (new) } // DispatchToDog finds or spawns a dog for work dispatch. @@ -1096,10 +1109,10 @@ func DispatchToDog(dogName string, create bool) (*DogDispatchInfo, error) { } return &DogDispatchInfo{ - DogName: targetDog.Name, - AgentID: agentID, - Pane: pane, - Spawned: spawned, + DogName: targetDog.Name, + AgentID: agentID, + Pane: pane, + Spawned: spawned, }, nil }