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.
@@ -7,9 +7,9 @@ import { fileURLToPath } from "url";
7
7
  import { existsSync as existsSync3 } from "fs";
8
8
 
9
9
  // src/server/services/executor.ts
10
- import { spawn } from "child_process";
10
+ import { spawn, execSync } from "child_process";
11
11
  import { join as join4 } from "path";
12
- import { writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync2, existsSync as existsSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync4 } from "fs";
12
+ import { writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync2, existsSync as existsSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync4, rmSync } from "fs";
13
13
  import { EventEmitter } from "events";
14
14
 
15
15
  // src/server/services/project.ts
@@ -206,6 +206,7 @@ function getRecentProgress(projectPath, lines = 100) {
206
206
  // src/server/services/executor.ts
207
207
  var KANBAN_DIR4 = ".claude-kanban";
208
208
  var LOGS_DIR = "logs";
209
+ var WORKTREES_DIR = "worktrees";
209
210
  var TaskExecutor = class extends EventEmitter {
210
211
  projectPath;
211
212
  runningTasks = /* @__PURE__ */ new Map();
@@ -255,6 +256,136 @@ var TaskExecutor = class extends EventEmitter {
255
256
  if (!existsSync2(logPath)) return null;
256
257
  return readFileSync4(logPath, "utf-8");
257
258
  }
259
+ /**
260
+ * Check if project is a git repository
261
+ */
262
+ isGitRepo() {
263
+ try {
264
+ execSync("git rev-parse --is-inside-work-tree", {
265
+ cwd: this.projectPath,
266
+ stdio: "pipe"
267
+ });
268
+ return true;
269
+ } catch {
270
+ return false;
271
+ }
272
+ }
273
+ /**
274
+ * Get worktrees directory path
275
+ */
276
+ getWorktreesDir() {
277
+ return join4(this.projectPath, KANBAN_DIR4, WORKTREES_DIR);
278
+ }
279
+ /**
280
+ * Get worktree path for a task
281
+ */
282
+ getWorktreePath(taskId) {
283
+ return join4(this.getWorktreesDir(), taskId);
284
+ }
285
+ /**
286
+ * Get branch name for a task
287
+ */
288
+ getBranchName(taskId) {
289
+ return `task/${taskId}`;
290
+ }
291
+ /**
292
+ * Create a git worktree for isolated task execution
293
+ */
294
+ createWorktree(taskId) {
295
+ if (!this.isGitRepo()) {
296
+ console.log("[executor] Not a git repo, skipping worktree creation");
297
+ return null;
298
+ }
299
+ const worktreePath = this.getWorktreePath(taskId);
300
+ const branchName = this.getBranchName(taskId);
301
+ try {
302
+ const worktreesDir = this.getWorktreesDir();
303
+ if (!existsSync2(worktreesDir)) {
304
+ mkdirSync2(worktreesDir, { recursive: true });
305
+ }
306
+ if (existsSync2(worktreePath)) {
307
+ this.removeWorktree(taskId);
308
+ }
309
+ try {
310
+ execSync(`git rev-parse --verify ${branchName}`, {
311
+ cwd: this.projectPath,
312
+ stdio: "pipe"
313
+ });
314
+ execSync(`git branch -D ${branchName}`, {
315
+ cwd: this.projectPath,
316
+ stdio: "pipe"
317
+ });
318
+ } catch {
319
+ }
320
+ execSync(`git worktree add -b ${branchName} "${worktreePath}"`, {
321
+ cwd: this.projectPath,
322
+ stdio: "pipe"
323
+ });
324
+ console.log(`[executor] Created worktree at ${worktreePath} on branch ${branchName}`);
325
+ return { worktreePath, branchName };
326
+ } catch (error) {
327
+ console.error("[executor] Failed to create worktree:", error);
328
+ return null;
329
+ }
330
+ }
331
+ /**
332
+ * Remove a git worktree
333
+ */
334
+ removeWorktree(taskId) {
335
+ const worktreePath = this.getWorktreePath(taskId);
336
+ const branchName = this.getBranchName(taskId);
337
+ try {
338
+ if (existsSync2(worktreePath)) {
339
+ execSync(`git worktree remove "${worktreePath}" --force`, {
340
+ cwd: this.projectPath,
341
+ stdio: "pipe"
342
+ });
343
+ console.log(`[executor] Removed worktree at ${worktreePath}`);
344
+ }
345
+ } catch (error) {
346
+ console.error("[executor] Failed to remove worktree via git:", error);
347
+ try {
348
+ if (existsSync2(worktreePath)) {
349
+ rmSync(worktreePath, { recursive: true, force: true });
350
+ execSync("git worktree prune", {
351
+ cwd: this.projectPath,
352
+ stdio: "pipe"
353
+ });
354
+ }
355
+ } catch {
356
+ console.error("[executor] Failed to force remove worktree directory");
357
+ }
358
+ }
359
+ try {
360
+ execSync(`git branch -D ${branchName}`, {
361
+ cwd: this.projectPath,
362
+ stdio: "pipe"
363
+ });
364
+ console.log(`[executor] Deleted branch ${branchName}`);
365
+ } catch {
366
+ }
367
+ }
368
+ /**
369
+ * Merge worktree branch back to main branch
370
+ */
371
+ mergeWorktreeBranch(taskId) {
372
+ const branchName = this.getBranchName(taskId);
373
+ try {
374
+ const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
375
+ cwd: this.projectPath,
376
+ encoding: "utf-8"
377
+ }).trim();
378
+ execSync(`git merge ${branchName} --no-edit`, {
379
+ cwd: this.projectPath,
380
+ stdio: "pipe"
381
+ });
382
+ console.log(`[executor] Merged ${branchName} into ${currentBranch}`);
383
+ return true;
384
+ } catch (error) {
385
+ console.error("[executor] Failed to merge branch:", error);
386
+ return false;
387
+ }
388
+ }
258
389
  /**
259
390
  * Get number of currently running tasks
260
391
  */
