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

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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.

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.

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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)
}
}