claude-kanban 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,9 +7,9 @@ import { fileURLToPath } from "url";
7
7
  import { existsSync as existsSync3 } from "fs";
8
8
 
9
9
  // src/server/services/executor.ts
10
- import { spawn, execSync } from "child_process";
10
+ import { spawn } from "child_process";
11
11
  import { join as join4 } from "path";
12
- import { writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync2, existsSync as existsSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync4, rmSync } from "fs";
12
+ import { writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync2, existsSync as existsSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync4 } from "fs";
13
13
  import { EventEmitter } from "events";
14
14
 
15
15
  // src/server/services/project.ts
@@ -134,7 +134,6 @@ function getTaskCounts(projectPath) {
134
134
  draft: 0,
135
135
  ready: 0,
136
136
  in_progress: 0,
137
- in_review: 0,
138
137
  completed: 0,
139
138
  failed: 0
140
139
  };
@@ -207,11 +206,9 @@ function getRecentProgress(projectPath, lines = 100) {
207
206
  // src/server/services/executor.ts
208
207
  var KANBAN_DIR4 = ".claude-kanban";
209
208
  var LOGS_DIR = "logs";
210
- var WORKTREES_DIR = "worktrees";
211
209
  var TaskExecutor = class extends EventEmitter {
212
210
  projectPath;
213
- runningTasks = /* @__PURE__ */ new Map();
214
- reviewingTasks = /* @__PURE__ */ new Map();
211
+ runningTask = null;
215
212
  afkMode = false;
216
213
  afkIteration = 0;
217
214
  afkMaxIterations = 0;
@@ -317,186 +314,47 @@ ${summary}
317
314
  }
318
315
  }
319
316
  /**
320
- * Check if project is a git repository
317
+ * Check if a task is currently running
321
318
  */
