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
111 changes: 111 additions & 0 deletions cmd/task/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,117 @@ func TestCLIExecuteTask(t *testing.T) {
}
}

// TestCLIExecuteTaskDangerous tests queueing a task for execution in dangerous mode
func TestCLIExecuteTaskDangerous(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db")

database, err := db.Open(dbPath)
if err != nil {
t.Fatalf("failed to open database: %v", err)
}
defer database.Close()
defer os.Remove(dbPath)

// Create a backlog task
task := &db.Task{
Title: "Task to execute dangerously",
Status: db.StatusBacklog,
Type: db.TypeCode,
}
if err := database.CreateTask(task); err != nil {
t.Fatalf("failed to create task: %v", err)
}

// Set dangerous mode and queue (simulate execute --dangerous)
if err := database.UpdateTaskDangerousMode(task.ID, true); err != nil {
t.Fatalf("UpdateTaskDangerousMode() error = %v", err)
}
if err := database.UpdateTaskStatus(task.ID, db.StatusQueued); err != nil {
t.Fatalf("UpdateTaskStatus() error = %v", err)
}

// Verify status and dangerous mode
fetched, err := database.GetTask(task.ID)
if err != nil {
t.Fatalf("GetTask() error = %v", err)
}
if fetched.Status != db.StatusQueued {
t.Errorf("Status = %v, want %v", fetched.Status, db.StatusQueued)
}
if !fetched.DangerousMode {
t.Error("DangerousMode = false, want true")
}
}

// TestCLICreateTaskDangerous tests creating a task with --execute --dangerous
func TestCLICreateTaskDangerous(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db")

database, err := db.Open(dbPath)
if err != nil {
t.Fatalf("failed to open database: %v", err)
}
defer database.Close()
defer os.Remove(dbPath)

// Create a task with dangerous mode (simulate create --execute --dangerous)
task := &db.Task{
Title: "Dangerous task",
Status: db.StatusQueued,
Type: db.TypeCode,
DangerousMode: true,
}
if err := database.CreateTask(task); err != nil {
t.Fatalf("failed to create task: %v", err)
}

// Verify dangerous mode persists through CreateTask
fetched, err := database.GetTask(task.ID)
if err != nil {
t.Fatalf("GetTask() error = %v", err)
}
if fetched.Status != db.StatusQueued {
t.Errorf("Status = %v, want %v", fetched.Status, db.StatusQueued)
}
if !fetched.DangerousMode {
t.Error("DangerousMode = false, want true")
}
}

// TestCLICreateTaskDangerousWithoutExecute tests that --dangerous without --execute doesn't set dangerous mode
func TestCLICreateTaskDangerousWithoutExecute(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db")

database, err := db.Open(dbPath)
if err != nil {
t.Fatalf("failed to open database: %v", err)
}
defer database.Close()
defer os.Remove(dbPath)

// Simulate create --dangerous (without --execute): DangerousMode should be false
task := &db.Task{
Title: "Not really dangerous",
Status: db.StatusBacklog,
Type: db.TypeCode,
DangerousMode: false, // --dangerous && --execute is false
}
if err := database.CreateTask(task); err != nil {
t.Fatalf("failed to create task: %v", err)
}

fetched, err := database.GetTask(task.ID)
if err != nil {
t.Fatalf("GetTask() error = %v", err)
}
if fetched.DangerousMode {
t.Error("DangerousMode = true, want false (--dangerous without --execute should not set dangerous mode)")
}
}

