feat: Add rig-level custom agent support (#12)

* feat: Add rig-level custom agent support

Implement rig-level custom agent configuration support to enable per-rig
agent definitions in <rig>/settings/config.json, following the same pattern as
town-level agents in settings/config.json.

Changes:
- Added RigSettings.Agents field to internal/config/types.go
- Added DefaultRigAgentRegistryPath() and LoadRigAgentRegistry() functions to internal/config/agents.go
- Updated ResolveAgentConfigWithOverride() to accept and pass rigSettings parameter
- Updated GetRuntimeCommandWithAgentOverride() to use rigSettings when available
- Updated GetRuntimeCommandWithPromptAndAgentOverride() to use rigSettings
- Updated all Build*WithOverride functions to pass rigSettings

This fixes the issue where rig-level agent settings were loaded but
ignored by lookupAgentConfig, enabling per-rig custom agents for
polecats and crew members.

* test: Add rig-level custom agent tests

Added comprehensive unit tests for rig agent registry functions:
- TestDefaultRigAgentRegistryPath: verifies path construction
- TestLoadRigAgentRegistry: verifies file loading and JSON parsing
- TestLookupAgentConfigWithRigSettings: verifies agent lookup priority (rig > town > builtin)

Added placeholder integration test for future CI/CD setup.

* initial commit

* fix: resolve compilation errors in rig-level custom agent support

- Add missing RigAgentRegistryPath function (alias for DefaultRigAgentRegistryPath)
- Restore ResolveAgentConfigWithOverride function that was incorrectly removed
- Fix ResolveAgentConfig to return single value (not triple)
- Add initRegistryLocked() call to LoadRigAgentRegistry to prevent nil panic
- Fix DefaultRigAgentRegistryPath to use rigPath directly (not parent dir)
- Fix test file syntax errors (remove EOF artifacts)
- Fix test parameter order for lookupAgentConfig calls
- Fix test expectations to match correct custom agent override behavior

* test: implement rig-level custom agent integration test

- Add stub agent script that simulates AI agent with Q&A capability
- Test ResolveAgentConfig correctly picks up rig-level agents
- Test BuildPolecatStartupCommand includes custom agent command
- Test ResolveAgentConfigWithOverride respects rig agents
- Test rig agents override town agents with same name
- Add tmux integration test that spawns session and verifies output
- Stub agent echoes 'STUB_AGENT_STARTED' and handles ping/pong Q&A
- All tests pass including real tmux session verification

* docs: add OpenCode custom agent example to reference

- Show settings/agents.json format for advanced configs
- Include OpenCode example with session resume flags
- Document OPENCODE_PERMISSION env var for autonomous mode

* fix: improve rig-level agent support with docs and test fixes

- Add rig-level agent documentation to reference.md
- Document agent resolution order (rig → town → built-in)
- Deduplicate LoadAgentRegistry/LoadRigAgentRegistry into shared helper
- Fix test isolation in TestLoadRigAgentRegistry
- Fix nil pointer dereference in test assertions (use t.Fatal not t.Error)
This commit is contained in:
Subhrajit Makur
2026-01-08 01:23:29 +05:30
committed by Steve Yegge
parent 2de2d6b7e4
commit 00a59dec44
7 changed files with 746 additions and 20 deletions

View File

@@ -215,16 +215,11 @@ func ensureRegistry() {
initRegistryLocked()
}
// LoadAgentRegistry loads agent definitions from a JSON file and merges with built-ins.
// User-defined agents override built-in presets with the same name.
// This function caches loaded paths to avoid redundant file reads.
func LoadAgentRegistry(path string) error {
registryMu.Lock()
defer registryMu.Unlock()
// loadAgentRegistryFromPath loads agent definitions from a JSON file and merges with built-ins.
// Caller must hold registryMu write lock.
func loadAgentRegistryFromPathLocked(path string) error {
initRegistryLocked()
// Check if already loaded from this path
if loadedPaths[path] {
return nil
}
@@ -232,8 +227,8 @@ func LoadAgentRegistry(path string) error {
data, err := os.ReadFile(path) //nolint:gosec // G304: path is from config
if err != nil {
if os.IsNotExist(err) {
loadedPaths[path] = true // Mark as "loaded" (no file)
return nil // No custom config, use built-ins only
loadedPaths[path] = true
return nil
}
return err
}
@@ -243,7 +238,6 @@ func LoadAgentRegistry(path string) error {
return err
}
// Merge user-defined agents (override built-ins)
for name, preset := range userRegistry.Agents {
preset.Name = AgentPreset(name)
globalRegistry.Agents[name] = preset
@@ -253,12 +247,41 @@ func LoadAgentRegistry(path string) error {
return nil
}
// LoadAgentRegistry loads agent definitions from a JSON file and merges with built-ins.
// User-defined agents override built-in presets with the same name.
// This function caches loaded paths to avoid redundant file reads.
func LoadAgentRegistry(path string) error {
registryMu.Lock()
defer registryMu.Unlock()
return loadAgentRegistryFromPathLocked(path)
}
// DefaultAgentRegistryPath returns the default path for agent registry.
// Located alongside other town settings.
func DefaultAgentRegistryPath(townRoot string) string {
return filepath.Join(townRoot, "settings", "agents.json")
}
// DefaultRigAgentRegistryPath returns the default path for rig-level agent registry.
// Located in <rig>/settings/agents.json.
func DefaultRigAgentRegistryPath(rigPath string) string {
return filepath.Join(rigPath, "settings", "agents.json")
}
// RigAgentRegistryPath returns the path for rig-level agent registry.
// Alias for DefaultRigAgentRegistryPath for consistency with other path functions.
func RigAgentRegistryPath(rigPath string) string {
return DefaultRigAgentRegistryPath(rigPath)
}
// LoadRigAgentRegistry loads agent definitions from a rig-level JSON file and merges with built-ins.
// This function works similarly to LoadAgentRegistry but for rig-level configurations.
func LoadRigAgentRegistry(path string) error {
registryMu.Lock()
defer registryMu.Unlock()
return loadAgentRegistryFromPathLocked(path)
}
// GetAgentPreset returns the preset info for a given agent name.
// Returns nil if the preset is not found.
func GetAgentPreset(name AgentPreset) *AgentPresetInfo {