Skip to content

Commit 2484936

Browse files
steveyeggeclaude
andcommitted
refactor: add role registry with autonomous/emoji properties from TOML configs (gt-2e5q)
Create IsAutonomous() and RoleEmoji() in config package that read from embedded TOML role definitions, establishing a single source of truth for role properties. Replace 3 duplicate hardcoded switch blocks: - claude/settings.go RoleTypeFor() - gemini/settings.go RoleTypeFor() - runtime/runtime.go isAutonomousRole() All now delegate to config.IsAutonomous() instead of maintaining parallel role lists. Boot role handled as deacon variant in the registry. Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent 0e63573 commit 2484936

12 files changed

Lines changed: 134 additions & 25 deletions

File tree

internal/claude/settings.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"fmt"
77
"os"
88
"path/filepath"
9+
10+
"github.com/steveyegge/gastown/internal/config"
911
)
1012

1113
//go:embed config/*.json
@@ -25,13 +27,13 @@ const (
2527
)
2628

2729
// RoleTypeFor returns the RoleType for a given role name.
30+
// Autonomous/interactive classification is driven by the TOML role definitions
31+
// in internal/config/roles/*.toml (single source of truth).
2832
func RoleTypeFor(role string) RoleType {
29-
switch role {
30-
case "polecat", "witness", "refinery", "deacon", "boot":
33+
if config.IsAutonomous(role) {
3134
return Autonomous
32-
default:
33-
return Interactive
3435
}
36+
return Interactive
3537
}
3638

3739
// EnsureSettings ensures .claude/settings.json exists in the given directory.

internal/config/roles.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"path/filepath"
99
"strings"
10+
"sync"
1011
"time"
1112

1213
"github.com/BurntSushi/toml"
@@ -24,6 +25,13 @@ type RoleDefinition struct {
2425
// Scope is "town" or "rig" - determines where the agent runs.
2526
Scope string `toml:"scope"`
2627

28+
// Autonomous indicates the role runs without user input (e.g., polecat, witness).
29+
// Non-autonomous (interactive) roles wait for user input (e.g., mayor, crew).
30+
Autonomous bool `toml:"autonomous"`
31+
32+
// Emoji is the visual identifier for this role in TUI and status displays.
33+
Emoji string `toml:"emoji,omitempty"`
34+
2735
// Session contains tmux session configuration.
2836
Session RoleSessionConfig `toml:"session"`
2937

@@ -115,6 +123,56 @@ func RigRoles() []string {
115123
return []string{"witness", "refinery", "polecat", "crew"}
116124
}
117125

126+
// IsAutonomous returns whether a role runs without user input.
127+
// Autonomous roles (polecat, witness, refinery, deacon, boot, dog) are triggered
128+
// externally. Interactive roles (mayor, crew) wait for user input.
129+
// This reads from the embedded TOML role definitions (single source of truth).
130+
func IsAutonomous(role string) bool {
131+
rolePropsOnce.Do(loadRoleProps)
132+
if props, ok := roleProps[role]; ok {
133+
return props.autonomous
134+
}
135+
return false
136+
}
137+
138+
// RoleEmoji returns the emoji for a given role name from the TOML role definitions.
139+
// Returns "❓" for unknown roles.
140+
func RoleEmoji(role string) string {
141+
rolePropsOnce.Do(loadRoleProps)
142+
if props, ok := roleProps[role]; ok && props.emoji != "" {
143+
return props.emoji
144+
}
145+
return "❓"
146+
}
147+
148+
type roleProperties struct {
149+
autonomous bool
150+
emoji string
151+
}
152+
153+
var (
154+
roleProps map[string]roleProperties
155+
rolePropsOnce sync.Once
156+
)
157+
158+
func loadRoleProps() {
159+
roleProps = make(map[string]roleProperties)
160+
for _, role := range AllRoles() {
161+
def, err := loadBuiltinRoleDefinition(role)
162+
if err != nil {
163+
continue
164+
}
165+
roleProps[role] = roleProperties{
166+
autonomous: def.Autonomous,
167+
emoji: def.Emoji,
168+
}
169+
}
170+
// "boot" is a deacon variant with no separate TOML — inherit from deacon.
171+
if deacon, ok := roleProps["deacon"]; ok {
172+
roleProps["boot"] = deacon
173+
}
174+
}
175+
118176
// isValidRoleName checks if the given name is a known role.
119177
func isValidRoleName(name string) bool {
120178
for _, r := range AllRoles() {

internal/config/roles/crew.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
role = "crew"
55
scope = "rig"
6+
emoji = "👷"
67
nudge = "Check your hook and mail, then act accordingly."
78
prompt_template = "crew.md.tmpl"
89

internal/config/roles/deacon.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
role = "deacon"
55
scope = "town"
6+
autonomous = true
7+
emoji = "🐺"
68
nudge = "Run 'gt prime' to check patrol status and begin heartbeat cycle."
79
prompt_template = "deacon.md.tmpl"
810

internal/config/roles/dog.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
role = "dog"
55
scope = "town"
6+
autonomous = true
7+
emoji = "🐕"
68
nudge = "Check your hook for work assignments."
79
prompt_template = "dog.md.tmpl"
810

internal/config/roles/mayor.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
role = "mayor"
55
scope = "town"
6+
emoji = "🎩"
67
nudge = "Check mail and hook status, then act accordingly."
78
prompt_template = "mayor.md.tmpl"
89

internal/config/roles/polecat.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
role = "polecat"
55
scope = "rig"
6+
autonomous = true
7+
emoji = "😺"
68
nudge = "Check your hook for work assignments."
79
prompt_template = "polecat.md.tmpl"
810

internal/config/roles/refinery.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
role = "refinery"
55
scope = "rig"
6+
autonomous = true
7+
emoji = "🏭"
68
nudge = "Run 'gt prime' to check merge queue and begin processing."
79
prompt_template = "refinery.md.tmpl"
810

internal/config/roles/witness.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
role = "witness"
55
scope = "rig"
6+
autonomous = true
7+
emoji = "🦉"
68
nudge = "Run 'gt prime' to check worker status and begin patrol cycle."
79
prompt_template = "witness.md.tmpl"
810

internal/config/roles_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,57 @@ func TestLoadBuiltinRoleDefinition(t *testing.T) {
9797
}
9898
}
9999

100+
func TestIsAutonomous(t *testing.T) {
101+
tests := []struct {
102+
role string
103+
want bool
104+
}{
105+
{"polecat", true},
106+
{"witness", true},
107+
{"refinery", true},
108+
{"deacon", true},
109+
{"boot", true}, // deacon variant, inherits autonomous
110+
{"dog", true},
111+
{"mayor", false},
112+
{"crew", false},
113+
{"unknown", false},
114+
{"", false},
115+
}
116+
117+
for _, tt := range tests {
118+
t.Run(tt.role, func(t *testing.T) {
119+
if got := IsAutonomous(tt.role); got != tt.want {
120+
t.Errorf("IsAutonomous(%q) = %v, want %v", tt.role, got, tt.want)
121+
}
122+
})
123+
}
124+
}
125+
126+
func TestLoadBuiltinRoleDefinition_Autonomous(t *testing.T) {
127+
// Verify the Autonomous field is correctly loaded from TOML
128+
autonomousRoles := map[string]bool{
129+
"polecat": true,
130+
"witness": true,
131+
"refinery": true,
132+
"deacon": true,
133+
"dog": true,
134+
"mayor": false,
135+
"crew": false,
136+
}
137+
138+
for role, wantAutonomous := range autonomousRoles {
139+
t.Run(role, func(t *testing.T) {
140+
def, err := loadBuiltinRoleDefinition(role)
141+
if err != nil {
142+
t.Fatalf("loadBuiltinRoleDefinition(%s) error: %v", role, err)
143+
}
144+
if def.Autonomous != wantAutonomous {
145+
t.Errorf("Autonomous = %v, want %v", def.Autonomous, wantAutonomous)
146+
}
147+
})
148+
}
149+
}
150+
100151
func TestLoadBuiltinRoleDefinition_UnknownRole(t *testing.T) {
101152
_, err := loadBuiltinRoleDefinition("nonexistent")
102153
if err == nil {

0 commit comments

Comments
 (0)