feat(namepool): auto-select theme per rig based on name hash

Each rig now gets a deterministic theme based on its name instead of
always defaulting to mad-max. Uses a prime multiplier hash (×31) for
good distribution across themes. Same rig name always gets the same
theme. Users can still override with `gt namepool set`.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/joe
2026-01-16 15:35:10 -08:00
committed by Steve Yegge
parent fbc67e89e1
commit 74050cd0ab
3 changed files with 51 additions and 12 deletions

View File

@@ -529,8 +529,9 @@ func TestReconcilePoolWith(t *testing.T) {
defer func() { _ = os.RemoveAll(tmpDir) }()
// Create rig and manager (nil tmux for unit test)
// Use "myrig" which hashes to mad-max theme
r := &rig.Rig{
Name: "testrig",
Name: "myrig",
Path: tmpDir,
}
m := NewManager(r, nil, nil)
@@ -591,8 +592,9 @@ func TestReconcilePoolWith_Allocation(t *testing.T) {
}
defer func() { _ = os.RemoveAll(tmpDir) }()
// Use "myrig" which hashes to mad-max theme
r := &rig.Rig{
Name: "testrig",
Name: "myrig",
Path: tmpDir,
}
m := NewManager(r, nil, nil)
@@ -627,8 +629,9 @@ func TestReconcilePoolWith_OrphanDoesNotBlockAllocation(t *testing.T) {
}
defer func() { _ = os.RemoveAll(tmpDir) }()
// Use "myrig" which hashes to mad-max theme
r := &rig.Rig{
Name: "testrig",
Name: "myrig",
Path: tmpDir,
}
m := NewManager(r, nil, nil)

View File

@@ -103,7 +103,7 @@ type NamePool struct {
func NewNamePool(rigPath, rigName string) *NamePool {
return &NamePool{
RigName: rigName,
Theme: DefaultTheme,
Theme: ThemeForRig(rigName),
InUse: make(map[string]bool),
OverflowNext: DefaultPoolSize + 1,
MaxSize: DefaultPoolSize,
@@ -352,6 +352,21 @@ func ListThemes() []string {
return themes
}
// ThemeForRig returns a deterministic theme for a rig based on its name.
// This provides variety across rigs without requiring manual configuration.
func ThemeForRig(rigName string) string {
themes := ListThemes()
if len(themes) == 0 {
return DefaultTheme
}
// Hash using prime multiplier for better distribution
var hash uint32
for _, b := range []byte(rigName) {
hash = hash*31 + uint32(b)
}
return themes[hash%uint32(len(themes))]
}
// GetThemeNames returns the names in a specific theme.
func GetThemeNames(theme string) ([]string, error) {
if names, ok := BuiltinThemes[theme]; ok {

View File

@@ -13,7 +13,7 @@ func TestNamePool_Allocate(t *testing.T) {
}
defer func() { _ = os.RemoveAll(tmpDir) }()
pool := NewNamePool(tmpDir, "testrig")
pool := NewNamePoolWithConfig(tmpDir, "testrig", "mad-max", nil, DefaultPoolSize)
// First allocation should be first themed name (furiosa)
name, err := pool.Allocate()
@@ -41,7 +41,7 @@ func TestNamePool_Release(t *testing.T) {
}
defer func() { _ = os.RemoveAll(tmpDir) }()
pool := NewNamePool(tmpDir, "testrig")
pool := NewNamePoolWithConfig(tmpDir, "testrig", "mad-max", nil, DefaultPoolSize)
// Allocate first two
name1, _ := pool.Allocate()
@@ -68,7 +68,7 @@ func TestNamePool_PrefersOrder(t *testing.T) {
}
defer func() { _ = os.RemoveAll(tmpDir) }()
pool := NewNamePool(tmpDir, "testrig")
pool := NewNamePoolWithConfig(tmpDir, "testrig", "mad-max", nil, DefaultPoolSize)
// Allocate first 5
for i := 0; i < 5; i++ {
@@ -209,7 +209,7 @@ func TestNamePool_Reconcile(t *testing.T) {
}
defer func() { _ = os.RemoveAll(tmpDir) }()
pool := NewNamePool(tmpDir, "testrig")
pool := NewNamePoolWithConfig(tmpDir, "testrig", "mad-max", nil, DefaultPoolSize)
// Simulate existing polecats from filesystem
existing := []string{"slit", "valkyrie", "some-other-name"}
@@ -234,7 +234,7 @@ func TestNamePool_IsPoolName(t *testing.T) {
}
defer func() { _ = os.RemoveAll(tmpDir) }()
pool := NewNamePool(tmpDir, "testrig")
pool := NewNamePoolWithConfig(tmpDir, "testrig", "mad-max", nil, DefaultPoolSize)
tests := []struct {
name string
@@ -263,7 +263,7 @@ func TestNamePool_ActiveNames(t *testing.T) {
}
defer func() { _ = os.RemoveAll(tmpDir) }()
pool := NewNamePool(tmpDir, "testrig")
pool := NewNamePoolWithConfig(tmpDir, "testrig", "mad-max", nil, DefaultPoolSize)
pool.Allocate() // furiosa
pool.Allocate() // nux
@@ -287,7 +287,7 @@ func TestNamePool_MarkInUse(t *testing.T) {
}
defer func() { _ = os.RemoveAll(tmpDir) }()
pool := NewNamePool(tmpDir, "testrig")
pool := NewNamePoolWithConfig(tmpDir, "testrig", "mad-max", nil, DefaultPoolSize)
// Mark some slots as in use
pool.MarkInUse("dementus")
@@ -417,7 +417,7 @@ func TestNamePool_Reset(t *testing.T) {
}
defer func() { _ = os.RemoveAll(tmpDir) }()
pool := NewNamePool(tmpDir, "testrig")
pool := NewNamePoolWithConfig(tmpDir, "testrig", "mad-max", nil, DefaultPoolSize)
// Allocate several names
for i := 0; i < 10; i++ {
@@ -441,3 +441,24 @@ func TestNamePool_Reset(t *testing.T) {
t.Errorf("expected furiosa after reset, got %s", name)
}
}
func TestThemeForRig(t *testing.T) {
// Different rigs should get different themes (with high probability)
themes := make(map[string]bool)
for _, rigName := range []string{"gastown", "beads", "myproject", "webapp"} {
themes[ThemeForRig(rigName)] = true
}
// Should have at least 2 different themes across 4 rigs
if len(themes) < 2 {
t.Errorf("expected variety in themes, got only %d unique theme(s)", len(themes))
}
}
func TestThemeForRigDeterministic(t *testing.T) {
// Same rig name should always get same theme
theme1 := ThemeForRig("myrig")
theme2 := ThemeForRig("myrig")
if theme1 != theme2 {
t.Errorf("theme not deterministic: got %q and %q", theme1, theme2)
}
}