Move mayor files into mayor/ subdirectory

Prevents mayor-specific files (CLAUDE.md, hooks) from polluting child
agent workspaces. Child agents inherit the parent's working directory,
so keeping mayor files in a dedicated subdirectory ensures they don't
interfere with agent operations.

Includes:
- MayorDir constant in templates for consistent path handling
- Updated hooks.go, prime.go, role.go to use mayor/ paths
- Documentation updates

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
julianknutsen
2026-01-06 15:17:48 -08:00
parent 72544cc06d
commit b7b8e141b1
7 changed files with 130 additions and 8 deletions

View File

@@ -105,12 +105,14 @@ func discoverHooks(townRoot string) ([]HookInfo, error) {
var hooks []HookInfo
// Scan known locations for .claude/settings.json
// NOTE: Mayor settings are at ~/gt/mayor/.claude/, NOT ~/gt/.claude/
// Settings at town root would pollute all child workspaces.
locations := []struct {
path string
agent string
}{
{filepath.Join(townRoot, "mayor", ".claude", "settings.json"), "mayor/"},
{filepath.Join(townRoot, ".claude", "settings.json"), "town-root"},
{filepath.Join(townRoot, "deacon", ".claude", "settings.json"), "deacon/"},
}
// Scan rigs

View File

@@ -3,6 +3,8 @@ package cmd
import (
"errors"
"fmt"
"os"
"path/filepath"
"time"
"github.com/spf13/cobra"
@@ -126,9 +128,16 @@ func startMayorSession(t *tmux.Tmux, sessionName, agentOverride string) error {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
// Create session in workspace root
// Mayor runs in mayor/ subdirectory to keep its files (CLAUDE.md, settings)
// separate from child agents that inherit the working directory
mayorDir := filepath.Join(townRoot, "mayor")
if err := os.MkdirAll(mayorDir, 0755); err != nil {
return fmt.Errorf("creating mayor directory: %w", err)
}
// Create session in mayor directory
fmt.Println("Starting Mayor session...")
if err := t.NewSession(sessionName, townRoot); err != nil {
if err := t.NewSession(sessionName, mayorDir); err != nil {
return fmt.Errorf("creating session: %w", err)
}
@@ -170,7 +179,7 @@ func startMayorSession(t *tmux.Tmux, sessionName, agentOverride string) error {
// Send the propulsion nudge to trigger autonomous coordination.
// Wait for beacon to be fully processed (needs to be separate prompt)
time.Sleep(2 * time.Second)
_ = t.NudgeSession(sessionName, session.PropulsionNudgeForRole("mayor", townRoot)) // Non-fatal
_ = t.NudgeSession(sessionName, session.PropulsionNudgeForRole("mayor", mayorDir)) // Non-fatal
return nil
}

View File

@@ -147,7 +147,10 @@ func runPrime(cmd *cobra.Command, args []string) error {
}
// Ensure beads redirect exists for worktree-based roles
ensureBeadsRedirect(ctx)
// Skip if there's a role/location mismatch to avoid creating bad redirects
if !roleInfo.Mismatch {
ensureBeadsRedirect(ctx)
}
// NOTE: reportAgentState("running") removed (gt-zecmc)
// Agent liveness is observable from tmux - no need to record it in bead.

View File

@@ -270,7 +270,7 @@ func (info RoleInfo) ActorString() string {
func getRoleHome(role Role, rig, polecat, townRoot string) string {
switch role {
case RoleMayor:
return townRoot
return filepath.Join(townRoot, "mayor")
case RoleDeacon:
return filepath.Join(townRoot, "deacon")
case RoleWitness:
@@ -423,7 +423,7 @@ func runRoleList(cmd *cobra.Command, args []string) error {
name Role
desc string
}{
{RoleMayor, "Global coordinator at town root"},
{RoleMayor, "Global coordinator at mayor/"},
{RoleDeacon, "Background supervisor daemon"},
{RoleWitness, "Per-rig polecat lifecycle manager"},
{RoleRefinery, "Per-rig merge queue processor"},

View File

@@ -136,6 +136,32 @@ func (t *Templates) MessageNames() []string {
return []string{"spawn", "nudge", "escalation", "handoff"}
}
// CreateMayorCLAUDEmd creates the Mayor's CLAUDE.md file at the specified directory.
// This is used by both gt install and gt doctor --fix.
func CreateMayorCLAUDEmd(mayorDir, townRoot, townName, mayorSession, deaconSession string) error {
tmpl, err := New()
if err != nil {
return err
}
data := RoleData{
Role: "mayor",
TownRoot: townRoot,
TownName: townName,
WorkDir: mayorDir,
MayorSession: mayorSession,
DeaconSession: deaconSession,
}
content, err := tmpl.RenderRole("mayor", data)
if err != nil {
return err
}
claudePath := filepath.Join(mayorDir, "CLAUDE.md")
return os.WriteFile(claudePath, []byte(content), 0644)
}
// GetAllRoleTemplates returns all role templates as a map of filename to content.
func GetAllRoleTemplates() (map[string][]byte, error) {
entries, err := templateFS.ReadDir("roles")