claude-kanban 0.3.0 → 0.4.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 +450 -64
- package/dist/bin/cli.js.map +1 -1
- package/dist/server/index.js +377 -64
- 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 as execSync2 } 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";
|
|
@@ -16,7 +20,7 @@ import { existsSync as existsSync3 } from "fs";
|
|
|
16
20
|
// src/server/services/executor.ts
|
|
17
21
|
import { spawn, execSync } 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, rmSync
|
|
23
|
+
import { writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync2, existsSync as existsSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync4, rmSync } from "fs";
|
|
20
24
|
import { EventEmitter } from "events";
|
|
21
25
|
|
|
22
26
|
// src/server/services/project.ts
|
|
@@ -454,6 +458,7 @@ function getTaskCounts(projectPath) {
|
|
|
454
458
|
draft: 0,
|
|
455
459
|
ready: 0,
|
|
456
460
|
in_progress: 0,
|
|
461
|
+
in_review: 0,
|
|
457
462
|
completed: 0,
|
|
458
463
|
failed: 0
|
|
459
464
|
};
|
|
@@ -530,6 +535,7 @@ var WORKTREES_DIR = "worktrees";
|
|
|
530
535
|
var TaskExecutor = class extends EventEmitter {
|
|
531
536
|
projectPath;
|
|
532
537
|
runningTasks = /* @__PURE__ */ new Map();
|
|
538
|
+
reviewingTasks = /* @__PURE__ */ new Map();
|
|
533
539
|
afkMode = false;
|
|
534
540
|
afkIteration = 0;
|
|
535
541
|
afkMaxIterations = 0;
|
|
@@ -699,7 +705,6 @@ ${summary}
|
|
|
699
705
|
cwd: this.projectPath,
|
|
700
706
|
stdio: "pipe"
|
|
701
707
|
});
|
|
702
|
-
this.symlinkDependencies(worktreePath);
|
|
703
708
|
console.log(`[executor] Created worktree at ${worktreePath} on branch ${branchName}`);
|
|
704
709
|
return { worktreePath, branchName };
|
|
705
710
|
} catch (error) {
|
|
@@ -707,38 +712,6 @@ ${summary}
|
|
|
707
712
|
return null;
|
|
708
713
|
}
|
|
709
714
|
}
|
|
710
|
-
/**
|
|
711
|
-
* Symlink dependency directories from main project to worktree
|
|
712
|
-
*/
|
|
713
|
-
symlinkDependencies(worktreePath) {
|
|
714
|
-
const depDirs = [
|
|
715
|
-
"node_modules",
|
|
716
|
-
".pnpm",
|
|
717
|
-
// pnpm store
|
|
718
|
-
".yarn",
|
|
719
|
-
// yarn cache
|
|
720
|
-
"vendor",
|
|
721
|
-
// PHP/Ruby deps
|
|
722
|
-
"__pycache__",
|
|
723
|
-
// Python cache
|
|
724
|
-
".venv",
|
|
725
|
-
// Python virtual env
|
|
726
|
-
"venv"
|
|
727
|
-
// Python virtual env
|
|
728
|
-
];
|
|
729
|
-
for (const dir of depDirs) {
|
|
730
|
-
const sourcePath = join4(this.projectPath, dir);
|
|
731
|
-
const targetPath = join4(worktreePath, dir);
|
|
732
|
-
if (existsSync2(sourcePath) && !existsSync2(targetPath)) {
|
|
733
|
-
try {
|
|
734
|
-
symlinkSync(sourcePath, targetPath, "junction");
|
|
735
|
-
console.log(`[executor] Symlinked ${dir} to worktree`);
|
|
736
|
-
} catch (error) {
|
|
737
|
-
console.log(`[executor] Could not symlink ${dir}:`, error);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
715
|
/**
|
|
743
716
|
* Remove a git worktree
|
|
744
717
|
*/
|
|
@@ -824,7 +797,7 @@ ${summary}
|
|
|
824
797
|
/**
|
|
825
798
|
* Build the prompt for a specific task
|
|
826
799
|
*/
|
|
827
|
-
buildTaskPrompt(task, config) {
|
|
800
|
+
buildTaskPrompt(task, config, worktreeInfo) {
|
|
828
801
|
const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
|
|
829
802
|
const prdPath = join4(kanbanDir, "prd.json");
|
|
830
803
|
const progressPath = join4(kanbanDir, "progress.txt");
|
|
@@ -838,9 +811,15 @@ ${task.steps.map((s, i) => `${i + 1}. ${s}`).join("\n")}` : "";
|
|
|
838
811
|
if (config.project.testCommand) {
|
|
839
812
|
verifySteps.push(`Run tests: ${config.project.testCommand}`);
|
|
840
813
|
}
|
|
841
|
-
const verifySection = verifySteps.length > 0 ? `
|
|
814
|
+
const verifySection = verifySteps.length > 0 ? `3. Verify your work:
|
|
842
815
|
${verifySteps.map((s) => ` - ${s}`).join("\n")}
|
|
843
816
|
|
|
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
|
+
|
|
844
823
|
` : "";
|
|
845
824
|
return `You are an AI coding agent. Complete the following task:
|
|
846
825
|
|
|
@@ -852,22 +831,24 @@ Priority: ${task.priority}
|
|
|
852
831
|
${task.description}
|
|
853
832
|
${stepsText}
|
|
854
833
|
|
|
855
|
-
## INSTRUCTIONS
|
|
856
|
-
1.
|
|
834
|
+
${worktreeSection}## INSTRUCTIONS
|
|
835
|
+
1. If dependencies are not installed, install them first.
|
|
836
|
+
|
|
837
|
+
2. Implement this task as described above.
|
|
857
838
|
|
|
858
|
-
${verifySection}${verifySteps.length > 0 ? "
|
|
839
|
+
${verifySection}${verifySteps.length > 0 ? "4" : "3"}. When complete, update the task in ${prdPath}:
|
|
859
840
|
- Find the task with id "${task.id}"
|
|
860
841
|
- Set "passes": true
|
|
861
|
-
|
|
842
|
+
(Note: Do NOT change the status - the system will move it to review)
|
|
862
843
|
|
|
863
|
-
${verifySteps.length > 0 ? "
|
|
844
|
+
${verifySteps.length > 0 ? "5" : "4"}. Document your work in ${progressPath}:
|
|
864
845
|
- What you implemented and files changed
|
|
865
846
|
- Key decisions made and why
|
|
866
847
|
- Gotchas, edge cases, or tricky parts discovered
|
|
867
848
|
- Useful patterns or approaches that worked well
|
|
868
849
|
- Anything a future agent should know about this area of the codebase
|
|
869
850
|
|
|
870
|
-
${verifySteps.length > 0 ? "
|
|
851
|
+
${verifySteps.length > 0 ? "6" : "5"}. Make a git commit with a descriptive message.
|
|
871
852
|
|
|
872
853
|
Focus only on this task. When successfully complete, output: <promise>COMPLETE</promise>`;
|
|
873
854
|
}
|
|
@@ -891,7 +872,7 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
891
872
|
const startedAt = /* @__PURE__ */ new Date();
|
|
892
873
|
const worktreeInfo = this.createWorktree(taskId);
|
|
893
874
|
const executionPath = worktreeInfo?.worktreePath || this.projectPath;
|
|
894
|
-
const prompt = this.buildTaskPrompt(task, config);
|
|
875
|
+
const prompt = this.buildTaskPrompt(task, config, worktreeInfo);
|
|
895
876
|
const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
|
|
896
877
|
const promptFile = join4(kanbanDir, `prompt-${taskId}.txt`);
|
|
897
878
|
writeFileSync4(promptFile, prompt);
|
|
@@ -1037,7 +1018,7 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
1037
1018
|
}, timeoutMs);
|
|
1038
1019
|
}
|
|
1039
1020
|
/**
|
|
1040
|
-
* Handle task completion
|
|
1021
|
+
* Handle task completion - moves to in_review, keeps worktree alive
|
|
1041
1022
|
*/
|
|
1042
1023
|
handleTaskComplete(taskId, exitCode, startedAt) {
|
|
1043
1024
|
const runningTask = this.runningTasks.get(taskId);
|
|
@@ -1049,31 +1030,25 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
1049
1030
|
const task = getTaskById(this.projectPath, taskId);
|
|
1050
1031
|
if (isComplete || exitCode === 0) {
|
|
1051
1032
|
if (runningTask.worktreePath && runningTask.branchName) {
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
}
|
|
1058
|
-
|
|
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}`);
|
|
1059
1040
|
}
|
|
1060
1041
|
updateTask(this.projectPath, taskId, {
|
|
1061
|
-
status: "
|
|
1042
|
+
status: "in_review",
|
|
1062
1043
|
passes: true
|
|
1063
1044
|
});
|
|
1064
|
-
addExecutionEntry(this.projectPath, taskId, {
|
|
1065
|
-
startedAt: startedAt.toISOString(),
|
|
1066
|
-
endedAt: endedAt.toISOString(),
|
|
1067
|
-
status: "completed",
|
|
1068
|
-
duration
|
|
1069
|
-
});
|
|
1070
1045
|
logTaskExecution(this.projectPath, {
|
|
1071
1046
|
taskId,
|
|
1072
1047
|
taskTitle: task?.title || "Unknown",
|
|
1073
|
-
status: "
|
|
1048
|
+
status: "in_review",
|
|
1074
1049
|
duration
|
|
1075
1050
|
});
|
|
1076
|
-
this.emit("task:completed", { taskId, duration });
|
|
1051
|
+
this.emit("task:completed", { taskId, duration, status: "in_review" });
|
|
1077
1052
|
this.afkTasksCompleted++;
|
|
1078
1053
|
} else {
|
|
1079
1054
|
if (runningTask.worktreePath) {
|
|
@@ -1105,6 +1080,151 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
1105
1080
|
this.continueAFKMode();
|
|
1106
1081
|
}
|
|
1107
1082
|
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Get info about a task in review
|
|
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
|
|
1166
|
+
*/
|
|
1167
|
+
discardTask(taskId) {
|
|
1168
|
+
const reviewInfo = this.reviewingTasks.get(taskId);
|
|
1169
|
+
if (!reviewInfo) {
|
|
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
|
+
}
|
|
1108
1228
|
/**
|
|
1109
1229
|
* Cancel a running task
|
|
1110
1230
|
*/
|
|
@@ -1771,6 +1891,90 @@ async function createServer(projectPath, port) {
|
|
|
1771
1891
|
res.status(500).json({ error: String(error) });
|
|
1772
1892
|
}
|
|
1773
1893
|
});
|
|
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
|
+
});
|
|
1774
1978
|
app.get("/api/tasks/:id/logs", (req, res) => {
|
|
1775
1979
|
try {
|
|
1776
1980
|
const logs = executor.getTaskLog(req.params.id);
|
|
@@ -2080,6 +2284,7 @@ function getClientHTML() {
|
|
|
2080
2284
|
.status-dot-draft { background: #a3a3a3; }
|
|
2081
2285
|
.status-dot-ready { background: #3b82f6; }
|
|
2082
2286
|
.status-dot-in_progress { background: #f97316; }
|
|
2287
|
+
.status-dot-in_review { background: #8b5cf6; }
|
|
2083
2288
|
.status-dot-completed { background: #22c55e; }
|
|
2084
2289
|
.status-dot-failed { background: #ef4444; }
|
|
2085
2290
|
|
|
@@ -2289,6 +2494,7 @@ function getClientHTML() {
|
|
|
2289
2494
|
.status-badge-draft { background: #f5f5f5; color: #737373; }
|
|
2290
2495
|
.status-badge-ready { background: #eff6ff; color: #3b82f6; }
|
|
2291
2496
|
.status-badge-in_progress { background: #fff7ed; color: #f97316; }
|
|
2497
|
+
.status-badge-in_review { background: #f5f3ff; color: #8b5cf6; }
|
|
2292
2498
|
.status-badge-completed { background: #f0fdf4; color: #22c55e; }
|
|
2293
2499
|
.status-badge-failed { background: #fef2f2; color: #ef4444; }
|
|
2294
2500
|
|
|
@@ -2614,11 +2820,16 @@ socket.on('task:output', ({ taskId, line }) => {
|
|
|
2614
2820
|
}
|
|
2615
2821
|
});
|
|
2616
2822
|
|
|
2617
|
-
socket.on('task:completed', ({ taskId }) => {
|
|
2823
|
+
socket.on('task:completed', ({ taskId, status }) => {
|
|
2618
2824
|
state.running = state.running.filter(id => id !== taskId);
|
|
2619
2825
|
const task = state.tasks.find(t => t.id === taskId);
|
|
2620
|
-
if (task) {
|
|
2621
|
-
|
|
2826
|
+
if (task) {
|
|
2827
|
+
// Use status from server (could be 'in_review' or 'completed')
|
|
2828
|
+
task.status = status || 'in_review';
|
|
2829
|
+
task.passes = true;
|
|
2830
|
+
}
|
|
2831
|
+
const statusMsg = (status === 'in_review') ? 'ready for review' : 'completed';
|
|
2832
|
+
showToast('Task ' + statusMsg + ': ' + (task?.title || taskId), 'success');
|
|
2622
2833
|
render();
|
|
2623
2834
|
});
|
|
2624
2835
|
|
|
@@ -2692,6 +2903,53 @@ async function retryTask(id) {
|
|
|
2692
2903
|
});
|
|
2693
2904
|
}
|
|
2694
2905
|
|
|
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
|
+
|
|
2695
2953
|
async function generateTask(prompt) {
|
|
2696
2954
|
state.aiGenerating = true;
|
|
2697
2955
|
render();
|
|
@@ -2895,6 +3153,7 @@ function renderColumn(status, title, tasks) {
|
|
|
2895
3153
|
draft: 'To Do',
|
|
2896
3154
|
ready: 'Ready',
|
|
2897
3155
|
in_progress: 'In Progress',
|
|
3156
|
+
in_review: 'In Review',
|
|
2898
3157
|
completed: 'Done',
|
|
2899
3158
|
failed: 'Failed'
|
|
2900
3159
|
};
|
|
@@ -3260,7 +3519,7 @@ function renderSidePanel() {
|
|
|
3260
3519
|
</div>
|
|
3261
3520
|
|
|
3262
3521
|
<!-- Action Buttons -->
|
|
3263
|
-
<div class="px-5 py-4 border-b border-canvas-200 flex gap-2 flex-shrink-0">
|
|
3522
|
+
<div class="px-5 py-4 border-b border-canvas-200 flex flex-wrap gap-2 flex-shrink-0">
|
|
3264
3523
|
\${task.status === 'draft' ? \`
|
|
3265
3524
|
<button onclick="updateTask('\${task.id}', { status: 'ready' })" class="btn btn-primary px-4 py-2 text-sm">
|
|
3266
3525
|
\u2192 Move to Ready
|
|
@@ -3276,6 +3535,20 @@ function renderSidePanel() {
|
|
|
3276
3535
|
\u23F9 Stop Attempt
|
|
3277
3536
|
</button>
|
|
3278
3537
|
\` : ''}
|
|
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
|
+
\` : ''}
|
|
3279
3552
|
\${task.status === 'failed' ? \`
|
|
3280
3553
|
<button onclick="retryTask('\${task.id}')" class="btn btn-primary px-4 py-2 text-sm">
|
|
3281
3554
|
\u21BB Retry
|
|
@@ -3476,6 +3749,41 @@ function handleStartAFK() {
|
|
|
3476
3749
|
render();
|
|
3477
3750
|
}
|
|
3478
3751
|
|
|
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
|
+
|
|
3479
3787
|
// Filter tasks based on search query
|
|
3480
3788
|
function filterTasks(tasks) {
|
|
3481
3789
|
if (!state.searchQuery) return tasks;
|
|
@@ -3537,6 +3845,7 @@ function render() {
|
|
|
3537
3845
|
\${renderColumn('draft', 'To Do', filterTasks(state.tasks))}
|
|
3538
3846
|
\${renderColumn('ready', 'Ready', filterTasks(state.tasks))}
|
|
3539
3847
|
\${renderColumn('in_progress', 'In Progress', filterTasks(state.tasks))}
|
|
3848
|
+
\${renderColumn('in_review', 'In Review', filterTasks(state.tasks))}
|
|
3540
3849
|
\${renderColumn('completed', 'Done', filterTasks(state.tasks))}
|
|
3541
3850
|
\${renderColumn('failed', 'Failed', filterTasks(state.tasks))}
|
|
3542
3851
|
</div>
|
|
@@ -3601,6 +3910,14 @@ window.closeSidePanel = closeSidePanel;
|
|
|
3601
3910
|
window.showTaskMenu = showTaskMenu;
|
|
3602
3911
|
window.filterTasks = filterTasks;
|
|
3603
3912
|
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;
|
|
3604
3921
|
|
|
3605
3922
|
// Keyboard shortcuts
|
|
3606
3923
|
document.addEventListener('keydown', (e) => {
|
|
@@ -3675,6 +3992,49 @@ async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
|
3675
3992
|
}
|
|
3676
3993
|
|
|
3677
3994
|
// src/bin/cli.ts
|
|
3995
|
+
function isGitRepo(dir) {
|
|
3996
|
+
return existsSync4(join6(dir, ".git"));
|
|
3997
|
+
}
|
|
3998
|
+
function hasGitRemote(dir) {
|
|
3999
|
+
try {
|
|
4000
|
+
const result = execSync2("git remote -v", { cwd: dir, stdio: "pipe" }).toString();
|
|
4001
|
+
return result.trim().length > 0;
|
|
4002
|
+
} catch {
|
|
4003
|
+
return false;
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
function detectRemoteType(dir) {
|
|
4007
|
+
try {
|
|
4008
|
+
const result = execSync2("git remote get-url origin", { cwd: dir, stdio: "pipe" }).toString().toLowerCase();
|
|
4009
|
+
if (result.includes("github.com")) return "github";
|
|
4010
|
+
if (result.includes("gitlab.com") || result.includes("gitlab")) return "gitlab";
|
|
4011
|
+
if (result.includes("bitbucket.org") || result.includes("bitbucket")) return "bitbucket";
|
|
4012
|
+
if (result.trim()) return "other";
|
|
4013
|
+
return null;
|
|
4014
|
+
} catch {
|
|
4015
|
+
return null;
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
function initializeGit(dir) {
|
|
4019
|
+
execSync2("git init", { cwd: dir, stdio: "pipe" });
|
|
4020
|
+
try {
|
|
4021
|
+
execSync2("git add -A", { cwd: dir, stdio: "pipe" });
|
|
4022
|
+
execSync2('git commit -m "Initial commit"', { cwd: dir, stdio: "pipe" });
|
|
4023
|
+
} catch {
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
async function promptYesNo(question) {
|
|
4027
|
+
const rl = createInterface({
|
|
4028
|
+
input: process.stdin,
|
|
4029
|
+
output: process.stdout
|
|
4030
|
+
});
|
|
4031
|
+
return new Promise((resolve) => {
|
|
4032
|
+
rl.question(question, (answer) => {
|
|
4033
|
+
rl.close();
|
|
4034
|
+
resolve(answer.toLowerCase().startsWith("y"));
|
|
4035
|
+
});
|
|
4036
|
+
});
|
|
4037
|
+
}
|
|
3678
4038
|
var VERSION = "0.1.0";
|
|
3679
4039
|
var banner = `
|
|
3680
4040
|
${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")}
|
|
@@ -3687,6 +4047,32 @@ async function main() {
|
|
|
3687
4047
|
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) => {
|
|
3688
4048
|
console.log(banner);
|
|
3689
4049
|
const cwd = process.cwd();
|
|
4050
|
+
if (!isGitRepo(cwd)) {
|
|
4051
|
+
console.log(chalk.yellow("\n\u26A0 This directory is not a git repository."));
|
|
4052
|
+
console.log(chalk.gray("Git is required for task isolation and parallel execution.\n"));
|
|
4053
|
+
const shouldInit = await promptYesNo(chalk.white("Would you like to initialize git now? (y/n): "));
|
|
4054
|
+
if (shouldInit) {
|
|
4055
|
+
try {
|
|
4056
|
+
console.log(chalk.gray("Initializing git repository..."));
|
|
4057
|
+
initializeGit(cwd);
|
|
4058
|
+
console.log(chalk.green("\u2713 Git repository initialized"));
|
|
4059
|
+
} catch (error) {
|
|
4060
|
+
console.error(chalk.red("Failed to initialize git:"), error);
|
|
4061
|
+
process.exit(1);
|
|
4062
|
+
}
|
|
4063
|
+
} else {
|
|
4064
|
+
console.log(chalk.red("\nGit is required to run Claude Kanban."));
|
|
4065
|
+
console.log(chalk.gray('Please run "git init" manually and try again.'));
|
|
4066
|
+
process.exit(1);
|
|
4067
|
+
}
|
|
4068
|
+
} else {
|
|
4069
|
+
const remoteType = detectRemoteType(cwd);
|
|
4070
|
+
if (remoteType) {
|
|
4071
|
+
console.log(chalk.gray(`Git remote detected: ${remoteType}`));
|
|
4072
|
+
} else if (!hasGitRemote(cwd)) {
|
|
4073
|
+
console.log(chalk.gray("Git repository (local only, no remote)"));
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
3690
4076
|
const initialized = await isProjectInitialized(cwd);
|
|
3691
4077
|
if (!initialized || options.init) {
|
|
3692
4078
|
console.log(chalk.yellow("Initializing project..."));
|