claude-kanban 0.3.1 → 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.
package/dist/bin/cli.js CHANGED
@@ -4,6 +4,10 @@
4
4
  import { Command } from "commander";
5
5
  import chalk from "chalk";
6
6
  import open from "open";
7
+ import { existsSync as existsSync4 } from "fs";
8
+ import { execSync } from "child_process";
9
+ import { createInterface } from "readline";
10
+ import { join as join6 } from "path";
7
11
 
8
12
  // src/server/index.ts
9
13
  import express from "express";
@@ -14,9 +18,9 @@ import { fileURLToPath } from "url";
14
18
  import { existsSync as existsSync3 } from "fs";
15
19
 
16
20
  // src/server/services/executor.ts
17
- import { spawn, execSync } from "child_process";
21
+ import { spawn } from "child_process";
18
22
  import { join as join4 } from "path";
19
- 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";
20
24
  import { EventEmitter } from "events";
21
25
 
22
26
  // src/server/services/project.ts
@@ -526,10 +530,9 @@ function getRecentProgress(projectPath, lines = 100) {
526
530
  // src/server/services/executor.ts
527
531
  var KANBAN_DIR4 = ".claude-kanban";
528
532
  var LOGS_DIR = "logs";
529
- var WORKTREES_DIR = "worktrees";
530
533
  var TaskExecutor = class extends EventEmitter {
531
534
  projectPath;
532
- runningTasks = /* @__PURE__ */ new Map();
535
+ runningTask = null;
533
536
  afkMode = false;
534
537
  afkIteration = 0;
535
538
  afkMaxIterations = 0;
@@ -635,186 +638,47 @@ ${summary}
635
638
  }
636
639
  }
637
640
  /**
638
- * Check if project is a git repository
641
+ * Check if a task is currently running
639
642
  */
