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:
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **New agent presets** - Added Cursor, Auggie (Augment Code), and Sourcegraph AMP as built-in agent presets
|
||||
- **Centralized agent list** - Agent preset configuration moved to `config.go` for easier maintenance
|
||||
|
||||
## [0.2.2] - 2026-01-07
|
||||
|
||||
Rig operational state management, unified agent startup, and extensive stability fixes.
|
||||
|
||||
@@ -274,12 +274,14 @@ gt crew add <name> --rig <rig> # Create crew workspace
|
||||
```bash
|
||||
gt agents # List active agents
|
||||
gt sling <issue> <rig> # Assign work to agent
|
||||
gt sling <issue> <rig> --agent codex # Override runtime for this sling/spawn
|
||||
gt sling <issue> <rig> --agent cursor # Override runtime for this sling/spawn
|
||||
gt mayor attach # Start Mayor session
|
||||
gt mayor start --agent gemini # Run Mayor with a specific agent alias
|
||||
gt mayor start --agent auggie # Run Mayor with a specific agent alias
|
||||
gt prime # Alternative to mayor attach
|
||||
```
|
||||
|
||||
**Built-in agent presets**: `claude`, `gemini`, `codex`, `cursor`, `auggie`, `amp`
|
||||
|
||||
### Convoy (Work Tracking)
|
||||
|
||||
```bash
|
||||
|
||||
@@ -359,7 +359,7 @@ gt config agent remove <name> # Remove custom agent (built-ins protected)
|
||||
gt config default-agent [name] # Get or set town default agent
|
||||
```
|
||||
|
||||
**Built-in agents**: `claude`, `gemini`, `codex`
|
||||
**Built-in agents**: `claude`, `gemini`, `codex`, `cursor`, `auggie`, `amp`
|
||||
|
||||
**Custom agents**: Define per-town in `mayor/town.json`:
|
||||
```bash
|
||||
|
||||
@@ -113,9 +113,9 @@ func resolveBeadsDirWithDepth(beadsDir string, maxDepth int) string {
|
||||
// cleanBeadsRuntimeFiles removes gitignored runtime files from a .beads directory
|
||||
// while preserving tracked files (formulas/, README.md, config.yaml, .gitignore).
|
||||
// This is safe to call even if the directory doesn't exist.
|
||||
func cleanBeadsRuntimeFiles(beadsDir string) error {
|
||||
func cleanBeadsRuntimeFiles(beadsDir string) {
|
||||
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
||||
return nil // Nothing to clean
|
||||
return // Nothing to clean
|
||||
}
|
||||
|
||||
// Runtime files/patterns that are gitignored and safe to remove
|
||||
@@ -144,11 +144,9 @@ func cleanBeadsRuntimeFiles(beadsDir string) error {
|
||||
continue // Invalid pattern, skip
|
||||
}
|
||||
for _, match := range matches {
|
||||
os.RemoveAll(match) // Best effort, ignore errors
|
||||
_ = os.RemoveAll(match) // Best effort, ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupRedirect creates a .beads/redirect file for a worktree to point to the rig's shared beads.
|
||||
@@ -192,9 +190,7 @@ func SetupRedirect(townRoot, worktreePath string) error {
|
||||
|
||||
// Clean up runtime files in .beads/ but preserve tracked files (formulas/, README.md, etc.)
|
||||
worktreeBeadsDir := filepath.Join(worktreePath, ".beads")
|
||||
if err := cleanBeadsRuntimeFiles(worktreeBeadsDir); err != nil {
|
||||
return fmt.Errorf("cleaning runtime files: %w", err)
|
||||
}
|
||||
cleanBeadsRuntimeFiles(worktreeBeadsDir)
|
||||
|
||||
// Create .beads directory if it doesn't exist
|
||||
if err := os.MkdirAll(worktreeBeadsDir, 0755); err != nil {
|
||||
|
||||
@@ -153,7 +153,7 @@ func runConfigAgentList(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Collect all agents
|
||||
builtInAgents := []string{"claude", "gemini", "codex"}
|
||||
builtInAgents := config.ListAgentPresets()
|
||||
customAgents := make(map[string]*config.RuntimeConfig)
|
||||
if townSettings.Agents != nil {
|
||||
for name, runtime := range townSettings.Agents {
|
||||
@@ -330,7 +330,7 @@ func runConfigAgentSet(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf("Agent '%s' set to: %s\n", style.Bold.Render(name), commandLine)
|
||||
|
||||
// Check if this overrides a built-in
|
||||
builtInAgents := []string{"claude", "gemini", "codex"}
|
||||
builtInAgents := config.ListAgentPresets()
|
||||
for _, builtin := range builtInAgents {
|
||||
if name == builtin {
|
||||
fmt.Printf("\n%s\n", style.Dim.Render("(overriding built-in '"+builtin+"' preset)"))
|
||||
@@ -350,7 +350,7 @@ func runConfigAgentRemove(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Check if trying to remove built-in
|
||||
builtInAgents := []string{"claude", "gemini", "codex"}
|
||||
builtInAgents := config.ListAgentPresets()
|
||||
for _, builtin := range builtInAgents {
|
||||
if name == builtin {
|
||||
return fmt.Errorf("cannot remove built-in agent '%s' (use 'gt config agent set' to override it)", name)
|
||||
@@ -415,7 +415,7 @@ func runConfigDefaultAgent(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// Verify agent exists
|
||||
isValid := false
|
||||
builtInAgents := []string{"claude", "gemini", "codex"}
|
||||
builtInAgents := config.ListAgentPresets()
|
||||
for _, builtin := range builtInAgents {
|
||||
if name == builtin {
|
||||
isValid = true
|
||||
|
||||
@@ -313,6 +313,16 @@ func initTownBeads(townPath string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Configure custom types for Gas Town (agent, role, rig, convoy).
|
||||
// These were extracted from beads core in v0.46.0 and now require explicit config.
|
||||
customTypes := "agent,role,rig,convoy,event"
|
||||
configCmd := exec.Command("bd", "config", "set", "types.custom", customTypes)
|
||||
configCmd.Dir = townPath
|
||||
if configOutput, configErr := configCmd.CombinedOutput(); configErr != nil {
|
||||
// Non-fatal: older beads versions don't need this, newer ones do
|
||||
fmt.Printf(" %s Could not set custom types: %s\n", style.Dim.Render("⚠"), strings.TrimSpace(string(configOutput)))
|
||||
}
|
||||
|
||||
// Ensure database has repository fingerprint (GH #25).
|
||||
// This is idempotent - safe on both new and legacy (pre-0.17.5) databases.
|
||||
// Without fingerprint, the bd daemon fails to start silently.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1034,6 +1034,10 @@ func (c *BeadsRedirectCheck) Fix(ctx *CheckContext) error {
|
||||
// Continue - minimal config created
|
||||
} else {
|
||||
_ = output // bd init succeeded
|
||||
// Configure custom types for Gas Town (beads v0.46.0+)
|
||||
configCmd := exec.Command("bd", "config", "set", "types.custom", "agent,role,rig,convoy,event")
|
||||
configCmd.Dir = rigPath
|
||||
_, _ = configCmd.CombinedOutput() // Ignore errors - older beads don't need this
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -359,6 +359,10 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) {
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
fmt.Printf(" Warning: Could not init bd database: %v (%s)\n", err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
// Configure custom types for Gas Town (beads v0.46.0+)
|
||||
configCmd := exec.Command("bd", "config", "set", "types.custom", "agent,role,rig,convoy,event")
|
||||
configCmd.Dir = mayorRigPath
|
||||
_, _ = configCmd.CombinedOutput() // Ignore errors - older beads don't need this
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -579,6 +583,14 @@ func (m *Manager) initBeads(rigPath, prefix string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Configure custom types for Gas Town (agent, role, rig, convoy).
|
||||
// These were extracted from beads core in v0.46.0 and now require explicit config.
|
||||
configCmd := exec.Command("bd", "config", "set", "types.custom", "agent,role,rig,convoy,event")
|
||||
configCmd.Dir = rigPath
|
||||
configCmd.Env = filteredEnv
|
||||
// Ignore errors - older beads versions don't need this
|
||||
_, _ = configCmd.CombinedOutput()
|
||||
|
||||
// Ensure database has repository fingerprint (GH #25).
|
||||
// This is idempotent - safe on both new and legacy (pre-0.17.5) databases.
|
||||
// Without fingerprint, the bd daemon fails to start silently.
|
||||
|
||||
@@ -309,3 +309,119 @@ func TestEnsureSessionFresh_IdempotentOnZombie(t *testing.T) {
|
||||
t.Error("expected session to exist after multiple EnsureSessionFresh calls")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAgentRunning(t *testing.T) {
|
||||
if !hasTmux() {
|
||||
t.Skip("tmux not installed")
|
||||
}
|
||||
|
||||
tm := NewTmux()
|
||||
sessionName := "gt-test-agent-" + t.Name()
|
||||
|
||||
// Clean up any existing session
|
||||
_ = tm.KillSession(sessionName)
|
||||
|
||||
// Create session (will run default shell)
|
||||
if err := tm.NewSession(sessionName, ""); err != nil {
|
||||
t.Fatalf("NewSession: %v", err)
|
||||
}
|
||||
defer func() { _ = tm.KillSession(sessionName) }()
|
||||
|
||||
// Get the current pane command (should be bash/zsh/etc)
|
||||
cmd, err := tm.GetPaneCommand(sessionName)
|
||||
if err != nil {
|
||||
t.Fatalf("GetPaneCommand: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
processNames []string
|
||||
wantRunning bool
|
||||
}{
|
||||
{
|
||||
name: "empty process list",
|
||||
processNames: []string{},
|
||||
wantRunning: false,
|
||||
},
|
||||
{
|
||||
name: "matching shell process",
|
||||
processNames: []string{cmd}, // Current shell
|
||||
wantRunning: true,
|
||||
},
|
||||
{
|
||||
name: "claude agent (node) - not running",
|
||||
processNames: []string{"node"},
|
||||
wantRunning: cmd == "node", // Only true if shell happens to be node
|
||||
},
|
||||
{
|
||||
name: "gemini agent - not running",
|
||||
processNames: []string{"gemini"},
|
||||
wantRunning: cmd == "gemini",
|
||||
},
|
||||
{
|
||||
name: "cursor agent - not running",
|
||||
processNames: []string{"cursor-agent"},
|
||||
wantRunning: cmd == "cursor-agent",
|
||||
},
|
||||
{
|
||||
name: "multiple process names with match",
|
||||
processNames: []string{"nonexistent", cmd, "also-nonexistent"},
|
||||
wantRunning: true,
|
||||
},
|
||||
{
|
||||
name: "multiple process names without match",
|
||||
processNames: []string{"nonexistent1", "nonexistent2"},
|
||||
wantRunning: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tm.IsAgentRunning(sessionName, tt.processNames...)
|
||||
if got != tt.wantRunning {
|
||||
t.Errorf("IsAgentRunning(%q, %v) = %v, want %v (current cmd: %q)",
|
||||
sessionName, tt.processNames, got, tt.wantRunning, cmd)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAgentRunning_NonexistentSession(t *testing.T) {
|
||||
if !hasTmux() {
|
||||
t.Skip("tmux not installed")
|
||||
}
|
||||
|
||||
tm := NewTmux()
|
||||
|
||||
// IsAgentRunning on nonexistent session should return false, not error
|
||||
got := tm.IsAgentRunning("nonexistent-session-xyz", "node", "gemini", "cursor-agent")
|
||||
if got {
|
||||
t.Error("IsAgentRunning on nonexistent session should return false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsClaudeRunning(t *testing.T) {
|
||||
if !hasTmux() {
|
||||
t.Skip("tmux not installed")
|
||||
}
|
||||
|
||||
tm := NewTmux()
|
||||
sessionName := "gt-test-claude-" + t.Name()
|
||||
|
||||
// Clean up any existing session
|
||||
_ = tm.KillSession(sessionName)
|
||||
|
||||
// Create session (will run default shell, not Claude)
|
||||
if err := tm.NewSession(sessionName, ""); err != nil {
|
||||
t.Fatalf("NewSession: %v", err)
|
||||
}
|
||||
defer func() { _ = tm.KillSession(sessionName) }()
|
||||
|
||||
// IsClaudeRunning should be false (shell is running, not node)
|
||||
cmd, _ := tm.GetPaneCommand(sessionName)
|
||||
wantRunning := cmd == "node"
|
||||
|
||||
if got := tm.IsClaudeRunning(sessionName); got != wantRunning {
|
||||
t.Errorf("IsClaudeRunning() = %v, want %v (pane cmd: %q)", got, wantRunning, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user