claude-kanban 0.1.0 → 0.2.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
@@ -14,9 +14,9 @@ import { fileURLToPath } from "url";
14
14
  import { existsSync as existsSync3 } from "fs";
15
15
 
16
16
  // src/server/services/executor.ts
17
- import { spawn } from "child_process";
17
+ import { spawn, execSync } from "child_process";
18
18
  import { join as join4 } from "path";
19
- import { writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync2, existsSync as existsSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync4 } from "fs";
19
+ import { writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync2, existsSync as existsSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync4, rmSync } from "fs";
20
20
  import { EventEmitter } from "events";
21
21
 
22
22
  // src/server/services/project.ts
@@ -444,6 +444,7 @@ function getRecentProgress(projectPath, lines = 100) {
444
444
  // src/server/services/executor.ts
445
445
  var KANBAN_DIR4 = ".claude-kanban";
446
446
  var LOGS_DIR = "logs";
447
+ var WORKTREES_DIR = "worktrees";
447
448
  var TaskExecutor = class extends EventEmitter {
448
449
  projectPath;
449
450
  runningTasks = /* @__PURE__ */ new Map();
@@ -493,6 +494,136 @@ var TaskExecutor = class extends EventEmitter {
493
494
  if (!existsSync2(logPath)) return null;
494
495
  return readFileSync4(logPath, "utf-8");
495
496
  }
497
+ /**
498
+ * Check if project is a git repository
499
+ */
500
+ isGitRepo() {
501
+ try {
502
+ execSync("git rev-parse --is-inside-work-tree", {
503
+ cwd: this.projectPath,
504
+ stdio: "pipe"
505
+ });
506
+ return true;
507
+ } catch {
508
+ return false;
509
+ }
510
+ }
511
+ /**
512
+ * Get worktrees directory path
513
+ */
514
+ getWorktreesDir() {
515
+ return join4(this.projectPath, KANBAN_DIR4, WORKTREES_DIR);
516
+ }
517
+ /**
518
+ * Get worktree path for a task
519
+ */
520
+ getWorktreePath(taskId) {
521
+ return join4(this.getWorktreesDir(), taskId);
522
+ }
523
+ /**
524
+ * Get branch name for a task
525
+ */
526
+ getBranchName(taskId) {
527
+ return `task/${taskId}`;
528
+ }
529
+ /**
530
+ * Create a git worktree for isolated task execution
531
+ */
532
+ createWorktree(taskId) {
533
+ if (!this.isGitRepo()) {
534
+ console.log("[executor] Not a git repo, skipping worktree creation");
535
+ return null;
536
+ }
537
+ const worktreePath = this.getWorktreePath(taskId);
538
+ const branchName = this.getBranchName(taskId);
539
+ try {
540
+ const worktreesDir = this.getWorktreesDir();
541
+ if (!existsSync2(worktreesDir)) {
542
+ mkdirSync2(worktreesDir, { recursive: true });
543
+ }
544
+ if (existsSync2(worktreePath)) {
545
+ this.removeWorktree(taskId);
546
+ }
547
+ try {
548
+ execSync(`git rev-parse --verify ${branchName}`, {
549
+ cwd: this.projectPath,
550
+ stdio: "pipe"
551
+ });
552
+ execSync(`git branch -D ${branchName}`, {
553
+ cwd: this.projectPath,
554
+ stdio: "pipe"
555
+ });
556
+ } catch {
557
+ }
558
+ execSync(`git worktree add -b ${branchName} "${worktreePath}"`, {
559
+ cwd: this.projectPath,
560
+ stdio: "pipe"
561
+ });
562
+ console.log(`[executor] Created worktree at ${worktreePath} on branch ${branchName}`);
563
+ return { worktreePath, branchName };
564
+ } catch (error) {
565
+ console.error("[executor] Failed to create worktree:", error);
566
+ return null;
567
+ }
568
+ }
569
+ /**
570
+ * Remove a git worktree
571
+ */
572
+ removeWorktree(taskId) {
573
+ const worktreePath = this.getWorktreePath(taskId);
574
+ const branchName = this.getBranchName(taskId);
575
+ try {
576
+ if (existsSync2(worktreePath)) {
577
+ execSync(`git worktree remove "${worktreePath}" --force`, {
578
+ cwd: this.projectPath,
579
+ stdio: "pipe"
580
+ });
581
+ console.log(`[executor] Removed worktree at ${worktreePath}`);
582
+ }
583
+ } catch (error) {
584
+ console.error("[executor] Failed to remove worktree via git:", error);
585
+ try {
586
+ if (existsSync2(worktreePath)) {
587
+ rmSync(worktreePath, { recursive: true, force: true });
588
+ execSync("git worktree prune", {
589
+ cwd: this.projectPath,
590
+ stdio: "pipe"
591
+ });
592
+ }
593
+ } catch {
594
+ console.error("[executor] Failed to force remove worktree directory");
595
+ }
596
+ }
597
+ try {
598
+ execSync(`git branch -D ${branchName}`, {
599
+ cwd: this.projectPath,
600
+ stdio: "pipe"
601
+ });
602
+ console.log(`[executor] Deleted branch ${branchName}`);
603
+ } catch {
604
+ }
605
+ }
606
+ /**
607
+ * Merge worktree branch back to main branch
608
+ */
609
+ mergeWorktreeBranch(taskId) {
610
+ const branchName = this.getBranchName(taskId);
611
+ try {
612
+ const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
613
+ cwd: this.projectPath,
614
+ encoding: "utf-8"
615
+ }).trim();
616
+ execSync(`git merge ${branchName} --no-edit`, {
617
+ cwd: this.projectPath,
618
+ stdio: "pipe"
619
+ });
620
+ console.log(`[executor] Merged ${branchName} into ${currentBranch}`);
621
+ return true;
622
+ } catch (error) {
623
+ console.error("[executor] Failed to merge branch:", error);
624
+ return false;
625
+ }
626
+ }
496
627
  /**
497
628
  * Get number of currently running tasks
498
629
  */
