Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Guide for AI agents working in this repository.
This is **Task You** - a personal task management system with:
- **SQLite storage** for tasks and projects
- **SSH-accessible TUI** via Wish
- **Background executor** with support for multiple AI coding agents (Claude, Codex, Gemini, Grok, Pi, OpenClaw, OpenCode)
- **Background executor** with support for multiple AI coding agents (Claude, Codex, Gemini, Grok, Cursor, Pi, OpenClaw, OpenCode)
- **Beautiful terminal UI** built with Charm libraries (Kanban board)
- **Git worktree isolation** for parallel task execution
- **Task lifecycle hooks** for real-time task state tracking
Expand Down Expand Up @@ -304,6 +304,7 @@ TaskYou supports multiple AI coding agent backends:
- **codex** - OpenAI Codex CLI with dangerous mode support
- **gemini** - Google Gemini CLI with configurable dangerous mode flags
- **grok** - Grok CLI with session resume and `--always-approve` dangerous mode
- **cursor** - Cursor Agent CLI with session resume and `--force` dangerous mode
- **pi** - Pi coding agent with session continuity
- **openclaw** - OpenClaw AI assistant
- **opencode** - OpenCode AI assistant
Expand Down Expand Up @@ -434,6 +435,7 @@ wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
| `WORKTREE_CWD` | Working directory for project detection | - |
| `GEMINI_DANGEROUS_ARGS` | Overrides the Gemini CLI flags used when dangerous mode is enabled | `--dangerously-allow-run` |
| `GROK_DANGEROUS_ARGS` | Overrides the Grok CLI flags used when dangerous mode is enabled | `--always-approve` |
| `CURSOR_DANGEROUS_ARGS` | Overrides the Cursor CLI flags used when dangerous mode is enabled | `--force` |

