Skip to content

Commit 8e2a78a

Browse files
authored
Merge pull request #518 from bborn/task/2047-add-dangerous-mode-execution-shortcut-op
Add dangerous mode execution shortcut option
2 parents c1b7ae5 + 072e257 commit 8e2a78a

File tree

10 files changed

+453
-33
lines changed

10 files changed

+453
-33
lines changed

cmd/task/cli_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,117 @@ func TestCLIExecuteTask(t *testing.T) {
446446
}
447447
}
448448

449+
// TestCLIExecuteTaskDangerous tests queueing a task for execution in dangerous mode
450+
func TestCLIExecuteTaskDangerous(t *testing.T) {
451+
tmpDir := t.TempDir()
452+
dbPath := filepath.Join(tmpDir, "test.db")
453+
454+
database, err := db.Open(dbPath)
455+
if err != nil {
456+
t.Fatalf("failed to open database: %v", err)
457+
}
458+
defer database.Close()
459+
defer os.Remove(dbPath)
460+
461+
// Create a backlog task
462+
task := &db.Task{
463+
Title: "Task to execute dangerously",
464+
Status: db.StatusBacklog,
465+
Type: db.TypeCode,
466+
}
467+
if err := database.CreateTask(task); err != nil {
468+
t.Fatalf("failed to create task: %v", err)
469+
}
470+
471+
// Set dangerous mode and queue (simulate execute --dangerous)
472+
if err := database.UpdateTaskDangerousMode(task.ID, true); err != nil {
473+
t.Fatalf("UpdateTaskDangerousMode() error = %v", err)
474+
}
475+
if err := database.UpdateTaskStatus(task.ID, db.StatusQueued); err != nil {
476+
t.Fatalf("UpdateTaskStatus() error = %v", err)
477+
}
478+
479+
// Verify status and dangerous mode
480+
fetched, err := database.GetTask(task.ID)
481+
if err != nil {
482+
t.Fatalf("GetTask() error = %v", err)
483+
}
484+
if fetched.Status != db.StatusQueued {
485+
t.Errorf("Status = %v, want %v", fetched.Status, db.StatusQueued)
486+
}
487+
if !fetched.DangerousMode {
488+
t.Error("DangerousMode = false, want true")
489+
}
490+
}
491+
492+
// TestCLICreateTaskDangerous tests creating a task with --execute --dangerous
493+
func TestCLICreateTaskDangerous(t *testing.T) {
494+
tmpDir := t.TempDir()
495+
dbPath := filepath.Join(tmpDir, "test.db")
496+
497+
database, err := db.Open(dbPath)
498+
if err != nil {
499+
t.Fatalf("failed to open database: %v", err)
500+
}
501+
defer database.Close()
502+
defer os.Remove(dbPath)
503+
504+
// Create a task with dangerous mode (simulate create --execute --dangerous)
505+
task := &db.Task{
506+
Title: "Dangerous task",
507+
Status: db.StatusQueued,
508+
Type: db.TypeCode,
509+
DangerousMode: true,
510+
}
511+
if err := database.CreateTask(task); err != nil {
512+
t.Fatalf("failed to create task: %v", err)
513+
}
514+
515+
// Verify dangerous mode persists through CreateTask
516+
fetched, err := database.GetTask(task.ID)
517+
if err != nil {
518+
t.Fatalf("GetTask() error = %v", err)
519+
}
520+
if fetched.Status != db.StatusQueued {
521+
t.Errorf("Status = %v, want %v", fetched.Status, db.StatusQueued)
522+
}
523+
if !fetched.DangerousMode {
524+
t.Error("DangerousMode = false, want true")
525+
}
526+
}
527+
528+
// TestCLICreateTaskDangerousWithoutExecute tests that --dangerous without --execute doesn't set dangerous mode
529+
func TestCLICreateTaskDangerousWithoutExecute(t *testing.T) {
530+
tmpDir := t.TempDir()
531+
dbPath := filepath.Join(tmpDir, "test.db")
532+
533+
database, err := db.Open(dbPath)
534+
if err != nil {
535+
t.Fatalf("failed to open database: %v", err)
536+
}
537+
defer database.Close()
538+
defer os.Remove(dbPath)
539+
540+
// Simulate create --dangerous (without --execute): DangerousMode should be false
541+
task := &db.Task{
542+
Title: "Not really dangerous",
543+
Status: db.StatusBacklog,
544+
Type: db.TypeCode,
545+
DangerousMode: false, // --dangerous && --execute is false
546+
}
547+
if err := database.CreateTask(task); err != nil {
548+
t.Fatalf("failed to create task: %v", err)
549+
}
550+
551+
fetched, err := database.GetTask(task.ID)
552+
if err != nil {
553+
t.Fatalf("GetTask() error = %v", err)
554+
}
555+
if fetched.DangerousMode {
556+
t.Error("DangerousMode = true, want false (--dangerous without --execute should not set dangerous mode)")
557+
}
558+
}
559+
449560
// TestCLICloseTask tests marking a task as done
450561
func TestCLICloseTask(t *testing.T) {
451562
tmpDir := t.TempDir()

cmd/task/main.go

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,7 @@ Examples:
477477
project, _ := cmd.Flags().GetString("project")
478478
taskExecutor, _ := cmd.Flags().GetString("executor")
479479
execute, _ := cmd.Flags().GetBool("execute")
480+
createDangerous, _ := cmd.Flags().GetBool("dangerous")
480481
tags, _ := cmd.Flags().GetString("tags")
481482
pinned, _ := cmd.Flags().GetBool("pinned")
482483
branch, _ := cmd.Flags().GetString("branch")
@@ -582,15 +583,16 @@ Examples:
582583

583584
// Create the task
584585
task := &db.Task{
585-
Title: title,
586-
Body: body,
587-
Status: status,
588-
Type: taskType,
589-
Project: project,
590-
Executor: taskExecutor,
591-
Tags: tags,
592-
Pinned: pinned,
593-
SourceBranch: branch,
586+
Title: title,
587+
Body: body,
588+
Status: status,
589+
Type: taskType,
590+
Project: project,
591+
Executor: taskExecutor,
592+
Tags: tags,
593+
Pinned: pinned,
594+
SourceBranch: branch,
595+
DangerousMode: createDangerous && execute,
594596
}
595597

596598
if err := database.CreateTask(task); err != nil {
@@ -618,7 +620,11 @@ Examples:
618620
msg += fmt.Sprintf(" (branch: %s)", branch)
619621
}
620622
if execute {
621-
msg += " (queued for execution)"
623+
if createDangerous {
624+
msg += " (queued for execution in dangerous mode)"
625+
} else {
626+
msg += " (queued for execution)"
627+
}
622628
}
623629
fmt.Println(successStyle.Render(msg))
624630
}
@@ -629,6 +635,7 @@ Examples:
629635
createCmd.Flags().StringP("project", "p", "", "Project name (auto-detected from cwd if not specified)")
630636
createCmd.Flags().StringP("executor", "e", "", "Task executor: claude, codex, gemini, pi, opencode, openclaw (default: claude)")
631637
createCmd.Flags().BoolP("execute", "x", false, "Queue task for immediate execution")
638+
createCmd.Flags().Bool("dangerous", false, "Execute in dangerous mode (requires --execute)")
632639
createCmd.Flags().String("tags", "", "Task tags (comma-separated)")
633640
createCmd.Flags().Bool("pinned", false, "Pin the task to the top of its column")
634641
createCmd.Flags().StringP("branch", "b", "", "Existing branch to checkout for worktree (e.g., fix/ui-overflow)")
@@ -1344,6 +1351,7 @@ Examples:
13441351