@@ -578,10 +709,10 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
578
709
  }
579
710
  updateTask(this.projectPath, taskId, { status: "in_progress" });
580
711
  const startedAt = /* @__PURE__ */ new Date();
712
+ const worktreeInfo = this.createWorktree(taskId);
713
+ const executionPath = worktreeInfo?.worktreePath || this.projectPath;
581
714
  const prompt = this.buildTaskPrompt(task, config);
582
715
  const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
583
- const prdPath = join4(kanbanDir, "prd.json");
584
- const progressPath = join4(kanbanDir, "progress.txt");
585
716
  const promptFile = join4(kanbanDir, `prompt-${taskId}.txt`);
586
717
  writeFileSync4(promptFile, prompt);
587
718
  const args = [];
@@ -596,9 +727,13 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
596
727
  const commandDisplay = `${config.agent.command} ${args.join(" ")}`;
597
728
  const fullCommand = `${config.agent.command} ${args.join(" ")}`;
598
729
  console.log("[executor] Command:", fullCommand);
599
- console.log("[executor] CWD:", this.projectPath);
730
+ console.log("[executor] CWD:", executionPath);
731
+ if (worktreeInfo) {
732
+ console.log("[executor] Using worktree:", worktreeInfo.worktreePath);
733
+ console.log("[executor] Branch:", worktreeInfo.branchName);
734
+ }
600
735
  const childProcess = spawn("bash", ["-c", fullCommand], {
601
- cwd: this.projectPath,
736
+ cwd: executionPath,
602
737
  env: {
603
738
  ...process.env,
604
739
  TERM: "xterm-256color",
@@ -614,7 +749,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
614
749
  taskId,
615
750
  process: childProcess,
616
751
  startedAt,
617
- output: []
752
+ output: [],
753
+ worktreePath: worktreeInfo?.worktreePath,
754
+ branchName: worktreeInfo?.branchName
618
755
  };
619
756
  this.runningTasks.set(taskId, runningTask);
620
757
  this.initLogFile(taskId);
@@ -626,6 +763,12 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
626
763
  this.emit("task:started", { taskId, timestamp: startedAt.toISOString() });
627
764
  logOutput(`[claude-kanban] Starting task: ${task.title}
628
765
  `);
766
+ if (worktreeInfo) {
767
+ logOutput(`[claude-kanban] Worktree: ${worktreeInfo.worktreePath}
768
+ `);
769
+ logOutput(`[claude-kanban] Branch: ${worktreeInfo.branchName}
770
+ `);
771
+ }
629
772
  logOutput(`[claude-kanban] Command: ${commandDisplay}
630
773
  `);
631
774
  logOutput(`[claude-kanban] Process spawned (PID: ${childProcess.pid})
@@ -682,6 +825,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
682
825
  unlinkSync(promptFile);
683
826
  } catch {
684
827
  }
828
+ if (worktreeInfo) {
829
+ this.removeWorktree(taskId);
830
+ }
685
831
  updateTask(this.projectPath, taskId, { status: "failed", passes: false });
686
832
  const endedAt = /* @__PURE__ */ new Date();
687
833
  addExecutionEntry(this.projectPath, taskId, {
@@ -723,6 +869,15 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
723
869
  const isComplete = output.includes("<promise>COMPLETE</promise>");
724
870
  const task = getTaskById(this.projectPath, taskId);
725
871
  if (isComplete || exitCode === 0) {
872
+ if (runningTask.worktreePath && runningTask.branchName) {
873
+ const merged = this.mergeWorktreeBranch(taskId);
874
+ if (merged) {
875
+ console.log(`[executor] Successfully merged ${runningTask.branchName}`);
876
+ } else {
877
+ console.log(`[executor] Failed to merge ${runningTask.branchName}, branch preserved for manual merge`);
878
+ }
879
+ this.removeWorktree(taskId);
880
+ }
726
881
  updateTask(this.projectPath, taskId, {
727
882
  status: "completed",
728
883
  passes: true
@@ -742,6 +897,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
742
897
  this.emit("task:completed", { taskId, duration });
743
898
  this.afkTasksCompleted++;
744
899
  } else {
900
+ if (runningTask.worktreePath) {
901
+ this.removeWorktree(taskId);
902
+ }
745
903
  updateTask(this.projectPath, taskId, {
746
904
  status: "failed",
747
905
  passes: false
@@ -790,6 +948,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
790
948
  }, 2e3);
791
949
  } catch {
792
950
  }
951
+ if (runningTask.worktreePath) {
952
+ this.removeWorktree(taskId);
953
+ }
793
954
  updateTask(this.projectPath, taskId, {
794
955
  status: "ready"
795
956
  });
@@ -889,6 +1050,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
889
1050
  runningTask.process.kill("SIGKILL");
890
1051
  } catch {
891
1052
  }
1053
+ if (runningTask.worktreePath) {
1054
+ this.removeWorktree(taskId);
1055
+ }
892
1056
  this.runningTasks.delete(taskId);
893
1057
  }
894
1058
  this.stopAFKMode();