From 117b91b87f4b7dc6d688bac0d4a2dbc28b63864f Mon Sep 17 00:00:00 2001 From: Julian Knutsen Date: Wed, 7 Jan 2026 20:41:57 -0800 Subject: [PATCH] fix(doctor): warn instead of killing sessions for stale town-root settings (#243) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When gt doctor --fix detects stale Claude settings at town root, it was automatically killing ALL Gas Town sessions (gt-* and hq-*). This is too disruptive because: 1. Deacon runs gt doctor automatically, creating a restart loop 2. Active crew/polecat work could be lost mid-task 3. Settings are only read at startup, so running agents already have the config loaded in memory Instead, warn the user and tell them to restart agents manually: "Town-root settings were moved. Restart agents to pick up new config: gt up --restart" Addresses PR #239 feedback. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 --- internal/doctor/claude_settings_check.go | 14 ++-- internal/doctor/claude_settings_check_test.go | 66 +++++++++++++++++++ 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/internal/doctor/claude_settings_check.go b/internal/doctor/claude_settings_check.go index fbce040c..54315740 100644 --- a/internal/doctor/claude_settings_check.go +++ b/internal/doctor/claude_settings_check.go @@ -10,6 +10,7 @@ import ( "github.com/steveyegge/gastown/internal/claude" "github.com/steveyegge/gastown/internal/session" + "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/templates" "github.com/steveyegge/gastown/internal/tmux" "github.com/steveyegge/gastown/internal/workspace" @@ -477,14 +478,11 @@ func (c *ClaudeSettingsCheck) Fix(ctx *CheckContext) error { } // Town-root files were inherited by ALL agents via directory traversal. - // Cycle all Gas Town sessions so they pick up the corrected file locations. - // This includes gt-* (rig agents) and hq-* (mayor, deacon). - sessions, _ := t.ListSessions() - for _, sess := range sessions { - if strings.HasPrefix(sess, session.Prefix) || strings.HasPrefix(sess, session.HQPrefix) { - _ = t.KillSession(sess) - } - } + // Warn user to restart agents - don't auto-kill sessions as that's too disruptive, + // especially since deacon runs gt doctor automatically which would create a loop. + // Settings are only read at startup, so running agents already have config loaded. + fmt.Printf("\n %s Town-root settings were moved. Restart agents to pick up new config:\n", style.Warning.Render("⚠")) + fmt.Printf(" gt up --restart\n\n") continue } diff --git a/internal/doctor/claude_settings_check_test.go b/internal/doctor/claude_settings_check_test.go index 278276a4..ca64ecd1 100644 --- a/internal/doctor/claude_settings_check_test.go +++ b/internal/doctor/claude_settings_check_test.go @@ -1016,3 +1016,69 @@ func TestClaudeSettingsCheck_FixMovesCLAUDEmdToMayor(t *testing.T) { t.Error("expected CLAUDE.md to be created at mayor/") } } + +func TestClaudeSettingsCheck_TownRootSettingsWarnsInsteadOfKilling(t *testing.T) { + tmpDir := t.TempDir() + + // Create mayor directory (needed for fix to recreate settings there) + mayorDir := filepath.Join(tmpDir, "mayor") + if err := os.MkdirAll(mayorDir, 0755); err != nil { + t.Fatal(err) + } + + // Create settings.json at town root (wrong location - pollutes all agents) + staleTownRootDir := filepath.Join(tmpDir, ".claude") + if err := os.MkdirAll(staleTownRootDir, 0755); err != nil { + t.Fatal(err) + } + staleTownRootSettings := filepath.Join(staleTownRootDir, "settings.json") + // Create valid settings content + settingsContent := `{ + "env": {"PATH": "/usr/bin"}, + "enabledPlugins": ["claude-code-expert"], + "hooks": { + "SessionStart": [{"matcher": "", "hooks": [{"type": "command", "command": "gt prime"}]}], + "Stop": [{"matcher": "", "hooks": [{"type": "command", "command": "gt handoff"}]}] + } + }` + if err := os.WriteFile(staleTownRootSettings, []byte(settingsContent), 0644); err != nil { + t.Fatal(err) + } + + check := NewClaudeSettingsCheck() + ctx := &CheckContext{TownRoot: tmpDir} + + // Run to detect + result := check.Run(ctx) + if result.Status != StatusError { + t.Fatalf("expected StatusError for town root settings, got %v", result.Status) + } + + // Verify it's flagged as wrong location + foundWrongLocation := false + for _, d := range result.Details { + if strings.Contains(d, "wrong location") { + foundWrongLocation = true + break + } + } + if !foundWrongLocation { + t.Errorf("expected details to mention wrong location, got %v", result.Details) + } + + // Apply fix - should NOT return error and should NOT kill sessions + // (session killing would require tmux which isn't available in tests) + if err := check.Fix(ctx); err != nil { + t.Fatalf("Fix failed: %v", err) + } + + // Verify stale file was deleted + if _, err := os.Stat(staleTownRootSettings); !os.IsNotExist(err) { + t.Error("expected settings.json at town root to be deleted") + } + + // Verify .claude directory was cleaned up (best-effort) + if _, err := os.Stat(staleTownRootDir); !os.IsNotExist(err) { + t.Error("expected .claude directory at town root to be deleted") + } +}