// TestCLICloseTask tests marking a task as done
func TestCLICloseTask(t *testing.T) {
tmpDir := t.TempDir()
Expand Down
61 changes: 48 additions & 13 deletions cmd/task/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ Examples:
project, _ := cmd.Flags().GetString("project")
taskExecutor, _ := cmd.Flags().GetString("executor")
execute, _ := cmd.Flags().GetBool("execute")
createDangerous, _ := cmd.Flags().GetBool("dangerous")
tags, _ := cmd.Flags().GetString("tags")
pinned, _ := cmd.Flags().GetBool("pinned")
branch, _ := cmd.Flags().GetString("branch")
Expand Down Expand Up @@ -581,15 +582,16 @@ Examples:

// Create the task
task := &db.Task{
Title: title,
Body: body,
Status: status,
Type: taskType,
Project: project,
Executor: taskExecutor,
Tags: tags,
Pinned: pinned,
SourceBranch: branch,
Title: title,
Body: body,
Status: status,
Type: taskType,
Project: project,
Executor: taskExecutor,
Tags: tags,
Pinned: pinned,
SourceBranch: branch,
DangerousMode: createDangerous && execute,
}

if err := database.CreateTask(task); err != nil {
Expand Down Expand Up @@ -617,7 +619,11 @@ Examples:
msg += fmt.Sprintf(" (branch: %s)", branch)
}
if execute {
msg += " (queued for execution)"
if createDangerous {
msg += " (queued for execution in dangerous mode)"
} else {
msg += " (queued for execution)"
}
}
fmt.Println(successStyle.Render(msg))
}
Expand All @@ -628,6 +634,7 @@ Examples:
createCmd.Flags().StringP("project", "p", "", "Project name (auto-detected from cwd if not specified)")
createCmd.Flags().StringP("executor", "e", "", "Task executor: claude, codex, gemini, pi, opencode, openclaw (default: claude)")
createCmd.Flags().BoolP("execute", "x", false, "Queue task for immediate execution")
createCmd.Flags().Bool("dangerous", false, "Execute in dangerous mode (requires --execute)")
createCmd.Flags().String("tags", "", "Task tags (comma-separated)")
createCmd.Flags().Bool("pinned", false, "Pin the task to the top of its column")
createCmd.Flags().StringP("branch", "b", "", "Existing branch to checkout for worktree (e.g., fix/ui-overflow)")
Expand Down Expand Up @@ -1343,6 +1350,7 @@ Examples:

oldProject := task.Project
execute, _ := cmd.Flags().GetBool("execute")
moveDangerous, _ := cmd.Flags().GetBool("dangerous")

// Confirm unless --force flag is set
force, _ := cmd.Flags().GetBool("force")
Expand All @@ -1368,16 +1376,27 @@ Examples:

// Queue for execution if requested
if execute {
if moveDangerous {
if err := database.UpdateTaskDangerousMode(newTaskID, true); err != nil {
fmt.Fprintln(os.Stderr, errorStyle.Render("Error setting dangerous mode: "+err.Error()))
os.Exit(1)
}
}
if err := database.UpdateTaskStatus(newTaskID, db.StatusQueued); err != nil {
fmt.Fprintln(os.Stderr, errorStyle.Render("Error queueing task: "+err.Error()))
os.Exit(1)
}
fmt.Println(successStyle.Render(fmt.Sprintf("Queued task #%d for execution", newTaskID)))
msg := fmt.Sprintf("Queued task #%d for execution", newTaskID)
if moveDangerous {
msg += " (dangerous mode)"
}
fmt.Println(successStyle.Render(msg))
}
},
}
moveCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
moveCmd.Flags().BoolP("execute", "e", false, "Queue the task for execution after moving")
moveCmd.Flags().Bool("dangerous", false, "Execute in dangerous mode (requires --execute)")
rootCmd.AddCommand(moveCmd)

// Execute subcommand - queue a task for execution
Expand All @@ -1390,7 +1409,8 @@ Examples:
Examples:
task execute 42
task queue 42
task run 42`,
task run 42
task execute 42 --dangerous # Execute in dangerous mode`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var taskID int64
Expand All @@ -1399,6 +1419,8 @@ Examples:
os.Exit(1)
}

executeDangerous, _ := cmd.Flags().GetBool("dangerous")

// Open database
dbPath := db.DefaultPath()
database, err := db.Open(dbPath)
Expand Down Expand Up @@ -1428,14 +1450,27 @@ Examples:
return
}

