feat(polecat): add bounded name pooling for polecats (gt-frs)

Implement reusable name pool for polecat workers:
- Pool of 50 names (polecat-01 through polecat-50)
- Prefers lower-numbered slots for allocation
- Overflow uses rigname-N format when pool exhausted
- Pool names are reusable, overflow names are not
- State persisted to .gastown/namepool.json

Changes:
- Add NamePool type with Allocate/Release/Reconcile
- Integrate with polecat.Manager (auto-loads/saves)
- Update gt spawn to use AllocateName() from pool
- Remove legacy polecatNames list and generatePolecatName()
- Add comprehensive tests for name pooling

Benefits:
- Tmux sessions survive polecat restarts (same name)
- Users can stay attached and see work continue
- Bounded resource usage for common case
- Scales beyond 50 with overflow naming

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-19 16:29:51 -08:00
parent 0716c4d48a
commit 4868a09e8e
4 changed files with 603 additions and 48 deletions

View File

@@ -4,7 +4,6 @@ import (
"bytes"
"encoding/json"
"fmt"
"math/rand"
"os/exec"
"path/filepath"
"strings"
@@ -22,13 +21,6 @@ import (
"github.com/steveyegge/gastown/internal/workspace"
)
// polecatNames are Mad Max: Fury Road themed names for auto-generated polecats.
var polecatNames = []string{
"Nux", "Toast", "Capable", "Cheedo", "Dag", "Rictus", "Slit", "Morsov",
"Ace", "Coma", "Valkyrie", "Keeper", "Vuvalini", "Organic", "Immortan",
"Corpus", "Doof", "Scabrous", "Splendid", "Fragile",
}
// Spawn command flags
var (
spawnIssue string
@@ -150,10 +142,13 @@ func runSpawn(cmd *cobra.Command, args []string) error {
if polecatName == "" {
polecatName, err = selectIdlePolecat(polecatMgr, r)
if err != nil {
// If --create is set, generate a new polecat name instead of failing
// If --create is set, allocate a name from the pool
if spawnCreate {
polecatName = generatePolecatName(polecatMgr)
fmt.Printf("Generated polecat name: %s\n", polecatName)
polecatName, err = polecatMgr.AllocateName()
if err != nil {
return fmt.Errorf("allocating polecat name: %w", err)
}
fmt.Printf("Allocated polecat name: %s\n", polecatName)
} else {
return fmt.Errorf("auto-select polecat: %w", err)
}
@@ -336,37 +331,6 @@ func parseSpawnAddress(addr string) (rigName, polecatName string, err error) {
return addr, "", nil
}
// generatePolecatName generates a unique polecat name that doesn't conflict with existing ones.
func generatePolecatName(mgr *polecat.Manager) string {
existing, _ := mgr.List()
existingNames := make(map[string]bool)
for _, p := range existing {
existingNames[p.Name] = true
}
// Try to find an unused name from the list
// Shuffle to avoid always picking the same name
shuffled := make([]string, len(polecatNames))
copy(shuffled, polecatNames)
rand.Shuffle(len(shuffled), func(i, j int) {
shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
})
for _, name := range shuffled {
if !existingNames[name] {
return name
}
}
// All names taken, generate one with a number suffix
base := shuffled[0]
for i := 2; ; i++ {
name := fmt.Sprintf("%s%d", base, i)
if !existingNames[name] {
return name
}
}
}
// selectIdlePolecat finds an idle polecat in the rig.
func selectIdlePolecat(mgr *polecat.Manager, r *rig.Rig) (string, error) {