640
- isGitRepo() {
641
- try {
642
- execSync("git rev-parse --is-inside-work-tree", {
643
- cwd: this.projectPath,
644
- stdio: "pipe"
645
- });
646
- return true;
647
- } catch {
648
- return false;
649
- }
650
- }
651
- /**
652
- * Get worktrees directory path
653
- */
654
- getWorktreesDir() {
655
- return join4(this.projectPath, KANBAN_DIR4, WORKTREES_DIR);
656
- }
657
- /**
658
- * Get worktree path for a task
659
- */
660
- getWorktreePath(taskId) {
661
- return join4(this.getWorktreesDir(), taskId);
643
+ isRunning() {
644
+ return this.runningTask !== null;
662
645
  }
663
646
  /**
664
- * Get branch name for a task
647
+ * Get the currently running task ID
665
648
  */
666
- getBranchName(taskId) {
667
- return `task/${taskId}`;
668
- }
669
- /**
670
- * Create a git worktree for isolated task execution
671
- */
672
- createWorktree(taskId) {
673
- if (!this.isGitRepo()) {
674
- console.log("[executor] Not a git repo, skipping worktree creation");
675
- return null;
676
- }
677
- const worktreePath = this.getWorktreePath(taskId);
678
- const branchName = this.getBranchName(taskId);
679
- try {
680
- const worktreesDir = this.getWorktreesDir();
681
- if (!existsSync2(worktreesDir)) {
682
- mkdirSync2(worktreesDir, { recursive: true });
683
- }
684
- if (existsSync2(worktreePath)) {
685
- this.removeWorktree(taskId);
686
- }
687
- try {
688
- execSync(`git rev-parse --verify ${branchName}`, {
689
- cwd: this.projectPath,
690
- stdio: "pipe"
691
- });
692
- execSync(`git branch -D ${branchName}`, {
693
- cwd: this.projectPath,
694
- stdio: "pipe"
695
- });
696
- } catch {
697
- }
698
- execSync(`git worktree add -b ${branchName} "${worktreePath}"`, {
699
- cwd: this.projectPath,
700
- stdio: "pipe"
701
- });
702
- console.log(`[executor] Created worktree at ${worktreePath} on branch ${branchName}`);
703
- return { worktreePath, branchName };
704
- } catch (error) {
705
- console.error("[executor] Failed to create worktree:", error);
706
- return null;
707
- }
708
- }
709
- /**
710
- * Remove a git worktree
711
- */
712
- removeWorktree(taskId) {
713
- const worktreePath = this.getWorktreePath(taskId);
714
- const branchName = this.getBranchName(taskId);
715
- try {
716
- if (existsSync2(worktreePath)) {
717
- execSync(`git worktree remove "${worktreePath}" --force`, {
718
- cwd: this.projectPath,
719
- stdio: "pipe"
720
- });
721
- console.log(`[executor] Removed worktree at ${worktreePath}`);
722
- }
723
- } catch (error) {
724
- console.error("[executor] Failed to remove worktree via git:", error);
725
- try {
726
- if (existsSync2(worktreePath)) {
727
- rmSync(worktreePath, { recursive: true, force: true });
728
- execSync("git worktree prune", {
729
- cwd: this.projectPath,
730
- stdio: "pipe"
731
- });
732
- }
733
- } catch {
734
- console.error("[executor] Failed to force remove worktree directory");
735
- }
736
- }
737
- try {
738
- execSync(`git branch -D ${branchName}`, {
739
- cwd: this.projectPath,
740
- stdio: "pipe"
741
- });
742
- console.log(`[executor] Deleted branch ${branchName}`);
743
- } catch {
744
- }
745
- }
746
- /**
747
- * Merge worktree branch back to main branch
748
- */
749
- mergeWorktreeBranch(taskId) {
750
- const branchName = this.getBranchName(taskId);
751
- try {
752
- const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
753
- cwd: this.projectPath,
754
- encoding: "utf-8"
755
- }).trim();
756
- execSync(`git merge ${branchName} --no-edit`, {
757
- cwd: this.projectPath,
758
- stdio: "pipe"
759
- });
760
- console.log(`[executor] Merged ${branchName} into ${currentBranch}`);
761
- return true;
762
- } catch (error) {
763
- console.error("[executor] Failed to merge branch:", error);
764
- return false;
765
- }
766
- }
767
- /**
768
- * Get number of currently running tasks
769
- */
770
- getRunningCount() {
771
- return this.runningTasks.size;
772
- }
773
- /**
774
- * Check if a task is running
775
- */
776
- isTaskRunning(taskId) {
777
- return this.runningTasks.has(taskId);
778
- }
779
- /**
780
- * Get all running task IDs
781
- */
782
- getRunningTaskIds() {
783
- return Array.from(this.runningTasks.keys());
649
+ getRunningTaskId() {
650
+ return this.runningTask?.taskId || null;
784
651
  }
785
652
  /**
786
653
  * Get running task output
787
654
  */
788
655
  getTaskOutput(taskId) {
789
- return this.runningTasks.get(taskId)?.output;
656
+ if (this.runningTask?.taskId === taskId) {
657
+ return this.runningTask.output;
658
+ }
659
+ return void 0;
790
660
  }
791
661
  /**
792
- * Build the prompt for a specific task
662
+ * Build the prompt for a task - simplified Ralph-style
793
663
  */
794
- buildTaskPrompt(task, config, worktreeInfo) {
664
+ buildTaskPrompt(task, config) {
795
665
  const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
796
666
  const prdPath = join4(kanbanDir, "prd.json");
797
667
  const progressPath = join4(kanbanDir, "progress.txt");
798
668
  const stepsText = task.steps.length > 0 ? `
799
669
  Verification steps:
800
670
  ${task.steps.map((s, i) => `${i + 1}. ${s}`).join("\n")}` : "";
801
- const verifySteps = [];
671
+ const verifyCommands = [];
802
672
  if (config.project.typecheckCommand) {
803
- verifySteps.push(`Run typecheck: ${config.project.typecheckCommand}`);
673
+ verifyCommands.push(config.project.typecheckCommand);
804
674
  }
805
675
  if (config.project.testCommand) {
806
- verifySteps.push(`Run tests: ${config.project.testCommand}`);
676
+ verifyCommands.push(config.project.testCommand);
807
677
  }
808
- const verifySection = verifySteps.length > 0 ? `3. Verify your work:
809
- ${verifySteps.map((s) => ` - ${s}`).join("\n")}
678
+ const verifySection = verifyCommands.length > 0 ? `3. Run quality checks:
679
+ ${verifyCommands.map((cmd) => ` - ${cmd}`).join("\n")}
810
680
 
811
- ` : "";
812
- const worktreeSection = worktreeInfo ? `## ENVIRONMENT
813
- You are running in an isolated git worktree on branch "${worktreeInfo.branchName}".
814
- This is a fresh checkout - dependencies (node_modules, vendor, etc.) are NOT installed.
815
- Before running any build/test commands, install dependencies first (e.g., npm install, composer install, pip install).
816
-
817
- ` : "";
681
+ 4.` : "3.";
818
682
  return `You are an AI coding agent. Complete the following task:
819
683
 
820
684
  ## TASK
@@ -825,48 +689,41 @@ Priority: ${task.priority}
825
689
  ${task.description}
826
690
  ${stepsText}
827
691
 
828
- ${worktreeSection}## INSTRUCTIONS
829
- 1. If dependencies are not installed, install them first.
692
+ ## INSTRUCTIONS
693
+
694
+ 1. Implement this task as described above.
830
695
 
831
- 2. Implement this task as described above.
696
+ 2. Make sure your changes work correctly.
832
697
 
833
- ${verifySection}${verifySteps.length > 0 ? "4" : "3"}. When complete, update the task in ${prdPath}:
698
+ ${verifySection} When complete, update the PRD file at ${prdPath}:
834
699
  - Find the task with id "${task.id}"
835
700
  - Set "passes": true
836
701
  - Set "status": "completed"
837
702
 
838
- ${verifySteps.length > 0 ? "5" : "4"}. Document your work in ${progressPath}:
839
- - What you implemented and files changed
840
- - Key decisions made and why
841
- - Gotchas, edge cases, or tricky parts discovered
842
- - Useful patterns or approaches that worked well
843
- - Anything a future agent should know about this area of the codebase
703
+ ${verifyCommands.length > 0 ? "5" : "4"}. Document your work in ${progressPath}:
704
+ - What you implemented
705
+ - Key decisions made
706
+ - Any gotchas or important notes for future work
844
707
 
845
- ${verifySteps.length > 0 ? "6" : "5"}. Make a git commit with a descriptive message.
708
+ ${verifyCommands.length > 0 ? "6" : "5"}. Commit your changes with a descriptive message.
846
709
 
847
- Focus only on this task. When successfully complete, output: <promise>COMPLETE</promise>`;
710
+ Focus only on this task. When done, output: <promise>COMPLETE</promise>`;
848
711
  }
849
712
  /**
850
- * Run a specific task
713
+ * Run a task
851
714
  */
852
715
  async runTask(taskId) {
716
+ if (this.isRunning()) {
717
+ throw new Error("A task is already running. Wait for it to complete or cancel it first.");
718
+ }
853
719
  const config = getConfig(this.projectPath);
854
720
  const task = getTaskById(this.projectPath, taskId);
855
721
  if (!task) {
856
722
  throw new Error(`Task not found: ${taskId}`);
857
723
  }
858
- if (this.isTaskRunning(taskId)) {
859
- throw new Error(`Task already running: ${taskId}`);
860
- }
861
- const maxConcurrent = config.execution.maxConcurrent || 3;
862
- if (this.getRunningCount() >= maxConcurrent) {
863
- throw new Error(`Maximum concurrent tasks (${maxConcurrent}) reached`);
864
- }
865
724
  updateTask(this.projectPath, taskId, { status: "in_progress" });
866
725
  const startedAt = /* @__PURE__ */ new Date();
867
- const worktreeInfo = this.createWorktree(taskId);
868
- const executionPath = worktreeInfo?.worktreePath || this.projectPath;
869
- const prompt = this.buildTaskPrompt(task, config, worktreeInfo);
726
+ const prompt = this.buildTaskPrompt(task, config);
870
727
  const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
871
728
  const promptFile = join4(kanbanDir, `prompt-${taskId}.txt`);
872
729
  writeFileSync4(promptFile, prompt);
@@ -882,48 +739,32 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
882
739
  const commandDisplay = `${config.agent.command} ${args.join(" ")}`;
883
740
  const fullCommand = `${config.agent.command} ${args.join(" ")}`;
884
741
  console.log("[executor] Command:", fullCommand);
885
- console.log("[executor] CWD:", executionPath);
886
- if (worktreeInfo) {
887
- console.log("[executor] Using worktree:", worktreeInfo.worktreePath);
888
- console.log("[executor] Branch:", worktreeInfo.branchName);
889
- }
742
+ console.log("[executor] CWD:", this.projectPath);
890
743
  const childProcess = spawn("bash", ["-c", fullCommand], {
891
- cwd: executionPath,
744
+ cwd: this.projectPath,
892
745
  env: {
893
746
  ...process.env,
894
747
  TERM: "xterm-256color",
895
748
  FORCE_COLOR: "0",
896
- // Disable colors to avoid escape codes
897
749
  NO_COLOR: "1"
898
- // Standard way to disable colors
899
750
  },
900
751
  stdio: ["ignore", "pipe", "pipe"]
901
- // Close stdin since we don't need interactive input
902
752
  });
903
- const runningTask = {
753
+ this.runningTask = {
904
754
  taskId,
905
755
  process: childProcess,
906
756
  startedAt,
907
- output: [],
908
- worktreePath: worktreeInfo?.worktreePath,
909
- branchName: worktreeInfo?.branchName
757
+ output: []
910
758
  };
911
- this.runningTasks.set(taskId, runningTask);
912
759
  this.initLogFile(taskId);
913
760
  const logOutput = (line) => {
914
761
  this.appendToLog(taskId, line);
915
- runningTask.output.push(line);
762
+ this.runningTask?.output.push(line);
916
763
  this.emit("task:output", { taskId, line, lineType: "stdout" });
917
764
  };
918
765
  this.emit("task:started", { taskId, timestamp: startedAt.toISOString() });
919
766
  logOutput(`[claude-kanban] Starting task: ${task.title}
920
767
  `);
921
- if (worktreeInfo) {
922
- logOutput(`[claude-kanban] Worktree: ${worktreeInfo.worktreePath}
923
- `);
924
- logOutput(`[claude-kanban] Branch: ${worktreeInfo.branchName}
925
- `);
926
- }
927
768
  logOutput(`[claude-kanban] Command: ${commandDisplay}
928
769
  `);
929
770
  logOutput(`[claude-kanban] Process spawned (PID: ${childProcess.pid})
@@ -968,9 +809,6 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
968
809
  const text = data.toString();
969
810
  logOutput(`[stderr] ${text}`);
970
811
  });
971
- childProcess.on("spawn", () => {
972
- console.log("[executor] Process spawned successfully");
973
- });
974
812
  childProcess.on("error", (error) => {
975
813
  console.log("[executor] Spawn error:", error.message);
976
814
  this.emit("task:output", { taskId, line: `[claude-kanban] Error: ${error.message}
@@ -979,9 +817,6 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
979
817
  unlinkSync(promptFile);
980
818
  } catch {
981
819
  }
982
- if (worktreeInfo) {
983
- this.removeWorktree(taskId);
984
- }
985
820
  updateTask(this.projectPath, taskId, { status: "failed", passes: false });
986
821
  const endedAt = /* @__PURE__ */ new Date();
987
822
  addExecutionEntry(this.projectPath, taskId, {
@@ -992,7 +827,7 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
992
827
  error: error.message
993
828
  });
994
829
  this.emit("task:failed", { taskId, error: error.message });
995
- this.runningTasks.delete(taskId);
830
+ this.runningTask = null;
996
831
  });
997
832
  childProcess.on("close", (code, signal) => {
998
833
  console.log("[executor] Process closed with code:", code, "signal:", signal);
@@ -1006,8 +841,8 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
1006
841
  });
1007
842
  const timeoutMs = (config.execution.timeout || 30) * 60 * 1e3;
1008
843
  setTimeout(() => {
1009
- if (this.isTaskRunning(taskId)) {
1010
- this.cancelTask(taskId, "Timeout exceeded");
844
+ if (this.runningTask?.taskId === taskId) {
845
+ this.cancelTask("Timeout exceeded");
1011
846
  }
1012
847
  }, timeoutMs);
1013
848
  }
@@ -1015,23 +850,13 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
1015
850
  * Handle task completion
1016
851
  */
1017
852
  handleTaskComplete(taskId, exitCode, startedAt) {
1018
- const runningTask = this.runningTasks.get(taskId);
1019
- if (!runningTask) return;
853
+ if (!this.runningTask || this.runningTask.taskId !== taskId) return;
1020
854
  const endedAt = /* @__PURE__ */ new Date();
1021
855
  const duration = endedAt.getTime() - startedAt.getTime();
1022
- const output = runningTask.output.join("");
856
+ const output = this.runningTask.output.join("");
1023
857
  const isComplete = output.includes("<promise>COMPLETE</promise>");
1024
858
  const task = getTaskById(this.projectPath, taskId);
1025
859
  if (isComplete || exitCode === 0) {
1026
- if (runningTask.worktreePath && runningTask.branchName) {
1027
- const merged = this.mergeWorktreeBranch(taskId);
1028
- if (merged) {
1029
- console.log(`[executor] Successfully merged ${runningTask.branchName}`);
1030
- } else {
1031
- console.log(`[executor] Failed to merge ${runningTask.branchName}, branch preserved for manual merge`);
1032
- }
1033
- this.removeWorktree(taskId);
1034
- }
1035
860
  updateTask(this.projectPath, taskId, {
1036
861
  status: "completed",
1037
862
  passes: true
@@ -1051,9 +876,6 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
1051
876
  this.emit("task:completed", { taskId, duration });
1052
877
  this.afkTasksCompleted++;
1053
878
  } else {
1054
- if (runningTask.worktreePath) {
1055
- this.removeWorktree(taskId);
1056
- }
1057
879
  updateTask(this.projectPath, taskId, {
1058
880
  status: "failed",
1059
881
  passes: false
@@ -1075,39 +897,33 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
1075
897
  });
1076
898
  this.emit("task:failed", { taskId, error });
1077
899
  }
1078
- this.runningTasks.delete(taskId);
900
+ this.runningTask = null;
1079
901
  if (this.afkMode) {
1080
902
  this.continueAFKMode();
1081
903
  }
1082
904
  }
1083
905
  /**
1084
- * Cancel a running task
906
+ * Cancel the running task
1085
907
  */
1086
- cancelTask(taskId, reason = "Cancelled by user") {
1087
- const runningTask = this.runningTasks.get(taskId);
1088
- if (!runningTask) return false;
1089
- const startedAt = runningTask.startedAt;
908
+ cancelTask(reason = "Cancelled by user") {
909
+ if (!this.runningTask) return false;
910
+ const { taskId, process: childProcess, startedAt } = this.runningTask;
1090
911
  const endedAt = /* @__PURE__ */ new Date();
1091
912
  const duration = endedAt.getTime() - startedAt.getTime();
1092
913
  const task = getTaskById(this.projectPath, taskId);
1093
914
  try {
1094
- runningTask.process.kill("SIGTERM");
915
+ childProcess.kill("SIGTERM");
1095
916
  setTimeout(() => {
1096
917
  try {
1097
- if (!runningTask.process.killed) {
1098
- runningTask.process.kill("SIGKILL");
918
+ if (!childProcess.killed) {
919
+ childProcess.kill("SIGKILL");
1099
920
  }
1100
921
  } catch {
1101
922
  }
1102
923
  }, 2e3);
1103
924
  } catch {
1104
925
  }
1105
- if (runningTask.worktreePath) {
1106
- this.removeWorktree(taskId);
1107
- }
1108
- updateTask(this.projectPath, taskId, {
1109
- status: "ready"
1110
- });
926
+ updateTask(this.projectPath, taskId, { status: "ready" });
1111
927
  addExecutionEntry(this.projectPath, taskId, {
1112
928
  startedAt: startedAt.toISOString(),
1113
929
  endedAt: endedAt.toISOString(),
@@ -1123,48 +939,46 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
1123
939
  error: reason
1124
940
  });
1125
941
  this.emit("task:cancelled", { taskId });
1126
- this.runningTasks.delete(taskId);
942
+ this.runningTask = null;
1127
943
  return true;
1128
944
  }
1129
945
  /**
1130
- * Start AFK mode
946
+ * Start AFK mode - run tasks sequentially until done
1131
947
  */
1132
- startAFKMode(maxIterations, concurrent) {
948
+ startAFKMode(maxIterations) {
1133
949
  if (this.afkMode) {
1134
950
  throw new Error("AFK mode already running");
1135
951
  }
952
+ if (this.isRunning()) {
953
+ throw new Error("Cannot start AFK mode while a task is running");
954
+ }
1136
955
  this.afkMode = true;
1137
956
  this.afkIteration = 0;
1138
957
  this.afkMaxIterations = maxIterations;
1139
958
  this.afkTasksCompleted = 0;
1140
959
  this.emitAFKStatus();
1141
- this.continueAFKMode(concurrent);
960
+ this.continueAFKMode();
1142
961
  }
1143
962
  /**
1144
- * Continue AFK mode - pick up next tasks
963
+ * Continue AFK mode - pick next task
1145
964
  */
1146
- continueAFKMode(concurrent = 1) {
965
+ continueAFKMode() {
1147
966
  if (!this.afkMode) return;
1148
967
  if (this.afkIteration >= this.afkMaxIterations) {
1149
968
  this.stopAFKMode();
1150
969
  return;
1151
970
  }
1152
- const config = getConfig(this.projectPath);
1153
- const maxConcurrent = Math.min(concurrent, config.execution.maxConcurrent || 3);
1154
- while (this.getRunningCount() < maxConcurrent) {
1155
- const nextTask = getNextReadyTask(this.projectPath);
1156
- if (!nextTask) {
1157
- if (this.getRunningCount() === 0) {
1158
- this.stopAFKMode();
1159
- }
1160
- break;
1161
- }
1162
- this.afkIteration++;
1163
- this.runTask(nextTask.id).catch((error) => {
1164
- console.error("AFK task error:", error);
1165
- });
1166
- this.emitAFKStatus();
971
+ if (this.isRunning()) return;
972
+ const nextTask = getNextReadyTask(this.projectPath);
973
+ if (!nextTask) {
974
+ this.stopAFKMode();
975
+ return;
1167
976
  }
977
+ this.afkIteration++;
978
+ this.runTask(nextTask.id).catch((error) => {
979
+ console.error("AFK task error:", error);
980
+ });
981
+ this.emitAFKStatus();
1168
982
  }
1169
983
  /**
1170
984
  * Stop AFK mode
@@ -1196,18 +1010,15 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
1196
1010
  };
1197
1011
  }
1198
1012
  /**
1199
- * Cancel all running tasks
1013
+ * Cancel running task and stop AFK mode
1200
1014
  */
1201
1015
  cancelAll() {
1202
- for (const [taskId, runningTask] of this.runningTasks.entries()) {
1016
+ if (this.runningTask) {
1203
1017
  try {
1204
- runningTask.process.kill("SIGKILL");
1018
+ this.runningTask.process.kill("SIGKILL");
1205
1019
  } catch {
1206
1020
  }
1207
- if (runningTask.worktreePath) {
1208
- this.removeWorktree(taskId);
1209
- }
1210
- this.runningTasks.delete(taskId);
1021
+ this.runningTask = null;
1211
1022
  }
1212
1023
  this.stopAFKMode();
1213
1024
  }
@@ -1717,7 +1528,12 @@ async function createServer(projectPath, port) {
1717
1528
  });
1718
1529
  app.post("/api/tasks/:id/cancel", (req, res) => {
1719
1530
  try {
1720
- const cancelled = executor.cancelTask(req.params.id);
1531
+ const runningId = executor.getRunningTaskId();
1532
+ if (runningId !== req.params.id) {
1533
+ res.status(404).json({ error: "Task not running" });
1534
+ return;
1535
+ }
1536
+ const cancelled = executor.cancelTask();
1721
1537
  if (!cancelled) {
1722
1538
  res.status(404).json({ error: "Task not running" });
1723
1539
  return;
@@ -1819,8 +1635,8 @@ async function createServer(projectPath, port) {
1819
1635
  });
1820
1636
  app.post("/api/afk/start", (req, res) => {
1821
1637
  try {
1822
- const { maxIterations, concurrent } = req.body;
1823
- executor.startAFKMode(maxIterations || 10, concurrent || 1);
1638
+ const { maxIterations } = req.body;
1639
+ executor.startAFKMode(maxIterations || 10);
1824
1640
  res.json({ success: true, status: executor.getAFKStatus() });
1825
1641
  } catch (error) {
1826
1642
  res.status(400).json({ error: String(error) });
@@ -1844,8 +1660,8 @@ async function createServer(projectPath, port) {
1844
1660
  });
1845
1661
  app.get("/api/running", (_req, res) => {
1846
1662
  try {
1847
- const taskIds = executor.getRunningTaskIds();
1848
- res.json({ running: taskIds, count: taskIds.length });
1663
+ const taskId = executor.getRunningTaskId();
1664
+ res.json({ running: taskId ? [taskId] : [], count: taskId ? 1 : 0 });
1849
1665
  } catch (error) {
1850
1666
  res.status(500).json({ error: String(error) });
1851
1667
  }
@@ -1853,7 +1669,7 @@ async function createServer(projectPath, port) {
1853
1669
  app.get("/api/stats", (_req, res) => {
1854
1670
  try {
1855
1671
  const counts = getTaskCounts(projectPath);
1856
- const running = executor.getRunningCount();
1672
+ const running = executor.isRunning() ? 1 : 0;
1857
1673
  const afk = executor.getAFKStatus();
1858
1674
  res.json({ counts, running, afk });
1859
1675
  } catch (error) {
@@ -1869,12 +1685,13 @@ async function createServer(projectPath, port) {
1869
1685
  });
1870
1686
  io.on("connection", (socket) => {
1871
1687
  console.log("Client connected");
1872
- const runningIds = executor.getRunningTaskIds();
1688
+ const runningId = executor.getRunningTaskId();
1689
+ const runningIds = runningId ? [runningId] : [];
1873
1690
  const taskLogs = {};
1874
- for (const taskId of runningIds) {
1875
- const logs = executor.getTaskLog(taskId);
1691
+ if (runningId) {
1692
+ const logs = executor.getTaskLog(runningId);
1876
1693
  if (logs) {
1877
- taskLogs[taskId] = logs;
1694
+ taskLogs[runningId] = logs;
1878
1695
  }
1879
1696
  }
1880
1697
  socket.emit("init", {
@@ -1882,7 +1699,7 @@ async function createServer(projectPath, port) {
1882
1699
  running: runningIds,
1883
1700
  afk: executor.getAFKStatus(),
1884
1701
  taskLogs
1885
- // Include logs for running tasks
1702
+ // Include logs for running task
1886
1703
  });
1887
1704
  socket.on("get-logs", (taskId) => {
1888
1705
  const logs = executor.getTaskLog(taskId);
@@ -2592,7 +2409,10 @@ socket.on('task:output', ({ taskId, line }) => {
2592
2409
  socket.on('task:completed', ({ taskId }) => {
2593
2410
  state.running = state.running.filter(id => id !== taskId);
2594
2411
  const task = state.tasks.find(t => t.id === taskId);
2595
- if (task) { task.status = 'completed'; task.passes = true; }
2412
+ if (task) {
2413
+ task.status = 'completed';
2414
+ task.passes = true;
2415
+ }
2596
2416
  showToast('Task completed: ' + (task?.title || taskId), 'success');
2597
2417
  render();
2598
2418
  });
@@ -2685,11 +2505,11 @@ async function generateTask(prompt) {
2685
2505
  }
2686
2506
  }
2687
2507
 
2688
- async function startAFK(maxIterations, concurrent) {
2508
+ async function startAFK(maxIterations) {
2689
2509
  await fetch('/api/afk/start', {
2690
2510
  method: 'POST',
2691
2511
  headers: { 'Content-Type': 'application/json' },
2692
- body: JSON.stringify({ maxIterations, concurrent })
2512
+ body: JSON.stringify({ maxIterations })
2693
2513
  });
2694
2514
  }
2695
2515
 
@@ -3122,21 +2942,13 @@ function renderModal() {
3122
2942
  <button onclick="state.showModal = null; render();" class="btn btn-ghost p-1.5 text-canvas-500 hover:text-canvas-700">\u2715</button>
3123
2943
  </div>
3124
2944
  <div class="p-6">
3125
- <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>
2945
+ <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>
3126
2946
  <div class="space-y-4">
3127
2947
  <div>
3128
2948
  <label class="block text-sm font-medium text-canvas-700 mb-2">Maximum Iterations</label>
3129
2949
  <input type="number" id="afk-iterations" value="10" min="1" max="100"
3130
2950
  class="input w-full">
3131
2951
  </div>
3132
- <div>
3133
- <label class="block text-sm font-medium text-canvas-700 mb-2">Concurrent Tasks</label>
3134
- <select id="afk-concurrent" class="input w-full">
3135
- <option value="1">1 (Sequential)</option>
3136
- <option value="2">2</option>
3137
- <option value="3">3 (Max)</option>
3138
- </select>
3139
- </div>
3140
2952
  <div class="bg-status-running/10 border border-status-running/20 rounded-lg p-3">
3141
2953
  <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>
3142
2954
  </div>
@@ -3235,7 +3047,7 @@ function renderSidePanel() {
3235
3047
  </div>
3236
3048
 
3237
3049
  <!-- Action Buttons -->
3238
- <div class="px-5 py-4 border-b border-canvas-200 flex gap-2 flex-shrink-0">
3050
+ <div class="px-5 py-4 border-b border-canvas-200 flex flex-wrap gap-2 flex-shrink-0">
3239
3051
  \${task.status === 'draft' ? \`
3240
3052
  <button onclick="updateTask('\${task.id}', { status: 'ready' })" class="btn btn-primary px-4 py-2 text-sm">
3241
3053
  \u2192 Move to Ready
@@ -3445,8 +3257,7 @@ function applyTemplate(templateId) {
3445
3257
 
3446
3258
  function handleStartAFK() {
3447
3259
  const iterations = parseInt(document.getElementById('afk-iterations').value) || 10;
3448
- const concurrent = parseInt(document.getElementById('afk-concurrent').value) || 1;
3449
- startAFK(iterations, concurrent);
3260
+ startAFK(iterations);
3450
3261
  state.showModal = null;
3451
3262
  render();
3452
3263
  }
@@ -3650,6 +3461,49 @@ async function findAvailablePort(startPort, maxAttempts = 10) {
3650
3461
  }
3651
3462
 
3652
3463
  // src/bin/cli.ts
3464
+ function isGitRepo(dir) {
3465
+ return existsSync4(join6(dir, ".git"));
3466
+ }
3467
+ function hasGitRemote(dir) {
3468
+ try {
3469
+ const result = execSync("git remote -v", { cwd: dir, stdio: "pipe" }).toString();
3470
+ return result.trim().length > 0;
3471
+ } catch {
3472
+ return false;
3473
+ }
3474
+ }
3475
+ function detectRemoteType(dir) {
3476
+ try {
3477
+ const result = execSync("git remote get-url origin", { cwd: dir, stdio: "pipe" }).toString().toLowerCase();
3478
+ if (result.includes("github.com")) return "github";
3479
+ if (result.includes("gitlab.com") || result.includes("gitlab")) return "gitlab";
3480
+ if (result.includes("bitbucket.org") || result.includes("bitbucket")) return "bitbucket";
3481
+ if (result.trim()) return "other";
3482
+ return null;
3483
+ } catch {
3484
+ return null;
3485
+ }
3486
+ }
3487
+ function initializeGit(dir) {
3488
+ execSync("git init", { cwd: dir, stdio: "pipe" });
3489
+ try {
3490
+ execSync("git add -A", { cwd: dir, stdio: "pipe" });
3491
+ execSync('git commit -m "Initial commit"', { cwd: dir, stdio: "pipe" });
3492
+ } catch {
3493
+ }
3494
+ }
3495
+ async function promptYesNo(question) {
3496
+ const rl = createInterface({
3497
+ input: process.stdin,
3498
+ output: process.stdout
3499
+ });
3500
+ return new Promise((resolve) => {
3501
+ rl.question(question, (answer) => {
3502
+ rl.close();
3503
+ resolve(answer.toLowerCase().startsWith("y"));
3504
+ });
3505
+ });
3506
+ }
3653
3507
  var VERSION = "0.1.0";
3654
3508
  var banner = `
3655
3509
  ${chalk.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557")}
@@ -3662,6 +3516,32 @@ async function main() {
3662
3516
  program.name("claude-kanban").description("Visual Kanban board for AI-powered development with Claude").version(VERSION).option("-p, --port <number>", "Port to run server on", "4242").option("-n, --no-open", "Do not auto-open browser").option("--init", "Re-initialize project files").option("--reset", "Reset all tasks (keeps config)").action(async (options) => {
3663
3517
  console.log(banner);
3664
3518
  const cwd = process.cwd();
3519
+ if (!isGitRepo(cwd)) {
3520
+ console.log(chalk.yellow("\n\u26A0 This directory is not a git repository."));
3521
+ console.log(chalk.gray("Git is required for task isolation and parallel execution.\n"));
3522
+ const shouldInit = await promptYesNo(chalk.white("Would you like to initialize git now? (y/n): "));
3523
+ if (shouldInit) {
3524
+ try {
3525
+ console.log(chalk.gray("Initializing git repository..."));
3526
+ initializeGit(cwd);
3527
+ console.log(chalk.green("\u2713 Git repository initialized"));
3528
+ } catch (error) {
3529
+ console.error(chalk.red("Failed to initialize git:"), error);
3530
+ process.exit(1);
3531
+ }
3532
+ } else {
3533
+ console.log(chalk.red("\nGit is required to run Claude Kanban."));
3534
+ console.log(chalk.gray('Please run "git init" manually and try again.'));
3535
+ process.exit(1);
3536
+ }
3537
+ } else {
3538
+ const remoteType = detectRemoteType(cwd);
3539
+ if (remoteType) {
3540
+ console.log(chalk.gray(`Git remote detected: ${remoteType}`));
3541
+ } else if (!hasGitRemote(cwd)) {
3542
+ console.log(chalk.gray("Git repository (local only, no remote)"));
3543
+ }
3544
+ }
3665
3545
  const initialized = await isProjectInitialized(cwd);
3666
3546
  if (!initialized || options.init) {
3667
3547
  console.log(chalk.yellow("Initializing project..."));