claude-kanban 0.1.0 → 0.2.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
@@ -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,194 @@ var TaskExecutor = class extends EventEmitter {
493
494
  if (!existsSync2(logPath)) return null;
494
495
  return readFileSync4(logPath, "utf-8");
495
496
  }
497
+ /**
498
+ * Format tool use for display in logs
499
+ */
500
+ formatToolUse(toolName, input) {
501
+ const truncate = (str, maxLen = 80) => {
502
+ if (str.length <= maxLen) return str;
503
+ return str.slice(0, maxLen) + "...";
504
+ };
505
+ switch (toolName) {
506
+ case "Bash":
507
+ return `[Bash] $ ${truncate(String(input.command || ""))}
508
+ `;
509
+ case "Read":
510
+ return `[Read] ${input.file_path}
511
+ `;
512
+ case "Edit":
513
+ return `[Edit] ${input.file_path}
514
+ `;
515
+ case "Write":
516
+ return `[Write] ${input.file_path}
517
+ `;
518
+ case "Grep":
519
+ return `[Grep] "${truncate(String(input.pattern || ""))}" in ${input.path || "."}
520
+ `;
521
+ case "Glob":
522
+ return `[Glob] ${input.pattern} in ${input.path || "."}
523
+ `;
524
+ case "Task":
525
+ return `[Task] ${input.description || truncate(String(input.prompt || ""))}
526
+ `;
527
+ case "TodoWrite":
528
+ const todos = input.todos;
529
+ if (todos && Array.isArray(todos)) {
530
+ const summary = todos.map((t) => ` ${t.status === "completed" ? "\u2713" : t.status === "in_progress" ? "\u2192" : "\u25CB"} ${truncate(t.content, 60)}`).join("\n");
531
+ return `[TodoWrite]
532
+ ${summary}
533
+ `;
534
+ }
535
+ return `[TodoWrite]
536
+ `;
537
+ case "WebFetch":
538
+ return `[WebFetch] ${input.url}
539
+ `;
540
+ case "WebSearch":
541
+ return `[WebSearch] "${truncate(String(input.query || ""))}"
542
+ `;
543
+ default:
544
+ const meaningfulParams = ["file_path", "path", "command", "query", "url", "pattern", "target"];
545
+ for (const param of meaningfulParams) {
546
+ if (input[param]) {
547
+ return `[${toolName}] ${truncate(String(input[param]))}
548
+ `;
549
+ }
550
+ }
551
+ return `[${toolName}]
552
+ `;
553
+ }
554
+ }
555
+ /**
556
+ * Check if project is a git repository
557
+ */
558
+ isGitRepo() {
559
+ try {
560
+ execSync("git rev-parse --is-inside-work-tree", {
561
+ cwd: this.projectPath,
562
+ stdio: "pipe"
563
+ });
564
+ return true;
565
+ } catch {
566
+ return false;
567
+ }
568
+ }
569
+ /**
570
+ * Get worktrees directory path
571
+ */
572
+ getWorktreesDir() {
573
+ return join4(this.projectPath, KANBAN_DIR4, WORKTREES_DIR);
574
+ }
575
+ /**
576
+ * Get worktree path for a task
577
+ */
578
+ getWorktreePath(taskId) {
579
+ return join4(this.getWorktreesDir(), taskId);
580
+ }
581
+ /**
582
+ * Get branch name for a task
583
+ */
584
+ getBranchName(taskId) {
585
+ return `task/${taskId}`;
586
+ }
587
+ /**
588
+ * Create a git worktree for isolated task execution
589
+ */
590
+ createWorktree(taskId) {
591
+ if (!this.isGitRepo()) {
592
+ console.log("[executor] Not a git repo, skipping worktree creation");
593
+ return null;
594
+ }
595
+ const worktreePath = this.getWorktreePath(taskId);
596
+ const branchName = this.getBranchName(taskId);
597
+ try {
598
+ const worktreesDir = this.getWorktreesDir();
599
+ if (!existsSync2(worktreesDir)) {
600
+ mkdirSync2(worktreesDir, { recursive: true });
601
+ }
602
+ if (existsSync2(worktreePath)) {
603
+ this.removeWorktree(taskId);
604
+ }
605
+ try {
606
+ execSync(`git rev-parse --verify ${branchName}`, {
607
+ cwd: this.projectPath,
608
+ stdio: "pipe"
609
+ });
610
+ execSync(`git branch -D ${branchName}`, {
611
+ cwd: this.projectPath,
612
+ stdio: "pipe"
613
+ });
614
+ } catch {
615
+ }
616
+ execSync(`git worktree add -b ${branchName} "${worktreePath}"`, {
617
+ cwd: this.projectPath,
618
+ stdio: "pipe"
619
+ });
620
+ console.log(`[executor] Created worktree at ${worktreePath} on branch ${branchName}`);
621
+ return { worktreePath, branchName };
622
+ } catch (error) {
623
+ console.error("[executor] Failed to create worktree:", error);
624
+ return null;
625
+ }
626
+ }
627
+ /**
628
+ * Remove a git worktree
629
+ */
630
+ removeWorktree(taskId) {
631
+ const worktreePath = this.getWorktreePath(taskId);
632
+ const branchName = this.getBranchName(taskId);
633
+ try {
634
+ if (existsSync2(worktreePath)) {
635
+ execSync(`git worktree remove "${worktreePath}" --force`, {
636
+ cwd: this.projectPath,
637
+ stdio: "pipe"
638
+ });
639
+ console.log(`[executor] Removed worktree at ${worktreePath}`);
640
+ }
641
+ } catch (error) {
642
+ console.error("[executor] Failed to remove worktree via git:", error);
643
+ try {
644
+ if (existsSync2(worktreePath)) {
645
+ rmSync(worktreePath, { recursive: true, force: true });
646
+ execSync("git worktree prune", {
647
+ cwd: this.projectPath,
648
+ stdio: "pipe"
649
+ });
650
+ }
651
+ } catch {
652
+ console.error("[executor] Failed to force remove worktree directory");
653
+ }
654
+ }
655
+ try {
656
+ execSync(`git branch -D ${branchName}`, {
657
+ cwd: this.projectPath,
658
+ stdio: "pipe"
659
+ });
660
+ console.log(`[executor] Deleted branch ${branchName}`);
661
+ } catch {
662
+ }
663
+ }
664
+ /**
665
+ * Merge worktree branch back to main branch
666
+ */
667
+ mergeWorktreeBranch(taskId) {
668
+ const branchName = this.getBranchName(taskId);
669
+ try {
670
+ const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
671
+ cwd: this.projectPath,
672
+ encoding: "utf-8"
673
+ }).trim();
674
+ execSync(`git merge ${branchName} --no-edit`, {
675
+ cwd: this.projectPath,
676
+ stdio: "pipe"
677
+ });
678
+ console.log(`[executor] Merged ${branchName} into ${currentBranch}`);
679
+ return true;
680
+ } catch (error) {
681
+ console.error("[executor] Failed to merge branch:", error);
682
+ return false;
683
+ }
684
+ }
496
685
  /**
497
686
  * Get number of currently running tasks
498
687
  */
