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/claude"
|
||||||
"github.com/steveyegge/gastown/internal/session"
|
"github.com/steveyegge/gastown/internal/session"
|
||||||
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
"github.com/steveyegge/gastown/internal/templates"
|
"github.com/steveyegge/gastown/internal/templates"
|
||||||
"github.com/steveyegge/gastown/internal/tmux"
|
"github.com/steveyegge/gastown/internal/tmux"
|
||||||
"github.com/steveyegge/gastown/internal/workspace"
|
"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.
|
// Town-root files were inherited by ALL agents via directory traversal.
|
||||||
// Cycle all Gas Town sessions so they pick up the corrected file locations.
|
// Warn user to restart agents - don't auto-kill sessions as that's too disruptive,
|
||||||
// This includes gt-* (rig agents) and hq-* (mayor, deacon).
|
// especially since deacon runs gt doctor automatically which would create a loop.
|
||||||
sessions, _ := t.ListSessions()
|
// Settings are only read at startup, so running agents already have config loaded.
|
||||||
for _, sess := range sessions {
|
fmt.Printf("\n %s Town-root settings were moved. Restart agents to pick up new config:\n", style.Warning.Render("⚠"))
|
||||||
if strings.HasPrefix(sess, session.Prefix) || strings.HasPrefix(sess, session.HQPrefix) {
|
fmt.Printf(" gt up --restart\n\n")
|
||||||
_ = t.KillSession(sess)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1016,3 +1016,69 @@ func TestClaudeSettingsCheck_FixMovesCLAUDEmdToMayor(t *testing.T) {
|
|||||||
t.Error("expected CLAUDE.md to be created at mayor/")
|
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