Merge pull request #34 from kustrun/fix/init-bugs

Fix init bugs when setting first gastown
This commit is contained in:
Steve Yegge
2026-01-02 16:02:39 -08:00
committed by GitHub
11 changed files with 366 additions and 61 deletions
+17
View File
@@ -931,7 +931,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
+13 -2
View File
@@ -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
// "<rig>/.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
+90 -37
View File
@@ -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
@@ -191,7 +199,7 @@ func runSling(cmd *cobra.Command, args []string) error {
// Determine target agent (self or specified)
var targetAgent string
var targetPane string
var err error
var hookWorkDir string // Working directory for running bd hook commands
if len(args) > 1 {
target := args[1]
@@ -250,6 +258,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)
@@ -257,17 +266,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
@@ -394,8 +413,16 @@ 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
// 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 {
hookCmd.Dir = townRoot
}
hookCmd.Stderr = os.Stderr
if err := hookCmd.Run(); err != nil {
return fmt.Errorf("hooking bead: %w", err)
@@ -408,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)
updateAgentHookBead(targetAgent, beadID, hookWorkDir, townBeadsDir)
// Store args in bead description (no-tmux mode: beads as data plane)
if slingArgs != "" {
@@ -653,6 +680,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 {
@@ -662,7 +696,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)
@@ -725,17 +758,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)
@@ -787,7 +825,10 @@ 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)
// 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 {
return fmt.Errorf("hooking wisp bead: %w", err)
@@ -801,7 +842,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, "", townBeadsDir)
// Store args in wisp bead if provided (no-tmux mode: beads as data plane)
if slingArgs != "" {
@@ -836,14 +878,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.
//
// 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) {
// 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 <bead> --assignee=<agent>`.
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
@@ -855,20 +902,26 @@ 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
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)
if err := bd.UpdateAgentState(agentBeadID, "running", &beadID); err != nil {
// 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", 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
}
}
@@ -974,10 +1027,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/<name>)
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/<name>)
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.
@@ -1057,10 +1110,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
}
+16
View File
@@ -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