claude-kanban 0.4.0 → 0.5.1

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.
package/dist/bin/cli.js CHANGED
@@ -5,7 +5,7 @@ import { Command } from "commander";
5
5
  import chalk from "chalk";
6
6
  import open from "open";
7
7
  import { existsSync as existsSync4 } from "fs";
8
- import { execSync as execSync2 } from "child_process";
8
+ import { execSync } from "child_process";
9
9
  import { createInterface } from "readline";
10
10
  import { join as join6 } from "path";
11
11
 
@@ -18,9 +18,9 @@ import { fileURLToPath } from "url";
18
18
  import { existsSync as existsSync3 } from "fs";
19
19
 
20
20
  // src/server/services/executor.ts
21
- import { spawn, execSync } from "child_process";
21
+ import { spawn } from "child_process";
22
22
  import { join as join4 } from "path";
23
- import { writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync2, existsSync as existsSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync4, rmSync } from "fs";
23
+ import { writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync2, existsSync as existsSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync4 } from "fs";
24
24
  import { EventEmitter } from "events";
25
25
 
26
26
  // src/server/services/project.ts
@@ -458,7 +458,6 @@ function getTaskCounts(projectPath) {
458
458
  draft: 0,
459
459
  ready: 0,
460
460
  in_progress: 0,
461
- in_review: 0,
462
461
  completed: 0,
463
462
  failed: 0
464
463
  };
@@ -531,11 +530,10 @@ function getRecentProgress(projectPath, lines = 100) {
531
530
  // src/server/services/executor.ts
532
531
  var KANBAN_DIR4 = ".claude-kanban";
533
532
  var LOGS_DIR = "logs";
534
- var WORKTREES_DIR = "worktrees";
535
533
  var TaskExecutor = class extends EventEmitter {
536
534
  projectPath;
537
- runningTasks = /* @__PURE__ */ new Map();
538
- reviewingTasks = /* @__PURE__ */ new Map();
535
+ runningTask = null;
536
+ planningSession = null;
539
537
  afkMode = false;
540
538
  afkIteration = 0;
541
539
  afkMaxIterations = 0;
@@ -641,186 +639,71 @@ ${summary}
641
639
  }
642
640
  }
643
641
  /**
644
- * Check if project is a git repository
642
+ * Check if a task is currently running
645
643
  */
