Files
gastown/internal/cmd/down.go
markov-kernel e7145cfd77 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>
2026-01-03 21:37:05 +01:00

177 lines
4.6 KiB
Go

package cmd
import (
"fmt"
"time"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/boot"
"github.com/steveyegge/gastown/internal/daemon"
"github.com/steveyegge/gastown/internal/events"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/tmux"
"github.com/steveyegge/gastown/internal/workspace"
)
var downCmd = &cobra.Command{
Use: "down",
GroupID: GroupServices,
Short: "Stop all Gas Town services",
Long: `Stop all Gas Town long-lived services.
This gracefully shuts down all infrastructure agents:
• Witnesses - Per-rig polecat managers
• Mayor - Global work coordinator
• Boot - Deacon's watchdog
• Deacon - Health orchestrator
• Daemon - Go background process
Polecats are NOT stopped by this command - use 'gt swarm stop' or
kill individual polecats with 'gt polecat kill'.
This is useful for:
• Taking a break (stop token consumption)
• Clean shutdown before system maintenance
• Resetting the town to a clean state`,
RunE: runDown,
}
var (
downQuiet bool
downForce bool
downAll bool
)
func init() {
downCmd.Flags().BoolVarP(&downQuiet, "quiet", "q", false, "Only show errors")
downCmd.Flags().BoolVarP(&downForce, "force", "f", false, "Force kill without graceful shutdown")
downCmd.Flags().BoolVarP(&downAll, "all", "a", false, "Also kill the tmux server")
rootCmd.AddCommand(downCmd)
}
func runDown(cmd *cobra.Command, args []string) error {
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
t := tmux.NewTmux()
allOK := true
// Stop in reverse order of startup
// 1. Stop witnesses first
rigs := discoverRigs(townRoot)
for _, rigName := range rigs {
sessionName := fmt.Sprintf("gt-%s-witness", rigName)
if err := stopSession(t, sessionName); err != nil {
printDownStatus(fmt.Sprintf("Witness (%s)", rigName), false, err.Error())
allOK = false
} else {
printDownStatus(fmt.Sprintf("Witness (%s)", rigName), true, "stopped")
}
}
// Get session names
mayorSession, _ := getMayorSessionName()
deaconSession, _ := getDeaconSessionName()
// 2. Stop Mayor
if err := stopSession(t, mayorSession); err != nil {
printDownStatus("Mayor", false, err.Error())
allOK = false
} else {
printDownStatus("Mayor", true, "stopped")
}
// 3. Stop Boot (Deacon's watchdog)
if err := stopSession(t, boot.SessionName); err != nil {
printDownStatus("Boot", false, err.Error())
allOK = false
} else {
printDownStatus("Boot", true, "stopped")
}
// 4. Stop Deacon
if err := stopSession(t, deaconSession); err != nil {
printDownStatus("Deacon", false, err.Error())
allOK = false
} else {
printDownStatus("Deacon", true, "stopped")
}
// 5. Stop Daemon last
running, _, _ := daemon.IsRunning(townRoot)
if running {
if err := daemon.StopDaemon(townRoot); err != nil {
printDownStatus("Daemon", false, err.Error())
allOK = false
} else {
printDownStatus("Daemon", true, "stopped")
}
} else {
printDownStatus("Daemon", true, "not running")
}
// 6. Kill tmux server if --all
if downAll {
if err := t.KillServer(); err != nil {
printDownStatus("Tmux server", false, err.Error())
allOK = false
} else {
printDownStatus("Tmux server", true, "killed")
}
}
fmt.Println()
if allOK {
fmt.Printf("%s All services stopped\n", style.Bold.Render("✓"))
// Log halt event with stopped services
stoppedServices := []string{"daemon", "deacon", "boot", "mayor"}
for _, rigName := range rigs {
stoppedServices = append(stoppedServices, fmt.Sprintf("%s/witness", rigName))
}
if downAll {
stoppedServices = append(stoppedServices, "tmux-server")
}
_ = events.LogFeed(events.TypeHalt, "gt", events.HaltPayload(stoppedServices))
} else {
fmt.Printf("%s Some services failed to stop\n", style.Bold.Render("✗"))
return fmt.Errorf("not all services stopped")
}
return nil
}
func printDownStatus(name string, ok bool, detail string) {
if downQuiet && ok {
return
}
if ok {
fmt.Printf("%s %s: %s\n", style.SuccessPrefix, name, style.Dim.Render(detail))
} else {
fmt.Printf("%s %s: %s\n", style.ErrorPrefix, name, detail)
}
}
// stopSession gracefully stops a tmux session.
func stopSession(t *tmux.Tmux, sessionName string) error {
running, err := t.HasSession(sessionName)
if err != nil {
return err
}
if !running {
return nil // Already stopped
}
// Try graceful shutdown first (Ctrl-C, best-effort interrupt)
if !downForce {
_ = t.SendKeysRaw(sessionName, "C-c")
time.Sleep(100 * time.Millisecond)
}
// Kill the session
return t.KillSession(sessionName)
}