* fix(config): implement role_agents support in BuildStartupCommand The role_agents field in TownSettings and RigSettings existed but was not being used by the startup command builders. All services fell back to the default agent instead of using role-specific agent assignments. Changes: - BuildStartupCommand now extracts GT_ROLE from envVars and uses ResolveRoleAgentConfig() for role-based agent selection - BuildStartupCommandWithAgentOverride follows the same pattern when no explicit override is provided - refinery/manager.go uses ResolveRoleAgentConfig with constants - cmd/start.go uses ResolveRoleAgentConfig with constants - Updated comments from hardcoded agent name to generic "agent" - Added ValidateAgentConfig() to check agent exists and binary is in PATH - Added lookupAgentConfigIfExists() helper for validation - ResolveRoleAgentConfig now warns to stderr and falls back to default if configured agent is invalid or binary is missing Resolution priority (now working): 1. Explicit --agent override 2. Rig's role_agents[role] (validated) 3. Town's role_agents[role] (validated) 4. Rig's agent setting 5. Town's default_agent 6. Hardcoded default fallback Adds tests for: - TestBuildStartupCommand_UsesRoleAgentsFromTownSettings - TestBuildStartupCommand_RigRoleAgentsOverridesTownRoleAgents - TestBuildAgentStartupCommand_UsesRoleAgents - TestValidateAgentConfig - TestResolveRoleAgentConfig_FallsBackOnInvalidAgent Fixes: role_agents configuration not being applied to services * fix(config): add GT_ROOT to BuildStartupCommandWithAgentOverride - Fixes missing GT_ROOT and GT_SESSION_ID_ENV exports in BuildStartupCommandWithAgentOverride, matching BuildStartupCommand behavior - Adds test for override priority over role_agents - Adds test verifying GT_ROOT is included in command This addresses the Greptile review comment about agents started with an override not having access to town-level resources. Co-authored-by: Steve Yegge <steve.yegge@gmail.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -898,6 +899,48 @@ func ResolveAgentConfigWithOverride(townRoot, rigPath, agentOverride string) (*R
|
||||
return lookupAgentConfig(agentName, townSettings, rigSettings), agentName, nil
|
||||
}
|
||||
|
||||
// ValidateAgentConfig checks if an agent configuration is valid and the binary exists.
|
||||
// Returns an error describing the issue, or nil if valid.
|
||||
func ValidateAgentConfig(agentName string, townSettings *TownSettings, rigSettings *RigSettings) error {
|
||||
// Check if agent exists in config
|
||||
rc := lookupAgentConfigIfExists(agentName, townSettings, rigSettings)
|
||||
if rc == nil {
|
||||
return fmt.Errorf("agent %q not found in config or built-in presets", agentName)
|
||||
}
|
||||
|
||||
// Check if binary exists on system
|
||||
if _, err := exec.LookPath(rc.Command); err != nil {
|
||||
return fmt.Errorf("agent %q binary %q not found in PATH", agentName, rc.Command)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// lookupAgentConfigIfExists looks up an agent by name but returns nil if not found
|
||||
// (instead of falling back to default). Used for validation.
|
||||
func lookupAgentConfigIfExists(name string, townSettings *TownSettings, rigSettings *RigSettings) *RuntimeConfig {
|
||||
// Check rig's custom agents
|
||||
if rigSettings != nil && rigSettings.Agents != nil {
|
||||
if custom, ok := rigSettings.Agents[name]; ok && custom != nil {
|
||||
return fillRuntimeDefaults(custom)
|
||||
}
|
||||
}
|
||||
|
||||
// Check town's custom agents
|
||||
if townSettings != nil && townSettings.Agents != nil {
|
||||
if custom, ok := townSettings.Agents[name]; ok && custom != nil {
|
||||
return fillRuntimeDefaults(custom)
|
||||
}
|
||||
}
|
||||
|
||||
// Check built-in presets
|
||||
if preset := GetAgentPresetByName(name); preset != nil {
|
||||
return RuntimeConfigFromPreset(AgentPreset(name))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveRoleAgentConfig resolves the agent configuration for a specific role.
|
||||
// It checks role-specific agent assignments before falling back to the default agent.
|
||||
//
|
||||
@@ -906,6 +949,9 @@ func ResolveAgentConfigWithOverride(townRoot, rigPath, agentOverride string) (*R
|
||||
// 2. Town's RoleAgents[role] - if set, look up that agent
|
||||
// 3. Fall back to ResolveAgentConfig (rig's Agent → town's DefaultAgent → "claude")
|
||||
//
|
||||
// If a configured agent is not found or its binary doesn't exist, a warning is
|
||||
// printed to stderr and it falls back to the default agent.
|
||||
//
|
||||
// role is one of: "mayor", "deacon", "witness", "refinery", "polecat", "crew".
|
||||
// townRoot is the path to the town directory (e.g., ~/gt).
|
||||
// rigPath is the path to the rig directory (e.g., ~/gt/gastown), or empty for town-level roles.
|
||||
@@ -935,14 +981,22 @@ func ResolveRoleAgentConfig(role, townRoot, rigPath string) *RuntimeConfig {
|
||||
// Check rig's RoleAgents first
|
||||
if rigSettings != nil && rigSettings.RoleAgents != nil {
|
||||
if agentName, ok := rigSettings.RoleAgents[role]; ok && agentName != "" {
|
||||
return lookupAgentConfig(agentName, townSettings, rigSettings)
|
||||
if err := ValidateAgentConfig(agentName, townSettings, rigSettings); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: role_agents[%s]=%s - %v, falling back to default\n", role, agentName, err)
|
||||
} else {
|
||||
return lookupAgentConfig(agentName, townSettings, rigSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check town's RoleAgents
|
||||
if townSettings.RoleAgents != nil {
|
||||
if agentName, ok := townSettings.RoleAgents[role]; ok && agentName != "" {
|
||||
return lookupAgentConfig(agentName, townSettings, rigSettings)
|
||||
if err := ValidateAgentConfig(agentName, townSettings, rigSettings); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: role_agents[%s]=%s - %v, falling back to default\n", role, agentName, err)
|
||||
} else {
|
||||
return lookupAgentConfig(agentName, townSettings, rigSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1150,13 +1204,26 @@ func findTownRootFromCwd() (string, error) {
|
||||
// envVars is a map of environment variable names to values.
|
||||
// rigPath is optional - if empty, tries to detect town root from cwd.
|
||||
// prompt is optional - if provided, appended as the initial prompt.
|
||||
//
|
||||
// If envVars contains GT_ROLE, the function uses role-based agent resolution
|
||||
// (ResolveRoleAgentConfig) to select the appropriate agent for the role.
|
||||
// This enables per-role model selection via role_agents in settings.
|
||||
func BuildStartupCommand(envVars map[string]string, rigPath, prompt string) string {
|
||||
var rc *RuntimeConfig
|
||||
var townRoot string
|
||||
|
||||
// Extract role from envVars for role-based agent resolution
|
||||
role := envVars["GT_ROLE"]
|
||||
|
||||
if rigPath != "" {
|
||||
// Derive town root from rig path
|
||||
townRoot = filepath.Dir(rigPath)
|
||||
rc = ResolveAgentConfig(townRoot, rigPath)
|
||||
if role != "" {
|
||||
// Use role-based agent resolution for per-role model selection
|
||||
rc = ResolveRoleAgentConfig(role, townRoot, rigPath)
|
||||
} else {
|
||||
rc = ResolveAgentConfig(townRoot, rigPath)
|
||||
}
|
||||
} else {
|
||||
// Try to detect town root from cwd for town-level agents (mayor, deacon)
|
||||
var err error
|
||||
@@ -1164,7 +1231,12 @@ func BuildStartupCommand(envVars map[string]string, rigPath, prompt string) stri
|
||||
if err != nil {
|
||||
rc = DefaultRuntimeConfig()
|
||||
} else {
|
||||
rc = ResolveAgentConfig(townRoot, "")
|
||||
if role != "" {
|
||||
// Use role-based agent resolution for per-role model selection
|
||||
rc = ResolveRoleAgentConfig(role, townRoot, "")
|
||||
} else {
|
||||
rc = ResolveAgentConfig(townRoot, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1222,32 +1294,69 @@ func PrependEnv(command string, envVars map[string]string) string {
|
||||
|
||||
// BuildStartupCommandWithAgentOverride builds a startup command like BuildStartupCommand,
|
||||
// but uses agentOverride if non-empty.
|
||||
//
|
||||
// Resolution priority:
|
||||
// 1. agentOverride (explicit override)
|
||||
// 2. role_agents[GT_ROLE] (if GT_ROLE is in envVars)
|
||||
// 3. Default agent resolution (rig's Agent → town's DefaultAgent → "claude")
|
||||
func BuildStartupCommandWithAgentOverride(envVars map[string]string, rigPath, prompt, agentOverride string) (string, error) {
|
||||
var rc *RuntimeConfig
|
||||
var townRoot string
|
||||
|
||||
// Extract role from envVars for role-based agent resolution (when no override)
|
||||
role := envVars["GT_ROLE"]
|
||||
|
||||
if rigPath != "" {
|
||||
townRoot := filepath.Dir(rigPath)
|
||||
var err error
|
||||
rc, _, err = ResolveAgentConfigWithOverride(townRoot, rigPath, agentOverride)
|
||||
if err != nil {
|
||||
return "", err
|
||||
townRoot = filepath.Dir(rigPath)
|
||||
if agentOverride != "" {
|
||||
var err error
|
||||
rc, _, err = ResolveAgentConfigWithOverride(townRoot, rigPath, agentOverride)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else if role != "" {
|
||||
// No override, use role-based agent resolution
|
||||
rc = ResolveRoleAgentConfig(role, townRoot, rigPath)
|
||||
} else {
|
||||
rc = ResolveAgentConfig(townRoot, rigPath)
|
||||
}
|
||||
} else {
|
||||
townRoot, err := findTownRootFromCwd()
|
||||
var err error
|
||||
townRoot, err = findTownRootFromCwd()
|
||||
if err != nil {
|
||||
rc = DefaultRuntimeConfig()
|
||||
} else {
|
||||
var resolveErr error
|
||||
rc, _, resolveErr = ResolveAgentConfigWithOverride(townRoot, "", agentOverride)
|
||||
if resolveErr != nil {
|
||||
return "", resolveErr
|
||||
if agentOverride != "" {
|
||||
var resolveErr error
|
||||
rc, _, resolveErr = ResolveAgentConfigWithOverride(townRoot, "", agentOverride)
|
||||
if resolveErr != nil {
|
||||
return "", resolveErr
|
||||
}
|
||||
} else if role != "" {
|
||||
// No override, use role-based agent resolution
|
||||
rc = ResolveRoleAgentConfig(role, townRoot, "")
|
||||
} else {
|
||||
rc = ResolveAgentConfig(townRoot, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy env vars to avoid mutating caller map
|
||||
resolvedEnv := make(map[string]string, len(envVars)+2)
|
||||
for k, v := range envVars {
|
||||
resolvedEnv[k] = v
|
||||
}
|
||||
// Add GT_ROOT so agents can find town-level resources (formulas, etc.)
|
||||
if townRoot != "" {
|
||||
resolvedEnv["GT_ROOT"] = townRoot
|
||||
}
|
||||
if rc.Session != nil && rc.Session.SessionIDEnv != "" {
|
||||
resolvedEnv["GT_SESSION_ID_ENV"] = rc.Session.SessionIDEnv
|
||||
}
|
||||
|
||||
// Build environment export prefix
|
||||
var exports []string
|
||||
for k, v := range envVars {
|
||||
for k, v := range resolvedEnv {
|
||||
exports = append(exports, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
sort.Strings(exports)
|
||||
@@ -1266,7 +1375,6 @@ func BuildStartupCommandWithAgentOverride(envVars map[string]string, rigPath, pr
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
|
||||
// BuildAgentStartupCommand is a convenience function for starting agent sessions.
|
||||
// It sets standard environment variables (GT_ROLE, BD_ACTOR, GIT_AUTHOR_NAME)
|
||||
// and builds the full startup command.
|
||||
|
||||
Reference in New Issue
Block a user