Complete three-tier config migration (gt-k1lr)
- Add doctor checks for new config architecture: - SettingsCheck: Verify rigs have settings/ directory - RuntimeGitignoreCheck: Verify .runtime/ is gitignored - LegacyGastownCheck: Detect/remove old .gastown/ dirs - Update .gitignore to include .runtime/ - Update architecture.md with new directory structure - Update hq.md to clarify PGT vs GGT config locations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -28,3 +28,4 @@ gt
|
|||||||
|
|
||||||
# Runtime state
|
# Runtime state
|
||||||
state.json
|
state.json
|
||||||
|
.runtime/
|
||||||
|
|||||||
@@ -1027,9 +1027,15 @@ The HQ (town root) is created by `gt install`:
|
|||||||
│ ├── beads.db # Mayor mail, coordination, handoffs
|
│ ├── beads.db # Mayor mail, coordination, handoffs
|
||||||
│ └── config.yaml
|
│ └── config.yaml
|
||||||
│
|
│
|
||||||
|
├── .runtime/ # Town runtime state (gitignored)
|
||||||
|
│ ├── daemon.json # Daemon PID, heartbeat
|
||||||
|
│ ├── deacon.json # Deacon cycle state
|
||||||
|
│ └── agent-requests.json # Lifecycle requests
|
||||||
|
│
|
||||||
├── mayor/ # Mayor configuration and state
|
├── mayor/ # Mayor configuration and state
|
||||||
│ ├── town.json # {"type": "town", "name": "..."}
|
│ ├── town.json # {"type": "town", "name": "..."}
|
||||||
│ ├── rigs.json # Registry of managed rigs
|
│ ├── rigs.json # Registry of managed rigs
|
||||||
|
│ ├── config.json # Town-level config (theme defaults, etc.)
|
||||||
│ └── state.json # Mayor agent state
|
│ └── state.json # Mayor agent state
|
||||||
│
|
│
|
||||||
├── rigs/ # Standard location for rigs
|
├── rigs/ # Standard location for rigs
|
||||||
@@ -1042,6 +1048,7 @@ The HQ (town root) is created by `gt install`:
|
|||||||
**Notes**:
|
**Notes**:
|
||||||
- Mayor's mail is in town beads (`hq-*` issues), not JSONL files
|
- Mayor's mail is in town beads (`hq-*` issues), not JSONL files
|
||||||
- Rigs can be in `rigs/` or at HQ root (both work)
|
- Rigs can be in `rigs/` or at HQ root (both work)
|
||||||
|
- `.runtime/` is gitignored - contains ephemeral process state
|
||||||
- See [docs/hq.md](hq.md) for advanced HQ configurations
|
- See [docs/hq.md](hq.md) for advanced HQ configurations
|
||||||
|
|
||||||
### Rig Level
|
### Rig Level
|
||||||
@@ -1050,9 +1057,18 @@ Created by `gt rig add <name> <git-url>`:
|
|||||||
|
|
||||||
```
|
```
|
||||||
gastown/ # Rig = container (NOT a git clone)
|
gastown/ # Rig = container (NOT a git clone)
|
||||||
├── config.json # Rig configuration (git_url, beads prefix)
|
├── config.json # Rig identity only (type, name, git_url, beads.prefix)
|
||||||
├── .beads/ → mayor/rig/.beads # Symlink to canonical beads in Mayor
|
├── .beads/ → mayor/rig/.beads # Symlink to canonical beads in Mayor
|
||||||
│
|
│
|
||||||
|
├── settings/ # Rig behavioral config (git-tracked)
|
||||||
|
│ ├── config.json # Theme, merge_queue, max_workers
|
||||||
|
│ └── namepool.json # Pool settings (style, max)
|
||||||
|
│
|
||||||
|
├── .runtime/ # Rig runtime state (gitignored)
|
||||||
|
│ ├── witness.json # Witness process state
|
||||||
|
│ ├── refinery.json # Refinery process state
|
||||||
|
│ └── namepool-state.json # In-use names, overflow counter
|
||||||
|
│
|
||||||
├── mayor/ # Mayor's per-rig presence
|
├── mayor/ # Mayor's per-rig presence
|
||||||
│ ├── rig/ # CANONICAL clone (beads authority)
|
│ ├── rig/ # CANONICAL clone (beads authority)
|
||||||
│ │ └── .beads/ # Canonical rig beads (prefix: gt-, etc.)
|
│ │ └── .beads/ # Canonical rig beads (prefix: gt-, etc.)
|
||||||
@@ -1073,6 +1089,11 @@ gastown/ # Rig = container (NOT a git clone)
|
|||||||
└── Toast/ # Worktree from Mayor's clone
|
└── Toast/ # Worktree from Mayor's clone
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Configuration tiers:**
|
||||||
|
- **Identity** (`config.json`): Rig name, git_url, beads prefix - rarely changes
|
||||||
|
- **Settings** (`settings/`): Behavioral config - git-tracked, shareable
|
||||||
|
- **Runtime** (`.runtime/`): Process state - gitignored, transient
|
||||||
|
|
||||||
**Beads architecture:**
|
**Beads architecture:**
|
||||||
- Mayor's clone holds the canonical `.beads/` for the rig
|
- Mayor's clone holds the canonical `.beads/` for the rig
|
||||||
- Rig root symlinks `.beads/` → `mayor/rig/.beads`
|
- Rig root symlinks `.beads/` → `mayor/rig/.beads`
|
||||||
@@ -1499,11 +1520,18 @@ sequenceDiagram
|
|||||||
- Simpler role detection
|
- Simpler role detection
|
||||||
- Cleaner directory structure
|
- Cleaner directory structure
|
||||||
|
|
||||||
### 5. Visible Config Directory
|
### 5. Three-Tier Configuration Architecture
|
||||||
|
|
||||||
**Decision**: Use `config/` not `.gastown/` for town configuration.
|
**Decision**: Separate identity, settings, and runtime into distinct locations:
|
||||||
|
- **Identity** (`config.json`): Rig name, git_url, beads prefix - rarely changes
|
||||||
|
- **Settings** (`settings/`): Behavioral config - git-tracked, shareable, visible
|
||||||
|
- **Runtime** (`.runtime/`): Process state - gitignored, transient
|
||||||
|
|
||||||
**Rationale**: AI models often miss hidden directories. Visible is better.
|
**Rationale**:
|
||||||
|
- AI models often miss hidden directories (so `.gastown/` was bad)
|
||||||
|
- Identity config rarely changes; behavioral config may change often
|
||||||
|
- Runtime state should never be committed
|
||||||
|
- `settings/` is visible to agents, unlike hidden `.gastown/`
|
||||||
|
|
||||||
### 6. Rig as Container, Not Clone
|
### 6. Rig as Container, Not Clone
|
||||||
|
|
||||||
@@ -1838,9 +1866,9 @@ Work is a continuous stream - you can add new issues, spawn new workers, reprior
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### rig.json (Per-Rig Config)
|
### rig.json (Per-Rig Identity)
|
||||||
|
|
||||||
Each rig has a `config.json` at its root:
|
Each rig has a `config.json` at its root containing **identity only** (rarely changes):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -1855,6 +1883,30 @@ Each rig has a `config.json` at its root:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Behavioral settings live in `settings/config.json` (git-tracked, shareable):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"theme": "desert",
|
||||||
|
"merge_queue": {
|
||||||
|
"enabled": true,
|
||||||
|
"auto_merge": true
|
||||||
|
},
|
||||||
|
"max_workers": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Runtime state lives in `.runtime/` (gitignored, transient):
|
||||||
|
|
||||||
|
```json
|
||||||
|
// .runtime/witness.json
|
||||||
|
{
|
||||||
|
"state": "running",
|
||||||
|
"started_at": "2024-01-15T10:30:00Z",
|
||||||
|
"stats": { "polecats_spawned": 42 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
The rig's `.beads/` directory is always at the rig root. Gas Town:
|
The rig's `.beads/` directory is always at the rig root. Gas Town:
|
||||||
1. Creates `.beads/` when adding a rig (`gt rig add`)
|
1. Creates `.beads/` when adding a rig (`gt rig add`)
|
||||||
2. Runs `bd init --prefix <prefix>` to initialize it
|
2. Runs `bd init --prefix <prefix>` to initialize it
|
||||||
|
|||||||
@@ -131,12 +131,16 @@ Sometimes you need to run multiple Gas Town systems from the same parent directo
|
|||||||
If Python Gas Town (PGT) and Go Gas Town (GGT) both use `~/ai/`:
|
If Python Gas Town (PGT) and Go Gas Town (GGT) both use `~/ai/`:
|
||||||
```
|
```
|
||||||
~/ai/
|
~/ai/
|
||||||
├── .gastown/ # PGT config
|
├── .gastown/ # PGT runtime config (hidden)
|
||||||
|
├── .runtime/ # GGT runtime state (gitignored)
|
||||||
├── .beads/ # Which system owns this?
|
├── .beads/ # Which system owns this?
|
||||||
├── mayor/ # PGT mayor? GGT mayor?
|
├── mayor/ # PGT mayor? GGT mayor?
|
||||||
└── gastown/ # PGT rig? GGT rig?
|
└── gastown/ # PGT rig? GGT rig?
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note: GGT uses `.runtime/` for runtime state and `settings/` for behavioral config.
|
||||||
|
PGT uses `.gastown/` for both.
|
||||||
|
|
||||||
### Solutions
|
### Solutions
|
||||||
|
|
||||||
**Option 1: Separate HQs (recommended)**
|
**Option 1: Separate HQs (recommended)**
|
||||||
|
|||||||
@@ -70,6 +70,11 @@ func runDoctor(cmd *cobra.Command, args []string) error {
|
|||||||
d.Register(doctor.NewWispSizeCheck())
|
d.Register(doctor.NewWispSizeCheck())
|
||||||
d.Register(doctor.NewWispStaleCheck())
|
d.Register(doctor.NewWispStaleCheck())
|
||||||
|
|
||||||
|
// Config architecture checks
|
||||||
|
d.Register(doctor.NewSettingsCheck())
|
||||||
|
d.Register(doctor.NewRuntimeGitignoreCheck())
|
||||||
|
d.Register(doctor.NewLegacyGastownCheck())
|
||||||
|
|
||||||
// Run checks
|
// Run checks
|
||||||
var report *doctor.Report
|
var report *doctor.Report
|
||||||
if doctorFix {
|
if doctorFix {
|
||||||
|
|||||||
302
internal/doctor/config_check.go
Normal file
302
internal/doctor/config_check.go
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
package doctor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/steveyegge/gastown/internal/constants"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SettingsCheck verifies each rig has a settings/ directory.
|
||||||
|
type SettingsCheck struct {
|
||||||
|
FixableCheck
|
||||||
|
missingSettings []string // Cached during Run for use in Fix
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSettingsCheck creates a new settings directory check.
|
||||||
|
func NewSettingsCheck() *SettingsCheck {
|
||||||
|
return &SettingsCheck{
|
||||||
|
FixableCheck: FixableCheck{
|
||||||
|
BaseCheck: BaseCheck{
|
||||||
|
CheckName: "rig-settings",
|
||||||
|
CheckDescription: "Check that rigs have settings/ directory",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run checks if all rigs have a settings/ directory.
|
||||||
|
func (c *SettingsCheck) Run(ctx *CheckContext) *CheckResult {
|
||||||
|
rigs := c.findRigs(ctx.TownRoot)
|
||||||
|
if len(rigs) == 0 {
|
||||||
|
return &CheckResult{
|
||||||
|
Name: c.Name(),
|
||||||
|
Status: StatusOK,
|
||||||
|
Message: "No rigs found",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var missing []string
|
||||||
|
var ok int
|
||||||
|
|
||||||
|
for _, rig := range rigs {
|
||||||
|
settingsPath := constants.RigSettingsPath(rig)
|
||||||
|
if _, err := os.Stat(settingsPath); os.IsNotExist(err) {
|
||||||
|
relPath, _ := filepath.Rel(ctx.TownRoot, rig)
|
||||||
|
missing = append(missing, relPath)
|
||||||
|
} else {
|
||||||
|
ok++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache for Fix
|
||||||
|
c.missingSettings = nil
|
||||||
|
for _, rig := range rigs {
|
||||||
|
settingsPath := constants.RigSettingsPath(rig)
|
||||||
|
if _, err := os.Stat(settingsPath); os.IsNotExist(err) {
|
||||||
|
c.missingSettings = append(c.missingSettings, settingsPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missing) == 0 {
|
||||||
|
return &CheckResult{
|
||||||
|
Name: c.Name(),
|
||||||
|
Status: StatusOK,
|
||||||
|
Message: fmt.Sprintf("All %d rig(s) have settings/ directory", ok),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
details := make([]string, len(missing))
|
||||||
|
for i, m := range missing {
|
||||||
|
details[i] = fmt.Sprintf("Missing: %s/settings/", m)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CheckResult{
|
||||||
|
Name: c.Name(),
|
||||||
|
Status: StatusWarning,
|
||||||
|
Message: fmt.Sprintf("%d rig(s) missing settings/ directory", len(missing)),
|
||||||
|
Details: details,
|
||||||
|
FixHint: "Run 'gt doctor --fix' to create missing directories",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix creates missing settings/ directories.
|
||||||
|
func (c *SettingsCheck) Fix(ctx *CheckContext) error {
|
||||||
|
for _, path := range c.missingSettings {
|
||||||
|
if err := os.MkdirAll(path, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create %s: %w", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RuntimeGitignoreCheck verifies .runtime/ is gitignored at town and rig levels.
|
||||||
|
type RuntimeGitignoreCheck struct {
|
||||||
|
BaseCheck
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRuntimeGitignoreCheck creates a new runtime gitignore check.
|
||||||
|
func NewRuntimeGitignoreCheck() *RuntimeGitignoreCheck {
|
||||||
|
return &RuntimeGitignoreCheck{
|
||||||
|
BaseCheck: BaseCheck{
|
||||||
|
CheckName: "runtime-gitignore",
|
||||||
|
CheckDescription: "Check that .runtime/ directories are gitignored",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run checks if .runtime/ is properly gitignored.
|
||||||
|
func (c *RuntimeGitignoreCheck) Run(ctx *CheckContext) *CheckResult {
|
||||||
|
var issues []string
|
||||||
|
|
||||||
|
// Check town-level .gitignore
|
||||||
|
townGitignore := filepath.Join(ctx.TownRoot, ".gitignore")
|
||||||
|
if !c.containsPattern(townGitignore, ".runtime") {
|
||||||
|
issues = append(issues, "Town .gitignore missing .runtime/ pattern")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each rig's .gitignore (in their git clones)
|
||||||
|
rigs := c.findRigs(ctx.TownRoot)
|
||||||
|
for _, rig := range rigs {
|
||||||
|
// Check crew members
|
||||||
|
crewPath := filepath.Join(rig, "crew")
|
||||||
|
if crewEntries, err := os.ReadDir(crewPath); err == nil {
|
||||||
|
for _, crew := range crewEntries {
|
||||||
|
if crew.IsDir() && !strings.HasPrefix(crew.Name(), ".") {
|
||||||
|
crewGitignore := filepath.Join(crewPath, crew.Name(), ".gitignore")
|
||||||
|
if !c.containsPattern(crewGitignore, ".runtime") {
|
||||||
|
relPath, _ := filepath.Rel(ctx.TownRoot, filepath.Join(crewPath, crew.Name()))
|
||||||
|
issues = append(issues, fmt.Sprintf("%s .gitignore missing .runtime/ pattern", relPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(issues) == 0 {
|
||||||
|
return &CheckResult{
|
||||||
|
Name: c.Name(),
|
||||||
|
Status: StatusOK,
|
||||||
|
Message: ".runtime/ properly gitignored",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CheckResult{
|
||||||
|
Name: c.Name(),
|
||||||
|
Status: StatusWarning,
|
||||||
|
Message: fmt.Sprintf("%d location(s) missing .runtime gitignore", len(issues)),
|
||||||
|
Details: issues,
|
||||||
|
FixHint: "Add '.runtime/' to .gitignore files",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// containsPattern checks if a gitignore file contains a pattern.
|
||||||
|
func (c *RuntimeGitignoreCheck) containsPattern(gitignorePath, pattern string) bool {
|
||||||
|
file, err := os.Open(gitignorePath)
|
||||||
|
if err != nil {
|
||||||
|
return false // File doesn't exist
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
// Check for pattern match (with or without trailing slash, with or without glob prefix)
|
||||||
|
// Accept: .runtime, .runtime/, /.runtime, /.runtime/, **/.runtime, **/.runtime/
|
||||||
|
if line == pattern || line == pattern+"/" ||
|
||||||
|
line == "/"+pattern || line == "/"+pattern+"/" ||
|
||||||
|
line == "**/"+pattern || line == "**/"+pattern+"/" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// findRigs returns rig directories within the town.
|
||||||
|
func (c *RuntimeGitignoreCheck) findRigs(townRoot string) []string {
|
||||||
|
return findAllRigs(townRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LegacyGastownCheck warns if old .gastown/ directories still exist.
|
||||||
|
type LegacyGastownCheck struct {
|
||||||
|
FixableCheck
|
||||||
|
legacyDirs []string // Cached during Run for use in Fix
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLegacyGastownCheck creates a new legacy gastown check.
|
||||||
|
func NewLegacyGastownCheck() *LegacyGastownCheck {
|
||||||
|
return &LegacyGastownCheck{
|
||||||
|
FixableCheck: FixableCheck{
|
||||||
|
BaseCheck: BaseCheck{
|
||||||
|
CheckName: "legacy-gastown",
|
||||||
|
CheckDescription: "Check for old .gastown/ directories that should be migrated",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run checks for legacy .gastown/ directories.
|
||||||
|
func (c *LegacyGastownCheck) Run(ctx *CheckContext) *CheckResult {
|
||||||
|
var found []string
|
||||||
|
|
||||||
|
// Check town-level .gastown/
|
||||||
|
townGastown := filepath.Join(ctx.TownRoot, ".gastown")
|
||||||
|
if info, err := os.Stat(townGastown); err == nil && info.IsDir() {
|
||||||
|
found = append(found, ".gastown/ (town root)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each rig for .gastown/
|
||||||
|
rigs := c.findRigs(ctx.TownRoot)
|
||||||
|
for _, rig := range rigs {
|
||||||
|
rigGastown := filepath.Join(rig, ".gastown")
|
||||||
|
if info, err := os.Stat(rigGastown); err == nil && info.IsDir() {
|
||||||
|
relPath, _ := filepath.Rel(ctx.TownRoot, rig)
|
||||||
|
found = append(found, fmt.Sprintf("%s/.gastown/", relPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache for Fix
|
||||||
|
c.legacyDirs = nil
|
||||||
|
if info, err := os.Stat(townGastown); err == nil && info.IsDir() {
|
||||||
|
c.legacyDirs = append(c.legacyDirs, townGastown)
|
||||||
|
}
|
||||||
|
for _, rig := range rigs {
|
||||||
|
rigGastown := filepath.Join(rig, ".gastown")
|
||||||
|
if info, err := os.Stat(rigGastown); err == nil && info.IsDir() {
|
||||||
|
c.legacyDirs = append(c.legacyDirs, rigGastown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(found) == 0 {
|
||||||
|
return &CheckResult{
|
||||||
|
Name: c.Name(),
|
||||||
|
Status: StatusOK,
|
||||||
|
Message: "No legacy .gastown/ directories found",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CheckResult{
|
||||||
|
Name: c.Name(),
|
||||||
|
Status: StatusWarning,
|
||||||
|
Message: fmt.Sprintf("%d legacy .gastown/ directory(ies) found", len(found)),
|
||||||
|
Details: found,
|
||||||
|
FixHint: "Run 'gt doctor --fix' to remove after verifying migration is complete",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix removes legacy .gastown/ directories.
|
||||||
|
func (c *LegacyGastownCheck) Fix(ctx *CheckContext) error {
|
||||||
|
for _, dir := range c.legacyDirs {
|
||||||
|
if err := os.RemoveAll(dir); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// findRigs returns rig directories within the town.
|
||||||
|
func (c *LegacyGastownCheck) findRigs(townRoot string) []string {
|
||||||
|
return findAllRigs(townRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
// findRigs returns rig directories within the town.
|
||||||
|
func (c *SettingsCheck) findRigs(townRoot string) []string {
|
||||||
|
return findAllRigs(townRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
// findAllRigs is a shared helper that returns all rig directories within a town.
|
||||||
|
func findAllRigs(townRoot string) []string {
|
||||||
|
var rigs []string
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(townRoot)
|
||||||
|
if err != nil {
|
||||||
|
return rigs
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Skip non-rig directories
|
||||||
|
name := entry.Name()
|
||||||
|
if name == "mayor" || name == ".beads" || strings.HasPrefix(name, ".") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rigPath := filepath.Join(townRoot, name)
|
||||||
|
|
||||||
|
// Check if this looks like a rig (has crew/, polecats/, witness/, or refinery/)
|
||||||
|
markers := []string{"crew", "polecats", "witness", "refinery"}
|
||||||
|
for _, marker := range markers {
|
||||||
|
if _, err := os.Stat(filepath.Join(rigPath, marker)); err == nil {
|
||||||
|
rigs = append(rigs, rigPath)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rigs
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user