Install Claude settings during rig and HQ creation
Creates settings.json automatically during initial setup, so Claude settings are available immediately on launch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,24 +7,38 @@ Technical reference for Gas Town internals. Read the README first.
|
|||||||
```
|
```
|
||||||
~/gt/ Town root
|
~/gt/ Town root
|
||||||
├── .beads/ Town-level beads (hq-* prefix)
|
├── .beads/ Town-level beads (hq-* prefix)
|
||||||
├── mayor/ Mayor config
|
├── mayor/ Mayor agent home (town coordinator)
|
||||||
│ └── town.json
|
│ ├── town.json Town configuration
|
||||||
|
│ ├── CLAUDE.md Mayor context (on disk)
|
||||||
|
│ └── .claude/settings.json Mayor Claude settings
|
||||||
|
├── deacon/ Deacon agent home (background supervisor)
|
||||||
|
│ └── .claude/settings.json Deacon settings (context via gt prime)
|
||||||
└── <rig>/ Project container (NOT a git clone)
|
└── <rig>/ Project container (NOT a git clone)
|
||||||
├── config.json Rig identity
|
├── config.json Rig identity
|
||||||
├── .beads/ → mayor/rig/.beads
|
├── .beads/ → mayor/rig/.beads
|
||||||
├── .repo.git/ Bare repo (shared by worktrees)
|
├── .repo.git/ Bare repo (shared by worktrees)
|
||||||
├── mayor/rig/ Mayor's clone (canonical beads)
|
├── mayor/rig/ Mayor's clone (canonical beads)
|
||||||
├── refinery/rig/ Worktree on main
|
│ └── CLAUDE.md Per-rig mayor context (on disk)
|
||||||
├── witness/ No clone (monitors only)
|
├── witness/ Witness agent home (monitors only)
|
||||||
├── crew/<name>/ Human workspaces
|
│ └── .claude/settings.json (context via gt prime)
|
||||||
└── polecats/<name>/ Worker worktrees
|
├── refinery/ Refinery settings parent
|
||||||
|
│ ├── .claude/settings.json
|
||||||
|
│ └── rig/ Worktree on main
|
||||||
|
│ └── CLAUDE.md Refinery context (on disk)
|
||||||
|
├── crew/ Crew settings parent (shared)
|
||||||
|
│ ├── .claude/settings.json (context via gt prime)
|
||||||
|
│ └── <name>/rig/ Human workspaces
|
||||||
|
└── polecats/ Polecat settings parent (shared)
|
||||||
|
├── .claude/settings.json (context via gt prime)
|
||||||
|
└── <name>/rig/ Worker worktrees
|
||||||
```
|
```
|
||||||
|
|
||||||
**Key points:**
|
**Key points:**
|
||||||
|
|
||||||
- Rig root is a container, not a clone
|
- Rig root is a container, not a clone
|
||||||
- `.repo.git/` is bare - refinery and polecats are worktrees
|
- `.repo.git/` is bare - refinery and polecats are worktrees
|
||||||
- Mayor clone holds canonical `.beads/`, others inherit via redirect
|
- Per-rig `mayor/rig/` holds canonical `.beads/`, others inherit via redirect
|
||||||
|
- Settings placed in parent dirs (not git clones) for upward traversal
|
||||||
|
|
||||||
## Beads Routing
|
## Beads Routing
|
||||||
|
|
||||||
@@ -213,46 +227,81 @@ Understanding this hierarchy is essential for proper configuration.
|
|||||||
|
|
||||||
| Role | Working Directory | Notes |
|
| Role | Working Directory | Notes |
|
||||||
|------|-------------------|-------|
|
|------|-------------------|-------|
|
||||||
| **Mayor** | `~/gt/mayor/` | Isolated from child agents |
|
| **Mayor** | `~/gt/mayor/` | Town-level coordinator, isolated from rigs |
|
||||||
| **Deacon** | `~/gt/deacon/` | Background supervisor |
|
| **Deacon** | `~/gt/deacon/` | Background supervisor daemon |
|
||||||
| **Witness** | `~/gt/<rig>/witness/` | No git clone, monitors only |
|
| **Witness** | `~/gt/<rig>/witness/` | No git clone, monitors polecats only |
|
||||||
| **Refinery** | `~/gt/<rig>/refinery/rig/` | Worktree on main branch |
|
| **Refinery** | `~/gt/<rig>/refinery/rig/` | Worktree on main branch |
|
||||||
| **Crew** | `~/gt/<rig>/crew/<name>/rig/` | Persistent clone |
|
| **Crew** | `~/gt/<rig>/crew/<name>/rig/` | Persistent human workspace clone |
|
||||||
| **Polecat** | `~/gt/<rig>/polecats/<name>/rig/` | Ephemeral worktree |
|
| **Polecat** | `~/gt/<rig>/polecats/<name>/rig/` | Ephemeral worker worktree |
|
||||||
|
|
||||||
|
Note: The per-rig `<rig>/mayor/rig/` directory is NOT a working directory—it's
|
||||||
|
a git clone that holds the canonical `.beads/` database for that rig.
|
||||||
|
|
||||||
### Settings File Locations
|
### Settings File Locations
|
||||||
|
|
||||||
Claude Code searches for `.claude/settings.json` starting from the working
|
Claude Code searches for `.claude/settings.json` starting from the working
|
||||||
directory and traversing upward. Each agent has settings at its working directory:
|
directory and traversing upward. Settings are placed in **parent directories**
|
||||||
|
(not inside git clones) so they're found via directory traversal without
|
||||||
|
polluting source repositories:
|
||||||
|
|
||||||
```
|
```
|
||||||
~/gt/
|
~/gt/
|
||||||
├── mayor/.claude/settings.json # Mayor settings
|
├── mayor/.claude/settings.json # Mayor settings
|
||||||
├── deacon/.claude/settings.json # Deacon settings
|
├── deacon/.claude/settings.json # Deacon settings
|
||||||
└── <rig>/
|
└── <rig>/
|
||||||
├── witness/.claude/settings.json # Witness settings
|
├── witness/.claude/settings.json # Witness settings (no rig/ subdir)
|
||||||
├── refinery/rig/.claude/settings.json
|
├── refinery/.claude/settings.json # Found by refinery/rig/ via traversal
|
||||||
├── crew/<name>/rig/.claude/settings.json
|
├── crew/.claude/settings.json # Shared by all crew/<name>/rig/
|
||||||
└── polecats/<name>/rig/.claude/settings.json
|
└── polecats/.claude/settings.json # Shared by all polecats/<name>/rig/
|
||||||
```
|
```
|
||||||
|
|
||||||
**Why this structure?** Child agents inherit the parent's working directory
|
**Why parent directories?** Agents working in git clones (like `refinery/rig/`)
|
||||||
when spawned. By keeping each role's files in separate directories, we prevent
|
would pollute the source repo if settings were placed there. By putting settings
|
||||||
the mayor's CLAUDE.md or settings from affecting polecat behavior.
|
one level up, Claude finds them via upward traversal, and all workers of the
|
||||||
|
same type share the same settings.
|
||||||
|
|
||||||
|
### CLAUDE.md Locations
|
||||||
|
|
||||||
|
Role context is delivered via CLAUDE.md files or ephemeral injection:
|
||||||
|
|
||||||
|
| Role | CLAUDE.md Location | Method |
|
||||||
|
|------|-------------------|--------|
|
||||||
|
| **Mayor** | `~/gt/mayor/CLAUDE.md` | On disk |
|
||||||
|
| **Deacon** | (none) | Injected via `gt prime` at SessionStart |
|
||||||
|
| **Witness** | (none) | Injected via `gt prime` at SessionStart |
|
||||||
|
| **Refinery** | `<rig>/refinery/rig/CLAUDE.md` | On disk (inside worktree) |
|
||||||
|
| **Crew** | (none) | Injected via `gt prime` at SessionStart |
|
||||||
|
| **Polecat** | (none) | Injected via `gt prime` at SessionStart |
|
||||||
|
|
||||||
|
Additionally, each rig has `<rig>/mayor/rig/CLAUDE.md` for the per-rig mayor clone
|
||||||
|
(used for beads operations, not a running agent).
|
||||||
|
|
||||||
|
**Why ephemeral injection?** Writing CLAUDE.md into git clones would:
|
||||||
|
1. Pollute source repos when agents commit/push
|
||||||
|
2. Leak Gas Town internals into project history
|
||||||
|
3. Conflict with project-specific CLAUDE.md files
|
||||||
|
|
||||||
|
The `gt prime` command runs at SessionStart hook and injects context without
|
||||||
|
persisting it to disk.
|
||||||
|
|
||||||
### Sparse Checkout (Source Repo Isolation)
|
### Sparse Checkout (Source Repo Isolation)
|
||||||
|
|
||||||
When agents work on source repositories that have their own `.claude/` directory,
|
When agents work on source repositories that have their own Claude Code configuration,
|
||||||
Gas Town uses git sparse checkout to exclude it:
|
Gas Town uses git sparse checkout to exclude all context files:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Automatically configured for worktrees
|
# Automatically configured for worktrees - excludes:
|
||||||
git sparse-checkout set --no-cone '/*' '!/.claude/'
|
# - .claude/ : settings, rules, agents, commands
|
||||||
|
# - CLAUDE.md : primary context file
|
||||||
|
# - CLAUDE.local.md: personal context file
|
||||||
|
# - .mcp.json : MCP server configuration
|
||||||
|
git sparse-checkout set --no-cone '/*' '!/.claude/' '!/CLAUDE.md' '!/CLAUDE.local.md' '!/.mcp.json'
|
||||||
```
|
```
|
||||||
|
|
||||||
This ensures the agent uses Gas Town's settings, not the source repo's.
|
This ensures agents use Gas Town's context, not the source repo's instructions.
|
||||||
|
|
||||||
**Doctor check**: `gt doctor` verifies sparse checkout is configured correctly.
|
**Doctor check**: `gt doctor` verifies sparse checkout is configured correctly.
|
||||||
|
Run `gt doctor --fix` to update legacy configurations missing the newer patterns.
|
||||||
|
|
||||||
### Settings Inheritance
|
### Settings Inheritance
|
||||||
|
|
||||||
|
|||||||
@@ -172,20 +172,37 @@ func runInstall(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
fmt.Printf(" ✓ Created mayor/rigs.json\n")
|
fmt.Printf(" ✓ Created mayor/rigs.json\n")
|
||||||
|
|
||||||
// Create Mayor CLAUDE.md at HQ root (Mayor runs from there)
|
// Create Mayor CLAUDE.md at mayor/ (Mayor's canonical home)
|
||||||
if err := createMayorCLAUDEmd(absPath, absPath); err != nil {
|
// IMPORTANT: CLAUDE.md must be in ~/gt/mayor/, NOT ~/gt/
|
||||||
|
// CLAUDE.md at town root would be inherited by ALL agents via directory traversal,
|
||||||
|
// causing crew/polecat/etc to receive Mayor-specific instructions.
|
||||||
|
if err := createMayorCLAUDEmd(mayorDir, absPath); err != nil {
|
||||||
fmt.Printf(" %s Could not create CLAUDE.md: %v\n", style.Dim.Render("⚠"), err)
|
fmt.Printf(" %s Could not create CLAUDE.md: %v\n", style.Dim.Render("⚠"), err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" ✓ Created CLAUDE.md\n")
|
fmt.Printf(" ✓ Created mayor/CLAUDE.md\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure Mayor has Claude settings with SessionStart hooks.
|
// Create mayor settings (mayor runs from ~/gt/mayor/)
|
||||||
// This ensures gt prime runs on Claude startup, which outputs the Mayor
|
// IMPORTANT: Settings must be in ~/gt/mayor/.claude/, NOT ~/gt/.claude/
|
||||||
// delegation protocol - critical for preventing direct implementation.
|
// Settings at town root would be found by ALL agents via directory traversal,
|
||||||
if err := claude.EnsureSettingsForRole(absPath, "mayor"); err != nil {
|
// causing crew/polecat/etc to cd to town root before running commands.
|
||||||
fmt.Printf(" %s Could not create .claude/settings.json: %v\n", style.Dim.Render("⚠"), err)
|
// mayorDir already defined above
|
||||||
|
if err := os.MkdirAll(mayorDir, 0755); err != nil {
|
||||||
|
fmt.Printf(" %s Could not create mayor directory: %v\n", style.Dim.Render("⚠"), err)
|
||||||
|
} else if err := claude.EnsureSettingsForRole(mayorDir, "mayor"); err != nil {
|
||||||
|
fmt.Printf(" %s Could not create mayor settings: %v\n", style.Dim.Render("⚠"), err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" ✓ Created .claude/settings.json\n")
|
fmt.Printf(" ✓ Created mayor/.claude/settings.json\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create deacon directory and settings (deacon runs from ~/gt/deacon/)
|
||||||
|
deaconDir := filepath.Join(absPath, "deacon")
|
||||||
|
if err := os.MkdirAll(deaconDir, 0755); err != nil {
|
||||||
|
fmt.Printf(" %s Could not create deacon directory: %v\n", style.Dim.Render("⚠"), err)
|
||||||
|
} else if err := claude.EnsureSettingsForRole(deaconDir, "deacon"); err != nil {
|
||||||
|
fmt.Printf(" %s Could not create deacon settings: %v\n", style.Dim.Render("⚠"), err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ✓ Created deacon/.claude/settings.json\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize git BEFORE beads so that bd can compute repository fingerprint.
|
// Initialize git BEFORE beads so that bd can compute repository fingerprint.
|
||||||
@@ -260,32 +277,15 @@ func runInstall(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createMayorCLAUDEmd(hqRoot, townRoot string) error {
|
func createMayorCLAUDEmd(mayorDir, townRoot string) error {
|
||||||
tmpl, err := templates.New()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get town name for session names
|
|
||||||
townName, _ := workspace.GetTownName(townRoot)
|
townName, _ := workspace.GetTownName(townRoot)
|
||||||
|
return templates.CreateMayorCLAUDEmd(
|
||||||
data := templates.RoleData{
|
mayorDir,
|
||||||
Role: "mayor",
|
townRoot,
|
||||||
TownRoot: townRoot,
|
townName,
|
||||||
TownName: townName,
|
session.MayorSessionName(),
|
||||||
WorkDir: hqRoot,
|
session.DeaconSessionName(),
|
||||||
DefaultBranch: "main", // Mayor doesn't merge, but field required
|
)
|
||||||
MayorSession: session.MayorSessionName(),
|
|
||||||
DeaconSession: session.DeaconSessionName(),
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := tmpl.RenderRole("mayor", data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
claudePath := filepath.Join(hqRoot, "CLAUDE.md")
|
|
||||||
return os.WriteFile(claudePath, []byte(content), 0644)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeJSON(path string, data interface{}) error {
|
func writeJSON(path string, data interface{}) error {
|
||||||
|
|||||||
@@ -241,32 +241,6 @@ func CommandNames() ([]string, error) {
|
|||||||
return names, nil
|
return names, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasCommands checks if a workspace has the .claude/commands/ directory provisioned.
|
// HasCommands checks if a workspace has the .claude/commands/ directory provisioned.
|
||||||
func HasCommands(workspacePath string) bool {
|
func HasCommands(workspacePath string) bool {
|
||||||
commandsDir := filepath.Join(workspacePath, ".claude", "commands")
|
commandsDir := filepath.Join(workspacePath, ".claude", "commands")
|
||||||
|
|||||||
Reference in New Issue
Block a user