feat(roles): switch daemon to config-based roles, remove role beads (Phase 2+3)

Phase 2: Daemon now uses config.LoadRoleDefinition() instead of role beads
- lifecycle.go: getRoleConfigForIdentity() reads from TOML configs
- Layered override resolution: builtin → town → rig

Phase 3: Remove role bead creation and references
- Remove RoleBead field from AgentFields struct
- gt install no longer creates role beads
- Remove 'role' from custom types list
- Delete migrate_agents.go (no longer needed)
- Deprecate beads_role.go (kept for reading existing beads)
- Rewrite role_beads_check.go to validate TOML configs

Existing role beads are orphaned but harmless.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/max
2026-01-20 12:49:52 -08:00
committed by Steve Yegge
parent 544cacf36d
commit a610283078
16 changed files with 347 additions and 713 deletions

View File

@@ -211,7 +211,7 @@ func (d *Daemon) executeLifecycleAction(request *LifecycleRequest) error {
}
// ParsedIdentity holds the components extracted from an agent identity string.
// This is used to look up the appropriate role bead for lifecycle config.
// This is used to look up the appropriate role config for lifecycle management.
type ParsedIdentity struct {
RoleType string // mayor, deacon, witness, refinery, crew, polecat
RigName string // Empty for town-level agents (mayor, deacon)
@@ -220,7 +220,7 @@ type ParsedIdentity struct {
// parseIdentity extracts role type, rig name, and agent name from an identity string.
// This is the ONLY place where identity string patterns are parsed.
// All other functions should use the extracted components to look up role beads.
// All other functions should use the extracted components to look up role config.
func parseIdentity(identity string) (*ParsedIdentity, error) {
switch identity {
case "mayor":
@@ -268,49 +268,50 @@ func parseIdentity(identity string) (*ParsedIdentity, error) {
return nil, fmt.Errorf("unknown identity format: %s", identity)
}
// getRoleConfigForIdentity looks up the role bead for an identity and returns its config.
// Falls back to default config if role bead doesn't exist or has no config.
// getRoleConfigForIdentity loads role configuration from the config-based role system.
// Uses config.LoadRoleDefinition() with layered override resolution (builtin → town → rig).
// Returns config in beads.RoleConfig format for backward compatibility.
func (d *Daemon) getRoleConfigForIdentity(identity string) (*beads.RoleConfig, *ParsedIdentity, error) {
parsed, err := parseIdentity(identity)
if err != nil {
return nil, nil, err
}
// Look up role bead
b := beads.New(d.config.TownRoot)
// Determine rig path for rig-scoped roles
rigPath := ""
if parsed.RigName != "" {
rigPath = filepath.Join(d.config.TownRoot, parsed.RigName)
}
roleBeadID := beads.RoleBeadIDTown(parsed.RoleType)
roleConfig, err := b.GetRoleConfig(roleBeadID)
// Load role definition from config system (Phase 2: config-based roles)
roleDef, err := config.LoadRoleDefinition(d.config.TownRoot, rigPath, parsed.RoleType)
if err != nil {
d.logger.Printf("Warning: failed to get role config for %s: %v", roleBeadID, err)
d.logger.Printf("Warning: failed to load role definition for %s: %v", parsed.RoleType, err)
// Return parsed identity even if config fails (caller can use defaults)
return nil, parsed, nil
}
// Backward compatibility: fall back to legacy role bead IDs.
if roleConfig == nil {
legacyRoleBeadID := beads.RoleBeadID(parsed.RoleType) // gt-<role>-role
if legacyRoleBeadID != roleBeadID {
legacyCfg, legacyErr := b.GetRoleConfig(legacyRoleBeadID)
if legacyErr != nil {
d.logger.Printf("Warning: failed to get legacy role config for %s: %v", legacyRoleBeadID, legacyErr)
} else if legacyCfg != nil {
roleConfig = legacyCfg
}
}
// Convert to beads.RoleConfig for backward compatibility
roleConfig := &beads.RoleConfig{
SessionPattern: roleDef.Session.Pattern,
WorkDirPattern: roleDef.Session.WorkDir,
NeedsPreSync: roleDef.Session.NeedsPreSync,
StartCommand: roleDef.Session.StartCommand,
EnvVars: roleDef.Env,
}
// Return parsed identity even if config is nil (caller can use defaults)
return roleConfig, parsed, nil
}
// identityToSession converts a beads identity to a tmux session name.
// Uses role bead config if available, falls back to hardcoded patterns.
// Uses role config if available, falls back to hardcoded patterns.
func (d *Daemon) identityToSession(identity string) string {
config, parsed, err := d.getRoleConfigForIdentity(identity)
if err != nil {
return ""
}
// If role bead has session_pattern, use it
// If role config has session_pattern, use it
if config != nil && config.SessionPattern != "" {
return beads.ExpandRolePattern(config.SessionPattern, d.config.TownRoot, parsed.RigName, parsed.AgentName, parsed.RoleType)
}
@@ -333,7 +334,7 @@ func (d *Daemon) identityToSession(identity string) string {
}
// restartSession starts a new session for the given agent.
// Uses role bead config if available, falls back to hardcoded defaults.
// Uses role config if available, falls back to hardcoded defaults.
func (d *Daemon) restartSession(sessionName, identity string) error {
// Get role config for this identity
config, parsed, err := d.getRoleConfigForIdentity(identity)
@@ -409,9 +410,9 @@ func (d *Daemon) restartSession(sessionName, identity string) error {
}
// getWorkDir determines the working directory for an agent.
// Uses role bead config if available, falls back to hardcoded defaults.
// Uses role config if available, falls back to hardcoded defaults.
func (d *Daemon) getWorkDir(config *beads.RoleConfig, parsed *ParsedIdentity) string {
// If role bead has work_dir_pattern, use it
// If role config has work_dir_pattern, use it
if config != nil && config.WorkDirPattern != "" {
return beads.ExpandRolePattern(config.WorkDirPattern, d.config.TownRoot, parsed.RigName, parsed.AgentName, parsed.RoleType)
}
@@ -442,9 +443,9 @@ func (d *Daemon) getWorkDir(config *beads.RoleConfig, parsed *ParsedIdentity) st
}
// getNeedsPreSync determines if a workspace needs git sync before starting.
// Uses role bead config if available, falls back to hardcoded defaults.
// Uses role config if available, falls back to hardcoded defaults.
func (d *Daemon) getNeedsPreSync(config *beads.RoleConfig, parsed *ParsedIdentity) bool {
// If role bead has explicit config, use it
// If role config is available, use it
if config != nil {
return config.NeedsPreSync
}
@@ -459,9 +460,9 @@ func (d *Daemon) getNeedsPreSync(config *beads.RoleConfig, parsed *ParsedIdentit
}
// getStartCommand determines the startup command for an agent.
// Uses role bead config if available, then role-based agent selection, then hardcoded defaults.
// Uses role config if available, then role-based agent selection, then hardcoded defaults.
func (d *Daemon) getStartCommand(roleConfig *beads.RoleConfig, parsed *ParsedIdentity) string {
// If role bead has explicit config, use it
// If role config is available, use it
if roleConfig != nil && roleConfig.StartCommand != "" {
// Expand any patterns in the command
return beads.ExpandRolePattern(roleConfig.StartCommand, d.config.TownRoot, parsed.RigName, parsed.AgentName, parsed.RoleType)
@@ -516,7 +517,7 @@ func (d *Daemon) getStartCommand(roleConfig *beads.RoleConfig, parsed *ParsedIde
}
// setSessionEnvironment sets environment variables for the tmux session.
// Uses centralized AgentEnv for consistency, plus role bead custom env vars if available.
// Uses centralized AgentEnv for consistency, plus custom env vars from role config if available.
func (d *Daemon) setSessionEnvironment(sessionName string, roleConfig *beads.RoleConfig, parsed *ParsedIdentity) {
// Use centralized AgentEnv for base environment variables
envVars := config.AgentEnv(config.AgentEnvConfig{
@@ -529,7 +530,7 @@ func (d *Daemon) setSessionEnvironment(sessionName string, roleConfig *beads.Rol
_ = d.tmux.SetEnvironment(sessionName, k, v)
}
// Set any custom env vars from role config (bead-defined overrides)
// Set any custom env vars from role config
if roleConfig != nil {
for k, v := range roleConfig.EnvVars {
expanded := beads.ExpandRolePattern(v, d.config.TownRoot, parsed.RigName, parsed.AgentName, parsed.RoleType)
@@ -637,10 +638,10 @@ type AgentBeadInfo struct {
Type string `json:"issue_type"`
State string // Parsed from description: agent_state
HookBead string // Parsed from description: hook_bead
RoleBead string // Parsed from description: role_bead
RoleType string // Parsed from description: role_type
Rig string // Parsed from description: rig
LastUpdate string `json:"updated_at"`
// Note: RoleBead field removed - role definitions are now config-based
}
// getAgentBeadState reads non-observable agent state from an agent bead.
@@ -699,7 +700,6 @@ func (d *Daemon) getAgentBeadInfo(agentBeadID string) (*AgentBeadInfo, error) {
if fields != nil {
info.State = fields.AgentState
info.RoleBead = fields.RoleBead
info.RoleType = fields.RoleType
info.Rig = fields.Rig
}