Skip to content
Open
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
6 changes: 3 additions & 3 deletions internal/db/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func (db *DB) RemoveDependency(blockerID, blockedID int64) error {
// GetBlockers returns all tasks that block the given task.
func (db *DB) GetBlockers(taskID int64) ([]*Task, error) {
rows, err := db.Query(`
SELECT t.id, t.title, t.body, t.status, t.type, t.project, COALESCE(t.executor, 'claude'),
SELECT t.id, t.title, t.body, t.status, t.type, t.project, COALESCE(t.executor, 'claude'), COALESCE(t.model, ''),
t.worktree_path, t.branch_name, t.port, t.claude_session_id,
COALESCE(t.daemon_session, ''), COALESCE(t.tmux_window_id, ''),
COALESCE(t.claude_pane_id, ''), COALESCE(t.shell_pane_id, ''),
Expand All @@ -128,7 +128,7 @@ func (db *DB) GetBlockers(taskID int64) ([]*Task, error) {
// GetBlockedBy returns all tasks that are blocked by the given task.
func (db *DB) GetBlockedBy(taskID int64) ([]*Task, error) {
rows, err := db.Query(`
SELECT t.id, t.title, t.body, t.status, t.type, t.project, COALESCE(t.executor, 'claude'),
SELECT t.id, t.title, t.body, t.status, t.type, t.project, COALESCE(t.executor, 'claude'), COALESCE(t.model, ''),
t.worktree_path, t.branch_name, t.port, t.claude_session_id,
COALESCE(t.daemon_session, ''), COALESCE(t.tmux_window_id, ''),
COALESCE(t.claude_pane_id, ''), COALESCE(t.shell_pane_id, ''),
Expand Down Expand Up @@ -303,7 +303,7 @@ func scanTaskRows(rows *sql.Rows) ([]*Task, error) {
for rows.Next() {
t := &Task{}
err := rows.Scan(
&t.ID, &t.Title, &t.Body, &t.Status, &t.Type, &t.Project, &t.Executor,
&t.ID, &t.Title, &t.Body, &t.Status, &t.Type, &t.Project, &t.Executor, &t.Model,
&t.WorktreePath, &t.BranchName, &t.Port, &t.ClaudeSessionID,
&t.DaemonSession, &t.TmuxWindowID, &t.ClaudePaneID, &t.ShellPaneID,
&t.PRURL, &t.PRNumber,
Expand Down
2 changes: 2 additions & 0 deletions internal/db/sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ func (db *DB) migrate() error {
`ALTER TABLE tasks ADD COLUMN archive_commit TEXT DEFAULT ''`, // Commit hash at time of archiving
`ALTER TABLE tasks ADD COLUMN archive_worktree_path TEXT DEFAULT ''`, // Original worktree path before archiving
`ALTER TABLE tasks ADD COLUMN archive_branch_name TEXT DEFAULT ''`, // Original branch name before archiving
// Model override for executor (e.g., "opus", "sonnet", "claude-opus-4-6")
`ALTER TABLE tasks ADD COLUMN model TEXT DEFAULT ''`, // Executor model override (empty = executor default)
}

for _, m := range alterMigrations {
Expand Down
39 changes: 20 additions & 19 deletions internal/db/tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Task struct {
Type string
Project string
Executor string // Task executor: "claude" (default), "codex", "gemini"
Model string // Model override for the executor (e.g., "opus", "sonnet", "claude-opus-4-6")
WorktreePath string
BranchName string
Port int // Unique port for running the application in this task's worktree
Expand Down Expand Up @@ -126,9 +127,9 @@ func (db *DB) CreateTask(t *Task) error {
}

result, err := db.Exec(`
INSERT INTO tasks (title, body, status, type, project, executor, pinned, tags)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, t.Title, t.Body, t.Status, t.Type, t.Project, t.Executor, t.Pinned, t.Tags)
INSERT INTO tasks (title, body, status, type, project, executor, model, pinned, tags)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, t.Title, t.Body, t.Status, t.Type, t.Project, t.Executor, t.Model, t.Pinned, t.Tags)
if err != nil {
return fmt.Errorf("insert task: %w", err)
}
Expand Down Expand Up @@ -167,7 +168,7 @@ func (db *DB) CreateTask(t *Task) error {
func (db *DB) GetTask(id int64) (*Task, error) {
t := &Task{}
err := db.QueryRow(`
SELECT id, title, body, status, type, project, COALESCE(executor, 'claude'),
SELECT id, title, body, status, type, project, COALESCE(executor, 'claude'), COALESCE(model, ''),
worktree_path, branch_name, port, claude_session_id,
COALESCE(daemon_session, ''), COALESCE(tmux_window_id, ''),
COALESCE(claude_pane_id, ''), COALESCE(shell_pane_id, ''),
Expand All @@ -179,7 +180,7 @@ func (db *DB) GetTask(id int64) (*Task, error) {
COALESCE(archive_worktree_path, ''), COALESCE(archive_branch_name, '')
FROM tasks WHERE id = ?
`, id).Scan(
&t.ID, &t.Title, &t.Body, &t.Status, &t.Type, &t.Project, &t.Executor,
&t.ID, &t.Title, &t.Body, &t.Status, &t.Type, &t.Project, &t.Executor, &t.Model,
&t.WorktreePath, &t.BranchName, &t.Port, &t.ClaudeSessionID,
&t.DaemonSession, &t.TmuxWindowID, &t.ClaudePaneID, &t.ShellPaneID,
&t.PRURL, &t.PRNumber,
Expand Down Expand Up @@ -210,7 +211,7 @@ type ListTasksOptions struct {
// ListTasks retrieves tasks with optional filters.
func (db *DB) ListTasks(opts ListTasksOptions) ([]*Task, error) {
query := `
SELECT id, title, body, status, type, project, COALESCE(executor, 'claude'),
SELECT id, title, body, status, type, project, COALESCE(executor, 'claude'), COALESCE(model, ''),
worktree_path, branch_name, port, claude_session_id,
COALESCE(daemon_session, ''), COALESCE(tmux_window_id, ''),
COALESCE(claude_pane_id, ''), COALESCE(shell_pane_id, ''),
Expand Down Expand Up @@ -265,7 +266,7 @@ func (db *DB) ListTasks(opts ListTasksOptions) ([]*Task, error) {
for rows.Next() {
t := &Task{}
err := rows.Scan(
&t.ID, &t.Title, &t.Body, &t.Status, &t.Type, &t.Project, &t.Executor,
&t.ID, &t.Title, &t.Body, &t.Status, &t.Type, &t.Project, &t.Executor, &t.Model,
&t.WorktreePath, &t.BranchName, &t.Port, &t.ClaudeSessionID,
&t.DaemonSession, &t.TmuxWindowID, &t.ClaudePaneID, &t.ShellPaneID,
&t.PRURL, &t.PRNumber,
Expand All @@ -288,7 +289,7 @@ func (db *DB) ListTasks(opts ListTasksOptions) ([]*Task, error) {
func (db *DB) GetMostRecentlyCreatedTask() (*Task, error) {
t := &Task{}
err := db.QueryRow(`
SELECT id, title, body, status, type, project, COALESCE(executor, 'claude'),
SELECT id, title, body, status, type, project, COALESCE(executor, 'claude'), COALESCE(model, ''),
worktree_path, branch_name, port, claude_session_id,
COALESCE(daemon_session, ''), COALESCE(tmux_window_id, ''),
COALESCE(claude_pane_id, ''), COALESCE(shell_pane_id, ''),
Expand All @@ -302,7 +303,7 @@ func (db *DB) GetMostRecentlyCreatedTask() (*Task, error) {
ORDER BY created_at DESC, id DESC
LIMIT 1
`).Scan(
&t.ID, &t.Title, &t.Body, &t.Status, &t.Type, &t.Project, &t.Executor,
&t.ID, &t.Title, &t.Body, &t.Status, &t.Type, &t.Project, &t.Executor, &t.Model,
&t.WorktreePath, &t.BranchName, &t.Port, &t.ClaudeSessionID,
&t.DaemonSession, &t.TmuxWindowID, &t.ClaudePaneID, &t.ShellPaneID,
&t.PRURL, &t.PRNumber,
Expand All @@ -329,7 +330,7 @@ func (db *DB) SearchTasks(query string, limit int) ([]*Task, error) {

// Build search query with LIKE clauses
sqlQuery := `
SELECT id, title, body, status, type, project, COALESCE(executor, 'claude'),
SELECT id, title, body, status, type, project, COALESCE(executor, 'claude'), COALESCE(model, ''),
worktree_path, branch_name, port, claude_session_id,
COALESCE(daemon_session, ''), COALESCE(tmux_window_id, ''),
COALESCE(claude_pane_id, ''), COALESCE(shell_pane_id, ''),
Expand Down Expand Up @@ -362,7 +363,7 @@ func (db *DB) SearchTasks(query string, limit int) ([]*Task, error) {
for rows.Next() {
t := &Task{}
err := rows.Scan(
&t.ID, &t.Title, &t.Body, &t.Status, &t.Type, &t.Project, &t.Executor,
&t.ID, &t.Title, &t.Body, &t.Status, &t.Type, &t.Project, &t.Executor, &t.Model,
&t.WorktreePath, &t.BranchName, &t.Port, &t.ClaudeSessionID,
&t.DaemonSession, &t.TmuxWindowID, &t.ClaudePaneID, &t.ShellPaneID,
&t.PRURL, &t.PRNumber,
Expand Down Expand Up @@ -455,13 +456,13 @@ func (db *DB) UpdateTask(t *Task) error {

_, err := db.Exec(`
UPDATE tasks SET
title = ?, body = ?, status = ?, type = ?, project = ?, executor = ?,
title = ?, body = ?, status = ?, type = ?, project = ?, executor = ?, model = ?,
worktree_path = ?, branch_name = ?, port = ?, claude_session_id = ?,
daemon_session = ?, pr_url = ?, pr_number = ?, dangerous_mode = ?,
pinned = ?, tags = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, t.Title, t.Body, t.Status, t.Type, t.Project, t.Executor,
`, t.Title, t.Body, t.Status, t.Type, t.Project, t.Executor, t.Model,
t.WorktreePath, t.BranchName, t.Port, t.ClaudeSessionID,
t.DaemonSession, t.PRURL, t.PRNumber, t.DangerousMode,
t.Pinned, t.Tags, t.ID)
Expand Down Expand Up @@ -703,7 +704,7 @@ func (db *DB) RetryTask(id int64, feedback string) error {
func (db *DB) GetNextQueuedTask() (*Task, error) {
t := &Task{}
err := db.QueryRow(`
SELECT id, title, body, status, type, project, COALESCE(executor, 'claude'),
SELECT id, title, body, status, type, project, COALESCE(executor, 'claude'), COALESCE(model, ''),
worktree_path, branch_name, port, claude_session_id,
COALESCE(daemon_session, ''), COALESCE(tmux_window_id, ''),
COALESCE(claude_pane_id, ''), COALESCE(shell_pane_id, ''),
Expand All @@ -718,7 +719,7 @@ func (db *DB) GetNextQueuedTask() (*Task, error) {
ORDER BY created_at ASC
LIMIT 1
`, StatusQueued).Scan(
&t.ID, &t.Title, &t.Body, &t.Status, &t.Type, &t.Project, &t.Executor,
&t.ID, &t.Title, &t.Body, &t.Status, &t.Type, &t.Project, &t.Executor, &t.Model,
&t.WorktreePath, &t.BranchName, &t.Port, &t.ClaudeSessionID,
&t.DaemonSession, &t.TmuxWindowID, &t.ClaudePaneID, &t.ShellPaneID,
&t.PRURL, &t.PRNumber,
Expand All @@ -739,7 +740,7 @@ func (db *DB) GetNextQueuedTask() (*Task, error) {
// GetQueuedTasks returns all queued tasks (waiting to be processed).
func (db *DB) GetQueuedTasks() ([]*Task, error) {
rows, err := db.Query(`
SELECT id, title, body, status, type, project, COALESCE(executor, 'claude'),
SELECT id, title, body, status, type, project, COALESCE(executor, 'claude'), COALESCE(model, ''),
worktree_path, branch_name, port, claude_session_id,
COALESCE(daemon_session, ''), COALESCE(tmux_window_id, ''),
COALESCE(claude_pane_id, ''), COALESCE(shell_pane_id, ''),
Expand All @@ -762,7 +763,7 @@ func (db *DB) GetQueuedTasks() ([]*Task, error) {
for rows.Next() {
t := &Task{}
if err := rows.Scan(
&t.ID, &t.Title, &t.Body, &t.Status, &t.Type, &t.Project, &t.Executor,
&t.ID, &t.Title, &t.Body, &t.Status, &t.Type, &t.Project, &t.Executor, &t.Model,
&t.WorktreePath, &t.BranchName, &t.Port, &t.ClaudeSessionID,
&t.DaemonSession, &t.TmuxWindowID, &t.ClaudePaneID, &t.ShellPaneID,
&t.PRURL, &t.PRNumber,
Expand All @@ -782,7 +783,7 @@ func (db *DB) GetQueuedTasks() ([]*Task, error) {
// These are candidates for automatic closure when their PR is merged.
func (db *DB) GetTasksWithBranches() ([]*Task, error) {
rows, err := db.Query(`
SELECT id, title, body, status, type, project, COALESCE(executor, 'claude'),
SELECT id, title, body, status, type, project, COALESCE(executor, 'claude'), COALESCE(model, ''),
worktree_path, branch_name, port, claude_session_id,
COALESCE(daemon_session, ''), COALESCE(tmux_window_id, ''),
COALESCE(claude_pane_id, ''), COALESCE(shell_pane_id, ''),
Expand All @@ -805,7 +806,7 @@ func (db *DB) GetTasksWithBranches() ([]*Task, error) {
for rows.Next() {
t := &Task{}
if err := rows.Scan(
&t.ID, &t.Title, &t.Body, &t.Status, &t.Type, &t.Project, &t.Executor,
&t.ID, &t.Title, &t.Body, &t.Status, &t.Type, &t.Project, &t.Executor, &t.Model,
&t.WorktreePath, &t.BranchName, &t.Port, &t.ClaudeSessionID,
&t.DaemonSession, &t.TmuxWindowID, &t.ClaudePaneID, &t.ShellPaneID,
&t.PRURL, &t.PRNumber,
Expand Down
22 changes: 14 additions & 8 deletions internal/executor/claude_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ func (c *ClaudeExecutor) BuildCommand(task *db.Task, sessionID, prompt string) s
dangerousFlag = "--dangerously-skip-permissions "
}

// Build model flag
modelFlag := ""
if task.Model != "" {
modelFlag = fmt.Sprintf("--model %s ", task.Model)
}

// Get session ID for environment
worktreeSessionID := os.Getenv("WORKTREE_SESSION_ID")
if worktreeSessionID == "" {
Expand All @@ -99,8 +105,8 @@ func (c *ClaudeExecutor) BuildCommand(task *db.Task, sessionID, prompt string) s

// Build command - resume if we have a session ID, otherwise start fresh
if sessionID != "" {
cmd := fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q claude %s%s--chrome --resume %s`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, systemPromptFlag, sessionID)
cmd := fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q claude %s%s%s--chrome --resume %s`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, modelFlag, systemPromptFlag, sessionID)
if systemFile != nil {
cmd += fmt.Sprintf(`; rm -f %q`, systemFile.Name())
}
Expand All @@ -113,8 +119,8 @@ func (c *ClaudeExecutor) BuildCommand(task *db.Task, sessionID, prompt string) s
promptFile, err := os.CreateTemp("", "task-prompt-*.txt")
if err != nil {
c.logger.Error("BuildCommand: failed to create temp file", "error", err)
cmd := fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q claude %s%s--chrome`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, systemPromptFlag)
cmd := fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q claude %s%s%s--chrome`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, modelFlag, systemPromptFlag)
if systemFile != nil {
cmd += fmt.Sprintf(`; rm -f %q`, systemFile.Name())
}
Expand All @@ -123,16 +129,16 @@ func (c *ClaudeExecutor) BuildCommand(task *db.Task, sessionID, prompt string) s
promptFile.WriteString(prompt)
promptFile.Close()

cmd := fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q claude %s%s--chrome "$(cat %q)"; rm -f %q`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, systemPromptFlag, promptFile.Name(), promptFile.Name())
cmd := fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q claude %s%s%s--chrome "$(cat %q)"; rm -f %q`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, modelFlag, systemPromptFlag, promptFile.Name(), promptFile.Name())
if systemFile != nil {
cmd += fmt.Sprintf(` %q`, systemFile.Name())
}
return cmd
}

cmd := fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q claude %s%s--chrome`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, systemPromptFlag)
cmd := fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q claude %s%s%s--chrome`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, modelFlag, systemPromptFlag)
if systemFile != nil {
cmd += fmt.Sprintf(`; rm -f %q`, systemFile.Name())
}
Expand Down
28 changes: 20 additions & 8 deletions internal/executor/codex_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ func (c *CodexExecutor) runCodex(ctx context.Context, task *db.Task, workDir, pr
dangerousFlag = "--dangerously-bypass-approvals-and-sandbox "
}

// Build model flag
modelFlag := ""
if task.Model != "" {
modelFlag = fmt.Sprintf("--model %s ", task.Model)
}

// Check for existing session to resume (validate file exists first)
resumeFlag := ""
existingSessionID := task.ClaudeSessionID
Expand All @@ -156,8 +162,8 @@ func (c *CodexExecutor) runCodex(ctx context.Context, task *db.Task, workDir, pr
}

envPrefix := claudeEnvPrefix(paths.configDir)
script = fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q %scodex %s%s"$(cat %q)"`,
task.ID, sessionID, task.Port, task.WorktreePath, envPrefix, dangerousFlag, resumeFlag, promptFile.Name())
script = fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q %scodex %s%s%s"$(cat %q)"`,
task.ID, sessionID, task.Port, task.WorktreePath, envPrefix, dangerousFlag, modelFlag, resumeFlag, promptFile.Name())

// Create new window in task-daemon session
actualSession, tmuxErr := createTmuxWindow(daemonSession, windowName, workDir, script)
Expand Down Expand Up @@ -356,6 +362,12 @@ func (c *CodexExecutor) BuildCommand(task *db.Task, sessionID, prompt string) st
worktreeSessionID = fmt.Sprintf("%d", os.Getpid())
}

// Build model flag
modelFlag := ""
if task.Model != "" {
modelFlag = fmt.Sprintf("--model %s ", task.Model)
}

// Build resume flag if we have a session ID
resumeFlag := ""
if sessionID != "" {
Expand All @@ -368,20 +380,20 @@ func (c *CodexExecutor) BuildCommand(task *db.Task, sessionID, prompt string) st
promptFile, err := os.CreateTemp("", "task-prompt-*.txt")
if err != nil {
c.logger.Error("BuildCommand: failed to create temp file", "error", err)
return fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q codex %s%s`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, resumeFlag)
return fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q codex %s%s%s`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, modelFlag, resumeFlag)
}
// Include system instructions in prompt (AGENTS.md could overwrite project files)
fullPrompt := prompt + "\n\n" + c.executor.buildSystemInstructions()
promptFile.WriteString(fullPrompt)
promptFile.Close()

return fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q codex %s%s"$(cat %q)"; rm -f %q`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, resumeFlag, promptFile.Name(), promptFile.Name())
return fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q codex %s%s%s"$(cat %q)"; rm -f %q`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, modelFlag, resumeFlag, promptFile.Name(), promptFile.Name())
}

return fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q codex %s%s`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, resumeFlag)
return fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q codex %s%s%s`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, modelFlag, resumeFlag)
}

// ---- Session and Dangerous Mode Support ----
Expand Down
Loading