13451352
oldProject := task.Project
13461353
execute, _ := cmd.Flags().GetBool("execute")
1354+
moveDangerous, _ := cmd.Flags().GetBool("dangerous")
13471355

13481356
// Confirm unless --force flag is set
13491357
force, _ := cmd.Flags().GetBool("force")
@@ -1369,16 +1377,27 @@ Examples:
13691377

13701378
// Queue for execution if requested
13711379
if execute {
1380+
if moveDangerous {
1381+
if err := database.UpdateTaskDangerousMode(newTaskID, true); err != nil {
1382+
fmt.Fprintln(os.Stderr, errorStyle.Render("Error setting dangerous mode: "+err.Error()))
1383+
os.Exit(1)
1384+
}
1385+
}
13721386
if err := database.UpdateTaskStatus(newTaskID, db.StatusQueued); err != nil {
13731387
fmt.Fprintln(os.Stderr, errorStyle.Render("Error queueing task: "+err.Error()))
13741388
os.Exit(1)
13751389
}
1376-
fmt.Println(successStyle.Render(fmt.Sprintf("Queued task #%d for execution", newTaskID)))
1390+
msg := fmt.Sprintf("Queued task #%d for execution", newTaskID)
1391+
if moveDangerous {
1392+
msg += " (dangerous mode)"
1393+
}
1394+
fmt.Println(successStyle.Render(msg))
13771395
}
13781396
},
13791397
}
13801398
moveCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
13811399
moveCmd.Flags().BoolP("execute", "e", false, "Queue the task for execution after moving")
1400+
moveCmd.Flags().Bool("dangerous", false, "Execute in dangerous mode (requires --execute)")
13821401
rootCmd.AddCommand(moveCmd)
13831402

13841403
// Execute subcommand - queue a task for execution
@@ -1391,7 +1410,8 @@ Examples:
13911410
Examples:
13921411
task execute 42
13931412
task queue 42
1394-
task run 42`,
1413+
task run 42
1414+
task execute 42 --dangerous # Execute in dangerous mode`,
13951415
Args: cobra.ExactArgs(1),
13961416
Run: func(cmd *cobra.Command, args []string) {
13971417
var taskID int64
@@ -1400,6 +1420,8 @@ Examples:
14001420
os.Exit(1)
14011421
}
14021422