// Set dangerous mode if requested
if executeDangerous {
if err := database.UpdateTaskDangerousMode(taskID, true); err != nil {
fmt.Fprintln(os.Stderr, errorStyle.Render("Error setting dangerous mode: "+err.Error()))
os.Exit(1)
}
}

if err := database.UpdateTaskStatus(taskID, db.StatusQueued); err != nil {
fmt.Fprintln(os.Stderr, errorStyle.Render("Error: "+err.Error()))
os.Exit(1)
}

fmt.Println(successStyle.Render(fmt.Sprintf("Queued task #%d: %s", taskID, task.Title)))
msg := fmt.Sprintf("Queued task #%d: %s", taskID, task.Title)
if executeDangerous {
msg += " (dangerous mode)"
}
fmt.Println(successStyle.Render(msg))
},
}
executeCmd.Flags().Bool("dangerous", false, "Execute in dangerous mode (skip permission prompts)")
rootCmd.AddCommand(executeCmd)

statusCmd := &cobra.Command{
Expand Down
5 changes: 5 additions & 0 deletions internal/config/keybindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type KeybindingsConfig struct {
ChangeStatus *KeybindingConfig `yaml:"change_status,omitempty"`
CommandPalette *KeybindingConfig `yaml:"command_palette,omitempty"`
ToggleDangerous *KeybindingConfig `yaml:"toggle_dangerous,omitempty"`
QueueDangerous *KeybindingConfig `yaml:"queue_dangerous,omitempty"`
TogglePin *KeybindingConfig `yaml:"toggle_pin,omitempty"`
Filter *KeybindingConfig `yaml:"filter,omitempty"`
OpenWorktree *KeybindingConfig `yaml:"open_worktree,omitempty"`
Expand Down Expand Up @@ -194,6 +195,10 @@ toggle_dangerous:
keys: ["!"]
help: "dangerous mode"

queue_dangerous:
keys: ["X"]
help: "execute dangerous"

toggle_pin:
keys: ["t"]
help: "pin/unpin"
Expand Down
6 changes: 3 additions & 3 deletions internal/db/tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ func (db *DB) CreateTask(t *Task) error {
t.Project = project.Name

result, err := db.Exec(`
INSERT INTO tasks (title, body, status, type, project, executor, pinned, tags, source_branch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, t.Title, t.Body, t.Status, t.Type, t.Project, t.Executor, t.Pinned, t.Tags, t.SourceBranch)
INSERT INTO tasks (title, body, status, type, project, executor, pinned, tags, source_branch, dangerous_mode)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, t.Title, t.Body, t.Status, t.Type, t.Project, t.Executor, t.Pinned, t.Tags, t.SourceBranch, t.DangerousMode)
if err != nil {
return fmt.Errorf("insert task: %w", err)
}
Expand Down
16 changes: 11 additions & 5 deletions internal/mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ func (s *Server) handleRequest(req *jsonRPCRequest) {
"type": "string",
"description": "Initial status (backlog, queued, defaults to backlog)",
},
"dangerous_mode": map[string]interface{}{
"type": "boolean",
"description": "Execute in dangerous mode (skip permission prompts). Only applies when status is 'queued'.",
},
},
"required": []string{"title"},
},
Expand Down Expand Up @@ -545,6 +549,7 @@ func (s *Server) handleToolCall(id interface{}, params *toolCallParams) {
project, _ := params.Arguments["project"].(string)
taskType, _ := params.Arguments["type"].(string)
status, _ := params.Arguments["status"].(string)
dangerousMode, _ := params.Arguments["dangerous_mode"].(bool)

// Default project to current task's project
if project == "" {
Expand All @@ -559,11 +564,12 @@ func (s *Server) handleToolCall(id interface{}, params *toolCallParams) {
}

newTask := &db.Task{
Title: title,
Body: body,
Project: project,
Type: taskType,
Status: status,
Title: title,
Body: body,
Project: project,
Type: taskType,
Status: status,
DangerousMode: dangerousMode && status == db.StatusQueued,
}

if err := s.db.CreateTask(newTask); err != nil {
Expand Down
Loading
Loading