@@ -340,10 +471,10 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
340
471
  }
341
472
  updateTask(this.projectPath, taskId, { status: "in_progress" });
342
473
  const startedAt = /* @__PURE__ */ new Date();
474
+ const worktreeInfo = this.createWorktree(taskId);
475
+ const executionPath = worktreeInfo?.worktreePath || this.projectPath;
343
476
  const prompt = this.buildTaskPrompt(task, config);
344
477
  const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
345
- const prdPath = join4(kanbanDir, "prd.json");
346
- const progressPath = join4(kanbanDir, "progress.txt");
347
478
  const promptFile = join4(kanbanDir, `prompt-${taskId}.txt`);
348
479
  writeFileSync4(promptFile, prompt);
349
480
  const args = [];
@@ -358,9 +489,13 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
358
489
  const commandDisplay = `${config.agent.command} ${args.join(" ")}`;
359
490
  const fullCommand = `${config.agent.command} ${args.join(" ")}`;
360
491
  console.log("[executor] Command:", fullCommand);
361
- console.log("[executor] CWD:", this.projectPath);
492
+ console.log("[executor] CWD:", executionPath);
493
+ if (worktreeInfo) {
494
+ console.log("[executor] Using worktree:", worktreeInfo.worktreePath);
495
+ console.log("[executor] Branch:", worktreeInfo.branchName);
496
+ }
362
497
  const childProcess = spawn("bash", ["-c", fullCommand], {
363
- cwd: this.projectPath,
498
+ cwd: executionPath,
364
499
  env: {
365
500
  ...process.env,
366
501
  TERM: "xterm-256color",
@@ -376,7 +511,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
376
511
  taskId,
377
512
  process: childProcess,
378
513
  startedAt,
379
- output: []
514
+ output: [],
515
+ worktreePath: worktreeInfo?.worktreePath,
516
+ branchName: worktreeInfo?.branchName
380
517
  };
381
518
  this.runningTasks.set(taskId, runningTask);
382
519
  this.initLogFile(taskId);
@@ -388,6 +525,12 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
388
525
  this.emit("task:started", { taskId, timestamp: startedAt.toISOString() });
389
526
  logOutput(`[claude-kanban] Starting task: ${task.title}
390
527
  `);
528
+ if (worktreeInfo) {
529
+ logOutput(`[claude-kanban] Worktree: ${worktreeInfo.worktreePath}
530
+ `);
531
+ logOutput(`[claude-kanban] Branch: ${worktreeInfo.branchName}
532
+ `);
533
+ }
391
534
  logOutput(`[claude-kanban] Command: ${commandDisplay}
392
535
  `);
393
536
  logOutput(`[claude-kanban] Process spawned (PID: ${childProcess.pid})
@@ -444,6 +587,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
444
587
  unlinkSync(promptFile);
445
588
  } catch {
446
589
  }
590
+ if (worktreeInfo) {
591
+ this.removeWorktree(taskId);
592
+ }
447
593
  updateTask(this.projectPath, taskId, { status: "failed", passes: false });
448
594
  const endedAt = /* @__PURE__ */ new Date();
449
595
  addExecutionEntry(this.projectPath, taskId, {
@@ -485,6 +631,15 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
485
631
  const isComplete = output.includes("<promise>COMPLETE</promise>");
486
632
  const task = getTaskById(this.projectPath, taskId);
487
633
  if (isComplete || exitCode === 0) {
634
+ if (runningTask.worktreePath && runningTask.branchName) {
635
+ const merged = this.mergeWorktreeBranch(taskId);
636
+ if (merged) {
637
+ console.log(`[executor] Successfully merged ${runningTask.branchName}`);
638
+ } else {
639
+ console.log(`[executor] Failed to merge ${runningTask.branchName}, branch preserved for manual merge`);
640
+ }
641
+ this.removeWorktree(taskId);
642
+ }
488
643
  updateTask(this.projectPath, taskId, {
489
644
  status: "completed",
490
645
  passes: true
@@ -504,6 +659,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
504
659
  this.emit("task:completed", { taskId, duration });
505
660
  this.afkTasksCompleted++;
506
661
  } else {
662
+ if (runningTask.worktreePath) {
663
+ this.removeWorktree(taskId);
664
+ }
507
665
  updateTask(this.projectPath, taskId, {
508
666
  status: "failed",
509
667
  passes: false
@@ -552,6 +710,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
552
710
  }, 2e3);
553
711
  } catch {
554
712
  }
713
+ if (runningTask.worktreePath) {
714
+ this.removeWorktree(taskId);
715
+ }
555
716
  updateTask(this.projectPath, taskId, {
556
717
  status: "ready"
557
718
  });
@@ -651,6 +812,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
651
812
  runningTask.process.kill("SIGKILL");
652
813
  } catch {
653
814
  }
815
+ if (runningTask.worktreePath) {
816
+ this.removeWorktree(taskId);
817
+ }
654
818
  this.runningTasks.delete(taskId);
655
819
  }
656
820
  this.stopAFKMode();