Set `TASK_EXECUTOR` before launching `task -l`/`task daemon` to change the executor label shown in the UI (e.g., `TASK_EXECUTOR=codex`). For compatibility, `WORKFLOW_EXECUTOR`, `TASKYOU_EXECUTOR`, and `WORKTREE_EXECUTOR` are also recognized.

Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -453,13 +453,14 @@ Task You supports multiple AI executors for processing tasks. You can choose the
| Codex | `codex` | [OpenAI Codex CLI](https://github.com/openai/codex) - OpenAI's coding assistant |
| Gemini | `gemini` | [Gemini CLI](https://ai.google.dev/gemini-api/docs/cli) - Google's Gemini-based coding assistant |
| Grok | `grok` | [Grok CLI](https://x.ai/cli) - xAI's coding assistant with session resumption |
| Cursor | `agent` / `cursor-agent` | [Cursor CLI](https://cursor.com/docs/cli) - Cursor's coding agent with session resumption |
| Pi | `pi` | [Pi Coding Agent](https://github.com/mariozechner/pi-coding-agent) - Multi-provider AI coding agent with session continuity |
| OpenCode | `opencode` | [OpenCode](https://opencode.ai) - Open-source AI coding assistant with multi-LLM support |
| OpenClaw | `openclaw` | [OpenClaw](https://openclaw.ai) - Open-source personal AI assistant with session resumption |

All executors run in tmux windows with the same worktree isolation and environment variables. The main differences:

- **Claude Code**, **Grok**, **Pi**, and **OpenClaw** support session resumption - when you retry a task, they continue with full conversation history
- **Claude Code**, **Grok**, **Cursor**, **Pi**, and **OpenClaw** support session resumption - when you retry a task, they continue with full conversation history
- **Codex** and **Gemini** start fresh on each execution but receive the full prompt with any feedback
- **OpenCode** does not support session resumption

Expand All @@ -480,6 +481,9 @@ npm install -g @openai/codex
# Grok CLI
curl -fsSL https://x.ai/cli/install.sh | bash

# Cursor Agent CLI
curl https://cursor.com/install -fsS | bash

# Pi Coding Agent
npm install -g @mariozechner/pi-coding-agent

Expand Down
2 changes: 2 additions & 0 deletions cmd/task/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1917,6 +1917,8 @@ func TestCreateModelValidation(t *testing.T) {
{db.ExecutorClaude, "claude-opus-5"},
{db.ExecutorClaude, "claude-opus-9"}, // released after this code
{db.ExecutorGrok, "grok-4"},
{db.ExecutorCursor, "gpt-5"},
{db.ExecutorCursor, "composer-1"},
{db.ExecutorCodex, ""}, // modelless executor, no override
}
for _, tt := range accepted {
Expand Down
1 change: 1 addition & 0 deletions cmd/task/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ func completeFlagExecutors(cmd *cobra.Command, args []string, toComplete string)
"codex\tOpenAI Codex",
"gemini\tGoogle Gemini",
"grok\txAI Grok",
"cursor\tCursor Agent",
"pi\tInflection Pi",
"opencode\tOpenCode",
"openclaw\tOpenClaw",
Expand Down
4 changes: 2 additions & 2 deletions cmd/task/completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ func TestCompleteFlagExecutors(t *testing.T) {
if directive != cobra.ShellCompDirectiveNoFileComp {
t.Errorf("expected NoFileComp directive")
}
if len(completions) != 7 {
t.Errorf("expected 7 executors, got %d", len(completions))
if len(completions) != 8 {
t.Errorf("expected 8 executors, got %d", len(completions))
}
}

Expand Down
38 changes: 28 additions & 10 deletions cmd/task/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ Tasks will automatically reconnect to their agent sessions when viewed.`,
worktreeGuardCmd := &cobra.Command{
Use: "worktree-guard",
Short: "Evaluate the worktree write-guard for a pre-tool hook",
Hidden: true, // Internal use only - invoked by codex/gemini/grok/opencode hooks
Hidden: true, // Internal use only - invoked by codex/gemini/grok/cursor/opencode hooks
Run: func(cmd *cobra.Command, args []string) {
format, _ := cmd.Flags().GetString("format")
// Never block the agent on our own failure: handle errors by allowing.
Expand All @@ -438,7 +438,7 @@ Tasks will automatically reconnect to their agent sessions when viewed.`,
}
},
}
worktreeGuardCmd.Flags().String("format", "", "Output format: codex | gemini | grok | opencode")
worktreeGuardCmd.Flags().String("format", "", "Output format: codex | gemini | grok | cursor | opencode")
rootCmd.AddCommand(worktreeGuardCmd)

// MCP server subcommand - runs the workflow MCP server for a task (internal use)
Expand Down Expand Up @@ -900,9 +900,9 @@ Examples:
createCmd.Flags().String("body", "", "Task body/description (if no title, AI generates from body)")
createCmd.Flags().StringP("type", "t", "", "Task type: code, writing, thinking (default: code)")
createCmd.Flags().StringP("project", "p", "", "Project name (auto-detected from cwd if not specified)")
createCmd.Flags().StringP("executor", "e", "", "Task executor: claude, codex, gemini, grok, pi, opencode, openclaw (default: claude)")
createCmd.Flags().StringP("executor", "e", "", "Task executor: claude, codex, gemini, grok, cursor, pi, opencode, openclaw (default: claude)")
createCmd.Flags().String("effort", "", "Per-task Claude effort override: low, medium, high, xhigh, max (default: Claude's global default)")
createCmd.Flags().String("model", "", "Per-task model override: opus, sonnet, haiku, fable, or a full model ID like claude-opus-5 (default: the agent's own default). Claude and grok only")
createCmd.Flags().String("model", "", "Per-task model override: opus, sonnet, haiku, fable, or a full model ID like claude-opus-5 (default: the agent's own default). Claude, grok, and cursor only")
createCmd.Flags().BoolP("execute", "x", false, "Queue task for immediate execution")
createCmd.Flags().Bool("dangerous", false, "Execute in dangerous mode (alias for --permission-mode dangerous)")
createCmd.Flags().String("permission-mode", "", "Permission mode: default (prompt), accept-edits (auto-accept file edits), auto (Claude Code auto mode: auto-approve safe actions, block risky ones), dangerous (skip all). Defaults to the project's setting")
Expand Down Expand Up @@ -1928,7 +1928,7 @@ Examples:
updateCmd.Flags().String("body", "", "Update task body/description")
updateCmd.Flags().StringP("type", "t", "", "Update task type: code, writing, thinking")
updateCmd.Flags().StringP("project", "p", "", "Update project name")
updateCmd.Flags().StringP("executor", "e", "", "Update task executor: claude, codex, gemini, grok, pi, opencode, openclaw")
updateCmd.Flags().StringP("executor", "e", "", "Update task executor: claude, codex, gemini, grok, cursor, pi, opencode, openclaw")
updateCmd.Flags().String("tags", "", "Update task tags (comma-separated)")
updateCmd.Flags().Bool("pinned", false, "Pin or unpin the task")
updateCmd.RegisterFlagCompletionFunc("project", completeFlagProjects)
Expand Down Expand Up @@ -4967,6 +4967,8 @@ type worktreeGuardHookInput struct {
ToolNameCamel string `json:"toolName"`
ToolInputCamel json.RawMessage `json:"toolInput"`
PermissionModeCamel string `json:"permissionMode"`
// Cursor beforeShellExecution puts the command at the top level.
Command string `json:"command"`
}

func (in *worktreeGuardHookInput) normalize() {
Expand All @@ -4979,10 +4981,16 @@ func (in *worktreeGuardHookInput) normalize() {
if in.PermissionMode == "" {
in.PermissionMode = in.PermissionModeCamel
}
if in.ToolName == "" && in.Command != "" {
in.ToolName = "Bash"
if raw, err := json.Marshal(map[string]string{"command": in.Command}); err == nil {
in.ToolInput = raw
}
}
}

// handleWorktreeGuardHook is the executor-agnostic transport for the worktree
// write-guard, shared by the codex/gemini/grok/opencode pre-tool hooks. It reads the
// write-guard, shared by the codex/gemini/grok/cursor/opencode pre-tool hooks. It reads the
// worktree root from WORKTREE_PATH (set by every executor when launching the CLI),
// evaluates the single shared policy (EvaluateWorktreeWriteGuard), and renders the
// decision in the requested executor's wire format. It fails open on any error so a
Expand Down Expand Up @@ -5016,7 +5024,7 @@ func handleWorktreeGuardHook(format string) error {
}

// renderWorktreeGuardDecision writes a guard decision in the wire format the given
// executor's hook expects. None of codex/gemini/grok/opencode support an interactive
// executor's hook expects. None of codex/gemini/grok/cursor/opencode support an interactive
// "ask" in their pre-tool hook, so an "ask" is downgraded to a hard deny to fail
// safe (the escape hatch is worktree.allow_external_writes in .taskyou.yml). When
// the guard allows the call, nothing is emitted (and exit stays 0) so the CLI's own
Expand Down Expand Up @@ -5049,6 +5057,15 @@ func renderWorktreeGuardDecision(format string, decision *executor.WorktreeGuard
"decision": "deny",
"reason": decision.Reason,
})
case "cursor":
// Cursor preToolUse / beforeShellExecution: JSON permission deny, plus
// exit 2 which Cursor documents as blocking the tool (Claude-compatible).
emitJSONLine(map[string]any{
"permission": "deny",
"user_message": decision.Reason,
"agent_message": decision.Reason,
})
os.Exit(2)
case "opencode":
// The OpenCode plugin reads the reason from stdout and treats exit code 1 as
// a denial (which it surfaces by throwing, aborting the tool call).
Expand Down Expand Up @@ -5678,12 +5695,13 @@ func getSessions() []agentSession {

// getAgentMemoryByTaskID returns a map of task ID -> memory (MB) for all agent processes.
// It identifies task IDs by examining each agent process's working directory.
// Supports all executors: claude, codex, gemini, grok, openclaw, opencode, pi.
// Supports all executors: claude, codex, gemini, grok, cursor, openclaw, opencode, pi.
func getAgentMemoryByTaskID() map[int]int {
result := make(map[int]int)

// Find processes for all supported executors
executorNames := []string{"claude", "codex", "gemini", "grok", "openclaw", "opencode", "pi"}
// Find processes for all supported executors. "cursor-agent" is the
// specific binary; skip the generic "agent" name (too many false positives).
executorNames := []string{"claude", "codex", "gemini", "grok", "cursor-agent", "openclaw", "opencode", "pi"}

for _, executorName := range executorNames {
pgrepOut, err := osexec.Command("pgrep", "-f", executorName).Output()
Expand Down
21 changes: 19 additions & 2 deletions desktop/src-tauri/src/env_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,17 @@ use serde::Serialize;
use std::process::Command;

/// Executor CLIs taskyou knows how to drive, in display order.
const EXECUTOR_CLIS: &[&str] = &["claude", "codex", "gemini", "grok", "opencode", "pi", "openclaw"];
/// Cursor ships as `cursor-agent` (preferred) or `agent`; see check_environment.
const EXECUTOR_CLIS: &[&str] = &[
"claude",
"codex",
"gemini",
"grok",
"cursor",
"opencode",
"pi",
"openclaw",
];

/// Replace this process's PATH with the login shell's PATH so child processes
/// (ty → tmux → executors) resolve tools the way the user's terminal does.
Expand Down Expand Up @@ -84,12 +94,19 @@ pub fn check_environment() -> EnvironmentReport {
.iter()
.map(|name| ToolCheck {
name: (*name).to_string(),
path: which(name),
path: which_executor(name),
})
.collect(),
}
}

fn which_executor(name: &str) -> Option<String> {
if name == "cursor" {
return which("cursor-agent").or_else(|| which("agent"));
}
which(name)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/components/SetupCheck.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export function SetupCheck({
detail={
executorOk
? foundExecutors.map((e) => e.name).join(", ")
: "none found (claude, codex, gemini, grok, …)"
: "none found (claude, codex, gemini, grok, cursor, …)"
}
/>
{!executorOk && <InstallHint>npm install -g @anthropic-ai/claude-code</InstallHint>}
Expand Down
6 changes: 6 additions & 0 deletions docs/executor_interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ Review `internal/executor/gemini_executor.go` for a reference implementation tha
The Grok executor launches the interactive TUI (`grok "prompt"`) inside tmux, the same pattern as Gemini. Dangerous mode maps to `--always-approve` (overridable via `GROK_DANGEROUS_ARGS`). Other permission modes pass through as `--permission-mode acceptEdits|auto`. Per-task `--effort` and `--model` match Claude. Session resume uses `--resume <session-id>`; Grok stores sessions under `~/.grok/sessions/<urlencoded-cwd>/<id>/`. Do **not** pass Grok's own `--worktree` flag — Task You already isolates each task in a git worktree. Project-local PreToolUse hooks (the write-guard) require folder trust, so the executor sets `GROK_FOLDER_TRUST=0` when launching. The taskyou MCP stdio server is written to `<worktree>/.grok/config.toml` (`mcp_servers.taskyou` → `ty mcp-server --task-id`), because Grok has no `--mcp-config` flag.

Review `internal/executor/grok_executor.go` for the implementation.

## Cursor CLI Notes

The Cursor executor launches the interactive Agent CLI (`agent "prompt"`, or `cursor-agent` when that binary is on PATH) inside tmux. Dangerous mode maps to `--force` (overridable via `CURSOR_DANGEROUS_ARGS`; `--yolo` is an alias). Cursor has no Claude-style `--permission-mode`, so accept-edits/auto launch without extra flags. Per-task `--model` is supported (Cursor is multi-vendor, so any well-formed model ID is accepted). Session resume uses `--resume <chat-id>`; Cursor stores CLI chats under `~/.cursor/chats/<md5-cwd>/<id>/`. `--approve-mcps` is always passed so the worktree-local taskyou MCP server is not blocked on a TUI prompt. Do **not** pass Cursor's own `--worktree` flag — Task You already isolates each task in a git worktree. The taskyou MCP stdio server is merged into `<worktree>/.cursor/mcp.json` (`mcpServers.taskyou` → `ty mcp-server --task-id`), because Cursor has no `--mcp-config` flag. The write-guard is wired via `<worktree>/.cursor/hooks.json` (`preToolUse` and `beforeShellExecution`).

Review `internal/executor/cursor_executor.go` for the implementation.
35 changes: 23 additions & 12 deletions internal/db/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ const (
ModelOpusPlan = "opusplan"
)

// executorModelPrefix maps an executor to the ID prefix its models carry. Only
// executors listed here pass a --model flag to their CLI at all; for the rest
// (codex, gemini, pi, opencode, openclaw) a model override is dead config —
// the launch command never mentions it.
var executorModelPrefix = map[string]string{
ExecutorClaude: "claude-",
ExecutorGrok: "grok-",
// executorModelPrefix maps an executor to the ID prefixes its models carry.
// Only executors listed here pass a --model flag to their CLI at all; for the
// rest (codex, gemini, pi, opencode, openclaw) a model override is dead config
// — the launch command never mentions it. Cursor is multi-vendor, so it has
// several prefixes rather than one.
var executorModelPrefix = map[string][]string{
ExecutorClaude: {"claude-"},
ExecutorGrok: {"grok-"},
ExecutorCursor: {"gpt-", "composer-", "claude-", "grok-", "gemini-", "sonnet-", "opus-"},
}

// executorModelAliases lists the short names an executor's CLI accepts in place
Expand All @@ -44,6 +46,9 @@ var executorModelAliases = map[string][]string{
// The grok CLI takes full IDs only; these are examples for help text and
// shell completion, not a closed set (any grok-* ID is accepted).
ExecutorGrok: {"grok-4", "grok-4-fast", "grok-code-fast-1"},
// Cursor is multi-vendor; these are examples for help text and shell
// completion. Any well-formed model ID is also accepted.
ExecutorCursor: {"auto", "composer-1", "gpt-5", "sonnet-4", "grok"},
}

// modelIDShape matches the shape of a vendor model ID: dash-separated
Expand Down Expand Up @@ -118,12 +123,18 @@ func ValidateModel(executor, model string) error {
return nil
}
}
prefix := executorModelPrefix[executor]
if strings.HasPrefix(lower, prefix) && modelIDShape.MatchString(lower) {
return nil
prefixes := executorModelPrefix[executor]
for _, prefix := range prefixes {
if strings.HasPrefix(lower, prefix) && modelIDShape.MatchString(lower) {
return nil
}
}
prefixHint := strings.Join(prefixes, "* / ") + "*"
if len(prefixes) == 1 {
prefixHint = prefixes[0] + "*"
}
return fmt.Errorf("unknown %s model %q — known models: %s (or any %s* model ID)",
executor, model, strings.Join(ModelsForExecutor(executor), ", "), prefix)
return fmt.Errorf("unknown %s model %q — known models: %s (or any %s model ID)",
executor, model, strings.Join(ModelsForExecutor(executor), ", "), prefixHint)
}

// ValidateTaskModel checks a task's model override against the executor that
Expand Down
Loading