fix(config): don't export empty GT_ROOT/BEADS_DIR in AgentEnv (#385)

* fix(config): don't export empty GT_ROOT/BEADS_DIR in AgentEnv

Fix polecats not having GT_ROOT environment variable set. The symptom was
polecat sessions showing GT_ROOT="" instead of the expected town root.

Root cause: AgentEnvSimple doesn't set TownRoot, but AgentEnv was always
setting env["GT_ROOT"] = cfg.TownRoot even when empty. This empty value
in export commands would override the tmux session environment.

Changes:
- Only set GT_ROOT and BEADS_DIR in env map if non-empty
- Refactor daemon.go to use AgentEnv with full AgentEnvConfig instead
  of AgentEnvSimple + manual additions
- Update test to verify keys are absent rather than empty

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(lint): silence unparam for unused executeExternalActions args

The external action params (beadID, severity, description) are reserved
for future email/SMS/slack implementations but currently unused.

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>
Co-authored-by: max <steve.yegge@gmail.com>
This commit is contained in:
Julian Knutsen
2026-01-12 10:45:03 +00:00
committed by GitHub
parent 3cdc98651e
commit 3caf32f9f7
4 changed files with 44 additions and 16 deletions

View File

@@ -540,7 +540,7 @@ func extractMailTargetsFromActions(actions []string) []string {
// executeExternalActions processes external notification actions (email:, sms:, slack). // executeExternalActions processes external notification actions (email:, sms:, slack).
// For now, this logs warnings if contacts aren't configured - actual sending is future work. // For now, this logs warnings if contacts aren't configured - actual sending is future work.
func executeExternalActions(actions []string, cfg *config.EscalationConfig, beadID, severity, description string) { func executeExternalActions(actions []string, cfg *config.EscalationConfig, _, _, _ string) {
for _, action := range actions { for _, action := range actions {
switch { switch {
case strings.HasPrefix(action, "email:"): case strings.HasPrefix(action, "email:"):

View File

@@ -81,8 +81,14 @@ func AgentEnv(cfg AgentEnvConfig) map[string]string {
env["GIT_AUTHOR_NAME"] = cfg.AgentName env["GIT_AUTHOR_NAME"] = cfg.AgentName
} }
env["GT_ROOT"] = cfg.TownRoot // Only set GT_ROOT and BEADS_DIR if provided
env["BEADS_DIR"] = cfg.BeadsDir // 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) // Set BEADS_AGENT_NAME for polecat/crew (uses same format as BD_ACTOR)
if cfg.Role == "polecat" || cfg.Role == "crew" { if cfg.Role == "polecat" || cfg.Role == "crew" {

View File

@@ -163,9 +163,32 @@ func TestAgentEnvSimple(t *testing.T) {
assertEnv(t, env, "GT_ROLE", "polecat") assertEnv(t, env, "GT_ROLE", "polecat")
assertEnv(t, env, "GT_RIG", "myrig") assertEnv(t, env, "GT_RIG", "myrig")
assertEnv(t, env, "GT_POLECAT", "Toast") assertEnv(t, env, "GT_POLECAT", "Toast")
// Simple doesn't set TownRoot/BeadsDir // Simple doesn't set TownRoot/BeadsDir, so keys should be absent
assertEnv(t, env, "GT_ROOT", "") // (not empty strings which would override tmux session environment)
assertEnv(t, env, "BEADS_DIR", "") assertNotSet(t, env, "GT_ROOT")
assertNotSet(t, env, "BEADS_DIR")
}
func TestAgentEnv_EmptyTownRootBeadsDirOmitted(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.
env := AgentEnv(AgentEnvConfig{
Role: "polecat",
Rig: "myrig",
AgentName: "Toast",
TownRoot: "", // explicitly empty
BeadsDir: "", // explicitly empty
})
// Keys should be absent, not empty strings
assertNotSet(t, env, "GT_ROOT")
assertNotSet(t, env, "BEADS_DIR")
// Other keys should still be set
assertEnv(t, env, "GT_ROLE", "polecat")
assertEnv(t, env, "GT_RIG", "myrig")
} }
func TestExportPrefix(t *testing.T) { func TestExportPrefix(t *testing.T) {

View File

@@ -849,17 +849,16 @@ func (d *Daemon) restartPolecatSession(rigName, polecatName, sessionName string)
return fmt.Errorf("creating session: %w", err) return fmt.Errorf("creating session: %w", err)
} }
// Set environment variables // Set environment variables using centralized AgentEnv
// Use centralized AgentEnvSimple for consistency across all role startup paths
envVars := config.AgentEnvSimple("polecat", rigName, polecatName)
// Add polecat-specific beads configuration
// Use ResolveBeadsDir to follow redirects for repos with tracked beads
rigPath := filepath.Join(d.config.TownRoot, rigName) rigPath := filepath.Join(d.config.TownRoot, rigName)
beadsDir := beads.ResolveBeadsDir(rigPath) envVars := config.AgentEnv(config.AgentEnvConfig{
envVars["BEADS_DIR"] = beadsDir Role: "polecat",
envVars["BEADS_NO_DAEMON"] = "1" Rig: rigName,
envVars["BEADS_AGENT_NAME"] = fmt.Sprintf("%s/%s", rigName, polecatName) AgentName: polecatName,
TownRoot: d.config.TownRoot,
BeadsDir: beads.ResolveBeadsDir(rigPath),
BeadsNoDaemon: true,
})
// Set all env vars in tmux session (for debugging) and they'll also be exported to Claude // Set all env vars in tmux session (for debugging) and they'll also be exported to Claude
for k, v := range envVars { for k, v := range envVars {