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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user