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:
@@ -204,6 +204,88 @@ gt mol step done <step> # Complete a molecule step
|
|||||||
| `GT_RIG` | Rig name for rig-level agents |
|
| `GT_RIG` | Rig name for rig-level agents |
|
||||||
| `GT_POLECAT` | Polecat name (for polecats only) |
|
| `GT_POLECAT` | Polecat name (for polecats only) |
|
||||||
|
|
||||||
|
## Agent Working Directories and Settings
|
||||||
|
|
||||||
|
Each agent runs in a specific working directory and has its own Claude settings.
|
||||||
|
Understanding this hierarchy is essential for proper configuration.
|
||||||
|
|
||||||
|
### Working Directories by Role
|
||||||
|
|
||||||
|
| Role | Working Directory | Notes |
|
||||||
|
|------|-------------------|-------|
|
||||||
|
| **Mayor** | `~/gt/mayor/` | Isolated from child agents |
|
||||||
|
| **Deacon** | `~/gt/deacon/` | Background supervisor |
|
||||||
|
| **Witness** | `~/gt/<rig>/witness/` | No git clone, monitors only |
|
||||||
|
| **Refinery** | `~/gt/<rig>/refinery/rig/` | Worktree on main branch |
|
||||||
|
| **Crew** | `~/gt/<rig>/crew/<name>/rig/` | Persistent clone |
|
||||||
|
| **Polecat** | `~/gt/<rig>/polecats/<name>/rig/` | Ephemeral worktree |
|
||||||
|
|
||||||
|
### Settings File Locations
|
||||||
|
|
||||||
|
Claude Code searches for `.claude/settings.json` starting from the working
|
||||||
|
directory and traversing upward. Each agent has settings at its working directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/gt/
|
||||||
|
├── mayor/.claude/settings.json # Mayor settings
|
||||||
|
├── deacon/.claude/settings.json # Deacon settings
|
||||||
|
└── <rig>/
|
||||||
|
├── witness/.claude/settings.json # Witness settings
|
||||||
|
├── refinery/rig/.claude/settings.json
|
||||||
|
├── crew/<name>/rig/.claude/settings.json
|
||||||
|
└── polecats/<name>/rig/.claude/settings.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this structure?** Child agents inherit the parent's working directory
|
||||||
|
when spawned. By keeping each role's files in separate directories, we prevent
|
||||||
|
the mayor's CLAUDE.md or settings from affecting polecat behavior.
|
||||||
|
|
||||||
|
### Sparse Checkout (Source Repo Isolation)
|
||||||
|
|
||||||
|
When agents work on source repositories that have their own `.claude/` directory,
|
||||||
|
Gas Town uses git sparse checkout to exclude it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Automatically configured for worktrees
|
||||||
|
git sparse-checkout set --no-cone '/*' '!/.claude/'
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures the agent uses Gas Town's settings, not the source repo's.
|
||||||
|
|
||||||
|
**Doctor check**: `gt doctor` verifies sparse checkout is configured correctly.
|
||||||
|
|
||||||
|
### Settings Inheritance
|
||||||
|
|
||||||
|
Claude Code's settings search order (first match wins):
|
||||||
|
|
||||||
|
1. `.claude/settings.json` in current working directory
|
||||||
|
2. `.claude/settings.json` in parent directories (traversing up)
|
||||||
|
3. `~/.claude/settings.json` (user global settings)
|
||||||
|
|
||||||
|
Gas Town places settings at each agent's working directory root, so agents
|
||||||
|
find their role-specific settings before reaching any parent or global config.
|
||||||
|
|
||||||
|
### Settings Templates
|
||||||
|
|
||||||
|
Gas Town uses two settings templates based on role type:
|
||||||
|
|
||||||
|
| Type | Roles | Key Difference |
|
||||||
|
|------|-------|----------------|
|
||||||
|
| **Interactive** | Mayor, Crew | Mail injected on `UserPromptSubmit` hook |
|
||||||
|
| **Autonomous** | Polecat, Witness, Refinery, Deacon | Mail injected on `SessionStart` hook |
|
||||||
|
|
||||||
|
Autonomous agents may start without user input, so they need mail checked
|
||||||
|
at session start. Interactive agents wait for user prompts.
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
| Problem | Solution |
|
||||||
|
|---------|----------|
|
||||||
|
| Agent using wrong settings | Check `gt doctor`, verify sparse checkout |
|
||||||
|
| Settings not found | Ensure `.claude/settings.json` exists at role home |
|
||||||
|
| Source repo settings leaking | Run `gt doctor --fix` to configure sparse checkout |
|
||||||
|
| Mayor settings affecting polecats | Mayor should run in `mayor/`, not town root |
|
||||||
|
|
||||||
## CLI Reference
|
## CLI Reference
|
||||||
|
|
||||||
### Town Management
|
### Town Management
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ These roles manage the Gas Town system itself:
|
|||||||
|
|
||||||
| Role | Description | Lifecycle |
|
| Role | Description | Lifecycle |
|
||||||
|------|-------------|-----------|
|
|------|-------------|-----------|
|
||||||
| **Mayor** | Global coordinator at town root | Singleton, persistent |
|
| **Mayor** | Global coordinator at mayor/ | Singleton, persistent |
|
||||||
| **Deacon** | Background supervisor daemon ([watchdog chain](watchdog-chain.md)) | Singleton, persistent |
|
| **Deacon** | Background supervisor daemon ([watchdog chain](watchdog-chain.md)) | Singleton, persistent |
|
||||||
| **Witness** | Per-rig polecat lifecycle manager | One per rig, persistent |
|
| **Witness** | Per-rig polecat lifecycle manager | One per rig, persistent |
|
||||||
| **Refinery** | Per-rig merge queue processor | One per rig, persistent |
|
| **Refinery** | Per-rig merge queue processor | One per rig, persistent |
|
||||||
|
|||||||
@@ -105,12 +105,14 @@ func discoverHooks(townRoot string) ([]HookInfo, error) {
|
|||||||
var hooks []HookInfo
|
var hooks []HookInfo
|
||||||
|
|
||||||
// Scan known locations for .claude/settings.json
|
// 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 {
|
locations := []struct {
|
||||||
path string
|
path string
|
||||||
agent string
|
agent string
|
||||||
}{
|
}{
|
||||||
{filepath.Join(townRoot, "mayor", ".claude", "settings.json"), "mayor/"},
|
{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
|
// Scan rigs
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"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)
|
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...")
|
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)
|
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.
|
// Send the propulsion nudge to trigger autonomous coordination.
|
||||||
// Wait for beacon to be fully processed (needs to be separate prompt)
|
// Wait for beacon to be fully processed (needs to be separate prompt)
|
||||||
time.Sleep(2 * time.Second)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,10 @@ func runPrime(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure beads redirect exists for worktree-based roles
|
// 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)
|
// NOTE: reportAgentState("running") removed (gt-zecmc)
|
||||||
// Agent liveness is observable from tmux - no need to record it in bead.
|
// Agent liveness is observable from tmux - no need to record it in bead.
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ func (info RoleInfo) ActorString() string {
|
|||||||
func getRoleHome(role Role, rig, polecat, townRoot string) string {
|
func getRoleHome(role Role, rig, polecat, townRoot string) string {
|
||||||
switch role {
|
switch role {
|
||||||
case RoleMayor:
|
case RoleMayor:
|
||||||
return townRoot
|
return filepath.Join(townRoot, "mayor")
|
||||||
case RoleDeacon:
|
case RoleDeacon:
|
||||||
return filepath.Join(townRoot, "deacon")
|
return filepath.Join(townRoot, "deacon")
|
||||||
case RoleWitness:
|
case RoleWitness:
|
||||||
@@ -423,7 +423,7 @@ func runRoleList(cmd *cobra.Command, args []string) error {
|
|||||||
name Role
|
name Role
|
||||||
desc string
|
desc string
|
||||||
}{
|
}{
|
||||||
{RoleMayor, "Global coordinator at town root"},
|
{RoleMayor, "Global coordinator at mayor/"},
|
||||||
{RoleDeacon, "Background supervisor daemon"},
|
{RoleDeacon, "Background supervisor daemon"},
|
||||||
{RoleWitness, "Per-rig polecat lifecycle manager"},
|
{RoleWitness, "Per-rig polecat lifecycle manager"},
|
||||||
{RoleRefinery, "Per-rig merge queue processor"},
|
{RoleRefinery, "Per-rig merge queue processor"},
|
||||||
|
|||||||
@@ -136,6 +136,32 @@ func (t *Templates) MessageNames() []string {
|
|||||||
return []string{"spawn", "nudge", "escalation", "handoff"}
|
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.
|
// GetAllRoleTemplates returns all role templates as a map of filename to content.
|
||||||
func GetAllRoleTemplates() (map[string][]byte, error) {
|
func GetAllRoleTemplates() (map[string][]byte, error) {
|
||||||
entries, err := templateFS.ReadDir("roles")
|
entries, err := templateFS.ReadDir("roles")
|
||||||
|
|||||||
Reference in New Issue
Block a user