refactor(config): remove BEADS_DIR from agent environment and add doctor check (#455)
* fix(sling_test): update test for cook dir change
The cook command no longer needs database context and runs from cwd,
not the target rig directory. Update test to match this behavior
change from bd2a5ab5.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(tests): skip tests requiring missing binaries, handle --allow-stale
- Add skipIfAgentBinaryMissing helper to skip tests when codex/gemini
binaries aren't available in the test environment
- Update rig manager test stub to handle --allow-stale flag
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(config): remove BEADS_DIR from agent environment
Stop exporting BEADS_DIR in AgentEnv - agents should use beads redirect
mechanism instead of relying on environment variable. This prevents
prefix mismatches when agents operate across different beads databases.
Changes:
- Remove BeadsDir field from AgentEnvConfig
- Remove BEADS_DIR from env vars set on agent sessions
- Update doctor env_check to not expect BEADS_DIR
- Update all manager Start() calls to not pass BeadsDir
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(doctor): detect BEADS_DIR in tmux session environment
Add a doctor check that warns when BEADS_DIR is set in any Gas Town
tmux session. BEADS_DIR in the environment overrides prefix-based
routing and breaks multi-rig lookups - agents should use the beads
redirect mechanism instead.
The check:
- Iterates over all Gas Town tmux sessions (gt-* and hq-*)
- Checks if BEADS_DIR is set in the session environment
- Returns a warning with fix hint to restart sessions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: julianknutsen <julianknutsen@users.noreply.github>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,6 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
)
|
||||
@@ -176,12 +175,12 @@ func (b *Boot) spawnTmux(agentOverride string) error {
|
||||
var startCmd string
|
||||
if agentOverride != "" {
|
||||
var err error
|
||||
startCmd, err = config.BuildAgentStartupCommandWithAgentOverride("boot", "deacon-boot", "", "gt boot triage", agentOverride)
|
||||
startCmd, err = config.BuildAgentStartupCommandWithAgentOverride("boot", "", b.townRoot, "", "gt boot triage", agentOverride)
|
||||
if err != nil {
|
||||
return fmt.Errorf("building startup command with agent override: %w", err)
|
||||
}
|
||||
} else {
|
||||
startCmd = config.BuildAgentStartupCommand("boot", "deacon-boot", "", "gt boot triage")
|
||||
startCmd = config.BuildAgentStartupCommand("boot", "", b.townRoot, "", "gt boot triage")
|
||||
}
|
||||
|
||||
// Create session with command directly to avoid send-keys race condition.
|
||||
@@ -194,7 +193,6 @@ func (b *Boot) spawnTmux(agentOverride string) error {
|
||||
envVars := config.AgentEnv(config.AgentEnvConfig{
|
||||
Role: "boot",
|
||||
TownRoot: b.townRoot,
|
||||
BeadsDir: beads.ResolveBeadsDir(b.townRoot),
|
||||
})
|
||||
for k, v := range envVars {
|
||||
_ = b.tmux.SetEnvironment(SessionName, k, v)
|
||||
@@ -215,7 +213,6 @@ func (b *Boot) spawnDegraded() error {
|
||||
envVars := config.AgentEnv(config.AgentEnvConfig{
|
||||
Role: "boot",
|
||||
TownRoot: b.townRoot,
|
||||
BeadsDir: beads.ResolveBeadsDir(b.townRoot),
|
||||
})
|
||||
cmd.Env = config.EnvForExecCommand(envVars)
|
||||
cmd.Env = append(cmd.Env, "GT_DEGRADED=true")
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
"github.com/steveyegge/gastown/internal/crew"
|
||||
@@ -166,7 +165,6 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
|
||||
Rig: r.Name,
|
||||
AgentName: name,
|
||||
TownRoot: townRoot,
|
||||
BeadsDir: beads.ResolveBeadsDir(r.Path),
|
||||
RuntimeConfigDir: claudeConfigDir,
|
||||
BeadsNoDaemon: true,
|
||||
})
|
||||
|
||||
@@ -353,7 +353,7 @@ func startDeaconSession(t *tmux.Tmux, sessionName, agentOverride string) error {
|
||||
|
||||
// Build startup command first
|
||||
// Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes
|
||||
startupCmd, err := config.BuildAgentStartupCommandWithAgentOverride("deacon", "deacon", "", "", agentOverride)
|
||||
startupCmd, err := config.BuildAgentStartupCommandWithAgentOverride("deacon", "", townRoot, "", "", agentOverride)
|
||||
if err != nil {
|
||||
return fmt.Errorf("building startup command: %w", err)
|
||||
}
|
||||
@@ -370,7 +370,6 @@ func startDeaconSession(t *tmux.Tmux, sessionName, agentOverride string) error {
|
||||
envVars := config.AgentEnv(config.AgentEnvConfig{
|
||||
Role: "deacon",
|
||||
TownRoot: townRoot,
|
||||
BeadsDir: beads.ResolveBeadsDir(townRoot),
|
||||
})
|
||||
for k, v := range envVars {
|
||||
_ = t.SetEnvironment(sessionName, k, v)
|
||||
|
||||
@@ -187,7 +187,7 @@ func runMayorAttach(cmd *cobra.Command, args []string) error {
|
||||
})
|
||||
|
||||
// Build startup command with beacon
|
||||
startupCmd, err := config.BuildAgentStartupCommandWithAgentOverride("mayor", "mayor", "", beacon, mayorAgentOverride)
|
||||
startupCmd, err := config.BuildAgentStartupCommandWithAgentOverride("mayor", "", townRoot, "", beacon, mayorAgentOverride)
|
||||
if err != nil {
|
||||
return fmt.Errorf("building startup command: %w", err)
|
||||
}
|
||||
|
||||
@@ -506,7 +506,12 @@ func runRoleEnv(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Get canonical env vars from shared source of truth
|
||||
envVars := config.AgentEnvSimple(string(info.Role), info.Rig, info.Polecat)
|
||||
envVars := config.AgentEnv(config.AgentEnvConfig{
|
||||
Role: string(info.Role),
|
||||
Rig: info.Rig,
|
||||
AgentName: info.Polecat,
|
||||
TownRoot: townRoot,
|
||||
})
|
||||
envVars[EnvGTRoleHome] = home
|
||||
|
||||
// Output in sorted order for consistent output
|
||||
|
||||
+10
-8
@@ -25,13 +25,13 @@ type AgentEnvConfig struct {
|
||||
// Sets GT_ROOT environment variable.
|
||||
TownRoot string
|
||||
|
||||
// BeadsDir is the resolved BEADS_DIR path.
|
||||
// Callers should use beads.ResolveBeadsDir() to compute this.
|
||||
BeadsDir string
|
||||
|
||||
// RuntimeConfigDir is the optional CLAUDE_CONFIG_DIR path
|
||||
RuntimeConfigDir string
|
||||
|
||||
// SessionIDEnv is the environment variable name that holds the session ID.
|
||||
// Sets GT_SESSION_ID_ENV so the runtime knows where to find the session ID.
|
||||
SessionIDEnv string
|
||||
|
||||
// BeadsNoDaemon sets BEADS_NO_DAEMON=1 if true
|
||||
// Used for polecats that should bypass the beads daemon
|
||||
BeadsNoDaemon bool
|
||||
@@ -81,14 +81,11 @@ func AgentEnv(cfg AgentEnvConfig) map[string]string {
|
||||
env["GIT_AUTHOR_NAME"] = cfg.AgentName
|
||||
}
|
||||
|
||||
// Only set GT_ROOT and BEADS_DIR if provided
|
||||
// Only set GT_ROOT if provided
|
||||
// Empty values would override tmux session environment
|
||||
if cfg.TownRoot != "" {
|
||||
env["GT_ROOT"] = cfg.TownRoot
|
||||
}
|
||||
if cfg.BeadsDir != "" {
|
||||
env["BEADS_DIR"] = cfg.BeadsDir
|
||||
}
|
||||
|
||||
// Set BEADS_AGENT_NAME for polecat/crew (uses same format as BD_ACTOR)
|
||||
if cfg.Role == "polecat" || cfg.Role == "crew" {
|
||||
@@ -104,6 +101,11 @@ func AgentEnv(cfg AgentEnvConfig) map[string]string {
|
||||
env["CLAUDE_CONFIG_DIR"] = cfg.RuntimeConfigDir
|
||||
}
|
||||
|
||||
// Add session ID env var name if provided
|
||||
if cfg.SessionIDEnv != "" {
|
||||
env["GT_SESSION_ID_ENV"] = cfg.SessionIDEnv
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
|
||||
@@ -9,14 +9,12 @@ func TestAgentEnv_Mayor(t *testing.T) {
|
||||
env := AgentEnv(AgentEnvConfig{
|
||||
Role: "mayor",
|
||||
TownRoot: "/town",
|
||||
BeadsDir: "/town/.beads",
|
||||
})
|
||||
|
||||
assertEnv(t, env, "GT_ROLE", "mayor")
|
||||
assertEnv(t, env, "BD_ACTOR", "mayor")
|
||||
assertEnv(t, env, "GIT_AUTHOR_NAME", "mayor")
|
||||
assertEnv(t, env, "GT_ROOT", "/town")
|
||||
assertEnv(t, env, "BEADS_DIR", "/town/.beads")
|
||||
assertNotSet(t, env, "GT_RIG")
|
||||
assertNotSet(t, env, "BEADS_NO_DAEMON")
|
||||
}
|
||||
@@ -27,7 +25,6 @@ func TestAgentEnv_Witness(t *testing.T) {
|
||||
Role: "witness",
|
||||
Rig: "myrig",
|
||||
TownRoot: "/town",
|
||||
BeadsDir: "/town/myrig/.beads",
|
||||
})
|
||||
|
||||
assertEnv(t, env, "GT_ROLE", "witness")
|
||||
@@ -35,7 +32,6 @@ func TestAgentEnv_Witness(t *testing.T) {
|
||||
assertEnv(t, env, "BD_ACTOR", "myrig/witness")
|
||||
assertEnv(t, env, "GIT_AUTHOR_NAME", "myrig/witness")
|
||||
assertEnv(t, env, "GT_ROOT", "/town")
|
||||
assertEnv(t, env, "BEADS_DIR", "/town/myrig/.beads")
|
||||
}
|
||||
|
||||
func TestAgentEnv_Polecat(t *testing.T) {
|
||||
@@ -45,7 +41,6 @@ func TestAgentEnv_Polecat(t *testing.T) {
|
||||
Rig: "myrig",
|
||||
AgentName: "Toast",
|
||||
TownRoot: "/town",
|
||||
BeadsDir: "/town/myrig/.beads",
|
||||
BeadsNoDaemon: true,
|
||||
})
|
||||
|
||||
@@ -65,7 +60,6 @@ func TestAgentEnv_Crew(t *testing.T) {
|
||||
Rig: "myrig",
|
||||
AgentName: "emma",
|
||||
TownRoot: "/town",
|
||||
BeadsDir: "/town/myrig/.beads",
|
||||
BeadsNoDaemon: true,
|
||||
})
|
||||
|
||||
@@ -84,7 +78,6 @@ func TestAgentEnv_Refinery(t *testing.T) {
|
||||
Role: "refinery",
|
||||
Rig: "myrig",
|
||||
TownRoot: "/town",
|
||||
BeadsDir: "/town/myrig/.beads",
|
||||
BeadsNoDaemon: true,
|
||||
})
|
||||
|
||||
@@ -100,14 +93,12 @@ func TestAgentEnv_Deacon(t *testing.T) {
|
||||
env := AgentEnv(AgentEnvConfig{
|
||||
Role: "deacon",
|
||||
TownRoot: "/town",
|
||||
BeadsDir: "/town/.beads",
|
||||
})
|
||||
|
||||
assertEnv(t, env, "GT_ROLE", "deacon")
|
||||
assertEnv(t, env, "BD_ACTOR", "deacon")
|
||||
assertEnv(t, env, "GIT_AUTHOR_NAME", "deacon")
|
||||
assertEnv(t, env, "GT_ROOT", "/town")
|
||||
assertEnv(t, env, "BEADS_DIR", "/town/.beads")
|
||||
assertNotSet(t, env, "GT_RIG")
|
||||
assertNotSet(t, env, "BEADS_NO_DAEMON")
|
||||
}
|
||||
@@ -117,14 +108,12 @@ func TestAgentEnv_Boot(t *testing.T) {
|
||||
env := AgentEnv(AgentEnvConfig{
|
||||
Role: "boot",
|
||||
TownRoot: "/town",
|
||||
BeadsDir: "/town/.beads",
|
||||
})
|
||||
|
||||
assertEnv(t, env, "GT_ROLE", "boot")
|
||||
assertEnv(t, env, "BD_ACTOR", "deacon-boot")
|
||||
assertEnv(t, env, "GIT_AUTHOR_NAME", "boot")
|
||||
assertEnv(t, env, "GT_ROOT", "/town")
|
||||
assertEnv(t, env, "BEADS_DIR", "/town/.beads")
|
||||
assertNotSet(t, env, "GT_RIG")
|
||||
assertNotSet(t, env, "BEADS_NO_DAEMON")
|
||||
}
|
||||
@@ -136,7 +125,6 @@ func TestAgentEnv_WithRuntimeConfigDir(t *testing.T) {
|
||||
Rig: "myrig",
|
||||
AgentName: "Toast",
|
||||
TownRoot: "/town",
|
||||
BeadsDir: "/town/myrig/.beads",
|
||||
RuntimeConfigDir: "/home/user/.config/claude",
|
||||
})
|
||||
|
||||
@@ -150,7 +138,6 @@ func TestAgentEnv_WithoutRuntimeConfigDir(t *testing.T) {
|
||||
Rig: "myrig",
|
||||
AgentName: "Toast",
|
||||
TownRoot: "/town",
|
||||
BeadsDir: "/town/myrig/.beads",
|
||||
})
|
||||
|
||||
assertNotSet(t, env, "CLAUDE_CONFIG_DIR")
|
||||
@@ -163,28 +150,25 @@ func TestAgentEnvSimple(t *testing.T) {
|
||||
assertEnv(t, env, "GT_ROLE", "polecat")
|
||||
assertEnv(t, env, "GT_RIG", "myrig")
|
||||
assertEnv(t, env, "GT_POLECAT", "Toast")
|
||||
// Simple doesn't set TownRoot/BeadsDir, so keys should be absent
|
||||
// (not empty strings which would override tmux session environment)
|
||||
// Simple doesn't set TownRoot, so key should be absent
|
||||
// (not empty string which would override tmux session environment)
|
||||
assertNotSet(t, env, "GT_ROOT")
|
||||
assertNotSet(t, env, "BEADS_DIR")
|
||||
}
|
||||
|
||||
func TestAgentEnv_EmptyTownRootBeadsDirOmitted(t *testing.T) {
|
||||
func TestAgentEnv_EmptyTownRootOmitted(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Regression test: empty TownRoot/BeadsDir should NOT create keys in the map.
|
||||
// If they were set to empty strings, ExportPrefix would generate "export GT_ROOT= ..."
|
||||
// which overrides tmux session environment where these are correctly set.
|
||||
// Regression test: empty TownRoot should NOT create keys in the map.
|
||||
// If it was set to empty string, ExportPrefix would generate "export GT_ROOT= ..."
|
||||
// which overrides tmux session environment where it's correctly set.
|
||||
env := AgentEnv(AgentEnvConfig{
|
||||
Role: "polecat",
|
||||
Rig: "myrig",
|
||||
AgentName: "Toast",
|
||||
TownRoot: "", // explicitly empty
|
||||
BeadsDir: "", // explicitly empty
|
||||
})
|
||||
|
||||
// Keys should be absent, not empty strings
|
||||
// Key should be absent, not empty string
|
||||
assertNotSet(t, env, "GT_ROOT")
|
||||
assertNotSet(t, env, "BEADS_DIR")
|
||||
|
||||
// Other keys should still be set
|
||||
assertEnv(t, env, "GT_ROLE", "polecat")
|
||||
|
||||
+61
-20
@@ -1376,47 +1376,88 @@ func BuildStartupCommandWithAgentOverride(envVars map[string]string, rigPath, pr
|
||||
}
|
||||
|
||||
// BuildAgentStartupCommand is a convenience function for starting agent sessions.
|
||||
// It sets standard environment variables (GT_ROLE, BD_ACTOR, GIT_AUTHOR_NAME)
|
||||
// and builds the full startup command.
|
||||
func BuildAgentStartupCommand(role, bdActor, rigPath, prompt string) string {
|
||||
envVars := map[string]string{
|
||||
"GT_ROLE": role,
|
||||
"BD_ACTOR": bdActor,
|
||||
"GIT_AUTHOR_NAME": bdActor,
|
||||
}
|
||||
// It uses AgentEnv to set all standard environment variables.
|
||||
// For rig-level roles (witness, refinery), pass the rig name and rigPath.
|
||||
// For town-level roles (mayor, deacon, boot), pass empty rig and rigPath, but provide townRoot.
|
||||
func BuildAgentStartupCommand(role, rig, townRoot, rigPath, prompt string) string {
|
||||
envVars := AgentEnv(AgentEnvConfig{
|
||||
Role: role,
|
||||
Rig: rig,
|
||||
TownRoot: townRoot,
|
||||
})
|
||||
return BuildStartupCommand(envVars, rigPath, prompt)
|
||||
}
|
||||
|
||||
// BuildAgentStartupCommandWithAgentOverride is like BuildAgentStartupCommand, but uses agentOverride if non-empty.
|
||||
func BuildAgentStartupCommandWithAgentOverride(role, bdActor, rigPath, prompt, agentOverride string) (string, error) {
|
||||
envVars := map[string]string{
|
||||
"GT_ROLE": role,
|
||||
"BD_ACTOR": bdActor,
|
||||
"GIT_AUTHOR_NAME": bdActor,
|
||||
}
|
||||
func BuildAgentStartupCommandWithAgentOverride(role, rig, townRoot, rigPath, prompt, agentOverride string) (string, error) {
|
||||
envVars := AgentEnv(AgentEnvConfig{
|
||||
Role: role,
|
||||
Rig: rig,
|
||||
TownRoot: townRoot,
|
||||
})
|
||||
return BuildStartupCommandWithAgentOverride(envVars, rigPath, prompt, agentOverride)
|
||||
}
|
||||
|
||||
// BuildPolecatStartupCommand builds the startup command for a polecat.
|
||||
// Sets GT_ROLE, GT_RIG, GT_POLECAT, BD_ACTOR, and GIT_AUTHOR_NAME.
|
||||
// Sets GT_ROLE, GT_RIG, GT_POLECAT, BD_ACTOR, GIT_AUTHOR_NAME, and GT_ROOT.
|
||||
func BuildPolecatStartupCommand(rigName, polecatName, rigPath, prompt string) string {
|
||||
return BuildStartupCommand(AgentEnvSimple("polecat", rigName, polecatName), rigPath, prompt)
|
||||
var townRoot string
|
||||
if rigPath != "" {
|
||||
townRoot = filepath.Dir(rigPath)
|
||||
}
|
||||
envVars := AgentEnv(AgentEnvConfig{
|
||||
Role: "polecat",
|
||||
Rig: rigName,
|
||||
AgentName: polecatName,
|
||||
TownRoot: townRoot,
|
||||
})
|
||||
return BuildStartupCommand(envVars, rigPath, prompt)
|
||||
}
|
||||
|
||||
// BuildPolecatStartupCommandWithAgentOverride is like BuildPolecatStartupCommand, but uses agentOverride if non-empty.
|
||||
func BuildPolecatStartupCommandWithAgentOverride(rigName, polecatName, rigPath, prompt, agentOverride string) (string, error) {
|
||||
return BuildStartupCommandWithAgentOverride(AgentEnvSimple("polecat", rigName, polecatName), rigPath, prompt, agentOverride)
|
||||
var townRoot string
|
||||
if rigPath != "" {
|
||||
townRoot = filepath.Dir(rigPath)
|
||||
}
|
||||
envVars := AgentEnv(AgentEnvConfig{
|
||||
Role: "polecat",
|
||||
Rig: rigName,
|
||||
AgentName: polecatName,
|
||||
TownRoot: townRoot,
|
||||
})
|
||||
return BuildStartupCommandWithAgentOverride(envVars, rigPath, prompt, agentOverride)
|
||||
}
|
||||
|
||||
// BuildCrewStartupCommand builds the startup command for a crew member.
|
||||
// Sets GT_ROLE, GT_RIG, GT_CREW, BD_ACTOR, and GIT_AUTHOR_NAME.
|
||||
// Sets GT_ROLE, GT_RIG, GT_CREW, BD_ACTOR, GIT_AUTHOR_NAME, and GT_ROOT.
|
||||
func BuildCrewStartupCommand(rigName, crewName, rigPath, prompt string) string {
|
||||
return BuildStartupCommand(AgentEnvSimple("crew", rigName, crewName), rigPath, prompt)
|
||||
var townRoot string
|
||||
if rigPath != "" {
|
||||
townRoot = filepath.Dir(rigPath)
|
||||
}
|
||||
envVars := AgentEnv(AgentEnvConfig{
|
||||
Role: "crew",
|
||||
Rig: rigName,
|
||||
AgentName: crewName,
|
||||
TownRoot: townRoot,
|
||||
})
|
||||
return BuildStartupCommand(envVars, rigPath, prompt)
|
||||
}
|
||||
|
||||
// BuildCrewStartupCommandWithAgentOverride is like BuildCrewStartupCommand, but uses agentOverride if non-empty.
|
||||
func BuildCrewStartupCommandWithAgentOverride(rigName, crewName, rigPath, prompt, agentOverride string) (string, error) {
|
||||
return BuildStartupCommandWithAgentOverride(AgentEnvSimple("crew", rigName, crewName), rigPath, prompt, agentOverride)
|
||||
var townRoot string
|
||||
if rigPath != "" {
|
||||
townRoot = filepath.Dir(rigPath)
|
||||
}
|
||||
envVars := AgentEnv(AgentEnvConfig{
|
||||
Role: "crew",
|
||||
Rig: rigName,
|
||||
AgentName: crewName,
|
||||
TownRoot: townRoot,
|
||||
})
|
||||
return BuildStartupCommandWithAgentOverride(envVars, rigPath, prompt, agentOverride)
|
||||
}
|
||||
|
||||
// ExpectedPaneCommands returns tmux pane command names that indicate the runtime is running.
|
||||
|
||||
@@ -939,7 +939,8 @@ func TestBuildAgentStartupCommand(t *testing.T) {
|
||||
t.Cleanup(func() { _ = os.Chdir(origWD) })
|
||||
|
||||
// Test without rig config (uses defaults)
|
||||
cmd := BuildAgentStartupCommand("witness", "gastown/witness", "", "")
|
||||
// New signature: (role, rig, townRoot, rigPath, prompt)
|
||||
cmd := BuildAgentStartupCommand("witness", "gastown", "", "", "")
|
||||
|
||||
// Should contain environment exports and claude command
|
||||
if !strings.Contains(cmd, "export") {
|
||||
@@ -1121,7 +1122,8 @@ func TestBuildAgentStartupCommandWithAgentOverride(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("empty override uses default agent", func(t *testing.T) {
|
||||
cmd, err := BuildAgentStartupCommandWithAgentOverride("mayor", "mayor", "", "", "")
|
||||
// New signature: (role, rig, townRoot, rigPath, prompt, agentOverride)
|
||||
cmd, err := BuildAgentStartupCommandWithAgentOverride("mayor", "", "", "", "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("BuildAgentStartupCommandWithAgentOverride: %v", err)
|
||||
}
|
||||
@@ -1137,7 +1139,8 @@ func TestBuildAgentStartupCommandWithAgentOverride(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("override switches agent", func(t *testing.T) {
|
||||
cmd, err := BuildAgentStartupCommandWithAgentOverride("mayor", "mayor", "", "", "codex")
|
||||
// New signature: (role, rig, townRoot, rigPath, prompt, agentOverride)
|
||||
cmd, err := BuildAgentStartupCommandWithAgentOverride("mayor", "", "", "", "", "codex")
|
||||
if err != nil {
|
||||
t.Fatalf("BuildAgentStartupCommandWithAgentOverride: %v", err)
|
||||
}
|
||||
@@ -1317,7 +1320,7 @@ func TestBuildAgentStartupCommand_UsesRoleAgents(t *testing.T) {
|
||||
}
|
||||
|
||||
// BuildAgentStartupCommand passes role via GT_ROLE env var
|
||||
cmd := BuildAgentStartupCommand(constants.RoleRefinery, "testrig/refinery", rigPath, "")
|
||||
cmd := BuildAgentStartupCommand(constants.RoleRefinery, "testrig", townRoot, rigPath, "")
|
||||
if !strings.Contains(cmd, "codex") {
|
||||
t.Fatalf("expected codex for refinery role, got: %q", cmd)
|
||||
}
|
||||
|
||||
@@ -528,7 +528,6 @@ func (m *Manager) Start(name string, opts StartOptions) error {
|
||||
Rig: m.rig.Name,
|
||||
AgentName: name,
|
||||
TownRoot: townRoot,
|
||||
BeadsDir: beads.ResolveBeadsDir(m.rig.Path),
|
||||
RuntimeConfigDir: opts.ClaudeConfigDir,
|
||||
BeadsNoDaemon: true,
|
||||
})
|
||||
|
||||
@@ -841,13 +841,16 @@ func (d *Daemon) restartPolecatSession(rigName, polecatName, sessionName string)
|
||||
return fmt.Errorf("cannot restart polecat: %s", reason)
|
||||
}
|
||||
|
||||
// Calculate rig path for agent config resolution
|
||||
rigPath := filepath.Join(d.config.TownRoot, rigName)
|
||||
|
||||
// Determine working directory (handle both new and old structures)
|
||||
// New structure: polecats/<name>/<rigname>/
|
||||
// Old structure: polecats/<name>/
|
||||
workDir := filepath.Join(d.config.TownRoot, rigName, "polecats", polecatName, rigName)
|
||||
workDir := filepath.Join(rigPath, "polecats", polecatName, rigName)
|
||||
if _, err := os.Stat(workDir); os.IsNotExist(err) {
|
||||
// Fall back to old structure
|
||||
workDir = filepath.Join(d.config.TownRoot, rigName, "polecats", polecatName)
|
||||
workDir = filepath.Join(rigPath, "polecats", polecatName)
|
||||
}
|
||||
|
||||
// Verify the worktree exists
|
||||
@@ -865,13 +868,11 @@ func (d *Daemon) restartPolecatSession(rigName, polecatName, sessionName string)
|
||||
}
|
||||
|
||||
// Set environment variables using centralized AgentEnv
|
||||
rigPath := filepath.Join(d.config.TownRoot, rigName)
|
||||
envVars := config.AgentEnv(config.AgentEnvConfig{
|
||||
Role: "polecat",
|
||||
Rig: rigName,
|
||||
AgentName: polecatName,
|
||||
TownRoot: d.config.TownRoot,
|
||||
BeadsDir: beads.ResolveBeadsDir(rigPath),
|
||||
BeadsNoDaemon: true,
|
||||
})
|
||||
|
||||
|
||||
@@ -483,21 +483,32 @@ func (d *Daemon) getStartCommand(roleConfig *beads.RoleConfig, parsed *ParsedIde
|
||||
|
||||
// Polecats and crew need environment variables set in the command
|
||||
if parsed.RoleType == "polecat" {
|
||||
envVars := config.AgentEnvSimple("polecat", parsed.RigName, parsed.AgentName)
|
||||
// Add GT_ROOT and session ID env if available
|
||||
envVars["GT_ROOT"] = d.config.TownRoot
|
||||
if runtimeConfig.Session != nil && runtimeConfig.Session.SessionIDEnv != "" {
|
||||
envVars["GT_SESSION_ID_ENV"] = runtimeConfig.Session.SessionIDEnv
|
||||
var sessionIDEnv string
|
||||
if runtimeConfig.Session != nil {
|
||||
sessionIDEnv = runtimeConfig.Session.SessionIDEnv
|
||||
}
|
||||
envVars := config.AgentEnv(config.AgentEnvConfig{
|
||||
Role: "polecat",
|
||||
Rig: parsed.RigName,
|
||||
AgentName: parsed.AgentName,
|
||||
TownRoot: d.config.TownRoot,
|
||||
SessionIDEnv: sessionIDEnv,
|
||||
})
|
||||
return config.PrependEnv("exec "+runtimeConfig.BuildCommand(), envVars)
|
||||
}
|
||||
|
||||
if parsed.RoleType == "crew" {
|
||||
envVars := config.AgentEnvSimple("crew", parsed.RigName, parsed.AgentName)
|
||||
envVars["GT_ROOT"] = d.config.TownRoot
|
||||
if runtimeConfig.Session != nil && runtimeConfig.Session.SessionIDEnv != "" {
|
||||
envVars["GT_SESSION_ID_ENV"] = runtimeConfig.Session.SessionIDEnv
|
||||
var sessionIDEnv string
|
||||
if runtimeConfig.Session != nil {
|
||||
sessionIDEnv = runtimeConfig.Session.SessionIDEnv
|
||||
}
|
||||
envVars := config.AgentEnv(config.AgentEnvConfig{
|
||||
Role: "crew",
|
||||
Rig: parsed.RigName,
|
||||
AgentName: parsed.AgentName,
|
||||
TownRoot: d.config.TownRoot,
|
||||
SessionIDEnv: sessionIDEnv,
|
||||
})
|
||||
return config.PrependEnv("exec "+runtimeConfig.BuildCommand(), envVars)
|
||||
}
|
||||
|
||||
@@ -507,21 +518,12 @@ func (d *Daemon) getStartCommand(roleConfig *beads.RoleConfig, parsed *ParsedIde
|
||||
// setSessionEnvironment sets environment variables for the tmux session.
|
||||
// Uses centralized AgentEnv for consistency, plus role bead custom env vars if available.
|
||||
func (d *Daemon) setSessionEnvironment(sessionName string, roleConfig *beads.RoleConfig, parsed *ParsedIdentity) {
|
||||
// Determine beads dir based on role type
|
||||
var beadsPath string
|
||||
if parsed.RigName != "" {
|
||||
beadsPath = filepath.Join(d.config.TownRoot, parsed.RigName)
|
||||
} else {
|
||||
beadsPath = d.config.TownRoot
|
||||
}
|
||||
|
||||
// Use centralized AgentEnv for base environment variables
|
||||
envVars := config.AgentEnv(config.AgentEnvConfig{
|
||||
Role: parsed.RoleType,
|
||||
Rig: parsed.RigName,
|
||||
AgentName: parsed.AgentName,
|
||||
TownRoot: d.config.TownRoot,
|
||||
BeadsDir: beads.ResolveBeadsDir(beadsPath),
|
||||
})
|
||||
for k, v := range envVars {
|
||||
_ = d.tmux.SetEnvironment(sessionName, k, v)
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/claude"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
@@ -82,7 +81,7 @@ func (m *Manager) Start(agentOverride string) error {
|
||||
|
||||
// Build startup command first
|
||||
// Restarts are handled by daemon via ensureDeaconRunning on each heartbeat
|
||||
startupCmd, err := config.BuildAgentStartupCommandWithAgentOverride("deacon", "deacon", "", "", agentOverride)
|
||||
startupCmd, err := config.BuildAgentStartupCommandWithAgentOverride("deacon", "", m.townRoot, "", "", agentOverride)
|
||||
if err != nil {
|
||||
return fmt.Errorf("building startup command: %w", err)
|
||||
}
|
||||
@@ -98,7 +97,6 @@ func (m *Manager) Start(agentOverride string) error {
|
||||
envVars := config.AgentEnv(config.AgentEnvConfig{
|
||||
Role: "deacon",
|
||||
TownRoot: m.townRoot,
|
||||
BeadsDir: beads.ResolveBeadsDir(m.townRoot),
|
||||
})
|
||||
for k, v := range envVars {
|
||||
_ = t.SetEnvironment(sessionID, k, v)
|
||||
|
||||
@@ -2,10 +2,8 @@ package doctor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
@@ -89,6 +87,7 @@ func (c *EnvVarsCheck) Run(ctx *CheckContext) *CheckResult {
|
||||
}
|
||||
|
||||
var mismatches []string
|
||||
var beadsDirWarnings []string
|
||||
checkedCount := 0
|
||||
|
||||
for _, sess := range gtSessions {
|
||||
@@ -98,21 +97,12 @@ func (c *EnvVarsCheck) Run(ctx *CheckContext) *CheckResult {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get expected env vars based on role, emulating real call sites
|
||||
// Town-level roles use TownRoot for beads, rig-level roles use rig path
|
||||
var beadsDir string
|
||||
if identity.Rig != "" {
|
||||
rigPath := filepath.Join(ctx.TownRoot, identity.Rig)
|
||||
beadsDir = beads.ResolveBeadsDir(rigPath)
|
||||
} else {
|
||||
beadsDir = beads.ResolveBeadsDir(ctx.TownRoot)
|
||||
}
|
||||
// Get expected env vars based on role
|
||||
expected := config.AgentEnv(config.AgentEnvConfig{
|
||||
Role: string(identity.Role),
|
||||
Rig: identity.Rig,
|
||||
AgentName: identity.Name,
|
||||
TownRoot: ctx.TownRoot,
|
||||
BeadsDir: beadsDir,
|
||||
})
|
||||
|
||||
// Get actual tmux env vars
|
||||
@@ -133,6 +123,31 @@ func (c *EnvVarsCheck) Run(ctx *CheckContext) *CheckResult {
|
||||
mismatches = append(mismatches, fmt.Sprintf("%s: %s=%q (expected %q)", sess, key, actualVal, expectedVal))
|
||||
}
|
||||
}
|
||||
|
||||
// Check for BEADS_DIR - this breaks routing-based lookups
|
||||
if beadsDir, exists := actual["BEADS_DIR"]; exists && beadsDir != "" {
|
||||
beadsDirWarnings = append(beadsDirWarnings, fmt.Sprintf("%s: BEADS_DIR=%q (breaks prefix routing)", sess, beadsDir))
|
||||
}
|
||||
}
|
||||
|
||||
// Check for BEADS_DIR issues first (higher priority warning)
|
||||
if len(beadsDirWarnings) > 0 {
|
||||
details := beadsDirWarnings
|
||||
if len(mismatches) > 0 {
|
||||
details = append(details, "", "Other env var issues:")
|
||||
details = append(details, mismatches...)
|
||||
}
|
||||
details = append(details,
|
||||
"",
|
||||
"BEADS_DIR overrides prefix-based routing and breaks multi-rig lookups.",
|
||||
)
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusWarning,
|
||||
Message: fmt.Sprintf("Found BEADS_DIR set in %d session(s)", len(beadsDirWarnings)),
|
||||
Details: details,
|
||||
FixHint: "Remove BEADS_DIR from session environment: gt shutdown && gt up",
|
||||
}
|
||||
}
|
||||
|
||||
if len(mismatches) == 0 {
|
||||
|
||||
@@ -2,6 +2,7 @@ package doctor
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
@@ -41,21 +42,12 @@ func (m *mockEnvReader) GetAllEnvironment(session string) (map[string]string, er
|
||||
const testTownRoot = "/town"
|
||||
|
||||
// expectedEnv generates expected env vars matching what the check generates.
|
||||
// For town-level roles (mayor, deacon), beadsDir is /town/.beads
|
||||
// For rig-level roles, beadsDir is /town/rigName/.beads
|
||||
func expectedEnv(role, rig, agentName string) map[string]string {
|
||||
var beadsDir string
|
||||
if rig != "" {
|
||||
beadsDir = testTownRoot + "/" + rig + "/.beads"
|
||||
} else {
|
||||
beadsDir = testTownRoot + "/.beads"
|
||||
}
|
||||
return config.AgentEnv(config.AgentEnvConfig{
|
||||
Role: role,
|
||||
Rig: rig,
|
||||
AgentName: agentName,
|
||||
TownRoot: testTownRoot,
|
||||
BeadsDir: beadsDir,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -348,3 +340,101 @@ func TestEnvVarsCheck_HyphenatedRig(t *testing.T) {
|
||||
t.Errorf("Status = %v, want StatusOK", result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_BeadsDirWarning(t *testing.T) {
|
||||
// BEADS_DIR being set breaks prefix-based routing
|
||||
expected := expectedEnv("witness", "myrig", "")
|
||||
expected["BEADS_DIR"] = "/some/path/.beads" // This shouldn't be set!
|
||||
reader := &mockEnvReader{
|
||||
sessions: []string{"gt-myrig-witness"},
|
||||
sessionEnvs: map[string]map[string]string{
|
||||
"gt-myrig-witness": expected,
|
||||
},
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(testCtx())
|
||||
|
||||
if result.Status != StatusWarning {
|
||||
t.Errorf("Status = %v, want StatusWarning", result.Status)
|
||||
}
|
||||
if !strings.Contains(result.Message, "BEADS_DIR") {
|
||||
t.Errorf("Message should mention BEADS_DIR, got: %q", result.Message)
|
||||
}
|
||||
if !strings.Contains(result.FixHint, "gt shutdown") {
|
||||
t.Errorf("FixHint should mention restart, got: %q", result.FixHint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_BeadsDirEmptyIsOK(t *testing.T) {
|
||||
// Empty BEADS_DIR should not warn
|
||||
expected := expectedEnv("witness", "myrig", "")
|
||||
expected["BEADS_DIR"] = "" // Empty is fine
|
||||
reader := &mockEnvReader{
|
||||
sessions: []string{"gt-myrig-witness"},
|
||||
sessionEnvs: map[string]map[string]string{
|
||||
"gt-myrig-witness": expected,
|
||||
},
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(testCtx())
|
||||
|
||||
if result.Status != StatusOK {
|
||||
t.Errorf("Status = %v, want StatusOK for empty BEADS_DIR", result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_BeadsDirMultipleSessions(t *testing.T) {
|
||||
// Multiple sessions, only one has BEADS_DIR
|
||||
witnessEnv := expectedEnv("witness", "myrig", "")
|
||||
polecatEnv := expectedEnv("polecat", "myrig", "Toast")
|
||||
polecatEnv["BEADS_DIR"] = "/bad/path" // This shouldn't be set!
|
||||
|
||||
reader := &mockEnvReader{
|
||||
sessions: []string{"gt-myrig-witness", "gt-myrig-Toast"},
|
||||
sessionEnvs: map[string]map[string]string{
|
||||
"gt-myrig-witness": witnessEnv,
|
||||
"gt-myrig-Toast": polecatEnv,
|
||||
},
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(testCtx())
|
||||
|
||||
if result.Status != StatusWarning {
|
||||
t.Errorf("Status = %v, want StatusWarning", result.Status)
|
||||
}
|
||||
if !strings.Contains(result.Message, "1 session") {
|
||||
t.Errorf("Message should mention 1 session with BEADS_DIR, got: %q", result.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_BeadsDirWithOtherMismatches(t *testing.T) {
|
||||
// Session has BEADS_DIR AND other mismatches - both should be reported
|
||||
reader := &mockEnvReader{
|
||||
sessions: []string{"gt-myrig-witness"},
|
||||
sessionEnvs: map[string]map[string]string{
|
||||
"gt-myrig-witness": {
|
||||
"GT_ROLE": "witness",
|
||||
"GT_RIG": "wrongrig", // Mismatch
|
||||
"BEADS_DIR": "/bad/path",
|
||||
},
|
||||
},
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(testCtx())
|
||||
|
||||
if result.Status != StatusWarning {
|
||||
t.Errorf("Status = %v, want StatusWarning", result.Status)
|
||||
}
|
||||
// BEADS_DIR takes priority in message
|
||||
if !strings.Contains(result.Message, "BEADS_DIR") {
|
||||
t.Errorf("Message should prioritize BEADS_DIR, got: %q", result.Message)
|
||||
}
|
||||
// But details should include both
|
||||
detailsStr := strings.Join(result.Details, "\n")
|
||||
if !strings.Contains(detailsStr, "BEADS_DIR") {
|
||||
t.Errorf("Details should mention BEADS_DIR")
|
||||
}
|
||||
if !strings.Contains(detailsStr, "Other env var issues") {
|
||||
t.Errorf("Details should mention other issues")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/claude"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
@@ -89,7 +88,7 @@ func (m *Manager) Start(agentOverride string) error {
|
||||
|
||||
// Build startup command WITH the beacon prompt - the startup hook handles 'gt prime' automatically
|
||||
// Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes
|
||||
startupCmd, err := config.BuildAgentStartupCommandWithAgentOverride("mayor", "mayor", "", beacon, agentOverride)
|
||||
startupCmd, err := config.BuildAgentStartupCommandWithAgentOverride("mayor", "", m.townRoot, "", beacon, agentOverride)
|
||||
if err != nil {
|
||||
return fmt.Errorf("building startup command: %w", err)
|
||||
}
|
||||
@@ -106,7 +105,6 @@ func (m *Manager) Start(agentOverride string) error {
|
||||
envVars := config.AgentEnv(config.AgentEnvConfig{
|
||||
Role: "mayor",
|
||||
TownRoot: m.townRoot,
|
||||
BeadsDir: beads.ResolveBeadsDir(m.townRoot),
|
||||
})
|
||||
for k, v := range envVars {
|
||||
_ = t.SetEnvironment(sessionID, k, v)
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
@@ -195,7 +194,6 @@ func (m *SessionManager) Start(polecat string, opts SessionStartOptions) error {
|
||||
Rig: m.rig.Name,
|
||||
AgentName: polecat,
|
||||
TownRoot: townRoot,
|
||||
BeadsDir: beads.ResolveBeadsDir(m.rig.Path),
|
||||
RuntimeConfigDir: opts.RuntimeConfigDir,
|
||||
BeadsNoDaemon: true,
|
||||
})
|
||||
|
||||
@@ -174,16 +174,16 @@ func (m *Manager) Start(foreground bool, agentOverride string) error {
|
||||
}
|
||||
|
||||
// Build startup command first
|
||||
bdActor := fmt.Sprintf("%s/refinery", m.rig.Name)
|
||||
townRoot := filepath.Dir(m.rig.Path)
|
||||
var command string
|
||||
if agentOverride != "" {
|
||||
var err error
|
||||
command, err = config.BuildAgentStartupCommandWithAgentOverride("refinery", bdActor, m.rig.Path, "", agentOverride)
|
||||
command, err = config.BuildAgentStartupCommandWithAgentOverride("refinery", m.rig.Name, townRoot, m.rig.Path, "", agentOverride)
|
||||
if err != nil {
|
||||
return fmt.Errorf("building startup command with agent override: %w", err)
|
||||
}
|
||||
} else {
|
||||
command = config.BuildAgentStartupCommand("refinery", bdActor, m.rig.Path, "")
|
||||
command = config.BuildAgentStartupCommand("refinery", m.rig.Name, townRoot, m.rig.Path, "")
|
||||
}
|
||||
|
||||
// Create session with command directly to avoid send-keys race condition.
|
||||
@@ -194,12 +194,10 @@ func (m *Manager) Start(foreground bool, agentOverride string) error {
|
||||
|
||||
// Set environment variables (non-fatal: session works without these)
|
||||
// Use centralized AgentEnv for consistency across all role startup paths
|
||||
townRoot := filepath.Dir(m.rig.Path)
|
||||
envVars := config.AgentEnv(config.AgentEnvConfig{
|
||||
Role: "refinery",
|
||||
Rig: m.rig.Name,
|
||||
TownRoot: townRoot,
|
||||
BeadsDir: beads.ResolveBeadsDir(m.rig.Path),
|
||||
BeadsNoDaemon: true,
|
||||
})
|
||||
|
||||
|
||||
@@ -181,7 +181,6 @@ func (m *Manager) Start(foreground bool, agentOverride string, envOverrides []st
|
||||
Role: "witness",
|
||||
Rig: m.rig.Name,
|
||||
TownRoot: townRoot,
|
||||
BeadsDir: beads.ResolveBeadsDir(m.rig.Path),
|
||||
})
|
||||
for k, v := range envVars {
|
||||
_ = t.SetEnvironment(sessionID, k, v)
|
||||
@@ -276,8 +275,7 @@ func buildWitnessStartCommand(rigPath, rigName, townRoot, agentOverride string,
|
||||
if roleConfig != nil && roleConfig.StartCommand != "" {
|
||||
return beads.ExpandRolePattern(roleConfig.StartCommand, townRoot, rigName, "", "witness"), nil
|
||||
}
|
||||
bdActor := fmt.Sprintf("%s/witness", rigName)
|
||||
command, err := config.BuildAgentStartupCommandWithAgentOverride("witness", bdActor, rigPath, "", agentOverride)
|
||||
command, err := config.BuildAgentStartupCommandWithAgentOverride("witness", rigName, townRoot, rigPath, "", agentOverride)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("building startup command: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user