@@ -578,10 +767,10 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
578
767
  }
579
768
  updateTask(this.projectPath, taskId, { status: "in_progress" });
580
769
  const startedAt = /* @__PURE__ */ new Date();
770
+ const worktreeInfo = this.createWorktree(taskId);
771
+ const executionPath = worktreeInfo?.worktreePath || this.projectPath;
581
772
  const prompt = this.buildTaskPrompt(task, config);
582
773
  const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
583
- const prdPath = join4(kanbanDir, "prd.json");
584
- const progressPath = join4(kanbanDir, "progress.txt");
585
774
  const promptFile = join4(kanbanDir, `prompt-${taskId}.txt`);
586
775
  writeFileSync4(promptFile, prompt);
587
776
  const args = [];
@@ -596,9 +785,13 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
596
785
  const commandDisplay = `${config.agent.command} ${args.join(" ")}`;
597
786
  const fullCommand = `${config.agent.command} ${args.join(" ")}`;
598
787
  console.log("[executor] Command:", fullCommand);
599
- console.log("[executor] CWD:", this.projectPath);
788
+ console.log("[executor] CWD:", executionPath);
789
+ if (worktreeInfo) {
790
+ console.log("[executor] Using worktree:", worktreeInfo.worktreePath);
791
+ console.log("[executor] Branch:", worktreeInfo.branchName);
792
+ }
600
793
  const childProcess = spawn("bash", ["-c", fullCommand], {
601
- cwd: this.projectPath,
794
+ cwd: executionPath,
602
795
  env: {
603
796
  ...process.env,
604
797
  TERM: "xterm-256color",
@@ -614,7 +807,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
614
807
  taskId,
615
808
  process: childProcess,
616
809
  startedAt,
617
- output: []
810
+ output: [],
811
+ worktreePath: worktreeInfo?.worktreePath,
812
+ branchName: worktreeInfo?.branchName
618
813
  };
619
814
  this.runningTasks.set(taskId, runningTask);
620
815
  this.initLogFile(taskId);
@@ -626,6 +821,12 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
626
821
  this.emit("task:started", { taskId, timestamp: startedAt.toISOString() });
627
822
  logOutput(`[claude-kanban] Starting task: ${task.title}
628
823
  `);
824
+ if (worktreeInfo) {
825
+ logOutput(`[claude-kanban] Worktree: ${worktreeInfo.worktreePath}
826
+ `);
827
+ logOutput(`[claude-kanban] Branch: ${worktreeInfo.branchName}
828
+ `);
829
+ }
629
830
  logOutput(`[claude-kanban] Command: ${commandDisplay}
630
831
  `);
631
832
  logOutput(`[claude-kanban] Process spawned (PID: ${childProcess.pid})
@@ -645,8 +846,7 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
645
846
  if (block.type === "text") {
646
847
  text += block.text;
647
848
  } else if (block.type === "tool_use") {
648
- text += `[Tool: ${block.name}]
649
- `;
849
+ text += this.formatToolUse(block.name, block.input);
650
850
  }
651
851
  }
652
852
  } else if (json.type === "content_block_delta" && json.delta?.text) {
@@ -682,6 +882,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
682
882
  unlinkSync(promptFile);
683
883
  } catch {
684
884
  }
885
+ if (worktreeInfo) {
886
+ this.removeWorktree(taskId);
887
+ }
685
888
  updateTask(this.projectPath, taskId, { status: "failed", passes: false });
686
889
  const endedAt = /* @__PURE__ */ new Date();
687
890
  addExecutionEntry(this.projectPath, taskId, {
@@ -723,6 +926,15 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
723
926
  const isComplete = output.includes("<promise>COMPLETE</promise>");
724
927
  const task = getTaskById(this.projectPath, taskId);
725
928
  if (isComplete || exitCode === 0) {
929
+ if (runningTask.worktreePath && runningTask.branchName) {
930
+ const merged = this.mergeWorktreeBranch(taskId);
931
+ if (merged) {
932
+ console.log(`[executor] Successfully merged ${runningTask.branchName}`);
933
+ } else {
934
+ console.log(`[executor] Failed to merge ${runningTask.branchName}, branch preserved for manual merge`);
935
+ }
936
+ this.removeWorktree(taskId);
937
+ }
726
938
  updateTask(this.projectPath, taskId, {
727
939
  status: "completed",
728
940
  passes: true
@@ -742,6 +954,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
742
954
  this.emit("task:completed", { taskId, duration });
743
955
  this.afkTasksCompleted++;
744
956
  } else {
957
+ if (runningTask.worktreePath) {
958
+ this.removeWorktree(taskId);
959
+ }
745
960
  updateTask(this.projectPath, taskId, {
746
961
  status: "failed",
747
962
  passes: false
@@ -790,6 +1005,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
790
1005
  }, 2e3);
791
1006
  } catch {
792
1007
  }
1008
+ if (runningTask.worktreePath) {
1009
+ this.removeWorktree(taskId);
1010
+ }
793
1011
  updateTask(this.projectPath, taskId, {
794
1012
  status: "ready"
795
1013
  });
@@ -889,6 +1107,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
889
1107
  runningTask.process.kill("SIGKILL");
890
1108
  } catch {
891
1109
  }
1110
+ if (runningTask.worktreePath) {
1111
+ this.removeWorktree(taskId);
1112
+ }
892
1113
  this.runningTasks.delete(taskId);
893
1114
  }
894
1115
  this.stopAFKMode();