Files
gastown/internal/doctor/env_check.go
Julian Knutsen e7ca4908dc 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>
2026-01-13 22:13:57 -08:00

175 lines
4.8 KiB
Go

package doctor
import (
"fmt"
"strings"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/session"
"github.com/steveyegge/gastown/internal/tmux"
)
// SessionEnvReader abstracts tmux session environment access for testing.
type SessionEnvReader interface {
ListSessions() ([]string, error)
GetAllEnvironment(session string) (map[string]string, error)
}
// tmuxEnvReader wraps real tmux operations.
type tmuxEnvReader struct {
t *tmux.Tmux
}
func (r *tmuxEnvReader) ListSessions() ([]string, error) {
return r.t.ListSessions()
}
func (r *tmuxEnvReader) GetAllEnvironment(session string) (map[string]string, error) {
return r.t.GetAllEnvironment(session)
}
// EnvVarsCheck verifies that tmux session environment variables match expected values.
type EnvVarsCheck struct {
BaseCheck
reader SessionEnvReader // nil means use real tmux
}
// NewEnvVarsCheck creates a new env vars check.
func NewEnvVarsCheck() *EnvVarsCheck {
return &EnvVarsCheck{
BaseCheck: BaseCheck{
CheckName: "env-vars",
CheckDescription: "Verify tmux session environment variables match expected values",
CheckCategory: CategoryConfig,
},
}
}
// NewEnvVarsCheckWithReader creates a check with a custom reader (for testing).
func NewEnvVarsCheckWithReader(reader SessionEnvReader) *EnvVarsCheck {
c := NewEnvVarsCheck()
c.reader = reader
return c
}
// Run checks environment variables for all Gas Town sessions.
func (c *EnvVarsCheck) Run(ctx *CheckContext) *CheckResult {
reader := c.reader
if reader == nil {
reader = &tmuxEnvReader{t: tmux.NewTmux()}
}
sessions, err := reader.ListSessions()
if err != nil {
// No tmux server - treat as success (valid when Gas Town is down)
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "No tmux sessions running",
}
}
// Filter to Gas Town sessions only (gt-* and hq-*)
var gtSessions []string
for _, sess := range sessions {
if strings.HasPrefix(sess, "gt-") || strings.HasPrefix(sess, "hq-") {
gtSessions = append(gtSessions, sess)
}
}
if len(gtSessions) == 0 {
// No Gas Town sessions - treat as success (valid when Gas Town is down)
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "No Gas Town sessions running",
}
}
var mismatches []string
var beadsDirWarnings []string
checkedCount := 0
for _, sess := range gtSessions {
identity, err := session.ParseSessionName(sess)
if err != nil {
// Skip unparseable sessions
continue
}
// 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,
})
// Get actual tmux env vars
actual, err := reader.GetAllEnvironment(sess)
if err != nil {
mismatches = append(mismatches, fmt.Sprintf("%s: could not read env vars: %v", sess, err))
continue
}
checkedCount++
// Compare each expected var
for key, expectedVal := range expected {
actualVal, exists := actual[key]
if !exists {
mismatches = append(mismatches, fmt.Sprintf("%s: missing %s (expected %q)", sess, key, expectedVal))
} else if actualVal != expectedVal {
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 {
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: fmt.Sprintf("All %d session(s) have correct environment variables", checkedCount),
}
}
// Add explanation about needing restart
details := append(mismatches,
"",
"Note: Mismatched session env vars won't affect running Claude until sessions restart.",
)
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: fmt.Sprintf("Found %d env var mismatch(es) across %d session(s)", len(mismatches), checkedCount),
Details: details,
FixHint: "Run 'gt shutdown && gt up' to restart sessions with correct env vars",
}
}