1423+
executeDangerous, _ := cmd.Flags().GetBool("dangerous")
1424+
14031425
// Open database
14041426
dbPath := db.DefaultPath()
14051427
database, err := db.Open(dbPath)
@@ -1429,14 +1451,27 @@ Examples:
14291451
return
14301452
}
14311453

1454+
// Set dangerous mode if requested
1455+
if executeDangerous {
1456+
if err := database.UpdateTaskDangerousMode(taskID, true); err != nil {
1457+
fmt.Fprintln(os.Stderr, errorStyle.Render("Error setting dangerous mode: "+err.Error()))
1458+
os.Exit(1)
1459+
}
1460+
}
1461+
14321462
if err := database.UpdateTaskStatus(taskID, db.StatusQueued); err != nil {
14331463
fmt.Fprintln(os.Stderr, errorStyle.Render("Error: "+err.Error()))
14341464
os.Exit(1)
14351465
}
14361466

1437-
fmt.Println(successStyle.Render(fmt.Sprintf("Queued task #%d: %s", taskID, task.Title)))
1467+
msg := fmt.Sprintf("Queued task #%d: %s", taskID, task.Title)
1468+
if executeDangerous {
1469+
msg += " (dangerous mode)"
1470+
}
1471+
fmt.Println(successStyle.Render(msg))
14381472
},
14391473
}
1474+
executeCmd.Flags().Bool("dangerous", false, "Execute in dangerous mode (skip permission prompts)")
14401475
rootCmd.AddCommand(executeCmd)
14411476

14421477
statusCmd := &cobra.Command{

internal/config/keybindings.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type KeybindingsConfig struct {
3636
ChangeStatus *KeybindingConfig `yaml:"change_status,omitempty"`
3737
CommandPalette *KeybindingConfig `yaml:"command_palette,omitempty"`
3838
ToggleDangerous *KeybindingConfig `yaml:"toggle_dangerous,omitempty"`
39+
QueueDangerous *KeybindingConfig `yaml:"queue_dangerous,omitempty"`
3940
TogglePin *KeybindingConfig `yaml:"toggle_pin,omitempty"`
4041
Filter *KeybindingConfig `yaml:"filter,omitempty"`
4142
OpenWorktree *KeybindingConfig `yaml:"open_worktree,omitempty"`
@@ -194,6 +195,10 @@ toggle_dangerous:
194195
keys: ["!"]
195196
help: "dangerous mode"
196197
198+
queue_dangerous:
199+
keys: ["X"]
200+
help: "execute dangerous"
201+
197202
toggle_pin:
198203
keys: ["t"]
199204
help: "pin/unpin"

internal/db/tasks.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,9 @@ func (db *DB) CreateTask(t *Task) error {
129129
t.Project = project.Name
130130

131131
result, err := db.Exec(`
132-
INSERT INTO tasks (title, body, status, type, project, executor, pinned, tags, source_branch)
133-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
134-
`, t.Title, t.Body, t.Status, t.Type, t.Project, t.Executor, t.Pinned, t.Tags, t.SourceBranch)
132+
INSERT INTO tasks (title, body, status, type, project, executor, pinned, tags, source_branch, dangerous_mode)
133+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
134+
`, t.Title, t.Body, t.Status, t.Type, t.Project, t.Executor, t.Pinned, t.Tags, t.SourceBranch, t.DangerousMode)
135135
if err != nil {
136136
return fmt.Errorf("insert task: %w", err)
137137
}

internal/mcp/server.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,10 @@ func (s *Server) handleRequest(req *jsonRPCRequest) {
236236
"type": "string",
237237
"description": "Initial status (backlog, queued, defaults to backlog)",
238238
},
239+
"dangerous_mode": map[string]interface{}{
240+
"type": "boolean",
241+
"description": "Execute in dangerous mode (skip permission prompts). Only applies when status is 'queued'.",
242+
},
239243
},
240244
"required": []string{"title"},
241245
},
@@ -545,6 +549,7 @@ func (s *Server) handleToolCall(id interface{}, params *toolCallParams) {
545549
project, _ := params.Arguments["project"].(string)
546550
taskType, _ := params.Arguments["type"].(string)
547551
status, _ := params.Arguments["status"].(string)
552+
dangerousMode, _ := params.Arguments["dangerous_mode"].(bool)
548553

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

561566
newTask := &db.Task{
562-
Title: title,
563-
Body: body,
564-
Project: project,
565-
Type: taskType,
566-
Status: status,
567+
Title: title,
568+
Body: body,
569+
Project: project,
570+
Type: taskType,
571+
Status: status,
572+
DangerousMode: dangerousMode && status == db.StatusQueued,
567573
}
568574

569575
if err := s.db.CreateTask(newTask); err != nil {

0 commit comments

Comments
 (0)