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 +120 -651
- package/dist/bin/cli.js.map +1 -1
- package/dist/server/index.js +114 -645
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
641
|
+
* Check if a task is currently running
|
|
645
642
|
*/
|
|
646
|
-
|
|
647
|
-
|
|
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
|
|
647
|
+
* Get the currently running task ID
|
|
671
648
|
*/
|
|
672
|
-
|
|
673
|
-
return
|
|
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
|
-
|
|
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
|
|
662
|
+
* Build the prompt for a task - simplified Ralph-style
|
|
799
663
|
*/
|
|
800
|
-
buildTaskPrompt(task, config
|
|
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
|
|
671
|
+
const verifyCommands = [];
|
|
808
672
|
if (config.project.typecheckCommand) {
|
|
809
|
-
|
|
673
|
+
verifyCommands.push(config.project.typecheckCommand);
|
|
810
674
|
}
|
|
811
675
|
if (config.project.testCommand) {
|
|
812
|
-
|
|
676
|
+
verifyCommands.push(config.project.testCommand);
|
|
813
677
|
}
|
|
814
|
-
const verifySection =
|
|
815
|
-
${
|
|
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
|
-
|
|
835
|
-
|
|
692
|
+
## INSTRUCTIONS
|
|
693
|
+
|
|
694
|
+
1. Implement this task as described above.
|
|
836
695
|
|
|
837
|
-
2.
|
|
696
|
+
2. Make sure your changes work correctly.
|
|
838
697
|
|
|
839
|
-
${verifySection}
|
|
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
|
-
|
|
701
|
+
- Set "status": "completed"
|
|
843
702
|
|
|
844
|
-
${
|
|
845
|
-
- What you implemented
|
|
846
|
-
- Key decisions made
|
|
847
|
-
-
|
|
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
|
-
${
|
|
708
|
+
${verifyCommands.length > 0 ? "6" : "5"}. Commit your changes with a descriptive message.
|
|
852
709
|
|
|
853
|
-
Focus only on this task. When
|
|
710
|
+
Focus only on this task. When done, output: <promise>COMPLETE</promise>`;
|
|
854
711
|
}
|
|
855
712
|
/**
|
|
856
|
-
* Run a
|
|
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
|
|
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:",
|
|
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:
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
1016
|
-
this.cancelTask(
|
|
844
|
+
if (this.runningTask?.taskId === taskId) {
|
|
845
|
+
this.cancelTask("Timeout exceeded");
|
|
1017
846
|
}
|
|
1018
847
|
}, timeoutMs);
|
|
1019
848
|
}
|
|
1020
849
|
/**
|
|
1021
|
-
* Handle task completion
|
|
850
|
+
* Handle task completion
|
|
1022
851
|
*/
|
|
1023
852
|
handleTaskComplete(taskId, exitCode, startedAt) {
|
|
1024
|
-
|
|
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: "
|
|
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: "
|
|
873
|
+
status: "completed",
|
|
1049
874
|
duration
|
|
1050
875
|
});
|
|
1051
|
-
this.emit("task:completed", { taskId, duration
|
|
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.
|
|
900
|
+
this.runningTask = null;
|
|
1079
901
|
if (this.afkMode) {
|
|
1080
902
|
this.continueAFKMode();
|
|
1081
903
|
}
|
|
1082
904
|
}
|
|
1083
905
|
/**
|
|
1084
|
-
*
|
|
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
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|
-
|
|
915
|
+
childProcess.kill("SIGTERM");
|
|
1240
916
|
setTimeout(() => {
|
|
1241
917
|
try {
|
|
1242
|
-
if (!
|
|
1243
|
-
|
|
918
|
+
if (!childProcess.killed) {
|
|
919
|
+
childProcess.kill("SIGKILL");
|
|
1244
920
|
}
|
|
1245
921
|
} catch {
|
|
1246
922
|
}
|
|
1247
923
|
}, 2e3);
|
|
1248
924
|
} catch {
|
|
1249
925
|
}
|
|
1250
|
-
|
|
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.
|
|
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
|
|
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(
|
|
960
|
+
this.continueAFKMode();
|
|
1287
961
|
}
|
|
1288
962
|
/**
|
|
1289
|
-
* Continue AFK mode - pick
|
|
963
|
+
* Continue AFK mode - pick next task
|
|
1290
964
|
*/
|
|
1291
|
-
continueAFKMode(
|
|
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
|
-
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
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
|
|
1013
|
+
* Cancel running task and stop AFK mode
|
|
1345
1014
|
*/
|
|
1346
1015
|
cancelAll() {
|
|
1347
|
-
|
|
1016
|
+
if (this.runningTask) {
|
|
1348
1017
|
try {
|
|
1349
|
-
runningTask.process.kill("SIGKILL");
|
|
1018
|
+
this.runningTask.process.kill("SIGKILL");
|
|
1350
1019
|
} catch {
|
|
1351
1020
|
}
|
|
1352
|
-
|
|
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
|
|
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
|
|
2052
|
-
executor.startAFKMode(maxIterations || 10
|
|
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
|
|
2077
|
-
res.json({ running:
|
|
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.
|
|
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
|
|
1688
|
+
const runningId = executor.getRunningTaskId();
|
|
1689
|
+
const runningIds = runningId ? [runningId] : [];
|
|
2102
1690
|
const taskLogs = {};
|
|
2103
|
-
|
|
2104
|
-
const logs = executor.getTaskLog(
|
|
1691
|
+
if (runningId) {
|
|
1692
|
+
const logs = executor.getTaskLog(runningId);
|
|
2105
1693
|
if (logs) {
|
|
2106
|
-
taskLogs[
|
|
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
|
|
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
|
|
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
|
-
|
|
2828
|
-
task.status = status || 'in_review';
|
|
2413
|
+
task.status = 'completed';
|
|
2829
2414
|
task.passes = true;
|
|
2830
2415
|
}
|
|
2831
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
3488
|
+
execSync("git init", { cwd: dir, stdio: "pipe" });
|
|
4020
3489
|
try {
|
|
4021
|
-
|
|
4022
|
-
|
|
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
|
}
|