From 4ed0505748cc682029392a640782b2d8f140271c Mon Sep 17 00:00:00 2001 From: gastown/crew/navani Date: Tue, 31 Mar 2026 16:41:25 -0700 Subject: [PATCH] fix: inject CLAUDE_CONFIG_DIR in witness, refinery, and deacon startup (gt-9he) Startup paths for witness, refinery, and deacon sessions were missing ResolveAccountConfigDir resolution, causing infra sessions to start without CLAUDE_CONFIG_DIR and use the wrong account. Add the same resolution pattern already used by daemon/lifecycle.go and mayor/manager.go. --- internal/cmd/deacon.go | 26 +++++++++++++++------- internal/refinery/manager.go | 33 ++++++++++++++++++---------- internal/witness/manager.go | 37 +++++++++++++++++++++----------- internal/witness/manager_test.go | 18 +++++++++++++--- 4 files changed, 79 insertions(+), 35 deletions(-) diff --git a/internal/cmd/deacon.go b/internal/cmd/deacon.go index d449edea19..ce3d96feb8 100644 --- a/internal/cmd/deacon.go +++ b/internal/cmd/deacon.go @@ -508,6 +508,14 @@ func startDeaconSession(t *tmux.Tmux, sessionName, agentOverride string) error { return fmt.Errorf("creating deacon directory: %w", err) } + // Resolve CLAUDE_CONFIG_DIR from accounts.json so deacon sessions + // use the correct account. Mirrors the daemon restart path (lifecycle.go). + accountsPath := constants.MayorAccountsPath(townRoot) + runtimeConfigDir, _, _ := config.ResolveAccountConfigDir(accountsPath, "") + if runtimeConfigDir == "" { + runtimeConfigDir = os.Getenv("CLAUDE_CONFIG_DIR") + } + // Ensure runtime settings exist (autonomous role needs mail in SessionStart) runtimeConfig := config.ResolveRoleAgentConfig("deacon", townRoot, deaconDir) if err := runtime.EnsureSettingsForRole(deaconDir, deaconDir, "deacon", runtimeConfig); err != nil { @@ -520,11 +528,12 @@ func startDeaconSession(t *tmux.Tmux, sessionName, agentOverride string) error { Topic: "patrol", }, "I am Deacon. First run `gt deacon heartbeat`. Then check gt hook, if empty create mol-deacon-patrol wisp and execute it.") startupCmd, err := config.BuildStartupCommandFromConfig(config.AgentEnvConfig{ - Role: "deacon", - TownRoot: townRoot, - Prompt: initialPrompt, - Topic: "patrol", - SessionName: sessionName, + Role: "deacon", + TownRoot: townRoot, + RuntimeConfigDir: runtimeConfigDir, + Prompt: initialPrompt, + Topic: "patrol", + SessionName: sessionName, }, "", initialPrompt, agentOverride) if err != nil { return fmt.Errorf("building startup command: %w", err) @@ -540,9 +549,10 @@ func startDeaconSession(t *tmux.Tmux, sessionName, agentOverride string) error { // Set environment (non-fatal: session works without these) // Use centralized AgentEnv for consistency across all role startup paths envVars := config.AgentEnv(config.AgentEnvConfig{ - Role: "deacon", - TownRoot: townRoot, - Agent: agentOverride, + Role: "deacon", + TownRoot: townRoot, + RuntimeConfigDir: runtimeConfigDir, + Agent: agentOverride, }) for k, v := range envVars { _ = t.SetEnvironment(sessionName, k, v) diff --git a/internal/refinery/manager.go b/internal/refinery/manager.go index 5d53b0b226..04bc0b8c68 100644 --- a/internal/refinery/manager.go +++ b/internal/refinery/manager.go @@ -167,6 +167,15 @@ func (m *Manager) Start(foreground bool, agentOverride string) error { // Ensure runtime settings exist in the shared refinery parent directory. // Settings are passed to Claude Code via --settings flag. townRoot := filepath.Dir(m.rig.Path) + + // Resolve CLAUDE_CONFIG_DIR from accounts.json so refinery sessions + // use the correct account. Mirrors the daemon restart path (lifecycle.go). + accountsPath := constants.MayorAccountsPath(townRoot) + runtimeConfigDir, _, _ := config.ResolveAccountConfigDir(accountsPath, "") + if runtimeConfigDir == "" { + runtimeConfigDir = os.Getenv("CLAUDE_CONFIG_DIR") + } + runtimeConfig := config.ResolveRoleAgentConfig("refinery", townRoot, m.rig.Path) refinerySettingsDir := config.RoleSettingsDir("refinery", m.rig.Path) if err := runtime.EnsureSettingsForRole(refinerySettingsDir, refineryRigDir, "refinery", runtimeConfig); err != nil { @@ -185,12 +194,13 @@ func (m *Manager) Start(foreground bool, agentOverride string) error { }, "Run `gt prime --hook` and begin patrol.") command, err := config.BuildStartupCommandFromConfig(config.AgentEnvConfig{ - Role: "refinery", - Rig: m.rig.Name, - TownRoot: townRoot, - Prompt: initialPrompt, - Topic: "patrol", - SessionName: sessionID, + Role: "refinery", + Rig: m.rig.Name, + TownRoot: townRoot, + RuntimeConfigDir: runtimeConfigDir, + Prompt: initialPrompt, + Topic: "patrol", + SessionName: sessionID, }, m.rig.Path, initialPrompt, agentOverride) if err != nil { return fmt.Errorf("building startup command: %w", err) @@ -208,11 +218,12 @@ 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 envVars := config.AgentEnv(config.AgentEnvConfig{ - Role: "refinery", - Rig: m.rig.Name, - TownRoot: townRoot, - Agent: agentOverride, - SessionName: sessionID, + Role: "refinery", + Rig: m.rig.Name, + TownRoot: townRoot, + RuntimeConfigDir: runtimeConfigDir, + Agent: agentOverride, + SessionName: sessionID, }) envVars = session.MergeRuntimeLivenessEnv(envVars, runtimeConfig) diff --git a/internal/witness/manager.go b/internal/witness/manager.go index badc748b74..c1ac24cb15 100644 --- a/internal/witness/manager.go +++ b/internal/witness/manager.go @@ -158,6 +158,15 @@ func (m *Manager) Start(foreground bool, agentOverride string, envOverrides []st // package config) to prevent concurrent rig starts from corrupting the // global agent registry. townRoot := m.townRoot() + + // Resolve CLAUDE_CONFIG_DIR from accounts.json so witness sessions + // use the correct account. Mirrors the daemon restart path (lifecycle.go). + accountsPath := constants.MayorAccountsPath(townRoot) + runtimeConfigDir, _, _ := config.ResolveAccountConfigDir(accountsPath, "") + if runtimeConfigDir == "" { + runtimeConfigDir = os.Getenv("CLAUDE_CONFIG_DIR") + } + runtimeConfig := config.ResolveRoleAgentConfig("witness", townRoot, m.rig.Path) witnessSettingsDir := config.RoleSettingsDir("witness", m.rig.Path) if err := runtime.EnsureSettingsForRole(witnessSettingsDir, witnessDir, "witness", runtimeConfig); err != nil { @@ -180,7 +189,7 @@ func (m *Manager) Start(foreground bool, agentOverride string, envOverrides []st // NOTE: No gt prime injection needed - SessionStart hook handles it automatically // Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes // Pass m.rig.Path so rig agent settings are honored (not town-level defaults) - command, err := buildWitnessStartCommand(m.rig.Path, m.rig.Name, townRoot, sessionID, agentOverride, roleConfig) + command, err := buildWitnessStartCommand(m.rig.Path, m.rig.Name, townRoot, sessionID, agentOverride, roleConfig, runtimeConfigDir) if err != nil { return err } @@ -197,11 +206,12 @@ func (m *Manager) Start(foreground bool, agentOverride string, envOverrides []st // Set environment variables (non-fatal: session works without these) // Use centralized AgentEnv for consistency across all role startup paths envVars := config.AgentEnv(config.AgentEnvConfig{ - Role: "witness", - Rig: m.rig.Name, - TownRoot: townRoot, - Agent: agentOverride, - SessionName: sessionID, + Role: "witness", + Rig: m.rig.Name, + TownRoot: townRoot, + RuntimeConfigDir: runtimeConfigDir, + Agent: agentOverride, + SessionName: sessionID, }) envVars = session.MergeRuntimeLivenessEnv(envVars, runtimeConfig) for k, v := range envVars { @@ -311,7 +321,7 @@ func roleConfigEnvVars(roleConfig *beads.RoleConfig, townRoot, rigName string) m return expanded } -func buildWitnessStartCommand(rigPath, rigName, townRoot, sessionName, agentOverride string, roleConfig *beads.RoleConfig) (string, error) { +func buildWitnessStartCommand(rigPath, rigName, townRoot, sessionName, agentOverride string, roleConfig *beads.RoleConfig, runtimeConfigDir string) (string, error) { if agentOverride != "" { roleConfig = nil } @@ -343,12 +353,13 @@ func buildWitnessStartCommand(rigPath, rigName, townRoot, sessionName, agentOver Topic: "patrol", }, "Run `gt prime --hook` and begin patrol.") command, err := config.BuildStartupCommandFromConfig(config.AgentEnvConfig{ - Role: "witness", - Rig: rigName, - TownRoot: townRoot, - Prompt: initialPrompt, - Topic: "patrol", - SessionName: sessionName, + Role: "witness", + Rig: rigName, + TownRoot: townRoot, + RuntimeConfigDir: runtimeConfigDir, + Prompt: initialPrompt, + Topic: "patrol", + SessionName: sessionName, }, rigPath, initialPrompt, agentOverride) if err != nil { return "", fmt.Errorf("building startup command: %w", err) diff --git a/internal/witness/manager_test.go b/internal/witness/manager_test.go index b3d4eb8f99..c010518d07 100644 --- a/internal/witness/manager_test.go +++ b/internal/witness/manager_test.go @@ -13,7 +13,7 @@ func TestBuildWitnessStartCommand_UsesRoleConfig(t *testing.T) { StartCommand: "exec run --town {town} --rig {rig} --role {role}", } - got, err := buildWitnessStartCommand("/town/rig", "gastown", "/town", "", "", roleCfg) + got, err := buildWitnessStartCommand("/town/rig", "gastown", "/town", "", "", roleCfg, "") if err != nil { t.Fatalf("buildWitnessStartCommand: %v", err) } @@ -26,7 +26,7 @@ func TestBuildWitnessStartCommand_UsesRoleConfig(t *testing.T) { func TestBuildWitnessStartCommand_DefaultsToRuntime(t *testing.T) { t.Parallel() - got, err := buildWitnessStartCommand("/town/rig", "gastown", "/town", "", "", nil) + got, err := buildWitnessStartCommand("/town/rig", "gastown", "/town", "", "", nil, "") if err != nil { t.Fatalf("buildWitnessStartCommand: %v", err) } @@ -68,13 +68,25 @@ func TestRoleConfigEnvVars_NilConfig(t *testing.T) { } } +func TestBuildWitnessStartCommand_IncludesConfigDir(t *testing.T) { + t.Parallel() + got, err := buildWitnessStartCommand("/town/rig", "gastown", "/town", "", "", nil, "/home/user/.claude-accounts/work") + if err != nil { + t.Fatalf("buildWitnessStartCommand: %v", err) + } + + if !strings.Contains(got, "CLAUDE_CONFIG_DIR=/home/user/.claude-accounts/work") { + t.Errorf("expected CLAUDE_CONFIG_DIR in command, got %q", got) + } +} + func TestBuildWitnessStartCommand_AgentOverrideWins(t *testing.T) { t.Parallel() roleCfg := &beads.RoleConfig{ StartCommand: "exec run --role {role}", } - got, err := buildWitnessStartCommand("/town/rig", "gastown", "/town", "", "codex", roleCfg) + got, err := buildWitnessStartCommand("/town/rig", "gastown", "/town", "", "codex", roleCfg, "") if err != nil { t.Fatalf("buildWitnessStartCommand: %v", err) }