fix(doctor): warn instead of killing sessions for stale town-root settings (#243)

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 <noreply@anthropic.com>
This commit is contained in:
Julian Knutsen
2026-01-07 20:41:57 -08:00
committed by GitHub
parent ffa8dd56cb
commit 117b91b87f
2 changed files with 72 additions and 8 deletions

View File

@@ -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
}

View File

@@ -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")
}
}