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 +178 -298
- package/dist/bin/cli.js.map +1 -1
- package/dist/server/index.js +105 -298
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
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
|
|
641
|
+
* Check if a task is currently running
|
|
639
642
|
*/
|
|
640
|
-
|
|
641
|
-
|
|
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
|
|
647
|
+
* Get the currently running task ID
|
|
665
648
|
*/
|
|
666
|
-
|
|
667
|
-
return
|
|
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
|
-
|
|
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
|
|
662
|
+
* Build the prompt for a task - simplified Ralph-style
|
|
793
663
|
*/
|
|
794
|
-
buildTaskPrompt(task, config
|
|
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
|
|
671
|
+
const verifyCommands = [];
|
|
802
672
|
if (config.project.typecheckCommand) {
|
|
803
|
-
|
|
673
|
+
verifyCommands.push(config.project.typecheckCommand);
|
|
804
674
|
}
|
|
805
675
|
if (config.project.testCommand) {
|
|
806
|
-
|
|
676
|
+
verifyCommands.push(config.project.testCommand);
|
|
807
677
|
}
|
|
808
|
-
const verifySection =
|
|
809
|
-
${
|
|
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
|
-
|
|
829
|
-
|
|
692
|
+
## INSTRUCTIONS
|
|
693
|
+
|
|
694
|
+
1. Implement this task as described above.
|
|
830
695
|
|
|
831
|
-
2.
|
|
696
|
+
2. Make sure your changes work correctly.
|
|
832
697
|
|
|
833
|
-
${verifySection}
|
|
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
|
-
${
|
|
839
|
-
- What you implemented
|
|
840
|
-
- Key decisions made
|
|
841
|
-
-
|
|
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
|
-
${
|
|
708
|
+
${verifyCommands.length > 0 ? "6" : "5"}. Commit your changes with a descriptive message.
|
|
846
709
|
|
|
847
|
-
Focus only on this task. When
|
|
710
|
+
Focus only on this task. When done, output: <promise>COMPLETE</promise>`;
|
|
848
711
|
}
|
|
849
712
|
/**
|
|
850
|
-
* Run a
|
|
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
|
|
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:",
|
|
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:
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
1010
|
-
this.cancelTask(
|
|
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
|
-
|
|
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.
|
|
900
|
+
this.runningTask = null;
|
|
1079
901
|
if (this.afkMode) {
|
|
1080
902
|
this.continueAFKMode();
|
|
1081
903
|
}
|
|
1082
904
|
}
|
|
1083
905
|
/**
|
|
1084
|
-
* Cancel
|
|
906
|
+
* Cancel the running task
|
|
1085
907
|
*/
|
|
1086
|
-
cancelTask(
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
-
|
|
915
|
+
childProcess.kill("SIGTERM");
|
|
1095
916
|
setTimeout(() => {
|
|
1096
917
|
try {
|
|
1097
|
-
if (!
|
|
1098
|
-
|
|
918
|
+
if (!childProcess.killed) {
|
|
919
|
+
childProcess.kill("SIGKILL");
|
|
1099
920
|
}
|
|
1100
921
|
} catch {
|
|
1101
922
|
}
|
|
1102
923
|
}, 2e3);
|
|
1103
924
|
} catch {
|
|
1104
925
|
}
|
|
1105
|
-
|
|
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.
|
|
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
|
|
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(
|
|
960
|
+
this.continueAFKMode();
|
|
1142
961
|
}
|
|
1143
962
|
/**
|
|
1144
|
-
* Continue AFK mode - pick
|
|
963
|
+
* Continue AFK mode - pick next task
|
|
1145
964
|
*/
|
|
1146
|
-
continueAFKMode(
|
|
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
|
-
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
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
|
|
1013
|
+
* Cancel running task and stop AFK mode
|
|
1200
1014
|
*/
|
|
1201
1015
|
cancelAll() {
|
|
1202
|
-
|
|
1016
|
+
if (this.runningTask) {
|
|
1203
1017
|
try {
|
|
1204
|
-
runningTask.process.kill("SIGKILL");
|
|
1018
|
+
this.runningTask.process.kill("SIGKILL");
|
|
1205
1019
|
} catch {
|
|
1206
1020
|
}
|
|
1207
|
-
|
|
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
|
|
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
|
|
1823
|
-
executor.startAFKMode(maxIterations || 10
|
|
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
|
|
1848
|
-
res.json({ running:
|
|
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.
|
|
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
|
|
1688
|
+
const runningId = executor.getRunningTaskId();
|
|
1689
|
+
const runningIds = runningId ? [runningId] : [];
|
|
1873
1690
|
const taskLogs = {};
|
|
1874
|
-
|
|
1875
|
-
const logs = executor.getTaskLog(
|
|
1691
|
+
if (runningId) {
|
|
1692
|
+
const logs = executor.getTaskLog(runningId);
|
|
1876
1693
|
if (logs) {
|
|
1877
|
-
taskLogs[
|
|
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
|
|
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) {
|
|
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
|
|
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
|
|
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
|
-
|
|
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..."));
|