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:
markov-kernel
2026-01-03 21:21:00 +01:00
parent 7f9795f630
commit e7145cfd77
46 changed files with 4772 additions and 1615 deletions

View File

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