fix: Make Mayor/Deacon session names include town name
Session names `gt-mayor` and `gt-deacon` were hardcoded, causing tmux
session name collisions when running multiple towns simultaneously.
Changed to `gt-{town}-mayor` and `gt-{town}-deacon` format (e.g.,
`gt-ai-mayor`) to allow concurrent multi-town operation.
Key changes:
- session.MayorSessionName() and DeaconSessionName() now take townName param
- Added workspace.GetTownName() helper to load town name from config
- Updated all callers in cmd/, daemon/, doctor/, mail/, rig/, templates/
- Updated tests with new session name format
- Bead IDs remain unchanged (already scoped by .beads/ directory)
Fixes #60
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -178,25 +178,35 @@ func runStart(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// startCoreAgents starts Mayor and Deacon sessions.
|
||||
func startCoreAgents(t *tmux.Tmux) error {
|
||||
// Get session names
|
||||
mayorSession, err := getMayorSessionName()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting Mayor session name: %w", err)
|
||||
}
|
||||
deaconSession, err := getDeaconSessionName()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting Deacon session name: %w", err)
|
||||
}
|
||||
|
||||
// Start Mayor first (so Deacon sees it as up)
|
||||
mayorRunning, _ := t.HasSession(MayorSessionName)
|
||||
mayorRunning, _ := t.HasSession(mayorSession)
|
||||
if mayorRunning {
|
||||
fmt.Printf(" %s Mayor already running\n", style.Dim.Render("○"))
|
||||
} else {
|
||||
fmt.Printf(" %s Starting Mayor...\n", style.Bold.Render("→"))
|
||||
if err := startMayorSession(t); err != nil {
|
||||
if err := startMayorSession(t, mayorSession); err != nil {
|
||||
return fmt.Errorf("starting Mayor: %w", err)
|
||||
}
|
||||
fmt.Printf(" %s Mayor started\n", style.Bold.Render("✓"))
|
||||
}
|
||||
|
||||
// Start Deacon (health monitor)
|
||||
deaconRunning, _ := t.HasSession(DeaconSessionName)
|
||||
deaconRunning, _ := t.HasSession(deaconSession)
|
||||
if deaconRunning {
|
||||
fmt.Printf(" %s Deacon already running\n", style.Dim.Render("○"))
|
||||
} else {
|
||||
fmt.Printf(" %s Starting Deacon...\n", style.Bold.Render("→"))
|
||||
if err := startDeaconSession(t); err != nil {
|
||||
if err := startDeaconSession(t, deaconSession); err != nil {
|
||||
return fmt.Errorf("starting Deacon: %w", err)
|
||||
}
|
||||
fmt.Printf(" %s Deacon started\n", style.Bold.Render("✓"))
|
||||
@@ -380,7 +390,10 @@ func runShutdown(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("listing sessions: %w", err)
|
||||
}
|
||||
|
||||
toStop, preserved := categorizeSessions(sessions)
|
||||
// Get session names for categorization
|
||||
mayorSession, _ := getMayorSessionName()
|
||||
deaconSession, _ := getDeaconSessionName()
|
||||
toStop, preserved := categorizeSessions(sessions, mayorSession, deaconSession)
|
||||
|
||||
if len(toStop) == 0 {
|
||||
fmt.Printf("%s Gas Town was not running\n", style.Dim.Render("○"))
|
||||
@@ -420,7 +433,8 @@ func runShutdown(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// categorizeSessions splits sessions into those to stop and those to preserve.
|
||||
func categorizeSessions(sessions []string) (toStop, preserved []string) {
|
||||
// mayorSession and deaconSession are the dynamic session names for the current town.
|
||||
func categorizeSessions(sessions []string, mayorSession, deaconSession string) (toStop, preserved []string) {
|
||||
for _, sess := range sessions {
|
||||
if !strings.HasPrefix(sess, "gt-") {
|
||||
continue // Not a Gas Town session
|
||||
@@ -431,7 +445,7 @@ func categorizeSessions(sessions []string) (toStop, preserved []string) {
|
||||
|
||||
// Check if it's a polecat session (pattern: gt-<rig>-<name> where name is not crew/witness/refinery)
|
||||
isPolecat := false
|
||||
if !isCrew && sess != MayorSessionName && sess != DeaconSessionName {
|
||||
if !isCrew && sess != mayorSession && sess != deaconSession {
|
||||
parts := strings.Split(sess, "-")
|
||||
if len(parts) >= 3 {
|
||||
role := parts[2]
|
||||
@@ -501,7 +515,9 @@ func runGracefulShutdown(t *tmux.Tmux, gtSessions []string, townRoot string) err
|
||||
|
||||
// Phase 4: Kill sessions in correct order
|
||||
fmt.Printf("\nPhase 4: Terminating sessions...\n")
|
||||
stopped := killSessionsInOrder(t, gtSessions)
|
||||
mayorSession, _ := getMayorSessionName()
|
||||
deaconSession, _ := getDeaconSessionName()
|
||||
stopped := killSessionsInOrder(t, gtSessions, mayorSession, deaconSession)
|
||||
|
||||
// Phase 5: Cleanup polecat worktrees and branches
|
||||
fmt.Printf("\nPhase 5: Cleaning up polecats...\n")
|
||||
@@ -517,7 +533,9 @@ func runGracefulShutdown(t *tmux.Tmux, gtSessions []string, townRoot string) err
|
||||
func runImmediateShutdown(t *tmux.Tmux, gtSessions []string, townRoot string) error {
|
||||
fmt.Println("Shutting down Gas Town...")
|
||||
|
||||
stopped := killSessionsInOrder(t, gtSessions)
|
||||
mayorSession, _ := getMayorSessionName()
|
||||
deaconSession, _ := getDeaconSessionName()
|
||||
stopped := killSessionsInOrder(t, gtSessions, mayorSession, deaconSession)
|
||||
|
||||
// Cleanup polecat worktrees and branches
|
||||
if townRoot != "" {
|
||||
@@ -536,7 +554,8 @@ func runImmediateShutdown(t *tmux.Tmux, gtSessions []string, townRoot string) er
|
||||
// 1. Deacon first (so it doesn't restart others)
|
||||
// 2. Everything except Mayor
|
||||
// 3. Mayor last
|
||||
func killSessionsInOrder(t *tmux.Tmux, sessions []string) int {
|
||||
// mayorSession and deaconSession are the dynamic session names for the current town.
|
||||
func killSessionsInOrder(t *tmux.Tmux, sessions []string, mayorSession, deaconSession string) int {
|
||||
stopped := 0
|
||||
|
||||
// Helper to check if session is in our list
|
||||
@@ -550,16 +569,16 @@ func killSessionsInOrder(t *tmux.Tmux, sessions []string) int {
|
||||
}
|
||||
|
||||
// 1. Stop Deacon first
|
||||
if inList(DeaconSessionName) {
|
||||
if err := t.KillSession(DeaconSessionName); err == nil {
|
||||
fmt.Printf(" %s %s stopped\n", style.Bold.Render("✓"), DeaconSessionName)
|
||||
if inList(deaconSession) {
|
||||
if err := t.KillSession(deaconSession); err == nil {
|
||||
fmt.Printf(" %s %s stopped\n", style.Bold.Render("✓"), deaconSession)
|
||||
stopped++
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Stop others (except Mayor)
|
||||
for _, sess := range sessions {
|
||||
if sess == DeaconSessionName || sess == MayorSessionName {
|
||||
if sess == deaconSession || sess == mayorSession {
|
||||
continue
|
||||
}
|
||||
if err := t.KillSession(sess); err == nil {
|
||||
@@ -569,9 +588,9 @@ func killSessionsInOrder(t *tmux.Tmux, sessions []string) int {
|
||||
}
|
||||
|
||||
// 3. Stop Mayor last
|
||||
if inList(MayorSessionName) {
|
||||
if err := t.KillSession(MayorSessionName); err == nil {
|
||||
fmt.Printf(" %s %s stopped\n", style.Bold.Render("✓"), MayorSessionName)
|
||||
if inList(mayorSession) {
|
||||
if err := t.KillSession(mayorSession); err == nil {
|
||||
fmt.Printf(" %s %s stopped\n", style.Bold.Render("✓"), mayorSession)
|
||||
stopped++
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user