feat: use hq- prefix for Mayor and Deacon session names

Town-level services (Mayor, Deacon) now use hq- prefix instead of gt-:
- hq-mayor (was gt-mayor)
- hq-deacon (was gt-deacon)

This distinguishes town-level sessions from rig-level sessions which
continue to use gt- prefix (gt-gastown-witness, gt-gastown-crew-max, etc).

Changes:
- session.MayorSessionName() returns "hq-mayor"
- session.DeaconSessionName() returns "hq-deacon"
- ParseSessionName() handles both hq- and gt- prefixes
- categorizeSession() handles both prefixes
- categorizeSessions() accepts both prefixes
- Updated all tests and documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/jack
2026-01-05 00:42:10 -08:00
committed by Steve Yegge
parent a459cd9fd6
commit 6b8c897e37
16 changed files with 92 additions and 74 deletions
+1 -1
View File
@@ -509,7 +509,7 @@ func FormatSynthesisFields(fields *SynthesisFields) string {
type RoleConfig struct {
// SessionPattern defines how to derive tmux session name.
// Supports placeholders: {rig}, {name}, {role}
// Examples: "gt-mayor", "gt-{rig}-{role}", "gt-{rig}-{name}"
// Examples: "hq-mayor", "hq-deacon", "gt-{rig}-{role}", "gt-{rig}-{name}"
SessionPattern string
// WorkDirPattern defines the working directory relative to town root.
+3 -3
View File
@@ -16,9 +16,9 @@ import (
)
// SessionName is the tmux session name for Boot.
// Note: We use "gt-boot" instead of "gt-deacon-boot" to avoid tmux prefix
// matching collisions. Tmux matches session names by prefix, so "gt-deacon-boot"
// would match when checking for "gt-deacon", causing HasSession("gt-deacon")
// Note: We use "gt-boot" instead of "hq-deacon-boot" to avoid tmux prefix
// matching collisions. Tmux matches session names by prefix, so "hq-deacon-boot"
// would match when checking for "hq-deacon", causing HasSession("hq-deacon")
// to return true when only Boot is running.
const SessionName = "gt-boot"
+17 -12
View File
@@ -127,24 +127,29 @@ func init() {
// categorizeSession determines the agent type from a session name.
func categorizeSession(name string) *AgentSession {
// Must start with gt- prefix
session := &AgentSession{Name: name}
// Town-level agents use hq- prefix: hq-mayor, hq-deacon
if strings.HasPrefix(name, "hq-") {
suffix := strings.TrimPrefix(name, "hq-")
if suffix == "mayor" {
session.Type = AgentMayor
return session
}
if suffix == "deacon" {
session.Type = AgentDeacon
return session
}
return nil // Unknown hq- session
}
// Rig-level agents use gt- prefix
if !strings.HasPrefix(name, "gt-") {
return nil
}
session := &AgentSession{Name: name}
suffix := strings.TrimPrefix(name, "gt-")
// Town-level agents: gt-mayor, gt-deacon (simple format, one per machine)
if suffix == "mayor" {
session.Type = AgentMayor
return session
}
if suffix == "deacon" {
session.Type = AgentDeacon
return session
}
// Witness sessions: legacy format gt-witness-<rig> (fallback)
if strings.HasPrefix(suffix, "witness-") {
session.Type = AgentWitness
+3 -3
View File
@@ -9,9 +9,9 @@ func TestAddressToAgentBeadID(t *testing.T) {
address string
expected string
}{
// Mayor and deacon use simple session names (no town qualifier)
{"mayor", "gt-mayor"},
{"deacon", "gt-deacon"},
// Mayor and deacon use hq- prefix (town-level)
{"mayor", "hq-mayor"},
{"deacon", "hq-deacon"},
{"gastown/witness", "gt-gastown-witness"},
{"gastown/refinery", "gt-gastown-refinery"},
{"gastown/alpha", "gt-gastown-polecat-alpha"},
+5 -5
View File
@@ -5,10 +5,10 @@ import (
)
func TestResolveNudgePattern(t *testing.T) {
// Create test agent sessions (no Town field for mayor/deacon anymore)
// Create test agent sessions (mayor/deacon use hq- prefix)
agents := []*AgentSession{
{Name: "gt-mayor", Type: AgentMayor},
{Name: "gt-deacon", Type: AgentDeacon},
{Name: "hq-mayor", Type: AgentMayor},
{Name: "hq-deacon", Type: AgentDeacon},
{Name: "gt-gastown-witness", Type: AgentWitness, Rig: "gastown"},
{Name: "gt-gastown-refinery", Type: AgentRefinery, Rig: "gastown"},
{Name: "gt-gastown-crew-max", Type: AgentCrew, Rig: "gastown", AgentName: "max"},
@@ -27,12 +27,12 @@ func TestResolveNudgePattern(t *testing.T) {
{
name: "mayor special case",
pattern: "mayor",
expected: []string{"gt-mayor"},
expected: []string{"hq-mayor"},
},
{
name: "deacon special case",
pattern: "deacon",
expected: []string{"gt-deacon"},
expected: []string{"hq-deacon"},
},
{
name: "specific witness",
+2 -1
View File
@@ -430,7 +430,8 @@ func runShutdown(cmd *cobra.Command, args []string) error {
// 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-") {
// Gas Town sessions use gt- (rig-level) or hq- (town-level) prefix
if !strings.HasPrefix(sess, "gt-") && !strings.HasPrefix(sess, "hq-") {
continue // Not a Gas Town session
}
+6 -6
View File
@@ -30,9 +30,9 @@ func TestCategorizeSessionRig(t *testing.T) {
// Edge cases
{"gt-a-b", "a"}, // minimum valid
// Town-level agents (no rig)
{"gt-mayor", ""},
{"gt-deacon", ""},
// Town-level agents (no rig, use hq- prefix)
{"hq-mayor", ""},
{"hq-deacon", ""},
}
for _, tt := range tests {
@@ -67,9 +67,9 @@ func TestCategorizeSessionType(t *testing.T) {
{"gt-gastown-crew-max", AgentCrew},
{"gt-myrig-crew-user", AgentCrew},
// Town-level agents
{"gt-mayor", AgentMayor},
{"gt-deacon", AgentDeacon},
// Town-level agents (hq- prefix)
{"hq-mayor", AgentMayor},
{"hq-deacon", AgentDeacon},
}
for _, tt := range tests {
+6 -2
View File
@@ -89,11 +89,15 @@ const (
)
// Tmux session names.
// Mayor and Deacon use simple session names: gt-mayor, gt-deacon (one per machine).
// Mayor and Deacon use hq- prefix: hq-mayor, hq-deacon (town-level, one per machine).
// Rig-level services use gt- prefix: gt-<rig>-witness, gt-<rig>-refinery, etc.
// Use session.MayorSessionName() and session.DeaconSessionName().
const (
// SessionPrefix is the prefix for all Gas Town tmux sessions.
// SessionPrefix is the prefix for rig-level Gas Town tmux sessions.
SessionPrefix = "gt-"
// HQSessionPrefix is the prefix for town-level services (Mayor, Deacon).
HQSessionPrefix = "hq-"
)
// Agent role names.
+3 -3
View File
@@ -184,10 +184,10 @@ func TestIdentityToSession_Mayor(t *testing.T) {
d, cleanup := testDaemonWithTown(t, "ai")
defer cleanup()
// Mayor session name is now fixed (one per machine, no town qualifier)
// Mayor session name is now fixed (one per machine, uses hq- prefix)
result := d.identityToSession("mayor")
if result != "gt-mayor" {
t.Errorf("identityToSession('mayor') = %q, expected 'gt-mayor'", result)
if result != "hq-mayor" {
t.Errorf("identityToSession('mayor') = %q, expected 'hq-mayor'", result)
}
}
+3 -3
View File
@@ -91,9 +91,9 @@ func TestAddressToSessionID(t *testing.T) {
address string
want string
}{
{"mayor", "gt-mayor"},
{"mayor/", "gt-mayor"},
{"deacon", "gt-deacon"},
{"mayor", "hq-mayor"},
{"mayor/", "hq-mayor"},
{"deacon", "hq-deacon"},
{"gastown/refinery", "gt-gastown-refinery"},
{"gastown/Toast", "gt-gastown-Toast"},
{"beads/witness", "gt-beads-witness"},
+16 -11
View File
@@ -28,8 +28,8 @@ type AgentIdentity struct {
// ParseSessionName parses a tmux session name into an AgentIdentity.
//
// Session name formats:
// - gt-mayor → Role: mayor (one per machine)
// - gt-deacon → Role: deacon (one per machine)
// - hq-mayor → Role: mayor (town-level, one per machine)
// - hq-deacon → Role: deacon (town-level, one per machine)
// - gt-<rig>-witness → Role: witness, Rig: <rig>
// - gt-<rig>-refinery → Role: refinery, Rig: <rig>
// - gt-<rig>-crew-<name> → Role: crew, Rig: <rig>, Name: <name>
@@ -39,8 +39,21 @@ type AgentIdentity struct {
// is assumed to be the polecat name. This works for simple rig names but may
// be ambiguous for rig names containing hyphens.
func ParseSessionName(session string) (*AgentIdentity, error) {
// Check for town-level roles (hq- prefix)
if strings.HasPrefix(session, HQPrefix) {
suffix := strings.TrimPrefix(session, HQPrefix)
if suffix == "mayor" {
return &AgentIdentity{Role: RoleMayor}, nil
}
if suffix == "deacon" {
return &AgentIdentity{Role: RoleDeacon}, nil
}
return nil, fmt.Errorf("invalid session name %q: unknown hq- role", session)
}
// Rig-level roles use gt- prefix
if !strings.HasPrefix(session, Prefix) {
return nil, fmt.Errorf("invalid session name %q: missing %q prefix", session, Prefix)
return nil, fmt.Errorf("invalid session name %q: missing %q or %q prefix", session, HQPrefix, Prefix)
}
suffix := strings.TrimPrefix(session, Prefix)
@@ -48,14 +61,6 @@ func ParseSessionName(session string) (*AgentIdentity, error) {
return nil, fmt.Errorf("invalid session name %q: empty after prefix", session)
}
// Check for simple town-level roles (no rig qualifier)
if suffix == "mayor" {
return &AgentIdentity{Role: RoleMayor}, nil
}
if suffix == "deacon" {
return &AgentIdentity{Role: RoleDeacon}, nil
}
// Parse into parts for rig-level roles
parts := strings.Split(suffix, "-")
if len(parts) < 2 {
+7 -7
View File
@@ -13,15 +13,15 @@ func TestParseSessionName(t *testing.T) {
wantName string
wantErr bool
}{
// Town-level roles (simple gt-mayor, gt-deacon)
// Town-level roles (hq-mayor, hq-deacon)
{
name: "mayor",
session: "gt-mayor",
session: "hq-mayor",
wantRole: RoleMayor,
},
{
name: "deacon",
session: "gt-deacon",
session: "hq-deacon",
wantRole: RoleDeacon,
},
@@ -142,12 +142,12 @@ func TestAgentIdentity_SessionName(t *testing.T) {
{
name: "mayor",
identity: AgentIdentity{Role: RoleMayor},
want: "gt-mayor",
want: "hq-mayor",
},
{
name: "deacon",
identity: AgentIdentity{Role: RoleDeacon},
want: "gt-deacon",
want: "hq-deacon",
},
{
name: "witness",
@@ -230,8 +230,8 @@ func TestAgentIdentity_Address(t *testing.T) {
func TestParseSessionName_RoundTrip(t *testing.T) {
// Test that parsing then reconstructing gives the same result
sessions := []string{
"gt-mayor",
"gt-deacon",
"hq-mayor",
"hq-deacon",
"gt-gastown-witness",
"gt-foo-bar-refinery",
"gt-gastown-crew-max",
+6 -3
View File
@@ -8,19 +8,22 @@ import (
"strings"
)
// Prefix is the common prefix for all Gas Town tmux session names.
// Prefix is the common prefix for rig-level Gas Town tmux sessions.
const Prefix = "gt-"
// HQPrefix is the prefix for town-level services (Mayor, Deacon).
const HQPrefix = "hq-"
// MayorSessionName returns the session name for the Mayor agent.
// One mayor per machine - multi-town requires containers/VMs for isolation.
func MayorSessionName() string {
return Prefix + "mayor"
return HQPrefix + "mayor"
}
// DeaconSessionName returns the session name for the Deacon agent.
// One deacon per machine - multi-town requires containers/VMs for isolation.
func DeaconSessionName() string {
return Prefix + "deacon"
return HQPrefix + "deacon"
}
// WitnessSessionName returns the session name for a rig's Witness agent.
+4 -4
View File
@@ -8,8 +8,8 @@ import (
)
func TestMayorSessionName(t *testing.T) {
// Mayor session name is now fixed (one per machine)
want := "gt-mayor"
// Mayor session name is now fixed (one per machine), uses HQ prefix
want := "hq-mayor"
got := MayorSessionName()
if got != want {
t.Errorf("MayorSessionName() = %q, want %q", got, want)
@@ -17,8 +17,8 @@ func TestMayorSessionName(t *testing.T) {
}
func TestDeaconSessionName(t *testing.T) {
// Deacon session name is now fixed (one per machine)
want := "gt-deacon"
// Deacon session name is now fixed (one per machine), uses HQ prefix
want := "hq-deacon"
got := DeaconSessionName()
if got != want {
t.Errorf("DeaconSessionName() = %q, want %q", got, want)
+1 -1
View File
@@ -12,7 +12,7 @@ import (
// TownSession represents a town-level tmux session.
type TownSession struct {
Name string // Display name (e.g., "Mayor")
SessionID string // Tmux session ID (e.g., "gt-mayor")
SessionID string // Tmux session ID (e.g., "hq-mayor")
}
// TownSessions returns the list of town-level sessions in shutdown order.