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]
|
## [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
|
## [0.2.2] - 2026-01-07
|
||||||
|
|
||||||
Rig operational state management, unified agent startup, and extensive stability fixes.
|
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
|
```bash
|
||||||
gt agents # List active agents
|
gt agents # List active agents
|
||||||
gt sling <issue> <rig> # Assign work to agent
|
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 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
|
gt prime # Alternative to mayor attach
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Built-in agent presets**: `claude`, `gemini`, `codex`, `cursor`, `auggie`, `amp`
|
||||||
|
|
||||||
### Convoy (Work Tracking)
|
### Convoy (Work Tracking)
|
||||||
|
|
||||||
```bash
|
```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
|
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`:
|
**Custom agents**: Define per-town in `mayor/town.json`:
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -113,9 +113,9 @@ func resolveBeadsDirWithDepth(beadsDir string, maxDepth int) string {
|
|||||||
// cleanBeadsRuntimeFiles removes gitignored runtime files from a .beads directory
|
// cleanBeadsRuntimeFiles removes gitignored runtime files from a .beads directory
|
||||||
// while preserving tracked files (formulas/, README.md, config.yaml, .gitignore).
|
// while preserving tracked files (formulas/, README.md, config.yaml, .gitignore).
|
||||||
// This is safe to call even if the directory doesn't exist.
|
// 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) {
|
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
|
// Runtime files/patterns that are gitignored and safe to remove
|
||||||
@@ -144,11 +144,9 @@ func cleanBeadsRuntimeFiles(beadsDir string) error {
|
|||||||
continue // Invalid pattern, skip
|
continue // Invalid pattern, skip
|
||||||
}
|
}
|
||||||
for _, match := range matches {
|
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.
|
// 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.)
|
// Clean up runtime files in .beads/ but preserve tracked files (formulas/, README.md, etc.)
|
||||||
worktreeBeadsDir := filepath.Join(worktreePath, ".beads")
|
worktreeBeadsDir := filepath.Join(worktreePath, ".beads")
|
||||||
if err := cleanBeadsRuntimeFiles(worktreeBeadsDir); err != nil {
|
cleanBeadsRuntimeFiles(worktreeBeadsDir)
|
||||||
return fmt.Errorf("cleaning runtime files: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create .beads directory if it doesn't exist
|
// Create .beads directory if it doesn't exist
|
||||||
if err := os.MkdirAll(worktreeBeadsDir, 0755); err != nil {
|
if err := os.MkdirAll(worktreeBeadsDir, 0755); err != nil {
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ func runConfigAgentList(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Collect all agents
|
// Collect all agents
|
||||||
builtInAgents := []string{"claude", "gemini", "codex"}
|
builtInAgents := config.ListAgentPresets()
|
||||||
customAgents := make(map[string]*config.RuntimeConfig)
|
customAgents := make(map[string]*config.RuntimeConfig)
|
||||||
if townSettings.Agents != nil {
|
if townSettings.Agents != nil {
|
||||||
for name, runtime := range townSettings.Agents {
|
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)
|
fmt.Printf("Agent '%s' set to: %s\n", style.Bold.Render(name), commandLine)
|
||||||
|
|
||||||
// Check if this overrides a built-in
|
// Check if this overrides a built-in
|
||||||
builtInAgents := []string{"claude", "gemini", "codex"}
|
builtInAgents := config.ListAgentPresets()
|
||||||
for _, builtin := range builtInAgents {
|
for _, builtin := range builtInAgents {
|
||||||
if name == builtin {
|
if name == builtin {
|
||||||
fmt.Printf("\n%s\n", style.Dim.Render("(overriding built-in '"+builtin+"' preset)"))
|
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
|
// Check if trying to remove built-in
|
||||||
builtInAgents := []string{"claude", "gemini", "codex"}
|
builtInAgents := config.ListAgentPresets()
|
||||||
for _, builtin := range builtInAgents {
|
for _, builtin := range builtInAgents {
|
||||||
if name == builtin {
|
if name == builtin {
|
||||||
return fmt.Errorf("cannot remove built-in agent '%s' (use 'gt config agent set' to override it)", name)
|
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
|
// Verify agent exists
|
||||||
isValid := false
|
isValid := false
|
||||||
builtInAgents := []string{"claude", "gemini", "codex"}
|
builtInAgents := config.ListAgentPresets()
|
||||||
for _, builtin := range builtInAgents {
|
for _, builtin := range builtInAgents {
|
||||||
if name == builtin {
|
if name == builtin {
|
||||||
isValid = true
|
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).
|
// Ensure database has repository fingerprint (GH #25).
|
||||||
// This is idempotent - safe on both new and legacy (pre-0.17.5) databases.
|
// This is idempotent - safe on both new and legacy (pre-0.17.5) databases.
|
||||||
// Without fingerprint, the bd daemon fails to start silently.
|
// Without fingerprint, the bd daemon fails to start silently.
|
||||||
|
|||||||
@@ -21,12 +21,18 @@ const (
|
|||||||
AgentGemini AgentPreset = "gemini"
|
AgentGemini AgentPreset = "gemini"
|
||||||
// AgentCodex is OpenAI Codex.
|
// AgentCodex is OpenAI Codex.
|
||||||
AgentCodex AgentPreset = "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.
|
// AgentPresetInfo contains the configuration details for an agent preset.
|
||||||
// This extends the basic RuntimeConfig with agent-specific metadata.
|
// This extends the basic RuntimeConfig with agent-specific metadata.
|
||||||
type AgentPresetInfo struct {
|
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"`
|
Name AgentPreset `json:"name"`
|
||||||
|
|
||||||
// Command is the CLI binary to invoke.
|
// 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 are the default command-line arguments for autonomous mode.
|
||||||
Args []string `json:"args"`
|
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.
|
// SessionIDEnv is the environment variable for session ID.
|
||||||
// Used for resuming sessions across restarts.
|
// Used for resuming sessions across restarts.
|
||||||
SessionIDEnv string `json:"session_id_env,omitempty"`
|
SessionIDEnv string `json:"session_id_env,omitempty"`
|
||||||
@@ -91,6 +102,7 @@ var builtinPresets = map[AgentPreset]*AgentPresetInfo{
|
|||||||
Name: AgentClaude,
|
Name: AgentClaude,
|
||||||
Command: "claude",
|
Command: "claude",
|
||||||
Args: []string{"--dangerously-skip-permissions"},
|
Args: []string{"--dangerously-skip-permissions"},
|
||||||
|
ProcessNames: []string{"node"}, // Claude runs as Node.js
|
||||||
SessionIDEnv: "CLAUDE_SESSION_ID",
|
SessionIDEnv: "CLAUDE_SESSION_ID",
|
||||||
ResumeFlag: "--resume",
|
ResumeFlag: "--resume",
|
||||||
ResumeStyle: "flag",
|
ResumeStyle: "flag",
|
||||||
@@ -102,6 +114,7 @@ var builtinPresets = map[AgentPreset]*AgentPresetInfo{
|
|||||||
Name: AgentGemini,
|
Name: AgentGemini,
|
||||||
Command: "gemini",
|
Command: "gemini",
|
||||||
Args: []string{"--approval-mode", "yolo"},
|
Args: []string{"--approval-mode", "yolo"},
|
||||||
|
ProcessNames: []string{"gemini"}, // Gemini CLI binary
|
||||||
SessionIDEnv: "GEMINI_SESSION_ID",
|
SessionIDEnv: "GEMINI_SESSION_ID",
|
||||||
ResumeFlag: "--resume",
|
ResumeFlag: "--resume",
|
||||||
ResumeStyle: "flag",
|
ResumeStyle: "flag",
|
||||||
@@ -116,6 +129,7 @@ var builtinPresets = map[AgentPreset]*AgentPresetInfo{
|
|||||||
Name: AgentCodex,
|
Name: AgentCodex,
|
||||||
Command: "codex",
|
Command: "codex",
|
||||||
Args: []string{"--yolo"},
|
Args: []string{"--yolo"},
|
||||||
|
ProcessNames: []string{"codex"}, // Codex CLI binary
|
||||||
SessionIDEnv: "", // Codex captures from JSONL output
|
SessionIDEnv: "", // Codex captures from JSONL output
|
||||||
ResumeFlag: "resume",
|
ResumeFlag: "resume",
|
||||||
ResumeStyle: "subcommand",
|
ResumeStyle: "subcommand",
|
||||||
@@ -126,6 +140,43 @@ var builtinPresets = map[AgentPreset]*AgentPresetInfo{
|
|||||||
OutputFlag: "--json",
|
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.
|
// Registry state with proper synchronization.
|
||||||
@@ -305,6 +356,18 @@ func GetSessionIDEnvVar(agentName string) string {
|
|||||||
return info.SessionIDEnv
|
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.
|
// MergeWithPreset applies preset defaults to a RuntimeConfig.
|
||||||
// User-specified values take precedence over preset defaults.
|
// User-specified values take precedence over preset defaults.
|
||||||
// Returns a new RuntimeConfig without modifying the original.
|
// Returns a new RuntimeConfig without modifying the original.
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestBuiltinPresets(t *testing.T) {
|
func TestBuiltinPresets(t *testing.T) {
|
||||||
// Ensure all built-in presets are accessible (E2E tested agents only)
|
// Ensure all built-in presets are accessible
|
||||||
presets := []AgentPreset{AgentClaude, AgentGemini, AgentCodex}
|
presets := []AgentPreset{AgentClaude, AgentGemini, AgentCodex, AgentCursor, AgentAuggie, AgentAmp}
|
||||||
|
|
||||||
for _, preset := range presets {
|
for _, preset := range presets {
|
||||||
info := GetAgentPreset(preset)
|
info := GetAgentPreset(preset)
|
||||||
@@ -22,6 +22,11 @@ func TestBuiltinPresets(t *testing.T) {
|
|||||||
if info.Command == "" {
|
if info.Command == "" {
|
||||||
t.Errorf("preset %s has empty Command", preset)
|
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},
|
{"claude", AgentClaude, false},
|
||||||
{"gemini", AgentGemini, false},
|
{"gemini", AgentGemini, false},
|
||||||
{"codex", AgentCodex, false},
|
{"codex", AgentCodex, false},
|
||||||
|
{"cursor", AgentCursor, false},
|
||||||
|
{"auggie", AgentAuggie, false},
|
||||||
|
{"amp", AgentAmp, false},
|
||||||
{"aider", "", true}, // Not built-in, can be added via config
|
{"aider", "", true}, // Not built-in, can be added via config
|
||||||
{"opencode", "", true}, // Not built-in, can be added via config
|
{"opencode", "", true}, // Not built-in, can be added via config
|
||||||
{"unknown", "", true},
|
{"unknown", "", true},
|
||||||
@@ -63,6 +71,9 @@ func TestRuntimeConfigFromPreset(t *testing.T) {
|
|||||||
{AgentClaude, "claude"},
|
{AgentClaude, "claude"},
|
||||||
{AgentGemini, "gemini"},
|
{AgentGemini, "gemini"},
|
||||||
{AgentCodex, "codex"},
|
{AgentCodex, "codex"},
|
||||||
|
{AgentCursor, "cursor-agent"},
|
||||||
|
{AgentAuggie, "auggie"},
|
||||||
|
{AgentAmp, "amp"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -84,6 +95,9 @@ func TestIsKnownPreset(t *testing.T) {
|
|||||||
{"claude", true},
|
{"claude", true},
|
||||||
{"gemini", true},
|
{"gemini", true},
|
||||||
{"codex", true},
|
{"codex", true},
|
||||||
|
{"cursor", true},
|
||||||
|
{"auggie", true},
|
||||||
|
{"amp", true},
|
||||||
{"aider", false}, // Not built-in, can be added via config
|
{"aider", false}, // Not built-in, can be added via config
|
||||||
{"opencode", false}, // Not built-in, can be added via config
|
{"opencode", false}, // Not built-in, can be added via config
|
||||||
{"unknown", false},
|
{"unknown", false},
|
||||||
@@ -286,6 +300,9 @@ func TestSupportsSessionResume(t *testing.T) {
|
|||||||
{"claude", true},
|
{"claude", true},
|
||||||
{"gemini", true},
|
{"gemini", true},
|
||||||
{"codex", true},
|
{"codex", true},
|
||||||
|
{"cursor", true},
|
||||||
|
{"auggie", true},
|
||||||
|
{"amp", true},
|
||||||
{"unknown", false},
|
{"unknown", false},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +322,10 @@ func TestGetSessionIDEnvVar(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{"claude", "CLAUDE_SESSION_ID"},
|
{"claude", "CLAUDE_SESSION_ID"},
|
||||||
{"gemini", "GEMINI_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", ""},
|
{"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
|
Version int `json:"version"` // schema version
|
||||||
|
|
||||||
// DefaultAgent is the name of the agent preset to use by default.
|
// 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.
|
// or a custom agent name defined in settings/agents.json.
|
||||||
// Default: "claude"
|
// Default: "claude"
|
||||||
DefaultAgent string `json:"default_agent,omitempty"`
|
DefaultAgent string `json:"default_agent,omitempty"`
|
||||||
@@ -190,7 +190,7 @@ type RigSettings struct {
|
|||||||
Runtime *RuntimeConfig `json:"runtime,omitempty"` // LLM runtime settings (deprecated: use Agent)
|
Runtime *RuntimeConfig `json:"runtime,omitempty"` // LLM runtime settings (deprecated: use Agent)
|
||||||
|
|
||||||
// Agent selects which agent preset to use for this rig.
|
// 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.
|
// or a custom agent defined in settings/agents.json.
|
||||||
// If empty, uses the town's default_agent setting.
|
// If empty, uses the town's default_agent setting.
|
||||||
// Takes precedence over Runtime if both are set.
|
// Takes precedence over Runtime if both are set.
|
||||||
|
|||||||
@@ -1034,6 +1034,10 @@ func (c *BeadsRedirectCheck) Fix(ctx *CheckContext) error {
|
|||||||
// Continue - minimal config created
|
// Continue - minimal config created
|
||||||
} else {
|
} else {
|
||||||
_ = output // bd init succeeded
|
_ = 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -359,6 +359,10 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) {
|
|||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
fmt.Printf(" Warning: Could not init bd database: %v (%s)\n", err, strings.TrimSpace(string(output)))
|
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).
|
// Ensure database has repository fingerprint (GH #25).
|
||||||
// This is idempotent - safe on both new and legacy (pre-0.17.5) databases.
|
// This is idempotent - safe on both new and legacy (pre-0.17.5) databases.
|
||||||
// Without fingerprint, the bd daemon fails to start silently.
|
// 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")
|
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