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:
committed by
Steve Yegge
parent
a459cd9fd6
commit
6b8c897e37
@@ -66,10 +66,10 @@ The daemon could directly monitor agents without AI, but:
|
||||
| Agent | Session Name | Location | Lifecycle |
|
||||
|-------|--------------|----------|-----------|
|
||||
| Daemon | (Go process) | `~/gt/daemon/` | Persistent, auto-restart |
|
||||
| Boot | `gt-deacon-boot` | `~/gt/deacon/dogs/boot/` | Ephemeral, fresh each tick |
|
||||
| Deacon | `gt-deacon` | `~/gt/deacon/` | Long-running, handoff loop |
|
||||
| Boot | `gt-boot` | `~/gt/deacon/dogs/boot/` | Ephemeral, fresh each tick |
|
||||
| Deacon | `hq-deacon` | `~/gt/deacon/` | Long-running, handoff loop |
|
||||
|
||||
**Critical**: Boot runs in `gt-deacon-boot`, NOT `gt-deacon`. This prevents Boot
|
||||
**Critical**: Boot runs in `gt-boot`, NOT `hq-deacon`. This prevents Boot
|
||||
from conflicting with a running Deacon session.
|
||||
|
||||
## Heartbeat Mechanics
|
||||
@@ -227,15 +227,15 @@ gt deacon health-check
|
||||
|
||||
### Boot Spawns in Wrong Session
|
||||
|
||||
**Symptom**: Boot runs in `gt-deacon` instead of `gt-deacon-boot`
|
||||
**Symptom**: Boot runs in `hq-deacon` instead of `gt-boot`
|
||||
**Cause**: Session name confusion in spawn code
|
||||
**Fix**: Ensure `gt boot triage` specifies `--session=gt-deacon-boot`
|
||||
**Fix**: Ensure `gt boot triage` specifies `--session=gt-boot`
|
||||
|
||||
### Zombie Sessions Block Restart
|
||||
|
||||
**Symptom**: tmux session exists but Claude is dead
|
||||
**Cause**: Daemon checks session existence, not process health
|
||||
**Fix**: Kill zombie sessions before recreating: `gt session kill gt-deacon`
|
||||
**Fix**: Kill zombie sessions before recreating: `gt session kill hq-deacon`
|
||||
|
||||
### Status Shows Wrong State
|
||||
|
||||
@@ -250,15 +250,15 @@ The issue [gt-1847v] considered three options:
|
||||
### Option A: Keep Boot/Deacon Separation (CHOSEN)
|
||||
|
||||
- Boot is ephemeral, spawns fresh each heartbeat
|
||||
- Boot runs in `gt-deacon-boot`, exits after triage
|
||||
- Deacon runs in `gt-deacon`, continuous patrol
|
||||
- Boot runs in `gt-boot`, exits after triage
|
||||
- Deacon runs in `hq-deacon`, continuous patrol
|
||||
- Clear session boundaries, clear lifecycle
|
||||
|
||||
**Verdict**: This is the correct design. The implementation needs fixing, not the architecture.
|
||||
|
||||
### Option B: Merge Boot into Deacon (Rejected)
|
||||
|
||||
- Single `gt-deacon` session handles everything
|
||||
- Single `hq-deacon` session handles everything
|
||||
- Deacon checks "should I be awake?" internally
|
||||
|
||||
**Why rejected**:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user