claude-kanban 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,9 @@ 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;
539
536
  afkMode = false;
540
537
  afkIteration = 0;
541
538
  afkMaxIterations = 0;
@@ -641,186 +638,47 @@ ${summary}
641
638
  }
642
639
  }
643
640
  /**
644
- * Check if project is a git repository
641
+ * Check if a task is currently running
645
642
  */
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
- }
656
- }
657
- /**
658
- * Get worktrees directory path
659
- */
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);
643
+ isRunning() {
644
+ return this.runningTask !== null;
668
645
  }
669
646
  /**
670
- * Get branch name for a task
647
+ * Get the currently running task ID
671
648
  */
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
- }
714
- }
715
- /**
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
754
- */
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;
771
- }
772
- }
773
- /**
774
- * Get number of currently running tasks
775
- */
776
- getRunningCount() {
777
- return this.runningTasks.size;
778
- }
779
- /**
780
- * Check if a task is running
781
- */
782
- isTaskRunning(taskId) {
783
- return this.runningTasks.has(taskId);
784
- }
785
- /**
786
- * Get all running task IDs
787
- */
788
- getRunningTaskIds() {
789
- return Array.from(this.runningTasks.keys());
649
+ getRunningTaskId() {
650
+ return this.runningTask?.taskId || null;
790
651
  }
791
652
  /**
792
653
  * Get running task output
793
654
  */
794
655
  getTaskOutput(taskId) {
795
- return this.runningTasks.get(taskId)?.output;
656
+ if (this.runningTask?.taskId === taskId) {
657
+ return this.runningTask.output;
658
+ }
659
+ return void 0;
796
660
  }
797
661
  /**
798
- * Build the prompt for a specific task
662
+ * Build the prompt for a task - simplified Ralph-style
799
663
  */
800
- buildTaskPrompt(task, config, worktreeInfo) {
664
+ buildTaskPrompt(task, config) {
801
665
  const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
802
666
  const prdPath = join4(kanbanDir, "prd.json");
803
667
  const progressPath = join4(kanbanDir, "progress.txt");
804
668
  const stepsText = task.steps.length > 0 ? `
805
669
  Verification steps:
806
670
  ${task.steps.map((s, i) => `${i + 1}. ${s}`).join("\n")}` : "";
807
- const verifySteps = [];
671
+ const verifyCommands = [];
808
672
  if (config.project.typecheckCommand) {
809
- verifySteps.push(`Run typecheck: ${config.project.typecheckCommand}`);
673
+ verifyCommands.push(config.project.typecheckCommand);
810
674
  }
811
675
  if (config.project.testCommand) {
812
- verifySteps.push(`Run tests: ${config.project.testCommand}`);
676
+ verifyCommands.push(config.project.testCommand);
813
677
  }
814
- const verifySection = verifySteps.length > 0 ? `3. Verify your work:
815
- ${verifySteps.map((s) => ` - ${s}`).join("\n")}
678
+ const verifySection = verifyCommands.length > 0 ? `3. Run quality checks:
679
+ ${verifyCommands.map((cmd) => ` - ${cmd}`).join("\n")}
816
680
 
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).
822
-
823
- ` : "";
681
+ 4.` : "3.";
824
682
  return `You are an AI coding agent. Complete the following task:
825
683
 
826
684
  ## TASK
@@ -831,48 +689,41 @@ Priority: ${task.priority}
831
689
  ${task.description}
832
690
  ${stepsText}
833
691
 
834
- ${worktreeSection}## INSTRUCTIONS
835
- 1. If dependencies are not installed, install them first.
692
+ ## INSTRUCTIONS
693
+
694
+ 1. Implement this task as described above.
836
695
 
837
- 2. Implement this task as described above.
696
+ 2. Make sure your changes work correctly.
838
697
 
839
- ${verifySection}${verifySteps.length > 0 ? "4" : "3"}. When complete, update the task in ${prdPath}:
698
+ ${verifySection} When complete, update the PRD file at ${prdPath}:
840
699
  - Find the task with id "${task.id}"
841
700
  - Set "passes": true
842
- (Note: Do NOT change the status - the system will move it to review)
701
+ - Set "status": "completed"
843
702
 
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
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
850
707
 
851
- ${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.
852
709
 
853
- Focus only on this task. When successfully complete, output: <promise>COMPLETE</promise>`;
710
+ Focus only on this task. When done, output: <promise>COMPLETE</promise>`;
854
711
  }
855
712
  /**
856
- * Run a specific task
713
+ * Run a task
857
714
  */
858
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
+ }
859
719
  const config = getConfig(this.projectPath);
860
720
  const task = getTaskById(this.projectPath, taskId);
861
721
  if (!task) {
862
722
  throw new Error(`Task not found: ${taskId}`);
863
723
  }
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
724
  updateTask(this.projectPath, taskId, { status: "in_progress" });
872
725
  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);
726
+ const prompt = this.buildTaskPrompt(task, config);
876
727
  const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
877
728
  const promptFile = join4(kanbanDir, `prompt-${taskId}.txt`);
878
729
  writeFileSync4(promptFile, prompt);
@@ -888,48 +739,32 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
888
739
  const commandDisplay = `${config.agent.command} ${args.join(" ")}`;
889
740
  const fullCommand = `${config.agent.command} ${args.join(" ")}`;
890
741
  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
- }
742
+ console.log("[executor] CWD:", this.projectPath);
896
743
  const childProcess = spawn("bash", ["-c", fullCommand], {
897
- cwd: executionPath,
744
+ cwd: this.projectPath,
898
745
  env: {
899
746
  ...process.env,
900
747
  TERM: "xterm-256color",
901
748
  FORCE_COLOR: "0",
902
- // Disable colors to avoid escape codes
903
749
  NO_COLOR: "1"
904
- // Standard way to disable colors
905
750
  },
906
751
  stdio: ["ignore", "pipe", "pipe"]
907
- // Close stdin since we don't need interactive input
908
752
  });
909
- const runningTask = {
753
+ this.runningTask = {
910
754
  taskId,
911
755
  process: childProcess,
912
756
  startedAt,
913
- output: [],
914
- worktreePath: worktreeInfo?.worktreePath,
915
- branchName: worktreeInfo?.branchName
757
+ output: []
916
758
  };
917
- this.runningTasks.set(taskId, runningTask);
918
759
  this.initLogFile(taskId);
919
760
  const logOutput = (line) => {
920
761
  this.appendToLog(taskId, line);
921
- runningTask.output.push(line);
762
+ this.runningTask?.output.push(line);
922
763
  this.emit("task:output", { taskId, line, lineType: "stdout" });
923
764
  };
924
765
  this.emit("task:started", { taskId, timestamp: startedAt.toISOString() });
925
766
  logOutput(`[claude-kanban] Starting task: ${task.title}
926
767
  `);
927
- if (worktreeInfo) {
928
- logOutput(`[claude-kanban] Worktree: ${worktreeInfo.worktreePath}
929
- `);
930
- logOutput(`[claude-kanban] Branch: ${worktreeInfo.branchName}
931
- `);
932
- }
933
768
  logOutput(`[claude-kanban] Command: ${commandDisplay}
934
769
  `);
935
770
  logOutput(`[claude-kanban] Process spawned (PID: ${childProcess.pid})
@@ -974,9 +809,6 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
974
809
  const text = data.toString();
975
810
  logOutput(`[stderr] ${text}`);
976
811
  });
