diff --git a/internal/doctor/orphan_check.go b/internal/doctor/orphan_check.go index 570142fb..95c86c56 100644 --- a/internal/doctor/orphan_check.go +++ b/internal/doctor/orphan_check.go @@ -94,8 +94,8 @@ func (c *OrphanSessionCheck) Run(ctx *CheckContext) *CheckResult { continue } - // Only check gt-* sessions (Gas Town sessions) - if !strings.HasPrefix(sess, "gt-") { + // Only check gt-* and hq-* sessions (Gas Town sessions) + if !strings.HasPrefix(sess, "gt-") && !strings.HasPrefix(sess, "hq-") { continue } @@ -200,8 +200,8 @@ func (c *OrphanSessionCheck) getValidRigs(townRoot string) []string { // isValidSession checks if a session name matches expected Gas Town patterns. // Valid patterns: -// - gt-{town}-mayor (dynamic based on town name) -// - gt-{town}-deacon (dynamic based on town name) +// - hq-mayor (headquarters mayor session) +// - hq-deacon (headquarters deacon session) // - gt--witness // - gt--refinery // - gt-- (where polecat is any name) @@ -354,8 +354,9 @@ func (c *OrphanProcessCheck) getTmuxSessionPIDs() (map[int]bool, error) { //noli // Find tmux server processes using ps instead of pgrep. // pgrep -x tmux is unreliable on macOS - it often misses the actual server. - // We use ps with awk to find processes where comm is exactly "tmux". - out, err := exec.Command("sh", "-c", `ps ax -o pid,comm | awk '$2 == "tmux" || $2 ~ /\/tmux$/ { print $1 }'`).Output() + // We use ps with awk to find processes where comm is exactly "tmux" or starts with "tmux:". + // On Linux, tmux servers show as "tmux: server" in the comm field. + out, err := exec.Command("sh", "-c", `ps ax -o pid,comm | awk '$2 == "tmux" || $2 ~ /\/tmux$/ || $2 ~ /^tmux:/ { print $1 }'`).Output() if err != nil { // No tmux server running return pids, nil diff --git a/internal/doctor/orphan_check_test.go b/internal/doctor/orphan_check_test.go index 658933f7..f3820604 100644 --- a/internal/doctor/orphan_check_test.go +++ b/internal/doctor/orphan_check_test.go @@ -358,6 +358,37 @@ func TestIsCrewSession_ComprehensivePatterns(t *testing.T) { } } +// TestOrphanSessionCheck_HQSessions tests that hq-* sessions are properly recognized as valid. +func TestOrphanSessionCheck_HQSessions(t *testing.T) { + townRoot := t.TempDir() + mayorDir := filepath.Join(townRoot, "mayor") + if err := os.MkdirAll(mayorDir, 0o755); err != nil { + t.Fatalf("create mayor dir: %v", err) + } + if err := os.WriteFile(filepath.Join(mayorDir, "rigs.json"), []byte("{}"), 0o644); err != nil { + t.Fatalf("create rigs.json: %v", err) + } + + lister := &mockSessionLister{ + sessions: []string{ + "hq-mayor", // valid: headquarters mayor session + "hq-deacon", // valid: headquarters deacon session + }, + } + check := NewOrphanSessionCheckWithSessionLister(lister) + result := check.Run(&CheckContext{TownRoot: townRoot}) + + if result.Status != StatusOK { + t.Fatalf("expected StatusOK for valid hq sessions, got %v: %s", result.Status, result.Message) + } + if result.Message != "All 2 Gas Town sessions are valid" { + t.Fatalf("unexpected message: %q", result.Message) + } + if len(check.orphanSessions) != 0 { + t.Fatalf("expected no orphan sessions, got %v", check.orphanSessions) + } +} + // TestOrphanSessionCheck_Run_Deterministic tests the full Run path with a mock session // lister, ensuring deterministic behavior without depending on real tmux state. func TestOrphanSessionCheck_Run_Deterministic(t *testing.T) { @@ -383,9 +414,11 @@ func TestOrphanSessionCheck_Run_Deterministic(t *testing.T) { "gt-gastown-witness", // valid: gastown rig exists "gt-gastown-polecat1", // valid: gastown rig exists "gt-beads-refinery", // valid: beads rig exists + "hq-mayor", // valid: hq-mayor is recognized + "hq-deacon", // valid: hq-deacon is recognized "gt-unknown-witness", // orphan: unknown rig doesn't exist "gt-missing-crew-joe", // orphan: missing rig doesn't exist - "random-session", // ignored: doesn't match gt-* pattern + "random-session", // ignored: doesn't match gt-*/hq-* pattern }, } check := NewOrphanSessionCheckWithSessionLister(lister)