646
- isGitRepo() {
647
- try {
648
- execSync("git rev-parse --is-inside-work-tree", {
649
- cwd: this.projectPath,
650
- stdio: "pipe"
651
- });
652
- return true;
653
- } catch {
654
- return false;
655
- }
644
+ isRunning() {
645
+ return this.runningTask !== null;
656
646
  }
657
647
  /**
658
- * Get worktrees directory path
648
+ * Get the currently running task ID
659
649
  */
660
- getWorktreesDir() {
661
- return join4(this.projectPath, KANBAN_DIR4, WORKTREES_DIR);
662
- }
663
- /**
664
- * Get worktree path for a task
665
- */
666
- getWorktreePath(taskId) {
667
- return join4(this.getWorktreesDir(), taskId);
668
- }
669
- /**
670
- * Get branch name for a task
671
- */
672
- getBranchName(taskId) {
673
- return `task/${taskId}`;
674
- }
675
- /**
676
- * Create a git worktree for isolated task execution
677
- */
678
- createWorktree(taskId) {
679
- if (!this.isGitRepo()) {
680
- console.log("[executor] Not a git repo, skipping worktree creation");
681
- return null;
682
- }
683
- const worktreePath = this.getWorktreePath(taskId);
684
- const branchName = this.getBranchName(taskId);
685
- try {
686
- const worktreesDir = this.getWorktreesDir();
687
- if (!existsSync2(worktreesDir)) {
688
- mkdirSync2(worktreesDir, { recursive: true });
689
- }
690
- if (existsSync2(worktreePath)) {
691
- this.removeWorktree(taskId);
692
- }
693
- try {
694
- execSync(`git rev-parse --verify ${branchName}`, {
695
- cwd: this.projectPath,
696
- stdio: "pipe"
697
- });
698
- execSync(`git branch -D ${branchName}`, {
699
- cwd: this.projectPath,
700
- stdio: "pipe"
701
- });
702
- } catch {
703
- }
704
- execSync(`git worktree add -b ${branchName} "${worktreePath}"`, {
705
- cwd: this.projectPath,
706
- stdio: "pipe"
707
- });
708
- console.log(`[executor] Created worktree at ${worktreePath} on branch ${branchName}`);
709
- return { worktreePath, branchName };
710
- } catch (error) {
711
- console.error("[executor] Failed to create worktree:", error);
712
- return null;
713
- }
650
+ getRunningTaskId() {
651
+ return this.runningTask?.taskId || null;
714
652
  }
715
653
  /**
716
- * Remove a git worktree
717
- */
718
- removeWorktree(taskId) {
719
- const worktreePath = this.getWorktreePath(taskId);
720
- const branchName = this.getBranchName(taskId);
721
- try {
722
- if (existsSync2(worktreePath)) {
723
- execSync(`git worktree remove "${worktreePath}" --force`, {
724
- cwd: this.projectPath,
725
- stdio: "pipe"
726
- });
727
- console.log(`[executor] Removed worktree at ${worktreePath}`);
728
- }
729
- } catch (error) {
730
- console.error("[executor] Failed to remove worktree via git:", error);
731
- try {
732
- if (existsSync2(worktreePath)) {
733
- rmSync(worktreePath, { recursive: true, force: true });
734
- execSync("git worktree prune", {
735
- cwd: this.projectPath,
736
- stdio: "pipe"
737
- });
738
- }
739
- } catch {
740
- console.error("[executor] Failed to force remove worktree directory");
741
- }
742
- }
743
- try {
744
- execSync(`git branch -D ${branchName}`, {
745
- cwd: this.projectPath,
746
- stdio: "pipe"
747
- });
748
- console.log(`[executor] Deleted branch ${branchName}`);
749
- } catch {
750
- }
751
- }
752
- /**
753
- * Merge worktree branch back to main branch
654
+ * Get running task output
754
655
  */
755
- mergeWorktreeBranch(taskId) {
756
- const branchName = this.getBranchName(taskId);
757
- try {
758
- const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
759
- cwd: this.projectPath,
760
- encoding: "utf-8"
761
- }).trim();
762
- execSync(`git merge ${branchName} --no-edit`, {
763
- cwd: this.projectPath,
764
- stdio: "pipe"
765
- });
766
- console.log(`[executor] Merged ${branchName} into ${currentBranch}`);
767
- return true;
768
- } catch (error) {
769
- console.error("[executor] Failed to merge branch:", error);
770
- return false;
656
+ getTaskOutput(taskId) {
657
+ if (this.runningTask?.taskId === taskId) {
658
+ return this.runningTask.output;
771
659
  }
660
+ return void 0;
772
661
  }
773
662
  /**
774
- * Get number of currently running tasks
663
+ * Check if a planning session is running
775
664
  */
776
- getRunningCount() {
777
- return this.runningTasks.size;
665
+ isPlanning() {
666
+ return this.planningSession !== null;
778
667
  }
779
668
  /**
780
- * Check if a task is running
669
+ * Check if executor is busy (task or planning running)
781
670
  */
782
- isTaskRunning(taskId) {
783
- return this.runningTasks.has(taskId);
671
+ isBusy() {
672
+ return this.runningTask !== null || this.planningSession !== null;
784
673
  }
785
674
  /**
786
- * Get all running task IDs
675
+ * Get planning session output
787
676
  */
788
- getRunningTaskIds() {
789
- return Array.from(this.runningTasks.keys());
677
+ getPlanningOutput() {
678
+ return this.planningSession?.output;
790
679
  }
791
680
  /**
792
- * Get running task output
681
+ * Get planning session goal
793
682
  */
794
- getTaskOutput(taskId) {
795
- return this.runningTasks.get(taskId)?.output;
683
+ getPlanningGoal() {
684
+ return this.planningSession?.goal;
796
685
  }
797
686
  /**
798
- * Build the prompt for a specific task
687
+ * Build the prompt for a task - simplified Ralph-style
799
688
  */
800
- buildTaskPrompt(task, config, worktreeInfo) {
689
+ buildTaskPrompt(task, config) {
801
690
  const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
802
691
  const prdPath = join4(kanbanDir, "prd.json");
803
692
  const progressPath = join4(kanbanDir, "progress.txt");
804
693
  const stepsText = task.steps.length > 0 ? `
805
694
  Verification steps:
806
695
  ${task.steps.map((s, i) => `${i + 1}. ${s}`).join("\n")}` : "";
807
- const verifySteps = [];
696
+ const verifyCommands = [];
808
697
  if (config.project.typecheckCommand) {
809
- verifySteps.push(`Run typecheck: ${config.project.typecheckCommand}`);
698
+ verifyCommands.push(config.project.typecheckCommand);
810
699
  }
811
700
  if (config.project.testCommand) {
812
- verifySteps.push(`Run tests: ${config.project.testCommand}`);
701
+ verifyCommands.push(config.project.testCommand);
813
702
  }
814
- const verifySection = verifySteps.length > 0 ? `3. Verify your work:
815
- ${verifySteps.map((s) => ` - ${s}`).join("\n")}
816
-
817
- ` : "";
818
- const worktreeSection = worktreeInfo ? `## ENVIRONMENT
819
- You are running in an isolated git worktree on branch "${worktreeInfo.branchName}".
820
- This is a fresh checkout - dependencies (node_modules, vendor, etc.) are NOT installed.
821
- Before running any build/test commands, install dependencies first (e.g., npm install, composer install, pip install).
703
+ const verifySection = verifyCommands.length > 0 ? `3. Run quality checks:
704
+ ${verifyCommands.map((cmd) => ` - ${cmd}`).join("\n")}
822
705
 
823
- ` : "";
706
+ 4.` : "3.";
824
707
  return `You are an AI coding agent. Complete the following task:
825
708
 
826
709
  ## TASK
@@ -831,48 +714,254 @@ Priority: ${task.priority}
831
714
  ${task.description}
832
715
  ${stepsText}
833
716
 
834
- ${worktreeSection}## INSTRUCTIONS
835
- 1. If dependencies are not installed, install them first.
717
+ ## INSTRUCTIONS
836
718
 
837
- 2. Implement this task as described above.
719
+ 1. Implement this task as described above.
838
720
 
839
- ${verifySection}${verifySteps.length > 0 ? "4" : "3"}. When complete, update the task in ${prdPath}:
721
+ 2. Make sure your changes work correctly.
722
+
723
+ ${verifySection} When complete, update the PRD file at ${prdPath}:
840
724
  - Find the task with id "${task.id}"
841
725
  - Set "passes": true
842
- (Note: Do NOT change the status - the system will move it to review)
726
+ - Set "status": "completed"
843
727
 
844
- ${verifySteps.length > 0 ? "5" : "4"}. Document your work in ${progressPath}:
845
- - What you implemented and files changed
846
- - Key decisions made and why
847
- - Gotchas, edge cases, or tricky parts discovered
848
- - Useful patterns or approaches that worked well
849
- - Anything a future agent should know about this area of the codebase
728
+ ${verifyCommands.length > 0 ? "5" : "4"}. Document your work in ${progressPath}:
729
+ - What you implemented
730
+ - Key decisions made
731
+ - Any gotchas or important notes for future work
850
732
 
851
- ${verifySteps.length > 0 ? "6" : "5"}. Make a git commit with a descriptive message.
733
+ ${verifyCommands.length > 0 ? "6" : "5"}. Commit your changes with a descriptive message.
852
734
 
853
- Focus only on this task. When successfully complete, output: <promise>COMPLETE</promise>`;
735
+ Focus only on this task. When done, output: <promise>COMPLETE</promise>`;
736
+ }
737
+ /**
738
+ * Build the prompt for a planning session
739
+ */
740
+ buildPlanningPrompt(goal) {
741
+ const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
742
+ const prdPath = join4(kanbanDir, "prd.json");
743
+ return `You are an AI coding planner. Your job is to analyze a codebase and break down a user's goal into concrete, actionable tasks.
744
+
745
+ ## GOAL
746
+ ${goal}
747
+
748
+ ## INSTRUCTIONS
749
+
750
+ 1. Explore the codebase to understand:
751
+ - Project structure and architecture
752
+ - Existing patterns and conventions
753
+ - Related existing code that you'll build upon
754
+ - What technologies and frameworks are being used
755
+
756
+ 2. Break down the goal into 3-8 specific, actionable tasks that can each be completed in a single coding session.
757
+
758
+ 3. For each task, determine:
759
+ - Clear, action-oriented title (start with a verb: Add, Create, Implement, Fix, etc.)
760
+ - Detailed description with implementation guidance
761
+ - Category: functional, ui, bug, enhancement, testing, or refactor
762
+ - Priority: low, medium, high, or critical
763
+ - 3-7 verification steps to confirm the task is complete
764
+
765
+ 4. Read the current PRD file at ${prdPath}, then update it:
766
+ - Add your new tasks to the "tasks" array
767
+ - Each task must have this structure:
768
+ {
769
+ "id": "task_" + 8 random alphanumeric characters,
770
+ "title": "...",
771
+ "description": "...",
772
+ "category": "functional|ui|bug|enhancement|testing|refactor",
773
+ "priority": "low|medium|high|critical",
774
+ "status": "draft",
775
+ "steps": ["Step 1", "Step 2", ...],
776
+ "passes": false,
777
+ "createdAt": ISO timestamp,
778
+ "updatedAt": ISO timestamp,
779
+ "executionHistory": []
780
+ }
781
+
782
+ 5. Commit your changes with message: "Plan: ${goal}"
783
+
784
+ IMPORTANT:
785
+ - Tasks should be ordered logically (dependencies first)
786
+ - Each task should be completable independently
787
+ - Be specific about implementation details
788
+ - Consider edge cases and error handling
789
+
790
+ When done, output: <promise>PLANNING_COMPLETE</promise>`;
854
791
  }
855
792
  /**
856
- * Run a specific task
793
+ * Run a planning session to break down a goal into tasks
794
+ */
795
+ async runPlanningSession(goal) {
796
+ if (this.isBusy()) {
797
+ throw new Error("Another operation is in progress. Wait for it to complete or cancel it first.");
798
+ }
799
+ const config = getConfig(this.projectPath);
800
+ const startedAt = /* @__PURE__ */ new Date();
801
+ const prompt = this.buildPlanningPrompt(goal);
802
+ const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
803
+ const promptFile = join4(kanbanDir, "prompt-planning.txt");
804
+ writeFileSync4(promptFile, prompt);
805
+ const args = [];
806
+ if (config.agent.model) {
807
+ args.push("--model", config.agent.model);
808
+ }
809
+ args.push("--permission-mode", config.agent.permissionMode);
810
+ args.push("-p");
811
+ args.push("--verbose");
812
+ args.push("--output-format", "stream-json");
813
+ args.push(`@${promptFile}`);
814
+ const commandDisplay = `${config.agent.command} ${args.join(" ")}`;
815
+ const fullCommand = `${config.agent.command} ${args.join(" ")}`;
816
+ console.log("[executor] Planning command:", fullCommand);
817
+ console.log("[executor] CWD:", this.projectPath);
818
+ const childProcess = spawn("bash", ["-c", fullCommand], {
819
+ cwd: this.projectPath,
820
+ env: {
821
+ ...process.env,
822
+ TERM: "xterm-256color",
823
+ FORCE_COLOR: "0",
824
+ NO_COLOR: "1"
825
+ },
826
+ stdio: ["ignore", "pipe", "pipe"]
827
+ });
828
+ this.planningSession = {
829
+ goal,
830
+ process: childProcess,
831
+ startedAt,
832
+ output: []
833
+ };
834
+ const logOutput = (line) => {
835
+ this.planningSession?.output.push(line);
836
+ this.emit("planning:output", { line, lineType: "stdout" });
837
+ };
838
+ this.emit("planning:started", { goal, timestamp: startedAt.toISOString() });
839
+ logOutput(`[claude-kanban] Starting planning session
840
+ `);
841
+ logOutput(`[claude-kanban] Goal: ${goal}
842
+ `);
843
+ logOutput(`[claude-kanban] Command: ${commandDisplay}
844
+ `);
845
+ let stdoutBuffer = "";
846
+ childProcess.stdout?.on("data", (data) => {
847
+ stdoutBuffer += data.toString();
848
+ const lines = stdoutBuffer.split("\n");
849
+ stdoutBuffer = lines.pop() || "";
850
+ for (const line of lines) {
851
+ if (!line.trim()) continue;
852
+ try {
853
+ const json = JSON.parse(line);
854
+ let text = "";
855
+ if (json.type === "assistant" && json.message?.content) {
856
+ for (const block of json.message.content) {
857
+ if (block.type === "text") {
858
+ text += block.text;
859
+ } else if (block.type === "tool_use") {
860
+ text += this.formatToolUse(block.name, block.input);
861
+ }
862
+ }
863
+ } else if (json.type === "content_block_delta" && json.delta?.text) {
864
+ text = json.delta.text;
865
+ } else if (json.type === "result" && json.result) {
866
+ text = `
867
+ [Result: ${json.result}]
868
+ `;
869
+ }
870
+ if (text) {
871
+ logOutput(text);
872
+ }
873
+ } catch {
874
+ const cleanText = line.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "");
875
+ if (cleanText.trim()) {
876
+ logOutput(cleanText + "\n");
877
+ }
878
+ }
879
+ }
880
+ });
881
+ childProcess.stderr?.on("data", (data) => {
882
+ const text = data.toString();
883
+ logOutput(`[stderr] ${text}`);
884
+ });
885
+ childProcess.on("error", (error) => {
886
+ console.log("[executor] Planning spawn error:", error.message);
887
+ this.emit("planning:output", { line: `[claude-kanban] Error: ${error.message}
888
+ `, lineType: "stderr" });
889
+ try {
890
+ unlinkSync(promptFile);
891
+ } catch {
892
+ }
893
+ this.emit("planning:failed", { error: error.message });
894
+ this.planningSession = null;
895
+ });
896
+ childProcess.on("close", (code, signal) => {
897
+ console.log("[executor] Planning process closed with code:", code, "signal:", signal);
898
+ try {
899
+ unlinkSync(promptFile);
900
+ } catch {
901
+ }
902
+ logOutput(`[claude-kanban] Planning process exited with code ${code}
903
+ `);
904
+ this.handlePlanningComplete(code);
905
+ });
906
+ const timeoutMs = 15 * 60 * 1e3;
907
+ setTimeout(() => {
908
+ if (this.planningSession) {
909
+ this.cancelPlanning("Planning timeout exceeded");
910
+ }
911
+ }, timeoutMs);
912
+ }
913
+ /**
914
+ * Handle planning session completion
915
+ */
916
+ handlePlanningComplete(exitCode) {
917
+ if (!this.planningSession) return;
918
+ const output = this.planningSession.output.join("");
919
+ const isComplete = output.includes("<promise>PLANNING_COMPLETE</promise>");
920
+ if (isComplete || exitCode === 0) {
921
+ this.emit("planning:completed", { success: true });
922
+ } else {
923
+ const error = `Planning process exited with code ${exitCode}`;
924
+ this.emit("planning:failed", { error });
925
+ }
926
+ this.planningSession = null;
927
+ }
928
+ /**
929
+ * Cancel the planning session
930
+ */
931
+ cancelPlanning(reason = "Cancelled by user") {
932
+ if (!this.planningSession) return false;
933
+ const { process: childProcess } = this.planningSession;
934
+ try {
935
+ childProcess.kill("SIGTERM");
936
+ setTimeout(() => {
937
+ try {
938
+ if (!childProcess.killed) {
939
+ childProcess.kill("SIGKILL");
940
+ }
941
+ } catch {
942
+ }
943
+ }, 2e3);
944
+ } catch {
945
+ }
946
+ this.emit("planning:cancelled", { reason });
947
+ this.planningSession = null;
948
+ return true;
949
+ }
950
+ /**
951
+ * Run a task
857
952
  */
858
953
  async runTask(taskId) {
954
+ if (this.isBusy()) {
955
+ throw new Error("Another operation is in progress. Wait for it to complete or cancel it first.");
956
+ }
859
957
  const config = getConfig(this.projectPath);
860
958
  const task = getTaskById(this.projectPath, taskId);
861
959
  if (!task) {
862
960
  throw new Error(`Task not found: ${taskId}`);
863
961
  }
864
- if (this.isTaskRunning(taskId)) {
865
- throw new Error(`Task already running: ${taskId}`);
866
- }
867
- const maxConcurrent = config.execution.maxConcurrent || 3;
868
- if (this.getRunningCount() >= maxConcurrent) {
869
- throw new Error(`Maximum concurrent tasks (${maxConcurrent}) reached`);
870
- }
871
962
  updateTask(this.projectPath, taskId, { status: "in_progress" });
872
963
  const startedAt = /* @__PURE__ */ new Date();
873
- const worktreeInfo = this.createWorktree(taskId);
874
- const executionPath = worktreeInfo?.worktreePath || this.projectPath;
875
- const prompt = this.buildTaskPrompt(task, config, worktreeInfo);
964
+ const prompt = this.buildTaskPrompt(task, config);
876
965
  const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
877
966
  const promptFile = join4(kanbanDir, `prompt-${taskId}.txt`);
878
967
  writeFileSync4(promptFile, prompt);
@@ -888,48 +977,32 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
888
977
  const commandDisplay = `${config.agent.command} ${args.join(" ")}`;
889
978
  const fullCommand = `${config.agent.command} ${args.join(" ")}`;
890
979
  console.log("[executor] Command:", fullCommand);
891
- console.log("[executor] CWD:", executionPath);
892
- if (worktreeInfo) {
893
- console.log("[executor] Using worktree:", worktreeInfo.worktreePath);
894
- console.log("[executor] Branch:", worktreeInfo.branchName);
895
- }
980
+ console.log("[executor] CWD:", this.projectPath);
896
981
  const childProcess = spawn("bash", ["-c", fullCommand], {
897
- cwd: executionPath,
982
+ cwd: this.projectPath,
898
983
  env: {
899
984
  ...process.env,
900
985
  TERM: "xterm-256color",
901
986
  FORCE_COLOR: "0",
902
- // Disable colors to avoid escape codes
903
987
  NO_COLOR: "1"
904
- // Standard way to disable colors
905
988
  },
906
989
  stdio: ["ignore", "pipe", "pipe"]
907
- // Close stdin since we don't need interactive input
908
990
  });
909
- const runningTask = {
991
+ this.runningTask = {
910
992
  taskId,
911
993
  process: childProcess,
912
994
  startedAt,
913
- output: [],
914
- worktreePath: worktreeInfo?.worktreePath,
915
- branchName: worktreeInfo?.branchName
995
+ output: []
916
996
  };
917
- this.runningTasks.set(taskId, runningTask);
918
997
  this.initLogFile(taskId);
919
998
  const logOutput = (line) => {
920
999
  this.appendToLog(taskId, line);
921
- runningTask.output.push(line);
1000
+ this.runningTask?.output.push(line);
922
1001
  this.emit("task:output", { taskId, line, lineType: "stdout" });
923
1002
  };
924
1003
  this.emit("task:started", { taskId, timestamp: startedAt.toISOString() });
925
1004
  logOutput(`[claude-kanban] Starting task: ${task.title}
926
1005
  `);
927
- if (worktreeInfo) {
928
- logOutput(`[claude-kanban] Worktree: ${worktreeInfo.worktreePath}
929
- `);
930
- logOutput(`[claude-kanban] Branch: ${worktreeInfo.branchName}
931
- `);
932
- }
933
1006
  logOutput(`[claude-kanban] Command: ${commandDisplay}
934
1007
  `);
935
1008
  logOutput(`[claude-kanban] Process spawned (PID: ${childProcess.pid})
@@ -974,9 +1047,6 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
974
1047
  const text = data.toString();
975
1048
  logOutput(`[stderr] ${text}`);
976
1049
  });
977
- childProcess.on("spawn", () => {
978
- console.log("[executor] Process spawned successfully");
979
- });
980
1050
  childProcess.on("error", (error) => {
981
1051
  console.log("[executor] Spawn error:", error.message);
982
1052
  this.emit("task:output", { taskId, line: `[claude-kanban] Error: ${error.message}
@@ -985,9 +1055,6 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
985
1055
  unlinkSync(promptFile);
986
1056
  } catch {
987
1057
  }
988
- if (worktreeInfo) {
989
- this.removeWorktree(taskId);
990
- }
991
1058
  updateTask(this.projectPath, taskId, { status: "failed", passes: false });
992
1059
  const endedAt = /* @__PURE__ */ new Date();
993
1060
  addExecutionEntry(this.projectPath, taskId, {
@@ -998,7 +1065,7 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
998
1065
  error: error.message
999
1066
  });
1000
1067
  this.emit("task:failed", { taskId, error: error.message });
1001
- this.runningTasks.delete(taskId);
1068
+ this.runningTask = null;
1002
1069
  });
1003
1070
  childProcess.on("close", (code, signal) => {
1004
1071
  console.log("[executor] Process closed with code:", code, "signal:", signal);
@@ -1012,48 +1079,41 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
1012
1079
  });
1013
1080
  const timeoutMs = (config.execution.timeout || 30) * 60 * 1e3;
1014
1081
  setTimeout(() => {
1015
- if (this.isTaskRunning(taskId)) {
1016
- this.cancelTask(taskId, "Timeout exceeded");
1082
+ if (this.runningTask?.taskId === taskId) {
1083
+ this.cancelTask("Timeout exceeded");
1017
1084
  }
1018
1085
  }, timeoutMs);
1019
1086
  }
1020
1087
  /**
1021
- * Handle task completion - moves to in_review, keeps worktree alive
1088
+ * Handle task completion
1022
1089
  */
1023
1090
  handleTaskComplete(taskId, exitCode, startedAt) {
1024
- const runningTask = this.runningTasks.get(taskId);
1025
- if (!runningTask) return;
1091
+ if (!this.runningTask || this.runningTask.taskId !== taskId) return;
1026
1092
  const endedAt = /* @__PURE__ */ new Date();
1027
1093
  const duration = endedAt.getTime() - startedAt.getTime();
1028
- const output = runningTask.output.join("");
1094
+ const output = this.runningTask.output.join("");
1029
1095
  const isComplete = output.includes("<promise>COMPLETE</promise>");
1030
1096
  const task = getTaskById(this.projectPath, taskId);
1031
1097
  if (isComplete || exitCode === 0) {
1032
- if (runningTask.worktreePath && runningTask.branchName) {
1033
- this.reviewingTasks.set(taskId, {
1034
- worktreePath: runningTask.worktreePath,
1035
- branchName: runningTask.branchName,
1036
- startedAt,
1037
- endedAt
1038
- });
1039
- console.log(`[executor] Task ${taskId} ready for review at ${runningTask.worktreePath}`);
1040
- }
1041
1098
  updateTask(this.projectPath, taskId, {
1042
- status: "in_review",
1099
+ status: "completed",
1043
1100
  passes: true
1044
1101
  });
1102
+ addExecutionEntry(this.projectPath, taskId, {
1103
+ startedAt: startedAt.toISOString(),
1104
+ endedAt: endedAt.toISOString(),
1105
+ status: "completed",
1106
+ duration
1107
+ });
1045
1108
  logTaskExecution(this.projectPath, {
1046
1109
  taskId,
1047
1110
  taskTitle: task?.title || "Unknown",
1048
- status: "in_review",
1111
+ status: "completed",
1049
1112
  duration
1050
1113
  });
1051
- this.emit("task:completed", { taskId, duration, status: "in_review" });
1114
+ this.emit("task:completed", { taskId, duration });
1052
1115
  this.afkTasksCompleted++;
1053
1116
  } else {
1054
- if (runningTask.worktreePath) {
1055
- this.removeWorktree(taskId);
1056
- }
1057
1117
  updateTask(this.projectPath, taskId, {
1058
1118
  status: "failed",
1059
1119
  passes: false
@@ -1075,184 +1135,33 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
1075
1135
  });
1076
1136
  this.emit("task:failed", { taskId, error });
1077
1137
  }
1078
- this.runningTasks.delete(taskId);
1138
+ this.runningTask = null;
1079
1139
  if (this.afkMode) {
1080
1140
  this.continueAFKMode();
1081
1141
  }
1082
1142
  }
1083
1143
  /**
1084
- * Get info about a task in review
1085
- */
1086
- getReviewingTask(taskId) {
1087
- return this.reviewingTasks.get(taskId);
1088
- }
1089
- /**
1090
- * Merge a reviewed task into the base branch
1091
- */
1092
- mergeTask(taskId) {
1093
- const reviewInfo = this.reviewingTasks.get(taskId);
1094
- if (!reviewInfo) {
1095
- return { success: false, error: "Task not in review or no worktree found" };
1096
- }
1097
- const merged = this.mergeWorktreeBranch(taskId);
1098
- if (!merged) {
1099
- return { success: false, error: "Merge failed - may have conflicts" };
1100
- }
1101
- this.removeWorktree(taskId);
1102
- this.reviewingTasks.delete(taskId);
1103
- updateTask(this.projectPath, taskId, {
1104
- status: "completed"
1105
- });
1106
- addExecutionEntry(this.projectPath, taskId, {
1107
- startedAt: reviewInfo.startedAt.toISOString(),
1108
- endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1109
- status: "completed",
1110
- duration: (/* @__PURE__ */ new Date()).getTime() - reviewInfo.startedAt.getTime()
1111
- });
1112
- console.log(`[executor] Task ${taskId} merged successfully`);
1113
- return { success: true };
1114
- }
1115
- /**
1116
- * Create a PR for a reviewed task
1117
- */
1118
- createPR(taskId) {
1119
- const reviewInfo = this.reviewingTasks.get(taskId);
1120
- const task = getTaskById(this.projectPath, taskId);
1121
- if (!reviewInfo) {
1122
- return { success: false, error: "Task not in review or no worktree found" };
1123
- }
1124
- try {
1125
- execSync(`git push -u origin ${reviewInfo.branchName}`, {
1126
- cwd: this.projectPath,
1127
- stdio: "pipe"
1128
- });
1129
- const prTitle = task?.title || `Task ${taskId}`;
1130
- const prBody = task?.description || "";
1131
- const result = execSync(
1132
- `gh pr create --title "${prTitle.replace(/"/g, '\\"')}" --body "${prBody.replace(/"/g, '\\"')}" --head ${reviewInfo.branchName}`,
1133
- {
1134
- cwd: this.projectPath,
1135
- encoding: "utf-8"
1136
- }
1137
- );
1138
- const prUrl = result.trim();
1139
- try {
1140
- execSync(`git worktree remove "${reviewInfo.worktreePath}" --force`, {
1141
- cwd: this.projectPath,
1142
- stdio: "pipe"
1143
- });
1144
- } catch {
1145
- }
1146
- this.reviewingTasks.delete(taskId);
1147
- updateTask(this.projectPath, taskId, {
1148
- status: "completed"
1149
- });
1150
- addExecutionEntry(this.projectPath, taskId, {
1151
- startedAt: reviewInfo.startedAt.toISOString(),
1152
- endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1153
- status: "completed",
1154
- duration: (/* @__PURE__ */ new Date()).getTime() - reviewInfo.startedAt.getTime()
1155
- });
1156
- console.log(`[executor] PR created for task ${taskId}: ${prUrl}`);
1157
- return { success: true, prUrl };
1158
- } catch (error) {
1159
- const errorMsg = error instanceof Error ? error.message : String(error);
1160
- console.error(`[executor] Failed to create PR:`, errorMsg);
1161
- return { success: false, error: errorMsg };
1162
- }
1163
- }
1164
- /**
1165
- * Discard a reviewed task - delete worktree and return to ready
1144
+ * Cancel the running task
1166
1145
  */
1167
- discardTask(taskId) {
1168
- const reviewInfo = this.reviewingTasks.get(taskId);
1169
- if (!reviewInfo) {
1170
- return { success: false, error: "Task not in review or no worktree found" };
1171
- }
1172
- this.removeWorktree(taskId);
1173
- this.reviewingTasks.delete(taskId);
1174
- updateTask(this.projectPath, taskId, {
1175
- status: "ready",
1176
- passes: false
1177
- });
1178
- addExecutionEntry(this.projectPath, taskId, {
1179
- startedAt: reviewInfo.startedAt.toISOString(),
1180
- endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1181
- status: "discarded",
1182
- duration: (/* @__PURE__ */ new Date()).getTime() - reviewInfo.startedAt.getTime()
1183
- });
1184
- console.log(`[executor] Task ${taskId} discarded, returned to ready`);
1185
- return { success: true };
1186
- }
1187
- /**
1188
- * Get diff for a task in review
1189
- */
1190
- getTaskDiff(taskId) {
1191
- const reviewInfo = this.reviewingTasks.get(taskId);
1192
- if (!reviewInfo) {
1193
- return { success: false, error: "Task not in review or no worktree found" };
1194
- }
1195
- try {
1196
- const diff = execSync(`git diff main...${reviewInfo.branchName}`, {
1197
- cwd: this.projectPath,
1198
- encoding: "utf-8",
1199
- maxBuffer: 10 * 1024 * 1024
1200
- // 10MB buffer for large diffs
1201
- });
1202
- return { success: true, diff };
1203
- } catch (error) {
1204
- const errorMsg = error instanceof Error ? error.message : String(error);
1205
- return { success: false, error: errorMsg };
1206
- }
1207
- }
1208
- /**
1209
- * Get list of changed files for a task in review
1210
- */
1211
- getTaskChangedFiles(taskId) {
1212
- const reviewInfo = this.reviewingTasks.get(taskId);
1213
- if (!reviewInfo) {
1214
- return { success: false, error: "Task not in review or no worktree found" };
1215
- }
1216
- try {
1217
- const result = execSync(`git diff --name-only main...${reviewInfo.branchName}`, {
1218
- cwd: this.projectPath,
1219
- encoding: "utf-8"
1220
- });
1221
- const files = result.trim().split("\n").filter((f) => f);
1222
- return { success: true, files };
1223
- } catch (error) {
1224
- const errorMsg = error instanceof Error ? error.message : String(error);
1225
- return { success: false, error: errorMsg };
1226
- }
1227
- }
1228
- /**
1229
- * Cancel a running task
1230
- */
1231
- cancelTask(taskId, reason = "Cancelled by user") {
1232
- const runningTask = this.runningTasks.get(taskId);
1233
- if (!runningTask) return false;
1234
- const startedAt = runningTask.startedAt;
1146
+ cancelTask(reason = "Cancelled by user") {
1147
+ if (!this.runningTask) return false;
1148
+ const { taskId, process: childProcess, startedAt } = this.runningTask;
1235
1149
  const endedAt = /* @__PURE__ */ new Date();
1236
1150
  const duration = endedAt.getTime() - startedAt.getTime();
1237
1151
  const task = getTaskById(this.projectPath, taskId);
1238
1152
  try {
1239
- runningTask.process.kill("SIGTERM");
1153
+ childProcess.kill("SIGTERM");
1240
1154
  setTimeout(() => {
1241
1155
  try {
1242
- if (!runningTask.process.killed) {
1243
- runningTask.process.kill("SIGKILL");
1156
+ if (!childProcess.killed) {
1157
+ childProcess.kill("SIGKILL");
1244
1158
  }
1245
1159
  } catch {
1246
1160
  }
1247
1161
  }, 2e3);
1248
1162
  } catch {
1249
1163
  }
1250
- if (runningTask.worktreePath) {
1251
- this.removeWorktree(taskId);
1252
- }
1253
- updateTask(this.projectPath, taskId, {
1254
- status: "ready"
1255
- });
1164
+ updateTask(this.projectPath, taskId, { status: "ready" });
1256
1165
  addExecutionEntry(this.projectPath, taskId, {
1257
1166
  startedAt: startedAt.toISOString(),
1258
1167
  endedAt: endedAt.toISOString(),
@@ -1268,48 +1177,46 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
1268
1177
  error: reason
1269
1178
  });
1270
1179
  this.emit("task:cancelled", { taskId });
1271
- this.runningTasks.delete(taskId);
1180
+ this.runningTask = null;
1272
1181
  return true;
1273
1182
  }
1274
1183
  /**
1275
- * Start AFK mode
1184
+ * Start AFK mode - run tasks sequentially until done
1276
1185
  */
1277
- startAFKMode(maxIterations, concurrent) {
1186
+ startAFKMode(maxIterations) {
1278
1187
  if (this.afkMode) {
1279
1188
  throw new Error("AFK mode already running");
1280
1189
  }
1190
+ if (this.isBusy()) {
1191
+ throw new Error("Cannot start AFK mode while another operation is in progress");
1192
+ }
1281
1193
  this.afkMode = true;
1282
1194
  this.afkIteration = 0;
1283
1195
  this.afkMaxIterations = maxIterations;
1284
1196
  this.afkTasksCompleted = 0;
1285
1197
  this.emitAFKStatus();
1286
- this.continueAFKMode(concurrent);
1198
+ this.continueAFKMode();
1287
1199
  }
1288
1200
  /**
1289
- * Continue AFK mode - pick up next tasks
1201
+ * Continue AFK mode - pick next task
1290
1202
  */
1291
- continueAFKMode(concurrent = 1) {
1203
+ continueAFKMode() {
1292
1204
  if (!this.afkMode) return;
1293
1205
  if (this.afkIteration >= this.afkMaxIterations) {
1294
1206
  this.stopAFKMode();
1295
1207
  return;
1296
1208
  }
1297
- const config = getConfig(this.projectPath);
1298
- const maxConcurrent = Math.min(concurrent, config.execution.maxConcurrent || 3);
1299
- while (this.getRunningCount() < maxConcurrent) {
1300
- const nextTask = getNextReadyTask(this.projectPath);
1301
- if (!nextTask) {
1302
- if (this.getRunningCount() === 0) {
1303
- this.stopAFKMode();
1304
- }
1305
- break;
1306
- }
1307
- this.afkIteration++;
1308
- this.runTask(nextTask.id).catch((error) => {
1309
- console.error("AFK task error:", error);
1310
- });
1311
- this.emitAFKStatus();
1209
+ if (this.isBusy()) return;
1210
+ const nextTask = getNextReadyTask(this.projectPath);
1211
+ if (!nextTask) {
1212
+ this.stopAFKMode();
1213
+ return;
1312
1214
  }
1215
+ this.afkIteration++;
1216
+ this.runTask(nextTask.id).catch((error) => {
1217
+ console.error("AFK task error:", error);
1218
+ });
1219
+ this.emitAFKStatus();
1313
1220
  }
1314
1221
  /**
1315
1222
  * Stop AFK mode
@@ -1341,18 +1248,22 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
1341
1248
  };
1342
1249
  }
1343
1250
  /**
1344
- * Cancel all running tasks
1251
+ * Cancel running task/planning and stop AFK mode
1345
1252
  */
1346
1253
  cancelAll() {
1347
- for (const [taskId, runningTask] of this.runningTasks.entries()) {
1254
+ if (this.runningTask) {
1348
1255
  try {
1349
- runningTask.process.kill("SIGKILL");
1256
+ this.runningTask.process.kill("SIGKILL");
1350
1257
  } catch {
1351
1258
  }
1352
- if (runningTask.worktreePath) {
1353
- this.removeWorktree(taskId);
1259
+ this.runningTask = null;
1260
+ }
1261
+ if (this.planningSession) {
1262
+ try {
1263
+ this.planningSession.process.kill("SIGKILL");
1264
+ } catch {
1354
1265
  }
1355
- this.runningTasks.delete(taskId);
1266
+ this.planningSession = null;
1356
1267
  }
1357
1268
  this.stopAFKMode();
1358
1269
  }
@@ -1778,6 +1689,11 @@ async function createServer(projectPath, port) {
1778
1689
  executor.on("task:failed", (data) => io.emit("task:failed", data));
1779
1690
  executor.on("task:cancelled", (data) => io.emit("task:cancelled", data));
1780
1691
  executor.on("afk:status", (data) => io.emit("afk:status", data));
1692
+ executor.on("planning:started", (data) => io.emit("planning:started", data));
1693
+ executor.on("planning:output", (data) => io.emit("planning:output", data));
1694
+ executor.on("planning:completed", (data) => io.emit("planning:completed", data));
1695
+ executor.on("planning:failed", (data) => io.emit("planning:failed", data));
1696
+ executor.on("planning:cancelled", (data) => io.emit("planning:cancelled", data));
1781
1697
  app.get("/api/tasks", (_req, res) => {
1782
1698
  try {
1783
1699
  const tasks = getAllTasks(projectPath);
@@ -1862,7 +1778,12 @@ async function createServer(projectPath, port) {
1862
1778
  });
1863
1779
  app.post("/api/tasks/:id/cancel", (req, res) => {
1864
1780
  try {
1865
- const cancelled = executor.cancelTask(req.params.id);
1781
+ const runningId = executor.getRunningTaskId();
1782
+ if (runningId !== req.params.id) {
1783
+ res.status(404).json({ error: "Task not running" });
1784
+ return;
1785
+ }
1786
+ const cancelled = executor.cancelTask();
1866
1787
  if (!cancelled) {
1867
1788
  res.status(404).json({ error: "Task not running" });
1868
1789
  return;
@@ -1891,90 +1812,6 @@ async function createServer(projectPath, port) {
1891
1812
  res.status(500).json({ error: String(error) });
1892
1813
  }
1893
1814
  });
1894
- app.post("/api/tasks/:id/merge", (req, res) => {
1895
- try {
1896
- const result = executor.mergeTask(req.params.id);
1897
- if (!result.success) {
1898
- res.status(400).json({ error: result.error });
1899
- return;
1900
- }
1901
- const task = getTaskById(projectPath, req.params.id);
1902
- if (task) {
1903
- io.emit("task:updated", task);
1904
- }
1905
- res.json({ success: true });
1906
- } catch (error) {
1907
- res.status(500).json({ error: String(error) });
1908
- }
1909
- });
1910
- app.post("/api/tasks/:id/create-pr", (req, res) => {
1911
- try {
1912
- const result = executor.createPR(req.params.id);
1913
- if (!result.success) {
1914
- res.status(400).json({ error: result.error });
1915
- return;
1916
- }
1917
- const task = getTaskById(projectPath, req.params.id);
1918
- if (task) {
1919
- io.emit("task:updated", task);
1920
- }
1921
- res.json({ success: true, prUrl: result.prUrl });
1922
- } catch (error) {
1923
- res.status(500).json({ error: String(error) });
1924
- }
1925
- });
1926
- app.post("/api/tasks/:id/discard", (req, res) => {
1927
- try {
1928
- const result = executor.discardTask(req.params.id);
1929
- if (!result.success) {
1930
- res.status(400).json({ error: result.error });
1931
- return;
1932
- }
1933
- const task = getTaskById(projectPath, req.params.id);
1934
- if (task) {
1935
- io.emit("task:updated", task);
1936
- }
1937
- res.json({ success: true });
1938
- } catch (error) {
1939
- res.status(500).json({ error: String(error) });
1940
- }
1941
- });
1942
- app.get("/api/tasks/:id/diff", (req, res) => {
1943
- try {
1944
- const result = executor.getTaskDiff(req.params.id);
1945
- if (!result.success) {
1946
- res.status(400).json({ error: result.error });
1947
- return;
1948
- }
1949
- res.json({ diff: result.diff });
1950
- } catch (error) {
1951
- res.status(500).json({ error: String(error) });
1952
- }
1953
- });
1954
- app.get("/api/tasks/:id/changed-files", (req, res) => {
1955
- try {
1956
- const result = executor.getTaskChangedFiles(req.params.id);
1957
- if (!result.success) {
1958
- res.status(400).json({ error: result.error });
1959
- return;
1960
- }
1961
- res.json({ files: result.files });
1962
- } catch (error) {
1963
- res.status(500).json({ error: String(error) });
1964
- }
1965
- });
1966
- app.get("/api/tasks/:id/review-info", (req, res) => {
1967
- try {
1968
- const info = executor.getReviewingTask(req.params.id);
1969
- if (!info) {
1970
- res.status(404).json({ error: "Task not in review" });
1971
- return;
1972
- }
1973
- res.json(info);
1974
- } catch (error) {
1975
- res.status(500).json({ error: String(error) });
1976
- }
1977
- });
1978
1815
  app.get("/api/tasks/:id/logs", (req, res) => {
1979
1816
  try {
1980
1817
  const logs = executor.getTaskLog(req.params.id);
@@ -2048,8 +1885,8 @@ async function createServer(projectPath, port) {
2048
1885
  });
2049
1886
  app.post("/api/afk/start", (req, res) => {
2050
1887
  try {
2051
- const { maxIterations, concurrent } = req.body;
2052
- executor.startAFKMode(maxIterations || 10, concurrent || 1);
1888
+ const { maxIterations } = req.body;
1889
+ executor.startAFKMode(maxIterations || 10);
2053
1890
  res.json({ success: true, status: executor.getAFKStatus() });
2054
1891
  } catch (error) {
2055
1892
  res.status(400).json({ error: String(error) });
@@ -2071,10 +1908,44 @@ async function createServer(projectPath, port) {
2071
1908
  res.status(500).json({ error: String(error) });
2072
1909
  }
2073
1910
  });
1911
+ app.post("/api/plan", async (req, res) => {
1912
+ try {
1913
+ const { goal } = req.body;
1914
+ if (!goal) {
1915
+ res.status(400).json({ error: "Goal is required" });
1916
+ return;
1917
+ }
1918
+ await executor.runPlanningSession(goal);
1919
+ res.json({ success: true });
1920
+ } catch (error) {
1921
+ res.status(400).json({ error: String(error) });
1922
+ }
1923
+ });
1924
+ app.post("/api/plan/cancel", (_req, res) => {
1925
+ try {
1926
+ const cancelled = executor.cancelPlanning();
1927
+ if (!cancelled) {
1928
+ res.status(404).json({ error: "No planning session running" });
1929
+ return;
1930
+ }
1931
+ res.json({ success: true });
1932
+ } catch (error) {
1933
+ res.status(500).json({ error: String(error) });
1934
+ }
1935
+ });
1936
+ app.get("/api/plan/status", (_req, res) => {
1937
+ try {
1938
+ const planning = executor.isPlanning();
1939
+ const goal = executor.getPlanningGoal();
1940
+ res.json({ planning, goal: goal || null });
1941
+ } catch (error) {
1942
+ res.status(500).json({ error: String(error) });
1943
+ }
1944
+ });
2074
1945
  app.get("/api/running", (_req, res) => {
2075
1946
  try {
2076
- const taskIds = executor.getRunningTaskIds();
2077
- res.json({ running: taskIds, count: taskIds.length });
1947
+ const taskId = executor.getRunningTaskId();
1948
+ res.json({ running: taskId ? [taskId] : [], count: taskId ? 1 : 0 });
2078
1949
  } catch (error) {
2079
1950
  res.status(500).json({ error: String(error) });
2080
1951
  }
@@ -2082,9 +1953,10 @@ async function createServer(projectPath, port) {
2082
1953
  app.get("/api/stats", (_req, res) => {
2083
1954
  try {
2084
1955
  const counts = getTaskCounts(projectPath);
2085
- const running = executor.getRunningCount();
1956
+ const running = executor.isRunning() ? 1 : 0;
2086
1957
  const afk = executor.getAFKStatus();
2087
- res.json({ counts, running, afk });
1958
+ const planning = executor.isPlanning();
1959
+ res.json({ counts, running, afk, planning });
2088
1960
  } catch (error) {
2089
1961
  res.status(500).json({ error: String(error) });
2090
1962
  }
@@ -2098,20 +1970,24 @@ async function createServer(projectPath, port) {
2098
1970
  });
2099
1971
  io.on("connection", (socket) => {
2100
1972
  console.log("Client connected");
2101
- const runningIds = executor.getRunningTaskIds();
1973
+ const runningId = executor.getRunningTaskId();
1974
+ const runningIds = runningId ? [runningId] : [];
2102
1975
  const taskLogs = {};
2103
- for (const taskId of runningIds) {
2104
- const logs = executor.getTaskLog(taskId);
1976
+ if (runningId) {
1977
+ const logs = executor.getTaskLog(runningId);
2105
1978
  if (logs) {
2106
- taskLogs[taskId] = logs;
1979
+ taskLogs[runningId] = logs;
2107
1980
  }
2108
1981
  }
2109
1982
  socket.emit("init", {
2110
1983
  tasks: getAllTasks(projectPath),
2111
1984
  running: runningIds,
2112
1985
  afk: executor.getAFKStatus(),
2113
- taskLogs
2114
- // Include logs for running tasks
1986
+ taskLogs,
1987
+ // Include logs for running task
1988
+ planning: executor.isPlanning(),
1989
+ planningGoal: executor.getPlanningGoal() || null,
1990
+ planningOutput: executor.getPlanningOutput() || []
2115
1991
  });
2116
1992
  socket.on("get-logs", (taskId) => {
2117
1993
  const logs = executor.getTaskLog(taskId);
@@ -2284,7 +2160,6 @@ function getClientHTML() {
2284
2160
  .status-dot-draft { background: #a3a3a3; }
2285
2161
  .status-dot-ready { background: #3b82f6; }
2286
2162
  .status-dot-in_progress { background: #f97316; }
2287
- .status-dot-in_review { background: #8b5cf6; }
2288
2163
  .status-dot-completed { background: #22c55e; }
2289
2164
  .status-dot-failed { background: #ef4444; }
2290
2165
 
@@ -2342,7 +2217,7 @@ function getClientHTML() {
2342
2217
  transition: all 0.2s var(--ease-out-expo);
2343
2218
  }
2344
2219
  .side-panel-header {
2345
- padding: 16px 20px;
2220
+ padding: 12px 16px;
2346
2221
  border-bottom: 1px solid #e5e5e5;
2347
2222
  flex-shrink: 0;
2348
2223
  }
@@ -2360,7 +2235,8 @@ function getClientHTML() {
2360
2235
  flex-shrink: 0;
2361
2236
  }
2362
2237
  .side-panel-tab {
2363
- padding: 12px 16px;
2238
+ padding: 10px 14px;
2239
+ font-size: 13px;
2364
2240
  color: #737373;
2365
2241
  cursor: pointer;
2366
2242
  border-bottom: 2px solid transparent;
@@ -2494,7 +2370,6 @@ function getClientHTML() {
2494
2370
  .status-badge-draft { background: #f5f5f5; color: #737373; }
2495
2371
  .status-badge-ready { background: #eff6ff; color: #3b82f6; }
2496
2372
  .status-badge-in_progress { background: #fff7ed; color: #f97316; }
2497
- .status-badge-in_review { background: #f5f3ff; color: #8b5cf6; }
2498
2373
  .status-badge-completed { background: #f0fdf4; color: #22c55e; }
2499
2374
  .status-badge-failed { background: #fef2f2; color: #ef4444; }
2500
2375
 
@@ -2654,6 +2529,10 @@ let state = {
2654
2529
  logFullscreen: false,
2655
2530
  closedTabs: new Set(),
2656
2531
  sidePanel: null, // task id for side panel
2532
+ // Planning state
2533
+ planning: false,
2534
+ planningGoal: '',
2535
+ planningOutput: [],
2657
2536
  sidePanelTab: 'logs', // 'logs' or 'details'
2658
2537
  darkMode: localStorage.getItem('darkMode') === 'true', // Add dark mode state
2659
2538
  };
@@ -2748,6 +2627,14 @@ socket.on('init', (data) => {
2748
2627
  state.running = data.running;
2749
2628
  state.afk = data.afk;
2750
2629
 
2630
+ // Planning state
2631
+ state.planning = data.planning || false;
2632
+ state.planningGoal = data.planningGoal || '';
2633
+ state.planningOutput = (data.planningOutput || []).map(text => ({
2634
+ text: text,
2635
+ timestamp: new Date().toISOString()
2636
+ }));
2637
+
2751
2638
  // Load persisted logs for running tasks
2752
2639
  if (data.taskLogs) {
2753
2640
  for (const [taskId, logs] of Object.entries(data.taskLogs)) {
@@ -2820,16 +2707,14 @@ socket.on('task:output', ({ taskId, line }) => {
2820
2707
  }
2821
2708
  });
2822
2709
 
2823
- socket.on('task:completed', ({ taskId, status }) => {
2710
+ socket.on('task:completed', ({ taskId }) => {
2824
2711
  state.running = state.running.filter(id => id !== taskId);
2825
2712
  const task = state.tasks.find(t => t.id === taskId);
2826
2713
  if (task) {
2827
- // Use status from server (could be 'in_review' or 'completed')
2828
- task.status = status || 'in_review';
2714
+ task.status = 'completed';
2829
2715
  task.passes = true;
2830
2716
  }
2831
- const statusMsg = (status === 'in_review') ? 'ready for review' : 'completed';
2832
- showToast('Task ' + statusMsg + ': ' + (task?.title || taskId), 'success');
2717
+ showToast('Task completed: ' + (task?.title || taskId), 'success');
2833
2718
  render();
2834
2719
  });
2835
2720
 
@@ -2854,6 +2739,48 @@ socket.on('afk:status', (status) => {
2854
2739
  render();
2855
2740
  });
2856
2741
 
2742
+ // Planning socket handlers
2743
+ socket.on('planning:started', ({ goal, timestamp }) => {
2744
+ state.planning = true;
2745
+ state.planningGoal = goal;
2746
+ state.planningOutput = [];
2747
+ state.showModal = 'planning';
2748
+ showToast('Planning started: ' + goal.substring(0, 50) + (goal.length > 50 ? '...' : ''), 'info');
2749
+ render();
2750
+ });
2751
+
2752
+ socket.on('planning:output', ({ line }) => {
2753
+ state.planningOutput.push({
2754
+ text: line,
2755
+ timestamp: new Date().toISOString()
2756
+ });
2757
+ render();
2758
+ });
2759
+
2760
+ socket.on('planning:completed', () => {
2761
+ state.planning = false;
2762
+ showToast('Planning complete! New tasks added to board.', 'success');
2763
+ // Refresh tasks from server
2764
+ fetch('/api/tasks').then(r => r.json()).then(data => {
2765
+ state.tasks = data.tasks;
2766
+ state.showModal = null;
2767
+ render();
2768
+ });
2769
+ });
2770
+
2771
+ socket.on('planning:failed', ({ error }) => {
2772
+ state.planning = false;
2773
+ showToast('Planning failed: ' + error, 'error');
2774
+ render();
2775
+ });
2776
+
2777
+ socket.on('planning:cancelled', () => {
2778
+ state.planning = false;
2779
+ state.showModal = null;
2780
+ showToast('Planning cancelled', 'warning');
2781
+ render();
2782
+ });
2783
+
2857
2784
  // Load templates
2858
2785
  fetch('/api/templates').then(r => r.json()).then(data => {
2859
2786
  state.templates = data.templates;
@@ -2903,53 +2830,6 @@ async function retryTask(id) {
2903
2830
  });
2904
2831
  }
2905
2832
 
2906
- async function mergeTask(id) {
2907
- const res = await fetch('/api/tasks/' + id + '/merge', { method: 'POST' });
2908
- const data = await res.json();
2909
- if (!res.ok) {
2910
- throw new Error(data.error || 'Merge failed');
2911
- }
2912
- return data;
2913
- }
2914
-
2915
- async function createPR(id) {
2916
- const res = await fetch('/api/tasks/' + id + '/create-pr', { method: 'POST' });
2917
- const data = await res.json();
2918
- if (!res.ok) {
2919
- throw new Error(data.error || 'PR creation failed');
2920
- }
2921
- return data;
2922
- }
2923
-
2924
- async function discardTask(id) {
2925
- const res = await fetch('/api/tasks/' + id + '/discard', { method: 'POST' });
2926
- const data = await res.json();
2927
- if (!res.ok) {
2928
- throw new Error(data.error || 'Discard failed');
2929
- }
2930
- return data;
2931
- }
2932
-
2933
- async function getReviewInfo(id) {
2934
- const res = await fetch('/api/tasks/' + id + '/review-info');
2935
- const data = await res.json();
2936
- if (!res.ok) {
2937
- throw new Error(data.error || 'Failed to get review info');
2938
- }
2939
- return data;
2940
- }
2941
-
2942
- async function openInVSCode(id) {
2943
- try {
2944
- const info = await getReviewInfo(id);
2945
- // Open VS Code at the worktree path
2946
- window.open('vscode://file/' + encodeURIComponent(info.worktreePath), '_blank');
2947
- showToast('Opening in VS Code...', 'info');
2948
- } catch (e) {
2949
- showToast('Failed to open in VS Code: ' + e.message, 'error');
2950
- }
2951
- }
2952
-
2953
2833
  async function generateTask(prompt) {
2954
2834
  state.aiGenerating = true;
2955
2835
  render();
@@ -2968,11 +2848,11 @@ async function generateTask(prompt) {
2968
2848
  }
2969
2849
  }
2970
2850
 
2971
- async function startAFK(maxIterations, concurrent) {
2851
+ async function startAFK(maxIterations) {
2972
2852
  await fetch('/api/afk/start', {
2973
2853
  method: 'POST',
2974
2854
  headers: { 'Content-Type': 'application/json' },
2975
- body: JSON.stringify({ maxIterations, concurrent })
2855
+ body: JSON.stringify({ maxIterations })
2976
2856
  });
2977
2857
  }
2978
2858
 
@@ -2980,6 +2860,18 @@ async function stopAFK() {
2980
2860
  await fetch('/api/afk/stop', { method: 'POST' });
2981
2861
  }
2982
2862
 
2863
+ async function startPlanning(goal) {
2864
+ await fetch('/api/plan', {
2865
+ method: 'POST',
2866
+ headers: { 'Content-Type': 'application/json' },
2867
+ body: JSON.stringify({ goal })
2868
+ });
2869
+ }
2870
+
2871
+ async function cancelPlanning() {
2872
+ await fetch('/api/plan/cancel', { method: 'POST' });
2873
+ }
2874
+
2983
2875
  // Enhanced Drag and drop
2984
2876
  let draggedTask = null;
2985
2877
  let draggedElement = null;
@@ -3124,6 +3016,11 @@ function openSidePanel(taskId) {
3124
3016
  state.activeTab = taskId;
3125
3017
  state.closedTabs.delete(taskId);
3126
3018
 
3019
+ // Auto-switch to logs tab if task is running
3020
+ if (state.running.includes(taskId)) {
3021
+ state.sidePanelTab = 'logs';
3022
+ }
3023
+
3127
3024
  // Request logs from server if not already loaded
3128
3025
  if (!state.taskOutput[taskId] || state.taskOutput[taskId].length === 0) {
3129
3026
  socket.emit('get-logs', taskId);
@@ -3153,7 +3050,6 @@ function renderColumn(status, title, tasks) {
3153
3050
  draft: 'To Do',
3154
3051
  ready: 'Ready',
3155
3052
  in_progress: 'In Progress',
3156
- in_review: 'In Review',
3157
3053
  completed: 'Done',
3158
3054
  failed: 'Failed'
3159
3055
  };
@@ -3406,21 +3302,13 @@ function renderModal() {
3406
3302
  <button onclick="state.showModal = null; render();" class="btn btn-ghost p-1.5 text-canvas-500 hover:text-canvas-700">\u2715</button>
3407
3303
  </div>
3408
3304
  <div class="p-6">
3409
- <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>
3305
+ <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>
3410
3306
  <div class="space-y-4">
3411
3307
  <div>
3412
3308
  <label class="block text-sm font-medium text-canvas-700 mb-2">Maximum Iterations</label>
3413
3309
  <input type="number" id="afk-iterations" value="10" min="1" max="100"
3414
3310
  class="input w-full">
3415
3311
  </div>
3416
- <div>
3417
- <label class="block text-sm font-medium text-canvas-700 mb-2">Concurrent Tasks</label>
3418
- <select id="afk-concurrent" class="input w-full">
3419
- <option value="1">1 (Sequential)</option>
3420
- <option value="2">2</option>
3421
- <option value="3">3 (Max)</option>
3422
- </select>
3423
- </div>
3424
3312
  <div class="bg-status-running/10 border border-status-running/20 rounded-lg p-3">
3425
3313
  <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>
3426
3314
  </div>
@@ -3437,6 +3325,71 @@ function renderModal() {
3437
3325
  \`;
3438
3326
  }
3439
3327
 
3328
+ // Planning input modal
3329
+ if (state.showModal === 'plan-input') {
3330
+ return \`
3331
+ <div class="modal-backdrop fixed inset-0 flex items-center justify-center z-50" onclick="if(event.target === event.currentTarget) { state.showModal = null; render(); }">
3332
+ <div class="modal-content card rounded-xl w-full max-w-lg mx-4">
3333
+ <div class="px-6 py-4 border-b border-white/5 flex justify-between items-center">
3334
+ <h3 class="font-display font-semibold text-canvas-800 text-lg">\u{1F3AF} AI Task Planner</h3>
3335
+ <button onclick="state.showModal = null; render();" class="btn btn-ghost p-1.5 text-canvas-500 hover:text-canvas-700">\u2715</button>
3336
+ </div>
3337
+ <div class="p-6">
3338
+ <p class="text-sm text-canvas-600 mb-4">Describe your goal and the AI will analyze the codebase and break it down into concrete tasks.</p>
3339
+ <div class="space-y-4">
3340
+ <div>
3341
+ <label class="block text-sm font-medium text-canvas-700 mb-2">What do you want to build?</label>
3342
+ <textarea id="planning-goal" rows="4" placeholder="e.g., Add user authentication with JWT tokens, login/logout functionality, and protected routes..."
3343
+ class="input w-full resize-none"></textarea>
3344
+ </div>
3345
+ <div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
3346
+ <p class="text-xs text-blue-700">\u{1F4A1} Be specific about what you want. The AI will explore your codebase and create 3-8 tasks with implementation guidance.</p>
3347
+ </div>
3348
+ </div>
3349
+ <div class="flex justify-end gap-3 mt-6">
3350
+ <button onclick="state.showModal = null; render();"
3351
+ class="btn btn-ghost px-4 py-2.5">Cancel</button>
3352
+ <button onclick="handleStartPlanning()"
3353
+ class="btn px-5 py-2.5 bg-blue-500 hover:bg-blue-600 text-white font-medium">\u{1F680} Start Planning</button>
3354
+ </div>
3355
+ </div>
3356
+ </div>
3357
+ </div>
3358
+ \`;
3359
+ }
3360
+
3361
+ // Planning progress modal
3362
+ if (state.showModal === 'planning') {
3363
+ const outputHtml = state.planningOutput.length > 0
3364
+ ? state.planningOutput.map(l => \`<div class="log-line">\${highlightLog(l.text || l)}</div>\`).join('')
3365
+ : '<div class="text-canvas-400 text-sm">Analyzing codebase and generating tasks...</div>';
3366
+
3367
+ return \`
3368
+ <div class="modal-backdrop fixed inset-0 flex items-center justify-center z-50">
3369
+ <div class="modal-content card rounded-xl w-full max-w-3xl mx-4 max-h-[80vh] flex flex-col">
3370
+ <div class="px-6 py-4 border-b border-white/5 flex justify-between items-center flex-shrink-0">
3371
+ <div class="flex items-center gap-3">
3372
+ <span class="text-xl animate-pulse">\u{1F3AF}</span>
3373
+ <div>
3374
+ <h3 class="font-display font-semibold text-canvas-800 text-lg">Planning in Progress</h3>
3375
+ <p class="text-xs text-canvas-500 truncate max-w-[400px]">\${escapeHtml(state.planningGoal)}</p>
3376
+ </div>
3377
+ </div>
3378
+ <button onclick="if(confirm('Cancel planning?')) cancelPlanning();"
3379
+ class="btn btn-ghost px-3 py-1.5 text-sm text-status-failed hover:bg-status-failed/10">
3380
+ \u23F9 Cancel
3381
+ </button>
3382
+ </div>
3383
+ <div class="flex-1 overflow-hidden p-4">
3384
+ <div class="log-container h-full overflow-y-auto" id="planning-log">
3385
+ \${outputHtml}
3386
+ </div>
3387
+ </div>
3388
+ </div>
3389
+ </div>
3390
+ \`;
3391
+ }
3392
+
3440
3393
  return '';
3441
3394
  }
3442
3395
 
@@ -3450,114 +3403,55 @@ function renderSidePanel() {
3450
3403
  const output = state.taskOutput[task.id] || [];
3451
3404
  const startTime = state.taskStartTime[task.id];
3452
3405
  const lastExec = task.executionHistory?.[task.executionHistory.length - 1];
3406
+ const elapsed = isRunning && startTime ? formatElapsed(startTime) : null;
3453
3407
 
3454
3408
  return \`
3455
3409
  <div class="side-panel">
3456
- <!-- Header -->
3457
- <div class="side-panel-header">
3458
- <div class="flex justify-between items-start">
3459
- <div class="flex-1 pr-4">
3460
- <h2 class="font-semibold text-canvas-900 text-lg leading-tight">\${escapeHtml(task.title)}</h2>
3461
- <div class="flex items-center gap-2 mt-2">
3462
- <span class="status-badge status-badge-\${task.status}">
3410
+ <!-- Compact Header -->
3411
+ <div class="side-panel-header" style="padding: 12px 16px;">
3412
+ <div class="flex justify-between items-start gap-2">
3413
+ <div class="flex-1 min-w-0">
3414
+ <h2 class="font-semibold text-canvas-900 text-base leading-tight truncate" title="\${escapeHtml(task.title)}">\${escapeHtml(task.title)}</h2>
3415
+ <div class="flex items-center gap-2 mt-1.5 flex-wrap">
3416
+ <span class="status-badge status-badge-\${task.status}" style="font-size: 11px; padding: 2px 8px;">
3463
3417
  <span class="w-1.5 h-1.5 rounded-full bg-current \${isRunning ? 'animate-pulse' : ''}"></span>
3464
3418
  \${task.status.replace('_', ' ')}
3465
3419
  </span>
3420
+ \${elapsed ? \`<span class="text-xs text-canvas-500">\u23F1 \${elapsed}</span>\` : ''}
3421
+ <span class="text-xs text-canvas-400">\${task.priority} \xB7 \${task.category}</span>
3466
3422
  </div>
3467
3423
  </div>
3468
- <div class="flex items-center gap-1">
3424
+ <div class="flex items-center gap-0.5 flex-shrink-0">
3425
+ \${task.status === 'in_progress' ? \`
3426
+ <button onclick="cancelTask('\${task.id}')" class="btn btn-ghost p-1.5 text-status-failed hover:bg-status-failed/10" title="Stop">
3427
+ \u23F9
3428
+ </button>
3429
+ \` : task.status === 'ready' ? \`
3430
+ <button onclick="runTask('\${task.id}')" class="btn btn-ghost p-1.5 text-status-success hover:bg-status-success/10" title="Run">
3431
+ \u25B6
3432
+ </button>
3433
+ \` : task.status === 'draft' ? \`
3434
+ <button onclick="updateTask('\${task.id}', { status: 'ready' })" class="btn btn-ghost p-1.5 text-blue-500 hover:bg-blue-50" title="Move to Ready">
3435
+ \u2192
3436
+ </button>
3437
+ \` : task.status === 'failed' ? \`
3438
+ <button onclick="retryTask('\${task.id}')" class="btn btn-ghost p-1.5 text-canvas-500 hover:bg-canvas-100" title="Retry">
3439
+ \u21BB
3440
+ </button>
3441
+ \` : ''}
3469
3442
  <button onclick="state.editingTask = state.tasks.find(t => t.id === '\${task.id}'); state.showModal = 'edit'; render();"
3470
- class="btn btn-ghost p-2 text-canvas-500 hover:text-canvas-700" title="Edit">
3443
+ class="btn btn-ghost p-1.5 text-canvas-400 hover:text-canvas-600" title="Edit">
3471
3444
  \u270F\uFE0F
3472
3445
  </button>
3473
- <button onclick="if(confirm('Delete this task?')) { deleteTask('\${task.id}'); closeSidePanel(); }"
3474
- class="btn btn-ghost p-2 text-canvas-500 hover:text-status-failed" title="Delete">
3475
- \u{1F5D1}\uFE0F
3476
- </button>
3477
- <button onclick="closeSidePanel()" class="btn btn-ghost p-2 text-canvas-500 hover:text-canvas-700" title="Close">
3446
+ <button onclick="closeSidePanel()" class="btn btn-ghost p-1.5 text-canvas-400 hover:text-canvas-600" title="Close">
3478
3447
  \u2715
3479
3448
  </button>
3480
3449
  </div>
3481
3450
  </div>
3482
3451
  </div>
3483
3452
 
3484
- <!-- Description -->
3485
- <div class="px-5 py-4 border-b border-canvas-200 flex-shrink-0">
3486
- <p class="text-sm text-canvas-600 leading-relaxed">\${escapeHtml(task.description)}</p>
3487
- \${task.steps && task.steps.length > 0 ? \`
3488
- <div class="mt-3">
3489
- <div class="text-xs font-medium text-canvas-500 mb-2">Steps:</div>
3490
- <ul class="text-sm text-canvas-600 space-y-1">
3491
- \${task.steps.map(s => \`<li class="flex gap-2"><span class="text-canvas-400">\u2022</span>\${escapeHtml(s)}</li>\`).join('')}
3492
- </ul>
3493
- </div>
3494
- \` : ''}
3495
- </div>
3496
-
3497
- <!-- Task Details Grid -->
3498
- <div class="details-grid">
3499
- <div class="details-item">
3500
- <span class="details-label">Priority</span>
3501
- <span class="details-value capitalize">\${task.priority}</span>
3502
- </div>
3503
- <div class="details-item">
3504
- <span class="details-label">Category</span>
3505
- <span class="details-value">\${categoryIcons[task.category] || ''} \${task.category}</span>
3506
- </div>
3507
- \${startTime || lastExec ? \`
3508
- <div class="details-item">
3509
- <span class="details-label">Started</span>
3510
- <span class="details-value">\${new Date(startTime || lastExec?.startedAt).toLocaleString()}</span>
3511
- </div>
3512
- \` : ''}
3513
- \${lastExec?.duration ? \`
3514
- <div class="details-item">
3515
- <span class="details-label">Duration</span>
3516
- <span class="details-value">\${Math.round(lastExec.duration / 1000)}s</span>
3517
- </div>
3518
- \` : ''}
3519
- </div>
3520
-
3521
- <!-- Action Buttons -->
3522
- <div class="px-5 py-4 border-b border-canvas-200 flex flex-wrap gap-2 flex-shrink-0">
3523
- \${task.status === 'draft' ? \`
3524
- <button onclick="updateTask('\${task.id}', { status: 'ready' })" class="btn btn-primary px-4 py-2 text-sm">
3525
- \u2192 Move to Ready
3526
- </button>
3527
- \` : ''}
3528
- \${task.status === 'ready' ? \`
3529
- <button onclick="runTask('\${task.id}')" class="btn btn-primary px-4 py-2 text-sm">
3530
- \u25B6 Run Task
3531
- </button>
3532
- \` : ''}
3533
- \${task.status === 'in_progress' ? \`
3534
- <button onclick="cancelTask('\${task.id}')" class="btn btn-danger px-4 py-2 text-sm">
3535
- \u23F9 Stop Attempt
3536
- </button>
3537
- \` : ''}
3538
- \${task.status === 'in_review' ? \`
3539
- <button onclick="handleMerge('\${task.id}')" class="btn btn-primary px-4 py-2 text-sm">
3540
- \u2713 Merge
3541
- </button>
3542
- <button onclick="handleCreatePR('\${task.id}')" class="btn btn-ghost px-4 py-2 text-sm">
3543
- \u21E1 Create PR
3544
- </button>
3545
- <button onclick="openInVSCode('\${task.id}')" class="btn btn-ghost px-4 py-2 text-sm">
3546
- \u{1F4C2} Open in VS Code
3547
- </button>
3548
- <button onclick="handleDiscard('\${task.id}')" class="btn btn-danger px-4 py-2 text-sm">
3549
- \u2715 Discard
3550
- </button>
3551
- \` : ''}
3552
- \${task.status === 'failed' ? \`
3553
- <button onclick="retryTask('\${task.id}')" class="btn btn-primary px-4 py-2 text-sm">
3554
- \u21BB Retry
3555
- </button>
3556
- \` : ''}
3557
- </div>
3558
-
3559
- <!-- Tabs -->
3560
- <div class="side-panel-tabs">
3453
+ <!-- Tabs (moved up, right after header) -->
3454
+ <div class="side-panel-tabs" style="padding: 0 12px;">
3561
3455
  <div class="side-panel-tab \${state.sidePanelTab === 'logs' ? 'active' : ''}" onclick="state.sidePanelTab = 'logs'; render();">
3562
3456
  \u{1F4CB} Logs
3563
3457
  </div>
@@ -3567,9 +3461,9 @@ function renderSidePanel() {
3567
3461
  </div>
3568
3462
 
3569
3463
  <!-- Tab Content -->
3570
- <div class="side-panel-body">
3464
+ <div class="side-panel-body" style="flex: 1; overflow: hidden; display: flex; flex-direction: column;">
3571
3465
  \${state.sidePanelTab === 'logs' ? \`
3572
- <div class="log-container" id="side-panel-log">
3466
+ <div class="log-container" id="side-panel-log" style="flex: 1; overflow-y: auto;">
3573
3467
  \${output.length > 0
3574
3468
  ? output.map((l, i) => {
3575
3469
  const text = l.text || l;
@@ -3579,34 +3473,62 @@ function renderSidePanel() {
3579
3473
  }
3580
3474
  </div>
3581
3475
  \` : \`
3582
- <div class="p-5">
3583
- <div class="text-sm text-canvas-600">
3584
- <div class="mb-4">
3585
- <div class="text-xs font-medium text-canvas-500 mb-1">Created</div>
3586
- <div>\${new Date(task.createdAt).toLocaleString()}</div>
3476
+ <div style="flex: 1; overflow-y: auto; padding: 16px;">
3477
+ <!-- Description -->
3478
+ <div class="mb-5">
3479
+ <div class="text-xs font-semibold text-canvas-500 uppercase tracking-wide mb-2">Description</div>
3480
+ <div class="text-sm text-canvas-700 leading-relaxed bg-canvas-50 rounded-lg p-3 border border-canvas-200">
3481
+ \${escapeHtml(task.description || 'No description provided.')}
3587
3482
  </div>
3588
- <div class="mb-4">
3589
- <div class="text-xs font-medium text-canvas-500 mb-1">Last Updated</div>
3590
- <div>\${new Date(task.updatedAt).toLocaleString()}</div>
3483
+ </div>
3484
+
3485
+ <!-- Steps -->
3486
+ \${task.steps && task.steps.length > 0 ? \`
3487
+ <div class="mb-5">
3488
+ <div class="text-xs font-semibold text-canvas-500 uppercase tracking-wide mb-2">Steps (\${task.steps.length})</div>
3489
+ <div class="bg-canvas-50 rounded-lg border border-canvas-200 divide-y divide-canvas-200">
3490
+ \${task.steps.map((step, i) => \`
3491
+ <div class="flex gap-3 p-3 text-sm">
3492
+ <span class="flex-shrink-0 w-5 h-5 rounded-full bg-canvas-200 text-canvas-500 flex items-center justify-center text-xs font-medium">\${i + 1}</span>
3493
+ <span class="text-canvas-700">\${escapeHtml(step)}</span>
3494
+ </div>
3495
+ \`).join('')}
3496
+ </div>
3591
3497
  </div>
3592
- \${task.executionHistory && task.executionHistory.length > 0 ? \`
3593
- <div>
3594
- <div class="text-xs font-medium text-canvas-500 mb-2">Execution History</div>
3595
- <div class="space-y-2">
3596
- \${task.executionHistory.slice(-5).reverse().map(exec => \`
3597
- <div class="bg-canvas-100 rounded p-2 text-xs">
3598
- <div class="flex justify-between">
3599
- <span class="\${exec.status === 'completed' ? 'text-status-success' : 'text-status-failed'}">\${exec.status}</span>
3600
- <span class="text-canvas-500">\${Math.round(exec.duration / 1000)}s</span>
3601
- </div>
3602
- <div class="text-canvas-500 mt-1">\${new Date(exec.startedAt).toLocaleString()}</div>
3603
- \${exec.error ? \`<div class="text-status-failed mt-1">\${escapeHtml(exec.error)}</div>\` : ''}
3498
+ \` : ''}
3499
+
3500
+ <!-- Metadata -->
3501
+ <div class="mb-5 grid grid-cols-2 gap-3">
3502
+ <div class="bg-canvas-50 rounded-lg p-3 border border-canvas-200">
3503
+ <div class="text-xs text-canvas-500 mb-1">Created</div>
3504
+ <div class="text-sm text-canvas-700">\${new Date(task.createdAt).toLocaleDateString()}</div>
3505
+ </div>
3506
+ <div class="bg-canvas-50 rounded-lg p-3 border border-canvas-200">
3507
+ <div class="text-xs text-canvas-500 mb-1">Updated</div>
3508
+ <div class="text-sm text-canvas-700">\${new Date(task.updatedAt).toLocaleDateString()}</div>
3509
+ </div>
3510
+ </div>
3511
+
3512
+ <!-- Execution History -->
3513
+ \${task.executionHistory && task.executionHistory.length > 0 ? \`
3514
+ <div>
3515
+ <div class="text-xs font-semibold text-canvas-500 uppercase tracking-wide mb-2">Execution History</div>
3516
+ <div class="space-y-2">
3517
+ \${task.executionHistory.slice(-5).reverse().map(exec => \`
3518
+ <div class="bg-canvas-50 rounded-lg p-3 border border-canvas-200">
3519
+ <div class="flex justify-between items-center">
3520
+ <span class="text-sm font-medium \${exec.status === 'completed' ? 'text-status-success' : 'text-status-failed'}">
3521
+ \${exec.status === 'completed' ? '\u2713' : '\u2717'} \${exec.status}
3522
+ </span>
3523
+ <span class="text-xs text-canvas-500">\${Math.round(exec.duration / 1000)}s</span>
3604
3524
  </div>
3605
- \`).join('')}
3606
- </div>
3525
+ <div class="text-xs text-canvas-500 mt-1">\${new Date(exec.startedAt).toLocaleString()}</div>
3526
+ \${exec.error ? \`<div class="text-xs text-status-failed mt-2 bg-status-failed/5 rounded p-2">\${escapeHtml(exec.error)}</div>\` : ''}
3527
+ </div>
3528
+ \`).join('')}
3607
3529
  </div>
3608
- \` : ''}
3609
- </div>
3530
+ </div>
3531
+ \` : ''}
3610
3532
  </div>
3611
3533
  \`}
3612
3534
  </div>
@@ -3743,45 +3665,28 @@ function applyTemplate(templateId) {
3743
3665
 
3744
3666
  function handleStartAFK() {
3745
3667
  const iterations = parseInt(document.getElementById('afk-iterations').value) || 10;
3746
- const concurrent = parseInt(document.getElementById('afk-concurrent').value) || 1;
3747
- startAFK(iterations, concurrent);
3668
+ startAFK(iterations);
3748
3669
  state.showModal = null;
3749
3670
  render();
3750
3671
  }
3751
3672
 
3752
- async function handleMerge(taskId) {
3753
- if (!confirm('Merge this task into the main branch?')) return;
3754
- try {
3755
- await mergeTask(taskId);
3756
- showToast('Task merged successfully!', 'success');
3757
- closeSidePanel();
3758
- } catch (e) {
3759
- showToast('Merge failed: ' + e.message, 'error');
3673
+ async function handleStartPlanning() {
3674
+ const goal = document.getElementById('planning-goal').value;
3675
+ if (!goal.trim()) {
3676
+ showToast('Please enter a goal', 'warning');
3677
+ return;
3760
3678
  }
3761
- }
3762
-
3763
- async function handleCreatePR(taskId) {
3764
3679
  try {
3765
- const result = await createPR(taskId);
3766
- showToast('PR created successfully!', 'success');
3767
- if (result.prUrl) {
3768
- window.open(result.prUrl, '_blank');
3769
- }
3770
- closeSidePanel();
3680
+ await startPlanning(goal);
3681
+ // Modal will be shown when planning:started event is received
3771
3682
  } catch (e) {
3772
- showToast('PR creation failed: ' + e.message, 'error');
3683
+ showToast('Failed to start planning: ' + e.message, 'error');
3773
3684
  }
3774
3685
  }
3775
3686
 
3776
- async function handleDiscard(taskId) {
3777
- if (!confirm('Discard this work? The task will return to Ready status.')) return;
3778
- try {
3779
- await discardTask(taskId);
3780
- showToast('Task discarded, returned to Ready', 'warning');
3781
- closeSidePanel();
3782
- } catch (e) {
3783
- showToast('Discard failed: ' + e.message, 'error');
3784
- }
3687
+ function openPlanningModal() {
3688
+ state.showModal = 'plan-input';
3689
+ render();
3785
3690
  }
3786
3691
 
3787
3692
  // Filter tasks based on search query
@@ -3822,6 +3727,12 @@ function render() {
3822
3727
  </div>
3823
3728
  </div>
3824
3729
  <div class="flex items-center gap-2">
3730
+ <button onclick="openPlanningModal();"
3731
+ class="btn px-4 py-2 text-sm bg-blue-500 hover:bg-blue-600 text-white \${state.planning ? 'opacity-50 cursor-not-allowed' : ''}"
3732
+ \${state.planning ? 'disabled' : ''}
3733
+ title="AI Task Planner">
3734
+ \u{1F3AF} \${state.planning ? 'Planning...' : 'Plan'}
3735
+ </button>
3825
3736
  <button onclick="state.showModal = 'new'; render();"
3826
3737
  class="btn btn-primary px-4 py-2 text-sm">
3827
3738
  + Add Task
@@ -3845,7 +3756,6 @@ function render() {
3845
3756
  \${renderColumn('draft', 'To Do', filterTasks(state.tasks))}
3846
3757
  \${renderColumn('ready', 'Ready', filterTasks(state.tasks))}
3847
3758
  \${renderColumn('in_progress', 'In Progress', filterTasks(state.tasks))}
3848
- \${renderColumn('in_review', 'In Review', filterTasks(state.tasks))}
3849
3759
  \${renderColumn('completed', 'Done', filterTasks(state.tasks))}
3850
3760
  \${renderColumn('failed', 'Failed', filterTasks(state.tasks))}
3851
3761
  </div>
@@ -3889,6 +3799,10 @@ window.retryTask = retryTask;
3889
3799
  window.generateTask = generateTask;
3890
3800
  window.startAFK = startAFK;
3891
3801
  window.stopAFK = stopAFK;
3802
+ window.startPlanning = startPlanning;
3803
+ window.cancelPlanning = cancelPlanning;
3804
+ window.handleStartPlanning = handleStartPlanning;
3805
+ window.openPlanningModal = openPlanningModal;
3892
3806
  window.handleDragStart = handleDragStart;
3893
3807
  window.handleDragEnd = handleDragEnd;
3894
3808
  window.handleDragOver = handleDragOver;
@@ -3910,14 +3824,6 @@ window.closeSidePanel = closeSidePanel;
3910
3824
  window.showTaskMenu = showTaskMenu;
3911
3825
  window.filterTasks = filterTasks;
3912
3826
  window.scrollSidePanelLog = scrollSidePanelLog;
3913
- window.mergeTask = mergeTask;
3914
- window.createPR = createPR;
3915
- window.discardTask = discardTask;
3916
- window.handleMerge = handleMerge;
3917
- window.handleCreatePR = handleCreatePR;
3918
- window.handleDiscard = handleDiscard;
3919
- window.openInVSCode = openInVSCode;
3920
- window.getReviewInfo = getReviewInfo;
3921
3827
 
3922
3828
  // Keyboard shortcuts
3923
3829
  document.addEventListener('keydown', (e) => {
@@ -3997,7 +3903,7 @@ function isGitRepo(dir) {
3997
3903
  }
3998
3904
  function hasGitRemote(dir) {
3999
3905
  try {
4000
- const result = execSync2("git remote -v", { cwd: dir, stdio: "pipe" }).toString();
3906
+ const result = execSync("git remote -v", { cwd: dir, stdio: "pipe" }).toString();
4001
3907
  return result.trim().length > 0;
4002
3908
  } catch {
4003
3909
  return false;
@@ -4005,7 +3911,7 @@ function hasGitRemote(dir) {
4005
3911
  }
4006
3912
  function detectRemoteType(dir) {
4007
3913
  try {
4008
- const result = execSync2("git remote get-url origin", { cwd: dir, stdio: "pipe" }).toString().toLowerCase();
3914
+ const result = execSync("git remote get-url origin", { cwd: dir, stdio: "pipe" }).toString().toLowerCase();
4009
3915
  if (result.includes("github.com")) return "github";
4010
3916
  if (result.includes("gitlab.com") || result.includes("gitlab")) return "gitlab";
4011
3917
  if (result.includes("bitbucket.org") || result.includes("bitbucket")) return "bitbucket";
@@ -4016,10 +3922,10 @@ function detectRemoteType(dir) {
4016
3922
  }
4017
3923
  }
4018
3924
  function initializeGit(dir) {
4019
- execSync2("git init", { cwd: dir, stdio: "pipe" });
3925
+ execSync("git init", { cwd: dir, stdio: "pipe" });
4020
3926
  try {
4021
- execSync2("git add -A", { cwd: dir, stdio: "pipe" });
4022
- execSync2('git commit -m "Initial commit"', { cwd: dir, stdio: "pipe" });
3927
+ execSync("git add -A", { cwd: dir, stdio: "pipe" });
3928
+ execSync('git commit -m "Initial commit"', { cwd: dir, stdio: "pipe" });
4023
3929
  } catch {
4024
3930
  }
4025
3931
  }