977
- childProcess.on("spawn", () => {
978
- console.log("[executor] Process spawned successfully");
979
- });
980
812
  childProcess.on("error", (error) => {
981
813
  console.log("[executor] Spawn error:", error.message);
982
814
  this.emit("task:output", { taskId, line: `[claude-kanban] Error: ${error.message}
@@ -985,9 +817,6 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
985
817
  unlinkSync(promptFile);
986
818
  } catch {
987
819
  }
988
- if (worktreeInfo) {
989
- this.removeWorktree(taskId);
990
- }
991
820
  updateTask(this.projectPath, taskId, { status: "failed", passes: false });
992
821
  const endedAt = /* @__PURE__ */ new Date();
993
822
  addExecutionEntry(this.projectPath, taskId, {
@@ -998,7 +827,7 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
998
827
  error: error.message
999
828
  });
1000
829
  this.emit("task:failed", { taskId, error: error.message });
1001
- this.runningTasks.delete(taskId);
830
+ this.runningTask = null;
1002
831
  });
1003
832
  childProcess.on("close", (code, signal) => {
1004
833
  console.log("[executor] Process closed with code:", code, "signal:", signal);
@@ -1012,48 +841,41 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
1012
841
  });
1013
842
  const timeoutMs = (config.execution.timeout || 30) * 60 * 1e3;
1014
843
  setTimeout(() => {
1015
- if (this.isTaskRunning(taskId)) {
1016
- this.cancelTask(taskId, "Timeout exceeded");
844
+ if (this.runningTask?.taskId === taskId) {
845
+ this.cancelTask("Timeout exceeded");
1017
846
  }
1018
847
  }, timeoutMs);
1019
848
  }
1020
849
  /**
1021
- * Handle task completion - moves to in_review, keeps worktree alive
850
+ * Handle task completion
1022
851
  */
1023
852
  handleTaskComplete(taskId, exitCode, startedAt) {
1024
- const runningTask = this.runningTasks.get(taskId);
1025
- if (!runningTask) return;
853
+ if (!this.runningTask || this.runningTask.taskId !== taskId) return;
1026
854
  const endedAt = /* @__PURE__ */ new Date();
1027
855
  const duration = endedAt.getTime() - startedAt.getTime();
1028
- const output = runningTask.output.join("");
856
+ const output = this.runningTask.output.join("");
1029
857
  const isComplete = output.includes("<promise>COMPLETE</promise>");
1030
858
  const task = getTaskById(this.projectPath, taskId);
1031
859
  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
860
  updateTask(this.projectPath, taskId, {
1042
- status: "in_review",
861
+ status: "completed",
1043
862
  passes: true
1044
863
  });
864
+ addExecutionEntry(this.projectPath, taskId, {
865
+ startedAt: startedAt.toISOString(),
866
+ endedAt: endedAt.toISOString(),
867
+ status: "completed",
868
+ duration
869
+ });
1045
870
  logTaskExecution(this.projectPath, {
1046
871
  taskId,
1047
872
  taskTitle: task?.title || "Unknown",
1048
- status: "in_review",
873
+ status: "completed",
1049
874
  duration
1050
875
  });
1051
- this.emit("task:completed", { taskId, duration, status: "in_review" });
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,184 +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
- * 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
906
+ * Cancel the running task
1166
907
  */
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;
908
+ cancelTask(reason = "Cancelled by user") {
909
+ if (!this.runningTask) return false;
910
+ const { taskId, process: childProcess, startedAt } = this.runningTask;
1235
911
  const endedAt = /* @__PURE__ */ new Date();
1236
912
  const duration = endedAt.getTime() - startedAt.getTime();
1237
913
  const task = getTaskById(this.projectPath, taskId);
1238
914
  try {
1239
- runningTask.process.kill("SIGTERM");
915
+ childProcess.kill("SIGTERM");
1240
916
  setTimeout(() => {
1241
917
  try {
1242
- if (!runningTask.process.killed) {
1243
- runningTask.process.kill("SIGKILL");
918
+ if (!childProcess.killed) {
919
+ childProcess.kill("SIGKILL");
1244
920
  }
1245
921
  } catch {
1246
922
  }
1247
923
  }, 2e3);
1248
924
  } catch {
1249
925
  }
1250
- if (runningTask.worktreePath) {
1251
- this.removeWorktree(taskId);
1252
- }
1253
- updateTask(this.projectPath, taskId, {
1254
- status: "ready"
1255
- });
926
+ updateTask(this.projectPath, taskId, { status: "ready" });
1256
927
  addExecutionEntry(this.projectPath, taskId, {
1257
928
  startedAt: startedAt.toISOString(),
1258
929
  endedAt: endedAt.toISOString(),
@@ -1268,48 +939,46 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
1268
939
  error: reason
1269
940
  });
1270
941
  this.emit("task:cancelled", { taskId });
1271
- this.runningTasks.delete(taskId);
942
+ this.runningTask = null;
1272
943
  return true;
1273
944
  }
1274
945
  /**
1275
- * Start AFK mode
946
+ * Start AFK mode - run tasks sequentially until done
1276
947
  */
1277
- startAFKMode(maxIterations, concurrent) {
948
+ startAFKMode(maxIterations) {
1278
949
  if (this.afkMode) {
1279
950
  throw new Error("AFK mode already running");
1280
951
  }
952
+ if (this.isRunning()) {
953
+ throw new Error("Cannot start AFK mode while a task is running");
954
+ }
1281
955
  this.afkMode = true;
1282
956
  this.afkIteration = 0;
1283
957
  this.afkMaxIterations = maxIterations;
1284
958
  this.afkTasksCompleted = 0;
1285
959
  this.emitAFKStatus();
1286
- this.continueAFKMode(concurrent);
960
+ this.continueAFKMode();
1287
961
  }
1288
962
  /**
1289
- * Continue AFK mode - pick up next tasks
963
+ * Continue AFK mode - pick next task
1290
964
  */
1291
- continueAFKMode(concurrent = 1) {
965
+ continueAFKMode() {
1292
966
  if (!this.afkMode) return;
1293
967
  if (this.afkIteration >= this.afkMaxIterations) {
1294
968
  this.stopAFKMode();
1295
969
  return;
1296
970
  }
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();
971
+ if (this.isRunning()) return;
972
+ const nextTask = getNextReadyTask(this.projectPath);
973
+ if (!nextTask) {
974
+ this.stopAFKMode();
975
+ return;
1312
976
  }
977
+ this.afkIteration++;
978
+ this.runTask(nextTask.id).catch((error) => {
979
+ console.error("AFK task error:", error);
980
+ });
981
+ this.emitAFKStatus();
1313
982
  }
1314
983
  /**
1315
984
  * Stop AFK mode
@@ -1341,18 +1010,15 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
1341
1010
  };
1342
1011
  }
1343
1012
  /**
1344
- * Cancel all running tasks
1013
+ * Cancel running task and stop AFK mode
1345
1014
  */
1346
1015
  cancelAll() {
1347
- for (const [taskId, runningTask] of this.runningTasks.entries()) {
1016
+ if (this.runningTask) {
1348
1017
  try {
1349
- runningTask.process.kill("SIGKILL");
1018
+ this.runningTask.process.kill("SIGKILL");
1350
1019
  } catch {
1351
1020
  }
1352
- if (runningTask.worktreePath) {
1353
- this.removeWorktree(taskId);
1354
- }
1355
- this.runningTasks.delete(taskId);
1021
+ this.runningTask = null;
1356
1022
  }
1357
1023
  this.stopAFKMode();
1358
1024
  }
@@ -1862,7 +1528,12 @@ async function createServer(projectPath, port) {
1862
1528
  });
1863
1529
  app.post("/api/tasks/:id/cancel", (req, res) => {
1864
1530
  try {
1865
- 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();
1866
1537
  if (!cancelled) {
1867
1538
  res.status(404).json({ error: "Task not running" });
1868
1539
  return;
@@ -1891,90 +1562,6 @@ async function createServer(projectPath, port) {
1891
1562
  res.status(500).json({ error: String(error) });
1892
1563
  }
1893
1564
  });
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
1565
  app.get("/api/tasks/:id/logs", (req, res) => {
1979
1566
  try {
1980
1567
  const logs = executor.getTaskLog(req.params.id);
@@ -2048,8 +1635,8 @@ async function createServer(projectPath, port) {
2048
1635
  });
2049
1636
  app.post("/api/afk/start", (req, res) => {
2050
1637
  try {
2051
- const { maxIterations, concurrent } = req.body;
2052
- executor.startAFKMode(maxIterations || 10, concurrent || 1);
1638
+ const { maxIterations } = req.body;
1639
+ executor.startAFKMode(maxIterations || 10);
2053
1640
  res.json({ success: true, status: executor.getAFKStatus() });
2054
1641
  } catch (error) {
2055
1642
  res.status(400).json({ error: String(error) });
@@ -2073,8 +1660,8 @@ async function createServer(projectPath, port) {
2073
1660
  });
2074
1661
  app.get("/api/running", (_req, res) => {
2075
1662
  try {
2076
- const taskIds = executor.getRunningTaskIds();
2077
- res.json({ running: taskIds, count: taskIds.length });
1663
+ const taskId = executor.getRunningTaskId();
1664
+ res.json({ running: taskId ? [taskId] : [], count: taskId ? 1 : 0 });
2078
1665
  } catch (error) {
2079
1666
  res.status(500).json({ error: String(error) });
2080
1667
  }
@@ -2082,7 +1669,7 @@ async function createServer(projectPath, port) {
2082
1669
  app.get("/api/stats", (_req, res) => {
2083
1670
  try {
2084
1671
  const counts = getTaskCounts(projectPath);
2085
- const running = executor.getRunningCount();
1672
+ const running = executor.isRunning() ? 1 : 0;
2086
1673
  const afk = executor.getAFKStatus();
2087
1674
  res.json({ counts, running, afk });
2088
1675
  } catch (error) {
@@ -2098,12 +1685,13 @@ async function createServer(projectPath, port) {
2098
1685
  });
2099
1686
  io.on("connection", (socket) => {
2100
1687
  console.log("Client connected");
2101
- const runningIds = executor.getRunningTaskIds();
1688
+ const runningId = executor.getRunningTaskId();
1689
+ const runningIds = runningId ? [runningId] : [];
2102
1690
  const taskLogs = {};
2103
- for (const taskId of runningIds) {
2104
- const logs = executor.getTaskLog(taskId);
1691
+ if (runningId) {
1692
+ const logs = executor.getTaskLog(runningId);
2105
1693
  if (logs) {
2106
- taskLogs[taskId] = logs;
1694
+ taskLogs[runningId] = logs;
2107
1695
  }
2108
1696
  }
2109
1697
  socket.emit("init", {
@@ -2111,7 +1699,7 @@ async function createServer(projectPath, port) {
2111
1699
  running: runningIds,
2112
1700
  afk: executor.getAFKStatus(),
2113
1701
  taskLogs
2114
- // Include logs for running tasks
1702
+ // Include logs for running task
2115
1703
  });
2116
1704
  socket.on("get-logs", (taskId) => {
2117
1705
  const logs = executor.getTaskLog(taskId);
@@ -2284,7 +1872,6 @@ function getClientHTML() {
2284
1872
  .status-dot-draft { background: #a3a3a3; }
2285
1873
  .status-dot-ready { background: #3b82f6; }
2286
1874
  .status-dot-in_progress { background: #f97316; }
2287
- .status-dot-in_review { background: #8b5cf6; }
2288
1875
  .status-dot-completed { background: #22c55e; }
2289
1876
  .status-dot-failed { background: #ef4444; }
2290
1877
 
@@ -2494,7 +2081,6 @@ function getClientHTML() {
2494
2081
  .status-badge-draft { background: #f5f5f5; color: #737373; }
2495
2082
  .status-badge-ready { background: #eff6ff; color: #3b82f6; }
2496
2083
  .status-badge-in_progress { background: #fff7ed; color: #f97316; }
2497
- .status-badge-in_review { background: #f5f3ff; color: #8b5cf6; }
2498
2084
  .status-badge-completed { background: #f0fdf4; color: #22c55e; }
2499
2085
  .status-badge-failed { background: #fef2f2; color: #ef4444; }
2500
2086
 
@@ -2820,16 +2406,14 @@ socket.on('task:output', ({ taskId, line }) => {
2820
2406
  }
2821
2407
  });
2822
2408
 
2823
- socket.on('task:completed', ({ taskId, status }) => {
2409
+ socket.on('task:completed', ({ taskId }) => {
2824
2410
  state.running = state.running.filter(id => id !== taskId);
2825
2411
  const task = state.tasks.find(t => t.id === taskId);
2826
2412
  if (task) {
2827
- // Use status from server (could be 'in_review' or 'completed')
2828
- task.status = status || 'in_review';
2413
+ task.status = 'completed';
2829
2414
  task.passes = true;
2830
2415
  }
2831
- const statusMsg = (status === 'in_review') ? 'ready for review' : 'completed';
2832
- showToast('Task ' + statusMsg + ': ' + (task?.title || taskId), 'success');
2416
+ showToast('Task completed: ' + (task?.title || taskId), 'success');
2833
2417
  render();
2834
2418
  });
2835
2419
 
@@ -2903,53 +2487,6 @@ async function retryTask(id) {
2903
2487
  });
2904
2488
  }
2905
2489
 
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
2490
  async function generateTask(prompt) {
2954
2491
  state.aiGenerating = true;
2955
2492
  render();
@@ -2968,11 +2505,11 @@ async function generateTask(prompt) {
2968
2505
  }
2969
2506
  }
2970
2507
 
2971
- async function startAFK(maxIterations, concurrent) {
2508
+ async function startAFK(maxIterations) {
2972
2509
  await fetch('/api/afk/start', {
2973
2510
  method: 'POST',
2974
2511
  headers: { 'Content-Type': 'application/json' },
2975
- body: JSON.stringify({ maxIterations, concurrent })
2512
+ body: JSON.stringify({ maxIterations })
2976
2513
  });
2977
2514
  }
2978
2515
 
@@ -3153,7 +2690,6 @@ function renderColumn(status, title, tasks) {
3153
2690
  draft: 'To Do',
3154
2691
  ready: 'Ready',
3155
2692
  in_progress: 'In Progress',
3156
- in_review: 'In Review',
3157
2693
  completed: 'Done',
3158
2694
  failed: 'Failed'
3159
2695
  };
@@ -3406,21 +2942,13 @@ function renderModal() {
3406
2942
  <button onclick="state.showModal = null; render();" class="btn btn-ghost p-1.5 text-canvas-500 hover:text-canvas-700">\u2715</button>
3407
2943
  </div>
3408
2944
  <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>
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>
3410
2946
  <div class="space-y-4">
3411
2947
  <div>
3412
2948
  <label class="block text-sm font-medium text-canvas-700 mb-2">Maximum Iterations</label>
3413
2949
  <input type="number" id="afk-iterations" value="10" min="1" max="100"
3414
2950
  class="input w-full">
3415
2951
  </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
2952
  <div class="bg-status-running/10 border border-status-running/20 rounded-lg p-3">
3425
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>
3426
2954
  </div>
@@ -3535,20 +3063,6 @@ function renderSidePanel() {
3535
3063
  \u23F9 Stop Attempt
3536
3064
  </button>
3537
3065
  \` : ''}
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
3066
  \${task.status === 'failed' ? \`
3553
3067
  <button onclick="retryTask('\${task.id}')" class="btn btn-primary px-4 py-2 text-sm">
3554
3068
  \u21BB Retry
@@ -3743,47 +3257,11 @@ function applyTemplate(templateId) {
3743
3257
 
3744
3258
  function handleStartAFK() {
3745
3259
  const iterations = parseInt(document.getElementById('afk-iterations').value) || 10;
3746
- const concurrent = parseInt(document.getElementById('afk-concurrent').value) || 1;
3747
- startAFK(iterations, concurrent);
3260
+ startAFK(iterations);
3748
3261
  state.showModal = null;
3749
3262
  render();
3750
3263
  }
3751
3264
 
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');
3760
- }
3761
- }
3762
-
3763
- async function handleCreatePR(taskId) {
3764
- 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();
3771
- } catch (e) {
3772
- showToast('PR creation failed: ' + e.message, 'error');
3773
- }
3774
- }
3775
-
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
- }
3785
- }
3786
-
3787
3265
  // Filter tasks based on search query
3788
3266
  function filterTasks(tasks) {
3789
3267
  if (!state.searchQuery) return tasks;
@@ -3845,7 +3323,6 @@ function render() {
3845
3323
  \${renderColumn('draft', 'To Do', filterTasks(state.tasks))}
3846
3324
  \${renderColumn('ready', 'Ready', filterTasks(state.tasks))}
3847
3325
  \${renderColumn('in_progress', 'In Progress', filterTasks(state.tasks))}
3848
- \${renderColumn('in_review', 'In Review', filterTasks(state.tasks))}
3849
3326
  \${renderColumn('completed', 'Done', filterTasks(state.tasks))}
3850
3327
  \${renderColumn('failed', 'Failed', filterTasks(state.tasks))}
3851
3328
  </div>
@@ -3910,14 +3387,6 @@ window.closeSidePanel = closeSidePanel;
3910
3387
  window.showTaskMenu = showTaskMenu;
3911
3388
  window.filterTasks = filterTasks;
3912
3389
  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
3390
 
3922
3391
  // Keyboard shortcuts
3923
3392
  document.addEventListener('keydown', (e) => {
@@ -3997,7 +3466,7 @@ function isGitRepo(dir) {
3997
3466
  }
3998
3467
  function hasGitRemote(dir) {
3999
3468
  try {
4000
- const result = execSync2("git remote -v", { cwd: dir, stdio: "pipe" }).toString();
3469
+ const result = execSync("git remote -v", { cwd: dir, stdio: "pipe" }).toString();
4001
3470
  return result.trim().length > 0;
4002
3471
  } catch {
4003
3472
  return false;
@@ -4005,7 +3474,7 @@ function hasGitRemote(dir) {
4005
3474
  }
4006
3475
  function detectRemoteType(dir) {
4007
3476
  try {
4008
- const result = execSync2("git remote get-url origin", { cwd: dir, stdio: "pipe" }).toString().toLowerCase();
3477
+ const result = execSync("git remote get-url origin", { cwd: dir, stdio: "pipe" }).toString().toLowerCase();
4009
3478
  if (result.includes("github.com")) return "github";
4010
3479
  if (result.includes("gitlab.com") || result.includes("gitlab")) return "gitlab";
4011
3480
  if (result.includes("bitbucket.org") || result.includes("bitbucket")) return "bitbucket";
@@ -4016,10 +3485,10 @@ function detectRemoteType(dir) {
4016
3485
  }
4017
3486
  }
4018
3487
  function initializeGit(dir) {
4019
- execSync2("git init", { cwd: dir, stdio: "pipe" });
3488
+ execSync("git init", { cwd: dir, stdio: "pipe" });
4020
3489
  try {
4021
- execSync2("git add -A", { cwd: dir, stdio: "pipe" });
4022
- execSync2('git commit -m "Initial commit"', { cwd: dir, stdio: "pipe" });
3490
+ execSync("git add -A", { cwd: dir, stdio: "pipe" });
3491
+ execSync('git commit -m "Initial commit"', { cwd: dir, stdio: "pipe" });
4023
3492
  } catch {
4024
3493
  }
4025
3494
  }