feat: Add Cursor, Auggie, and Sourcegraph AMP agent presets (#247)

* feat: add Cursor Agent as compatible agent for Gas Town

Add AgentCursor preset with ProcessNames field for multi-agent detection:
- AgentCursor preset: cursor-agent -p -f (headless + force mode)
- ProcessNames field on AgentPresetInfo for agent detection
- IsAgentRunning(session, processNames) in tmux package
- GetProcessNames(agentName) helper function

Closes: ga-vwr

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: centralize agent preset list in config.go

Replace hardcoded ["claude", "gemini", "codex"] arrays with calls to
config.ListAgentPresets() to dynamically include all registered agents.

This fixes cursor agent not appearing in `gt config agent list` and
ensures new agent presets are automatically included everywhere.

Also updated doc comments to include "cursor" in example lists.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: add comprehensive agent client tests

Add tests for agent detection and command generation:

- TestIsAgentRunning: validates process name detection for all agents
  (claude/node, gemini, codex, cursor-agent)
- TestIsAgentRunning_NonexistentSession: edge case handling
- TestIsClaudeRunning: backwards compatibility wrapper
- TestListAgentPresetsMatchesConstants: ensures ListAgentPresets()
  returns all AgentPreset constants
- TestAgentCommandGeneration: validates full command line generation
  for all supported agents

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add Auggie agent, fix Cursor interactive mode

Add Auggie CLI as supported agent:
- Command: auggie
- Args: --allow-indexing
- Supports session resume via --resume flag

Fix Cursor agent configuration:
- Remove -p flag (requires prompt, breaks interactive mode)
- Clear SessionIDEnv (cursor uses --resume with chatId directly)
- Keep -f flag for force/YOLO mode

Updated all test cases for both agents.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(agents): add Sourcegraph AMP as agent preset

Add AgentAmp constant and builtinPresets entry for Sourcegraph AMP CLI.

Configuration:
- Command: amp
- Args: --dangerously-allow-all --no-ide
- ResumeStyle: subcommand (amp threads continue <threadId>)
- ProcessNames: amp

Closes: ga-guq

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: lint error in cleanBeadsRuntimeFiles

Change function to not return error (was always nil).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: beads v0.46.0 compatibility and test fixes

- Add custom types config (agent,role,rig,convoy,event) after bd init calls
- Fix tmux_test.go to use variadic IsAgentRunning signature

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: update agent documentation for new presets

- README.md: Update agent examples to show cursor/auggie, add built-in presets list
- docs/reference.md: Add cursor, auggie, amp to built-in agents list
- CHANGELOG.md: Add entry for new agent presets under [Unreleased]

Addresses PR #247 review feedback.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mike Lady
2026-01-07 20:35:06 -08:00
committed by GitHub
parent 585c204648
commit 92042d679c
12 changed files with 413 additions and 21 deletions

View File

@@ -21,12 +21,18 @@ const (
AgentGemini AgentPreset = "gemini"
// AgentCodex is OpenAI Codex.
AgentCodex AgentPreset = "codex"
// AgentCursor is Cursor Agent.
AgentCursor AgentPreset = "cursor"
// AgentAuggie is Auggie CLI.
AgentAuggie AgentPreset = "auggie"
// AgentAmp is Sourcegraph AMP.
AgentAmp AgentPreset = "amp"
)
// AgentPresetInfo contains the configuration details for an agent preset.
// This extends the basic RuntimeConfig with agent-specific metadata.
type AgentPresetInfo struct {
// Name is the preset identifier (e.g., "claude", "gemini", "codex").
// Name is the preset identifier (e.g., "claude", "gemini", "codex", "cursor", "auggie", "amp").
Name AgentPreset `json:"name"`
// Command is the CLI binary to invoke.
@@ -35,6 +41,11 @@ type AgentPresetInfo struct {
// Args are the default command-line arguments for autonomous mode.
Args []string `json:"args"`
// ProcessNames are the process names to look for when detecting if the agent is running.
// Used by tmux.IsAgentRunning to check pane_current_command.
// E.g., ["node"] for Claude, ["cursor-agent"] for Cursor.
ProcessNames []string `json:"process_names,omitempty"`
// SessionIDEnv is the environment variable for session ID.
// Used for resuming sessions across restarts.
SessionIDEnv string `json:"session_id_env,omitempty"`
@@ -91,6 +102,7 @@ var builtinPresets = map[AgentPreset]*AgentPresetInfo{
Name: AgentClaude,
Command: "claude",
Args: []string{"--dangerously-skip-permissions"},
ProcessNames: []string{"node"}, // Claude runs as Node.js
SessionIDEnv: "CLAUDE_SESSION_ID",
ResumeFlag: "--resume",
ResumeStyle: "flag",
@@ -102,6 +114,7 @@ var builtinPresets = map[AgentPreset]*AgentPresetInfo{
Name: AgentGemini,
Command: "gemini",
Args: []string{"--approval-mode", "yolo"},
ProcessNames: []string{"gemini"}, // Gemini CLI binary
SessionIDEnv: "GEMINI_SESSION_ID",
ResumeFlag: "--resume",
ResumeStyle: "flag",
@@ -116,6 +129,7 @@ var builtinPresets = map[AgentPreset]*AgentPresetInfo{
Name: AgentCodex,
Command: "codex",
Args: []string{"--yolo"},
ProcessNames: []string{"codex"}, // Codex CLI binary
SessionIDEnv: "", // Codex captures from JSONL output
ResumeFlag: "resume",
ResumeStyle: "subcommand",
@@ -126,6 +140,43 @@ var builtinPresets = map[AgentPreset]*AgentPresetInfo{
OutputFlag: "--json",
},
},
AgentCursor: {
Name: AgentCursor,
Command: "cursor-agent",
Args: []string{"-f"}, // Force mode (YOLO equivalent), -p requires prompt
ProcessNames: []string{"cursor-agent"},
SessionIDEnv: "", // Uses --resume with chatId directly
ResumeFlag: "--resume",
ResumeStyle: "flag",
SupportsHooks: false, // TODO: verify hooks support
SupportsForkSession: false,
NonInteractive: &NonInteractiveConfig{
PromptFlag: "-p",
OutputFlag: "--output-format json",
},
},
AgentAuggie: {
Name: AgentAuggie,
Command: "auggie",
Args: []string{"--allow-indexing"},
ProcessNames: []string{"auggie"},
SessionIDEnv: "",
ResumeFlag: "--resume",
ResumeStyle: "flag",
SupportsHooks: false,
SupportsForkSession: false,
},
AgentAmp: {
Name: AgentAmp,
Command: "amp",
Args: []string{"--dangerously-allow-all", "--no-ide"},
ProcessNames: []string{"amp"},
SessionIDEnv: "",
ResumeFlag: "threads continue",
ResumeStyle: "subcommand", // 'amp threads continue <threadId>'
SupportsHooks: false,
SupportsForkSession: false,
},
}
// Registry state with proper synchronization.
@@ -305,6 +356,18 @@ func GetSessionIDEnvVar(agentName string) string {
return info.SessionIDEnv
}
// GetProcessNames returns the process names used to detect if an agent is running.
// Used by tmux.IsAgentRunning to check pane_current_command.
// Returns ["node"] for Claude (default) if agent is not found or has no ProcessNames.
func GetProcessNames(agentName string) []string {
info := GetAgentPresetByName(agentName)
if info == nil || len(info.ProcessNames) == 0 {
// Default to Claude's process name for backwards compatibility
return []string{"node"}
}
return info.ProcessNames
}
// MergeWithPreset applies preset defaults to a RuntimeConfig.
// User-specified values take precedence over preset defaults.
// Returns a new RuntimeConfig without modifying the original.

View File

@@ -9,8 +9,8 @@ import (
)
func TestBuiltinPresets(t *testing.T) {
// Ensure all built-in presets are accessible (E2E tested agents only)
presets := []AgentPreset{AgentClaude, AgentGemini, AgentCodex}
// Ensure all built-in presets are accessible
presets := []AgentPreset{AgentClaude, AgentGemini, AgentCodex, AgentCursor, AgentAuggie, AgentAmp}
for _, preset := range presets {
info := GetAgentPreset(preset)
@@ -22,6 +22,11 @@ func TestBuiltinPresets(t *testing.T) {
if info.Command == "" {
t.Errorf("preset %s has empty Command", preset)
}
// All presets should have ProcessNames for agent detection
if len(info.ProcessNames) == 0 {
t.Errorf("preset %s has empty ProcessNames", preset)
}
}
}
@@ -34,6 +39,9 @@ func TestGetAgentPresetByName(t *testing.T) {
{"claude", AgentClaude, false},
{"gemini", AgentGemini, false},
{"codex", AgentCodex, false},
{"cursor", AgentCursor, false},
{"auggie", AgentAuggie, false},
{"amp", AgentAmp, false},
{"aider", "", true}, // Not built-in, can be added via config
{"opencode", "", true}, // Not built-in, can be added via config
{"unknown", "", true},
@@ -63,6 +71,9 @@ func TestRuntimeConfigFromPreset(t *testing.T) {
{AgentClaude, "claude"},
{AgentGemini, "gemini"},
{AgentCodex, "codex"},
{AgentCursor, "cursor-agent"},
{AgentAuggie, "auggie"},
{AgentAmp, "amp"},
}
for _, tt := range tests {
@@ -84,6 +95,9 @@ func TestIsKnownPreset(t *testing.T) {
{"claude", true},
{"gemini", true},
{"codex", true},
{"cursor", true},
{"auggie", true},
{"amp", true},
{"aider", false}, // Not built-in, can be added via config
{"opencode", false}, // Not built-in, can be added via config
{"unknown", false},
@@ -286,6 +300,9 @@ func TestSupportsSessionResume(t *testing.T) {
{"claude", true},
{"gemini", true},
{"codex", true},
{"cursor", true},
{"auggie", true},
{"amp", true},
{"unknown", false},
}
@@ -305,7 +322,10 @@ func TestGetSessionIDEnvVar(t *testing.T) {
}{
{"claude", "CLAUDE_SESSION_ID"},
{"gemini", "GEMINI_SESSION_ID"},
{"codex", ""}, // Codex uses JSONL output instead
{"codex", ""}, // Codex uses JSONL output instead
{"cursor", ""}, // Cursor uses --resume with chatId directly
{"auggie", ""}, // Auggie uses --resume directly
{"amp", ""}, // AMP uses 'threads continue' subcommand
{"unknown", ""},
}
@@ -317,3 +337,168 @@ func TestGetSessionIDEnvVar(t *testing.T) {
})
}
}
func TestGetProcessNames(t *testing.T) {
tests := []struct {
agentName string
want []string
}{
{"claude", []string{"node"}},
{"gemini", []string{"gemini"}},
{"codex", []string{"codex"}},
{"cursor", []string{"cursor-agent"}},
{"auggie", []string{"auggie"}},
{"amp", []string{"amp"}},
{"unknown", []string{"node"}}, // Falls back to Claude's process
}
for _, tt := range tests {
t.Run(tt.agentName, func(t *testing.T) {
got := GetProcessNames(tt.agentName)
if len(got) != len(tt.want) {
t.Errorf("GetProcessNames(%s) = %v, want %v", tt.agentName, got, tt.want)
return
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("GetProcessNames(%s)[%d] = %q, want %q", tt.agentName, i, got[i], tt.want[i])
}
}
})
}
}
func TestListAgentPresetsMatchesConstants(t *testing.T) {
// Ensure all AgentPreset constants are returned by ListAgentPresets
allConstants := []AgentPreset{AgentClaude, AgentGemini, AgentCodex, AgentCursor, AgentAuggie, AgentAmp}
presets := ListAgentPresets()
// Convert to map for quick lookup
presetMap := make(map[string]bool)
for _, p := range presets {
presetMap[p] = true
}
// Verify all constants are in the list
for _, c := range allConstants {
if !presetMap[string(c)] {
t.Errorf("ListAgentPresets() missing constant %q", c)
}
}
// Verify no empty names
for _, p := range presets {
if p == "" {
t.Error("ListAgentPresets() contains empty string")
}
}
}
func TestAgentCommandGeneration(t *testing.T) {
// Test full command line generation for each agent
tests := []struct {
preset AgentPreset
wantCommand string
wantContains []string // Args that should be present
}{
{
preset: AgentClaude,
wantCommand: "claude",
wantContains: []string{"--dangerously-skip-permissions"},
},
{
preset: AgentGemini,
wantCommand: "gemini",
wantContains: []string{"--approval-mode", "yolo"},
},
{
preset: AgentCodex,
wantCommand: "codex",
wantContains: []string{"--yolo"},
},
{
preset: AgentCursor,
wantCommand: "cursor-agent",
wantContains: []string{"-f"},
},
{
preset: AgentAuggie,
wantCommand: "auggie",
wantContains: []string{"--allow-indexing"},
},
{
preset: AgentAmp,
wantCommand: "amp",
wantContains: []string{"--dangerously-allow-all", "--no-ide"},
},
}
for _, tt := range tests {
t.Run(string(tt.preset), func(t *testing.T) {
rc := RuntimeConfigFromPreset(tt.preset)
if rc == nil {
t.Fatal("RuntimeConfigFromPreset returned nil")
}
if rc.Command != tt.wantCommand {
t.Errorf("Command = %q, want %q", rc.Command, tt.wantCommand)
}
// Check required args are present
argsStr := strings.Join(rc.Args, " ")
for _, arg := range tt.wantContains {
found := false
for _, a := range rc.Args {
if a == arg {
found = true
break
}
}
if !found {
t.Errorf("Args %q missing expected %q", argsStr, arg)
}
}
})
}
}
func TestCursorAgentPreset(t *testing.T) {
// Verify cursor agent preset is correctly configured
info := GetAgentPreset(AgentCursor)
if info == nil {
t.Fatal("cursor preset not found")
}
// Check command
if info.Command != "cursor-agent" {
t.Errorf("cursor command = %q, want cursor-agent", info.Command)
}
// Check YOLO-equivalent flag (-f for force mode)
// Note: -p is for non-interactive mode with prompt, not used for default Args
hasF := false
for _, arg := range info.Args {
if arg == "-f" {
hasF = true
}
}
if !hasF {
t.Error("cursor args missing -f (force/YOLO mode)")
}
// Check ProcessNames for detection
if len(info.ProcessNames) == 0 {
t.Error("cursor ProcessNames is empty")
}
if info.ProcessNames[0] != "cursor-agent" {
t.Errorf("cursor ProcessNames[0] = %q, want cursor-agent", info.ProcessNames[0])
}
// Check resume support
if info.ResumeFlag != "--resume" {
t.Errorf("cursor ResumeFlag = %q, want --resume", info.ResumeFlag)
}
if info.ResumeStyle != "flag" {
t.Errorf("cursor ResumeStyle = %q, want flag", info.ResumeStyle)
}
}

View File

@@ -38,7 +38,7 @@ type TownSettings struct {
Version int `json:"version"` // schema version
// DefaultAgent is the name of the agent preset to use by default.
// Can be a built-in preset ("claude", "gemini", "codex")
// Can be a built-in preset ("claude", "gemini", "codex", "cursor", "auggie", "amp")
// or a custom agent name defined in settings/agents.json.
// Default: "claude"
DefaultAgent string `json:"default_agent,omitempty"`
@@ -190,7 +190,7 @@ type RigSettings struct {
Runtime *RuntimeConfig `json:"runtime,omitempty"` // LLM runtime settings (deprecated: use Agent)
// Agent selects which agent preset to use for this rig.
// Can be a built-in preset ("claude", "gemini", "codex")
// Can be a built-in preset ("claude", "gemini", "codex", "cursor", "auggie", "amp")
// or a custom agent defined in settings/agents.json.
// If empty, uses the town's default_agent setting.
// Takes precedence over Runtime if both are set.