322
- isGitRepo() {
323
- try {
324
- execSync("git rev-parse --is-inside-work-tree", {
325
- cwd: this.projectPath,
326
- stdio: "pipe"
327
- });
328
- return true;
329
- } catch {
330
- return false;
331
- }
332
- }
333
- /**
334
- * Get worktrees directory path
335
- */
336
- getWorktreesDir() {
337
- return join4(this.projectPath, KANBAN_DIR4, WORKTREES_DIR);
338
- }
339
- /**
340
- * Get worktree path for a task
341
- */
342
- getWorktreePath(taskId) {
343
- return join4(this.getWorktreesDir(), taskId);
344
- }
345
- /**
346
- * Get branch name for a task
347
- */
348
- getBranchName(taskId) {
349
- return `task/${taskId}`;
350
- }
351
- /**
352
- * Create a git worktree for isolated task execution
353
- */
354
- createWorktree(taskId) {
355
- if (!this.isGitRepo()) {
356
- console.log("[executor] Not a git repo, skipping worktree creation");
357
- return null;
358
- }
359
- const worktreePath = this.getWorktreePath(taskId);
360
- const branchName = this.getBranchName(taskId);
361
- try {
362
- const worktreesDir = this.getWorktreesDir();
363
- if (!existsSync2(worktreesDir)) {
364
- mkdirSync2(worktreesDir, { recursive: true });
365
- }
366
- if (existsSync2(worktreePath)) {
367
- this.removeWorktree(taskId);
368
- }
369
- try {
370
- execSync(`git rev-parse --verify ${branchName}`, {
371
- cwd: this.projectPath,
372
- stdio: "pipe"
373
- });
374
- execSync(`git branch -D ${branchName}`, {
375
- cwd: this.projectPath,
376
- stdio: "pipe"
377
- });
378
- } catch {
379
- }
380
- execSync(`git worktree add -b ${branchName} "${worktreePath}"`, {
381
- cwd: this.projectPath,
382
- stdio: "pipe"
383
- });
384
- console.log(`[executor] Created worktree at ${worktreePath} on branch ${branchName}`);
385
- return { worktreePath, branchName };
386
- } catch (error) {
387
- console.error("[executor] Failed to create worktree:", error);
388
- return null;
389
- }
390
- }
391
- /**
392
- * Remove a git worktree
393
- */
394
- removeWorktree(taskId) {
395
- const worktreePath = this.getWorktreePath(taskId);
396
- const branchName = this.getBranchName(taskId);
397
- try {
398
- if (existsSync2(worktreePath)) {
399
- execSync(`git worktree remove "${worktreePath}" --force`, {
400
- cwd: this.projectPath,
401
- stdio: "pipe"
402
- });
403
- console.log(`[executor] Removed worktree at ${worktreePath}`);
404
- }
405
- } catch (error) {
406
- console.error("[executor] Failed to remove worktree via git:", error);
407
- try {
408
- if (existsSync2(worktreePath)) {
409
- rmSync(worktreePath, { recursive: true, force: true });
410
- execSync("git worktree prune", {
411
- cwd: this.projectPath,
412
- stdio: "pipe"
413
- });
414
- }
415
- } catch {
416
- console.error("[executor] Failed to force remove worktree directory");
417
- }
418
- }
419
- try {
420
- execSync(`git branch -D ${branchName}`, {
421
- cwd: this.projectPath,
422
- stdio: "pipe"
423
- });
424
- console.log(`[executor] Deleted branch ${branchName}`);
425
- } catch {
426
- }
319
+ isRunning() {
320
+ return this.runningTask !== null;
427
321
  }
428
322
  /**
429
- * Merge worktree branch back to main branch
323
+ * Get the currently running task ID
430
324
  */
431
- mergeWorktreeBranch(taskId) {
432
- const branchName = this.getBranchName(taskId);
433
- try {
434
- const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
435
- cwd: this.projectPath,
436
- encoding: "utf-8"
437
- }).trim();
438
- execSync(`git merge ${branchName} --no-edit`, {
439
- cwd: this.projectPath,
440
- stdio: "pipe"
441
- });
442
- console.log(`[executor] Merged ${branchName} into ${currentBranch}`);
443
- return true;
444
- } catch (error) {
445
- console.error("[executor] Failed to merge branch:", error);
446
- return false;
447
- }
448
- }
449
- /**
450
- * Get number of currently running tasks
451
- */
452
- getRunningCount() {
453
- return this.runningTasks.size;
454
- }
455
- /**
456
- * Check if a task is running
457
- */
458
- isTaskRunning(taskId) {
459
- return this.runningTasks.has(taskId);
460
- }
461
- /**
462
- * Get all running task IDs
463
- */
464
- getRunningTaskIds() {
465
- return Array.from(this.runningTasks.keys());
325
+ getRunningTaskId() {
326
+ return this.runningTask?.taskId || null;
466
327
  }
467
328
  /**
468
329
  * Get running task output
469
330
  */
470
331
  getTaskOutput(taskId) {
471
- return this.runningTasks.get(taskId)?.output;
332
+ if (this.runningTask?.taskId === taskId) {
333
+ return this.runningTask.output;
334
+ }
335
+ return void 0;
472
336
  }
473
337
  /**
474
- * Build the prompt for a specific task
338
+ * Build the prompt for a task - simplified Ralph-style
475
339
  */
476
- buildTaskPrompt(task, config, worktreeInfo) {
340
+ buildTaskPrompt(task, config) {
477
341
  const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
478
342
  const prdPath = join4(kanbanDir, "prd.json");
479
343
  const progressPath = join4(kanbanDir, "progress.txt");
480
344
  const stepsText = task.steps.length > 0 ? `
481
345
  Verification steps:
482
346
  ${task.steps.map((s, i) => `${i + 1}. ${s}`).join("\n")}` : "";
483
- const verifySteps = [];
347
+ const verifyCommands = [];
484
348
  if (config.project.typecheckCommand) {
485
- verifySteps.push(`Run typecheck: ${config.project.typecheckCommand}`);
349
+ verifyCommands.push(config.project.typecheckCommand);
486
350
  }
487
351
  if (config.project.testCommand) {
488
- verifySteps.push(`Run tests: ${config.project.testCommand}`);
352
+ verifyCommands.push(config.project.testCommand);
489
353
  }
490
- const verifySection = verifySteps.length > 0 ? `3. Verify your work:
491
- ${verifySteps.map((s) => ` - ${s}`).join("\n")}
354
+ const verifySection = verifyCommands.length > 0 ? `3. Run quality checks:
355
+ ${verifyCommands.map((cmd) => ` - ${cmd}`).join("\n")}
492
356
 
493
- ` : "";
494
- const worktreeSection = worktreeInfo ? `## ENVIRONMENT
495
- You are running in an isolated git worktree on branch "${worktreeInfo.branchName}".
496
- This is a fresh checkout - dependencies (node_modules, vendor, etc.) are NOT installed.
497
- Before running any build/test commands, install dependencies first (e.g., npm install, composer install, pip install).
498
-
499
- ` : "";
357
+ 4.` : "3.";
500
358
  return `You are an AI coding agent. Complete the following task:
501
359
 
502
360
  ## TASK
@@ -507,48 +365,41 @@ Priority: ${task.priority}
507
365
  ${task.description}
508
366
  ${stepsText}
509
367
 
510
- ${worktreeSection}## INSTRUCTIONS
511
- 1. If dependencies are not installed, install them first.
368
+ ## INSTRUCTIONS
369
+
370
+ 1. Implement this task as described above.
512
371
 
513
- 2. Implement this task as described above.
372
+ 2. Make sure your changes work correctly.
514
373
 
515
- ${verifySection}${verifySteps.length > 0 ? "4" : "3"}. When complete, update the task in ${prdPath}:
374
+ ${verifySection} When complete, update the PRD file at ${prdPath}:
516
375
  - Find the task with id "${task.id}"
517
376
  - Set "passes": true
518
- (Note: Do NOT change the status - the system will move it to review)
377
+ - Set "status": "completed"
519
378
 
520
- ${verifySteps.length > 0 ? "5" : "4"}. Document your work in ${progressPath}:
521
- - What you implemented and files changed
522
- - Key decisions made and why
523
- - Gotchas, edge cases, or tricky parts discovered
524
- - Useful patterns or approaches that worked well
525
- - Anything a future agent should know about this area of the codebase
379
+ ${verifyCommands.length > 0 ? "5" : "4"}. Document your work in ${progressPath}:
380
+ - What you implemented
381
+ - Key decisions made
382
+ - Any gotchas or important notes for future work
526
383
 
527
- ${verifySteps.length > 0 ? "6" : "5"}. Make a git commit with a descriptive message.
384
+ ${verifyCommands.length > 0 ? "6" : "5"}. Commit your changes with a descriptive message.
528
385
 
529
- Focus only on this task. When successfully complete, output: <promise>COMPLETE</promise>`;
386
+ Focus only on this task. When done, output: <promise>COMPLETE</promise>`;
530
387
  }
531
388
  /**
532
- * Run a specific task
389
+ * Run a task
533
390
  */
534
391
  async runTask(taskId) {
392
+ if (this.isRunning()) {
393
+ throw new Error("A task is already running. Wait for it to complete or cancel it first.");
394
+ }
535
395
  const config = getConfig(this.projectPath);
536
396
  const task = getTaskById(this.projectPath, taskId);
537
397
  if (!task) {
538
398
  throw new Error(`Task not found: ${taskId}`);
539
399
  }
540
- if (this.isTaskRunning(taskId)) {
541
- throw new Error(`Task already running: ${taskId}`);
542
- }
543
- const maxConcurrent = config.execution.maxConcurrent || 3;
544
- if (this.getRunningCount() >= maxConcurrent) {
545
- throw new Error(`Maximum concurrent tasks (${maxConcurrent}) reached`);
546
- }
547
400
  updateTask(this.projectPath, taskId, { status: "in_progress" });
548
401
  const startedAt = /* @__PURE__ */ new Date();
549
- const worktreeInfo = this.createWorktree(taskId);
550
- const executionPath = worktreeInfo?.worktreePath || this.projectPath;
551
- const prompt = this.buildTaskPrompt(task, config, worktreeInfo);
402
+ const prompt = this.buildTaskPrompt(task, config);
552
403
  const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
553
404
  const promptFile = join4(kanbanDir, `prompt-${taskId}.txt`);
554
405
  writeFileSync4(promptFile, prompt);
@@ -564,48 +415,32 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
564
415
  const commandDisplay = `${config.agent.command} ${args.join(" ")}`;
565
416
  const fullCommand = `${config.agent.command} ${args.join(" ")}`;
566
417
  console.log("[executor] Command:", fullCommand);
567
- console.log("[executor] CWD:", executionPath);
568
- if (worktreeInfo) {
569
- console.log("[executor] Using worktree:", worktreeInfo.worktreePath);
570
- console.log("[executor] Branch:", worktreeInfo.branchName);
571
- }
418
+ console.log("[executor] CWD:", this.projectPath);
572
419
  const childProcess = spawn("bash", ["-c", fullCommand], {
573
- cwd: executionPath,
420
+ cwd: this.projectPath,
574
421
  env: {
575
422
  ...process.env,
576
423
  TERM: "xterm-256color",
577
424
  FORCE_COLOR: "0",
578
- // Disable colors to avoid escape codes
579
425
  NO_COLOR: "1"
580
- // Standard way to disable colors
581
426
  },
582
427
  stdio: ["ignore", "pipe", "pipe"]
583
- // Close stdin since we don't need interactive input
584
428
  });
585
- const runningTask = {
429
+ this.runningTask = {
586
430
  taskId,
587
431
  process: childProcess,
588
432
  startedAt,
589
- output: [],
590
- worktreePath: worktreeInfo?.worktreePath,
591
- branchName: worktreeInfo?.branchName
433
+ output: []
592
434
  };
593
- this.runningTasks.set(taskId, runningTask);
594
435
  this.initLogFile(taskId);
595
436
  const logOutput = (line) => {
596
437
  this.appendToLog(taskId, line);
597
- runningTask.output.push(line);
438
+ this.runningTask?.output.push(line);
598
439
  this.emit("task:output", { taskId, line, lineType: "stdout" });
599
440
  };
600
441
  this.emit("task:started", { taskId, timestamp: startedAt.toISOString() });
601
442
  logOutput(`[claude-kanban] Starting task: ${task.title}
602
443
  `);
603
- if (worktreeInfo) {
604
- logOutput(`[claude-kanban] Worktree: ${worktreeInfo.worktreePath}
605
- `);
606
- logOutput(`[claude-kanban] Branch: ${worktreeInfo.branchName}
607
- `);
608
- }
609
444
  logOutput(`[claude-kanban] Command: ${commandDisplay}
610
445
  `);
611
446
  logOutput(`[claude-kanban] Process spawned (PID: ${childProcess.pid})
@@ -650,9 +485,6 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
650
485
  const text = data.toString();
651
486
  logOutput(`[stderr] ${text}`);
652
487
  });
653
- childProcess.on("spawn", () => {
654
- console.log("[executor] Process spawned successfully");
655
- });
656
488
  childProcess.on("error", (error) => {
657
489
  console.log("[executor] Spawn error:", error.message);
658
490
  this.emit("task:output", { taskId, line: `[claude-kanban] Error: ${error.message}
@@ -661,9 +493,6 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
661
493
  unlinkSync(promptFile);
662
494
  } catch {
663
495
  }
664
- if (worktreeInfo) {
665
- this.removeWorktree(taskId);
666
- }
667
496
  updateTask(this.projectPath, taskId, { status: "failed", passes: false });
668
497
  const endedAt = /* @__PURE__ */ new Date();
669
498
  addExecutionEntry(this.projectPath, taskId, {
@@ -674,7 +503,7 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
674
503
  error: error.message
675
504
  });
676
505
  this.emit("task:failed", { taskId, error: error.message });
677
- this.runningTasks.delete(taskId);
506
+ this.runningTask = null;
678
507
  });
679
508
  childProcess.on("close", (code, signal) => {
680
509
  console.log("[executor] Process closed with code:", code, "signal:", signal);
@@ -688,48 +517,41 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
688
517
  });
689
518
  const timeoutMs = (config.execution.timeout || 30) * 60 * 1e3;
690
519
  setTimeout(() => {
691
- if (this.isTaskRunning(taskId)) {
692
- this.cancelTask(taskId, "Timeout exceeded");
520
+ if (this.runningTask?.taskId === taskId) {
521
+ this.cancelTask("Timeout exceeded");
693
522
  }
694
523
  }, timeoutMs);
695
524
  }
696
525
  /**
697
- * Handle task completion - moves to in_review, keeps worktree alive
526
+ * Handle task completion
698
527
  */
699
528
  handleTaskComplete(taskId, exitCode, startedAt) {
700
- const runningTask = this.runningTasks.get(taskId);
701
- if (!runningTask) return;
529
+ if (!this.runningTask || this.runningTask.taskId !== taskId) return;
702
530
  const endedAt = /* @__PURE__ */ new Date();
703
531
  const duration = endedAt.getTime() - startedAt.getTime();
704
- const output = runningTask.output.join("");
532
+ const output = this.runningTask.output.join("");
705
533
  const isComplete = output.includes("<promise>COMPLETE</promise>");
706
534
  const task = getTaskById(this.projectPath, taskId);
707
535
  if (isComplete || exitCode === 0) {
708
- if (runningTask.worktreePath && runningTask.branchName) {
709
- this.reviewingTasks.set(taskId, {
710
- worktreePath: runningTask.worktreePath,
711
- branchName: runningTask.branchName,
712
- startedAt,
713
- endedAt
714
- });
715
- console.log(`[executor] Task ${taskId} ready for review at ${runningTask.worktreePath}`);
716
- }
717
536
  updateTask(this.projectPath, taskId, {
718
- status: "in_review",
537
+ status: "completed",
719
538
  passes: true
720
539
  });
540
+ addExecutionEntry(this.projectPath, taskId, {
541
+ startedAt: startedAt.toISOString(),
542
+ endedAt: endedAt.toISOString(),
543
+ status: "completed",
544
+ duration
545
+ });
721
546
  logTaskExecution(this.projectPath, {
722
547
  taskId,
723
548
  taskTitle: task?.title || "Unknown",
724
- status: "in_review",
549
+ status: "completed",
725
550
  duration
726
551
  });
727
- this.emit("task:completed", { taskId, duration, status: "in_review" });
552
+ this.emit("task:completed", { taskId, duration });
728
553
  this.afkTasksCompleted++;
729
554
  } else {
730
- if (runningTask.worktreePath) {
731
- this.removeWorktree(taskId);
732
- }
733
555
  updateTask(this.projectPath, taskId, {
734
556
  status: "failed",
735
557
  passes: false
@@ -751,184 +573,33 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
751
573
  });
752
574
  this.emit("task:failed", { taskId, error });
753
575
  }
754
- this.runningTasks.delete(taskId);
576
+ this.runningTask = null;
755
577
  if (this.afkMode) {
756
578
  this.continueAFKMode();
757
579
  }
758
580
  }
759
581
  /**
760
- * Get info about a task in review
761
- */
762
- getReviewingTask(taskId) {
763
- return this.reviewingTasks.get(taskId);
764
- }
765
- /**
766
- * Merge a reviewed task into the base branch
767
- */
768
- mergeTask(taskId) {
769
- const reviewInfo = this.reviewingTasks.get(taskId);
770
- if (!reviewInfo) {
771
- return { success: false, error: "Task not in review or no worktree found" };
772
- }
773
- const merged = this.mergeWorktreeBranch(taskId);
774
- if (!merged) {
775
- return { success: false, error: "Merge failed - may have conflicts" };
776
- }
777
- this.removeWorktree(taskId);
778
- this.reviewingTasks.delete(taskId);
779
- updateTask(this.projectPath, taskId, {
780
- status: "completed"
781
- });
782
- addExecutionEntry(this.projectPath, taskId, {
783
- startedAt: reviewInfo.startedAt.toISOString(),
784
- endedAt: (/* @__PURE__ */ new Date()).toISOString(),
785
- status: "completed",
786
- duration: (/* @__PURE__ */ new Date()).getTime() - reviewInfo.startedAt.getTime()
787
- });
788
- console.log(`[executor] Task ${taskId} merged successfully`);
789
- return { success: true };
790
- }
791
- /**
792
- * Create a PR for a reviewed task
793
- */
794
- createPR(taskId) {
795
- const reviewInfo = this.reviewingTasks.get(taskId);
796
- const task = getTaskById(this.projectPath, taskId);
797
- if (!reviewInfo) {
798
- return { success: false, error: "Task not in review or no worktree found" };
799
- }
800
- try {
801
- execSync(`git push -u origin ${reviewInfo.branchName}`, {
802
- cwd: this.projectPath,
803
- stdio: "pipe"
804
- });
805
- const prTitle = task?.title || `Task ${taskId}`;
806
- const prBody = task?.description || "";
807
- const result = execSync(
808
- `gh pr create --title "${prTitle.replace(/"/g, '\\"')}" --body "${prBody.replace(/"/g, '\\"')}" --head ${reviewInfo.branchName}`,
809
- {
810
- cwd: this.projectPath,
811
- encoding: "utf-8"
812
- }
813
- );
814
- const prUrl = result.trim();
815
- try {
816
- execSync(`git worktree remove "${reviewInfo.worktreePath}" --force`, {
817
- cwd: this.projectPath,
818
- stdio: "pipe"
819
- });
820
- } catch {
821
- }
822
- this.reviewingTasks.delete(taskId);
823
- updateTask(this.projectPath, taskId, {
824
- status: "completed"
825
- });
826
- addExecutionEntry(this.projectPath, taskId, {
827
- startedAt: reviewInfo.startedAt.toISOString(),
828
- endedAt: (/* @__PURE__ */ new Date()).toISOString(),
829
- status: "completed",
830
- duration: (/* @__PURE__ */ new Date()).getTime() - reviewInfo.startedAt.getTime()
831
- });
832
- console.log(`[executor] PR created for task ${taskId}: ${prUrl}`);
833
- return { success: true, prUrl };
834
- } catch (error) {
835
- const errorMsg = error instanceof Error ? error.message : String(error);
836
- console.error(`[executor] Failed to create PR:`, errorMsg);
837
- return { success: false, error: errorMsg };
838
- }
839
- }
840
- /**
841
- * Discard a reviewed task - delete worktree and return to ready
842
- */
843
- discardTask(taskId) {
844
- const reviewInfo = this.reviewingTasks.get(taskId);
845
- if (!reviewInfo) {
846
- return { success: false, error: "Task not in review or no worktree found" };
847
- }
848
- this.removeWorktree(taskId);
849
- this.reviewingTasks.delete(taskId);
850
- updateTask(this.projectPath, taskId, {
851
- status: "ready",
852
- passes: false
853
- });
854
- addExecutionEntry(this.projectPath, taskId, {
855
- startedAt: reviewInfo.startedAt.toISOString(),
856
- endedAt: (/* @__PURE__ */ new Date()).toISOString(),
857
- status: "discarded",
858
- duration: (/* @__PURE__ */ new Date()).getTime() - reviewInfo.startedAt.getTime()
859
- });
860
- console.log(`[executor] Task ${taskId} discarded, returned to ready`);
861
- return { success: true };
862
- }
863
- /**
864
- * Get diff for a task in review
865
- */
866
- getTaskDiff(taskId) {
867
- const reviewInfo = this.reviewingTasks.get(taskId);
868
- if (!reviewInfo) {
869
- return { success: false, error: "Task not in review or no worktree found" };
870
- }
871
- try {
872
- const diff = execSync(`git diff main...${reviewInfo.branchName}`, {
873
- cwd: this.projectPath,
874
- encoding: "utf-8",
875
- maxBuffer: 10 * 1024 * 1024
876
- // 10MB buffer for large diffs
877
- });
878
- return { success: true, diff };
879
- } catch (error) {
880
- const errorMsg = error instanceof Error ? error.message : String(error);
881
- return { success: false, error: errorMsg };
882
- }
883
- }
884
- /**
885
- * Get list of changed files for a task in review
582
+ * Cancel the running task
886
583
  */
887
- getTaskChangedFiles(taskId) {
888
- const reviewInfo = this.reviewingTasks.get(taskId);
889
- if (!reviewInfo) {
890
- return { success: false, error: "Task not in review or no worktree found" };
891
- }
892
- try {
893
- const result = execSync(`git diff --name-only main...${reviewInfo.branchName}`, {
894
- cwd: this.projectPath,
895
- encoding: "utf-8"
896
- });
897
- const files = result.trim().split("\n").filter((f) => f);
898
- return { success: true, files };
899
- } catch (error) {
900
- const errorMsg = error instanceof Error ? error.message : String(error);
901
- return { success: false, error: errorMsg };
902
- }
903
- }
904
- /**
905
- * Cancel a running task
906
- */
907
- cancelTask(taskId, reason = "Cancelled by user") {
908
- const runningTask = this.runningTasks.get(taskId);
909
- if (!runningTask) return false;
910
- const startedAt = runningTask.startedAt;
584
+ cancelTask(reason = "Cancelled by user") {
585
+ if (!this.runningTask) return false;
586
+ const { taskId, process: childProcess, startedAt } = this.runningTask;
911
587
  const endedAt = /* @__PURE__ */ new Date();
912
588
  const duration = endedAt.getTime() - startedAt.getTime();
913
589
  const task = getTaskById(this.projectPath, taskId);
914
590
  try {
915
- runningTask.process.kill("SIGTERM");
591
+ childProcess.kill("SIGTERM");
916
592
  setTimeout(() => {
917
593
  try {
918
- if (!runningTask.process.killed) {
919
- runningTask.process.kill("SIGKILL");
594
+ if (!childProcess.killed) {
595
+ childProcess.kill("SIGKILL");
920
596
  }
921
597
  } catch {
922
598
  }
923
599
  }, 2e3);
924
600
  } catch {
925
601
  }
926
- if (runningTask.worktreePath) {
927
- this.removeWorktree(taskId);
928
- }
929
- updateTask(this.projectPath, taskId, {
930
- status: "ready"
931
- });
602
+ updateTask(this.projectPath, taskId, { status: "ready" });
932
603
  addExecutionEntry(this.projectPath, taskId, {
933
604
  startedAt: startedAt.toISOString(),
934
605
  endedAt: endedAt.toISOString(),
@@ -944,48 +615,46 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
944
615
  error: reason
945
616
  });
946
617
  this.emit("task:cancelled", { taskId });
947
- this.runningTasks.delete(taskId);
618
+ this.runningTask = null;
948
619
  return true;
949
620
  }
950
621
  /**
951
- * Start AFK mode
622
+ * Start AFK mode - run tasks sequentially until done
952
623
  */
953
- startAFKMode(maxIterations, concurrent) {
624
+ startAFKMode(maxIterations) {
954
625
  if (this.afkMode) {
955
626
  throw new Error("AFK mode already running");
956
627
  }
628
+ if (this.isRunning()) {
629
+ throw new Error("Cannot start AFK mode while a task is running");
630
+ }
957
631
  this.afkMode = true;
958
632
  this.afkIteration = 0;
959
633
  this.afkMaxIterations = maxIterations;
960
634
  this.afkTasksCompleted = 0;
961
635
  this.emitAFKStatus();
962
- this.continueAFKMode(concurrent);
636
+ this.continueAFKMode();
963
637
  }
964
638
  /**
965
- * Continue AFK mode - pick up next tasks
639
+ * Continue AFK mode - pick next task
966
640
  */
967
- continueAFKMode(concurrent = 1) {
641
+ continueAFKMode() {
968
642
  if (!this.afkMode) return;
969
643
  if (this.afkIteration >= this.afkMaxIterations) {
970
644
  this.stopAFKMode();
971
645
  return;
972
646
  }
973
- const config = getConfig(this.projectPath);
974
- const maxConcurrent = Math.min(concurrent, config.execution.maxConcurrent || 3);
975
- while (this.getRunningCount() < maxConcurrent) {
976
- const nextTask = getNextReadyTask(this.projectPath);
977
- if (!nextTask) {
978
- if (this.getRunningCount() === 0) {
979
- this.stopAFKMode();
980
- }
981
- break;
982
- }
983
- this.afkIteration++;
984
- this.runTask(nextTask.id).catch((error) => {
985
- console.error("AFK task error:", error);
986
- });
987
- this.emitAFKStatus();
647
+ if (this.isRunning()) return;
648
+ const nextTask = getNextReadyTask(this.projectPath);
649
+ if (!nextTask) {
650
+ this.stopAFKMode();
651
+ return;
988
652
  }
653
+ this.afkIteration++;
654
+ this.runTask(nextTask.id).catch((error) => {
655
+ console.error("AFK task error:", error);
656
+ });
657
+ this.emitAFKStatus();
989
658
  }
990
659
  /**
991
660
  * Stop AFK mode
@@ -1017,18 +686,15 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
1017
686
  };
1018
687
  }
1019
688
  /**
1020
- * Cancel all running tasks
689
+ * Cancel running task and stop AFK mode
1021
690
  */
1022
691
  cancelAll() {
1023
- for (const [taskId, runningTask] of this.runningTasks.entries()) {
692
+ if (this.runningTask) {
1024
693
  try {
1025
- runningTask.process.kill("SIGKILL");
694
+ this.runningTask.process.kill("SIGKILL");
1026
695
  } catch {
1027
696
  }
1028
- if (runningTask.worktreePath) {
1029
- this.removeWorktree(taskId);
1030
- }
1031
- this.runningTasks.delete(taskId);
697
+ this.runningTask = null;
1032
698
  }
1033
699
  this.stopAFKMode();
1034
700
  }
@@ -1538,7 +1204,12 @@ async function createServer(projectPath, port) {
1538
1204
  });
1539
1205
  app.post("/api/tasks/:id/cancel", (req, res) => {
1540
1206
  try {
1541
- const cancelled = executor.cancelTask(req.params.id);
1207
+ const runningId = executor.getRunningTaskId();
1208
+ if (runningId !== req.params.id) {
1209
+ res.status(404).json({ error: "Task not running" });
1210
+ return;
1211
+ }
1212
+ const cancelled = executor.cancelTask();
1542
1213
  if (!cancelled) {
1543
1214
  res.status(404).json({ error: "Task not running" });
1544
1215
  return;
@@ -1567,90 +1238,6 @@ async function createServer(projectPath, port) {
1567
1238
  res.status(500).json({ error: String(error) });
1568
1239
  }
1569
1240
  });
1570
- app.post("/api/tasks/:id/merge", (req, res) => {
1571
- try {
1572
- const result = executor.mergeTask(req.params.id);
1573
- if (!result.success) {
1574
- res.status(400).json({ error: result.error });
1575
- return;
1576
- }
1577
- const task = getTaskById(projectPath, req.params.id);
1578
- if (task) {
1579
- io.emit("task:updated", task);
1580
- }
1581
- res.json({ success: true });
1582
- } catch (error) {
1583
- res.status(500).json({ error: String(error) });
1584
- }
1585
- });
1586
- app.post("/api/tasks/:id/create-pr", (req, res) => {
1587
- try {
1588
- const result = executor.createPR(req.params.id);
1589
- if (!result.success) {
1590
- res.status(400).json({ error: result.error });
1591
- return;
1592
- }
1593
- const task = getTaskById(projectPath, req.params.id);
1594
- if (task) {
1595
- io.emit("task:updated", task);
1596
- }
1597
- res.json({ success: true, prUrl: result.prUrl });
1598
- } catch (error) {
1599
- res.status(500).json({ error: String(error) });
1600
- }
1601
- });
1602
- app.post("/api/tasks/:id/discard", (req, res) => {
1603
- try {
1604
- const result = executor.discardTask(req.params.id);
1605
- if (!result.success) {
1606
- res.status(400).json({ error: result.error });
1607
- return;
1608
- }
1609
- const task = getTaskById(projectPath, req.params.id);
1610
- if (task) {
1611
- io.emit("task:updated", task);
1612
- }
1613
- res.json({ success: true });
1614
- } catch (error) {
1615
- res.status(500).json({ error: String(error) });
1616
- }
1617
- });
1618
- app.get("/api/tasks/:id/diff", (req, res) => {
1619
- try {
1620
- const result = executor.getTaskDiff(req.params.id);
1621
- if (!result.success) {
1622
- res.status(400).json({ error: result.error });
1623
- return;
1624
- }
1625
- res.json({ diff: result.diff });
1626
- } catch (error) {
1627
- res.status(500).json({ error: String(error) });
1628
- }
1629
- });
1630
- app.get("/api/tasks/:id/changed-files", (req, res) => {
1631
- try {
1632
- const result = executor.getTaskChangedFiles(req.params.id);
1633
- if (!result.success) {
1634
- res.status(400).json({ error: result.error });
1635
- return;
1636
- }
1637
- res.json({ files: result.files });
1638
- } catch (error) {
1639
- res.status(500).json({ error: String(error) });
1640
- }
1641
- });
1642
- app.get("/api/tasks/:id/review-info", (req, res) => {
1643
- try {
1644
- const info = executor.getReviewingTask(req.params.id);
1645
- if (!info) {
1646
- res.status(404).json({ error: "Task not in review" });
1647
- return;
1648
- }
1649
- res.json(info);
1650
- } catch (error) {
1651
- res.status(500).json({ error: String(error) });
1652
- }
1653
- });
1654
1241
  app.get("/api/tasks/:id/logs", (req, res) => {
1655
1242
  try {
1656
1243
  const logs = executor.getTaskLog(req.params.id);
@@ -1724,8 +1311,8 @@ async function createServer(projectPath, port) {
1724
1311
  });
1725
1312
  app.post("/api/afk/start", (req, res) => {
1726
1313
  try {
1727
- const { maxIterations, concurrent } = req.body;
1728
- executor.startAFKMode(maxIterations || 10, concurrent || 1);
1314
+ const { maxIterations } = req.body;
1315
+ executor.startAFKMode(maxIterations || 10);
1729
1316
  res.json({ success: true, status: executor.getAFKStatus() });
1730
1317
  } catch (error) {
1731
1318
  res.status(400).json({ error: String(error) });
@@ -1749,8 +1336,8 @@ async function createServer(projectPath, port) {
1749
1336
  });
1750
1337
  app.get("/api/running", (_req, res) => {
1751
1338
  try {
1752
- const taskIds = executor.getRunningTaskIds();
1753
- res.json({ running: taskIds, count: taskIds.length });
1339
+ const taskId = executor.getRunningTaskId();
1340
+ res.json({ running: taskId ? [taskId] : [], count: taskId ? 1 : 0 });
1754
1341
  } catch (error) {
1755
1342
  res.status(500).json({ error: String(error) });
1756
1343
  }
@@ -1758,7 +1345,7 @@ async function createServer(projectPath, port) {
1758
1345
  app.get("/api/stats", (_req, res) => {
1759
1346
  try {
1760
1347
  const counts = getTaskCounts(projectPath);
1761
- const running = executor.getRunningCount();
1348
+ const running = executor.isRunning() ? 1 : 0;
1762
1349
  const afk = executor.getAFKStatus();
1763
1350
  res.json({ counts, running, afk });
1764
1351
  } catch (error) {
@@ -1774,12 +1361,13 @@ async function createServer(projectPath, port) {
1774
1361
  });
1775
1362
  io.on("connection", (socket) => {
1776
1363
  console.log("Client connected");
1777
- const runningIds = executor.getRunningTaskIds();
1364
+ const runningId = executor.getRunningTaskId();
1365
+ const runningIds = runningId ? [runningId] : [];
1778
1366
  const taskLogs = {};
1779
- for (const taskId of runningIds) {
1780
- const logs = executor.getTaskLog(taskId);
1367
+ if (runningId) {
1368
+ const logs = executor.getTaskLog(runningId);
1781
1369
  if (logs) {
1782
- taskLogs[taskId] = logs;
1370
+ taskLogs[runningId] = logs;
1783
1371
  }
1784
1372
  }
1785
1373
  socket.emit("init", {
@@ -1787,7 +1375,7 @@ async function createServer(projectPath, port) {
1787
1375
  running: runningIds,
1788
1376
  afk: executor.getAFKStatus(),
1789
1377
  taskLogs
1790
- // Include logs for running tasks
1378
+ // Include logs for running task
1791
1379
  });
1792
1380
  socket.on("get-logs", (taskId) => {
1793
1381
  const logs = executor.getTaskLog(taskId);
@@ -1960,7 +1548,6 @@ function getClientHTML() {
1960
1548
  .status-dot-draft { background: #a3a3a3; }
1961
1549
  .status-dot-ready { background: #3b82f6; }
1962
1550
  .status-dot-in_progress { background: #f97316; }
1963
- .status-dot-in_review { background: #8b5cf6; }
1964
1551
  .status-dot-completed { background: #22c55e; }
1965
1552
  .status-dot-failed { background: #ef4444; }
1966
1553
 
@@ -2170,7 +1757,6 @@ function getClientHTML() {
2170
1757
  .status-badge-draft { background: #f5f5f5; color: #737373; }
2171
1758
  .status-badge-ready { background: #eff6ff; color: #3b82f6; }
2172
1759
  .status-badge-in_progress { background: #fff7ed; color: #f97316; }
2173
- .status-badge-in_review { background: #f5f3ff; color: #8b5cf6; }
2174
1760
  .status-badge-completed { background: #f0fdf4; color: #22c55e; }
2175
1761
  .status-badge-failed { background: #fef2f2; color: #ef4444; }
2176
1762
 
@@ -2496,16 +2082,14 @@ socket.on('task:output', ({ taskId, line }) => {
2496
2082
  }
2497
2083
  });
2498
2084
 
2499
- socket.on('task:completed', ({ taskId, status }) => {
2085
+ socket.on('task:completed', ({ taskId }) => {
2500
2086
  state.running = state.running.filter(id => id !== taskId);
2501
2087
  const task = state.tasks.find(t => t.id === taskId);
2502
2088
  if (task) {
2503
- // Use status from server (could be 'in_review' or 'completed')
2504
- task.status = status || 'in_review';
2089
+ task.status = 'completed';
2505
2090
  task.passes = true;
2506
2091
  }
2507
- const statusMsg = (status === 'in_review') ? 'ready for review' : 'completed';
2508
- showToast('Task ' + statusMsg + ': ' + (task?.title || taskId), 'success');
2092
+ showToast('Task completed: ' + (task?.title || taskId), 'success');
2509
2093
  render();
2510
2094
  });
2511
2095
 
@@ -2579,53 +2163,6 @@ async function retryTask(id) {
2579
2163
  });
2580
2164
  }
2581
2165
 
2582
- async function mergeTask(id) {
2583
- const res = await fetch('/api/tasks/' + id + '/merge', { method: 'POST' });
2584
- const data = await res.json();
2585
- if (!res.ok) {
2586
- throw new Error(data.error || 'Merge failed');
2587
- }
2588
- return data;
2589
- }
2590
-
2591
- async function createPR(id) {
2592
- const res = await fetch('/api/tasks/' + id + '/create-pr', { method: 'POST' });
2593
- const data = await res.json();
2594
- if (!res.ok) {
2595
- throw new Error(data.error || 'PR creation failed');
2596
- }
2597
- return data;
2598
- }
2599
-
2600
- async function discardTask(id) {
2601
- const res = await fetch('/api/tasks/' + id + '/discard', { method: 'POST' });
2602
- const data = await res.json();
2603
- if (!res.ok) {
2604
- throw new Error(data.error || 'Discard failed');
2605
- }
2606
- return data;
2607
- }
2608
-
2609
- async function getReviewInfo(id) {
2610
- const res = await fetch('/api/tasks/' + id + '/review-info');
2611
- const data = await res.json();
2612
- if (!res.ok) {
2613
- throw new Error(data.error || 'Failed to get review info');
2614
- }
2615
- return data;
2616
- }
2617
-
2618
- async function openInVSCode(id) {
2619
- try {
2620
- const info = await getReviewInfo(id);
2621
- // Open VS Code at the worktree path
2622
- window.open('vscode://file/' + encodeURIComponent(info.worktreePath), '_blank');
2623
- showToast('Opening in VS Code...', 'info');
2624
- } catch (e) {
2625
- showToast('Failed to open in VS Code: ' + e.message, 'error');
2626
- }
2627
- }
2628
-
2629
2166
  async function generateTask(prompt) {
2630
2167
  state.aiGenerating = true;
2631
2168
  render();
@@ -2644,11 +2181,11 @@ async function generateTask(prompt) {
2644
2181
  }
2645
2182
  }
2646
2183
 
2647
- async function startAFK(maxIterations, concurrent) {
2184
+ async function startAFK(maxIterations) {
2648
2185
  await fetch('/api/afk/start', {
2649
2186
  method: 'POST',
2650
2187
  headers: { 'Content-Type': 'application/json' },
2651
- body: JSON.stringify({ maxIterations, concurrent })
2188
+ body: JSON.stringify({ maxIterations })
2652
2189
  });
2653
2190
  }
2654
2191
 
@@ -2829,7 +2366,6 @@ function renderColumn(status, title, tasks) {
2829
2366
  draft: 'To Do',
2830
2367
  ready: 'Ready',
2831
2368
  in_progress: 'In Progress',
2832
- in_review: 'In Review',
2833
2369
  completed: 'Done',
2834
2370
  failed: 'Failed'
2835
2371
  };
@@ -3082,21 +2618,13 @@ function renderModal() {
3082
2618
  <button onclick="state.showModal = null; render();" class="btn btn-ghost p-1.5 text-canvas-500 hover:text-canvas-700">\u2715</button>
3083
2619
  </div>
3084
2620
  <div class="p-6">
3085
- <p class="text-sm text-canvas-600 mb-5">Run the agent in a loop, automatically picking up tasks from the "Ready" column until complete.</p>
2621
+ <p class="text-sm text-canvas-600 mb-5">Run the agent in a loop, automatically picking up tasks from the "Ready" column one at a time until complete.</p>
3086
2622
  <div class="space-y-4">
3087
2623
  <div>
3088
2624
  <label class="block text-sm font-medium text-canvas-700 mb-2">Maximum Iterations</label>
3089
2625
  <input type="number" id="afk-iterations" value="10" min="1" max="100"
3090
2626
  class="input w-full">
3091
2627
  </div>
3092
- <div>
3093
- <label class="block text-sm font-medium text-canvas-700 mb-2">Concurrent Tasks</label>
3094
- <select id="afk-concurrent" class="input w-full">
3095
- <option value="1">1 (Sequential)</option>
3096
- <option value="2">2</option>
3097
- <option value="3">3 (Max)</option>
3098
- </select>
3099
- </div>
3100
2628
  <div class="bg-status-running/10 border border-status-running/20 rounded-lg p-3">
3101
2629
  <p class="text-xs text-status-running">\u26A0\uFE0F You can close this tab - the agent will continue running. Check back later or watch the terminal output.</p>
3102
2630
  </div>
@@ -3211,20 +2739,6 @@ function renderSidePanel() {
3211
2739
  \u23F9 Stop Attempt
3212
2740
  </button>
3213
2741
  \` : ''}
3214
- \${task.status === 'in_review' ? \`
3215
- <button onclick="handleMerge('\${task.id}')" class="btn btn-primary px-4 py-2 text-sm">
3216
- \u2713 Merge
3217
- </button>
3218
- <button onclick="handleCreatePR('\${task.id}')" class="btn btn-ghost px-4 py-2 text-sm">
3219
- \u21E1 Create PR
3220
- </button>
3221
- <button onclick="openInVSCode('\${task.id}')" class="btn btn-ghost px-4 py-2 text-sm">
3222
- \u{1F4C2} Open in VS Code
3223
- </button>
3224
- <button onclick="handleDiscard('\${task.id}')" class="btn btn-danger px-4 py-2 text-sm">
3225
- \u2715 Discard
3226
- </button>
3227
- \` : ''}
3228
2742
  \${task.status === 'failed' ? \`
3229
2743
  <button onclick="retryTask('\${task.id}')" class="btn btn-primary px-4 py-2 text-sm">
3230
2744
  \u21BB Retry
@@ -3419,47 +2933,11 @@ function applyTemplate(templateId) {
3419
2933
 
3420
2934
  function handleStartAFK() {
3421
2935
  const iterations = parseInt(document.getElementById('afk-iterations').value) || 10;
3422
- const concurrent = parseInt(document.getElementById('afk-concurrent').value) || 1;
3423
- startAFK(iterations, concurrent);
2936
+ startAFK(iterations);
3424
2937
  state.showModal = null;
3425
2938
  render();
3426
2939
  }
3427
2940
 
3428
- async function handleMerge(taskId) {
3429
- if (!confirm('Merge this task into the main branch?')) return;
3430
- try {
3431
- await mergeTask(taskId);
3432
- showToast('Task merged successfully!', 'success');
3433
- closeSidePanel();
3434
- } catch (e) {
3435
- showToast('Merge failed: ' + e.message, 'error');
3436
- }
3437
- }
3438
-
3439
- async function handleCreatePR(taskId) {
3440
- try {
3441
- const result = await createPR(taskId);
3442
- showToast('PR created successfully!', 'success');
3443
- if (result.prUrl) {
3444
- window.open(result.prUrl, '_blank');
3445
- }
3446
- closeSidePanel();
3447
- } catch (e) {
3448
- showToast('PR creation failed: ' + e.message, 'error');
3449
- }
3450
- }
3451
-
3452
- async function handleDiscard(taskId) {
3453
- if (!confirm('Discard this work? The task will return to Ready status.')) return;
3454
- try {
3455
- await discardTask(taskId);
3456
- showToast('Task discarded, returned to Ready', 'warning');
3457
- closeSidePanel();
3458
- } catch (e) {
3459
- showToast('Discard failed: ' + e.message, 'error');
3460
- }
3461
- }
3462
-
3463
2941
  // Filter tasks based on search query
3464
2942
  function filterTasks(tasks) {
3465
2943
  if (!state.searchQuery) return tasks;
@@ -3521,7 +2999,6 @@ function render() {
3521
2999
  \${renderColumn('draft', 'To Do', filterTasks(state.tasks))}
3522
3000
  \${renderColumn('ready', 'Ready', filterTasks(state.tasks))}
3523
3001
  \${renderColumn('in_progress', 'In Progress', filterTasks(state.tasks))}
3524
- \${renderColumn('in_review', 'In Review', filterTasks(state.tasks))}
3525
3002
  \${renderColumn('completed', 'Done', filterTasks(state.tasks))}
3526
3003
  \${renderColumn('failed', 'Failed', filterTasks(state.tasks))}
3527
3004
  </div>
@@ -3586,14 +3063,6 @@ window.closeSidePanel = closeSidePanel;
3586
3063
  window.showTaskMenu = showTaskMenu;
3587
3064
  window.filterTasks = filterTasks;
3588
3065
  window.scrollSidePanelLog = scrollSidePanelLog;
3589
- window.mergeTask = mergeTask;
3590
- window.createPR = createPR;
3591
- window.discardTask = discardTask;
3592
- window.handleMerge = handleMerge;
3593
- window.handleCreatePR = handleCreatePR;
3594
- window.handleDiscard = handleDiscard;
3595
- window.openInVSCode = openInVSCode;
3596
- window.getReviewInfo = getReviewInfo;
3597
3066
 
3598
3067
  // Keyboard shortcuts
3599
3068
  document.addEventListener('keydown', (e) => {