claude-kanban 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +1417 -108
- package/dist/bin/cli.js.map +1 -1
- package/dist/server/index.js +1414 -109
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/cli.js
CHANGED
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import open from "open";
|
|
7
|
-
import { existsSync as
|
|
7
|
+
import { existsSync as existsSync5 } from "fs";
|
|
8
8
|
import { execSync } from "child_process";
|
|
9
9
|
import { createInterface } from "readline";
|
|
10
|
-
import { join as
|
|
10
|
+
import { join as join7 } from "path";
|
|
11
11
|
|
|
12
12
|
// src/server/index.ts
|
|
13
13
|
import express from "express";
|
|
14
14
|
import { createServer as createHttpServer } from "http";
|
|
15
15
|
import { Server as SocketIOServer } from "socket.io";
|
|
16
|
-
import { join as
|
|
16
|
+
import { join as join6, dirname } from "path";
|
|
17
17
|
import { fileURLToPath } from "url";
|
|
18
|
-
import { existsSync as
|
|
18
|
+
import { existsSync as existsSync4 } from "fs";
|
|
19
19
|
|
|
20
20
|
// src/server/services/executor.ts
|
|
21
21
|
import { spawn } from "child_process";
|
|
@@ -166,8 +166,12 @@ function createInitialConfig(projectPath) {
|
|
|
166
166
|
},
|
|
167
167
|
execution: {
|
|
168
168
|
maxConcurrent: 3,
|
|
169
|
-
timeout: 30
|
|
169
|
+
timeout: 30,
|
|
170
170
|
// 30 minutes
|
|
171
|
+
enableQA: false,
|
|
172
|
+
// QA verification disabled by default
|
|
173
|
+
qaMaxRetries: 2
|
|
174
|
+
// Max QA retry attempts
|
|
171
175
|
}
|
|
172
176
|
};
|
|
173
177
|
}
|
|
@@ -391,7 +395,8 @@ function createTask(projectPath, request) {
|
|
|
391
395
|
passes: false,
|
|
392
396
|
createdAt: now,
|
|
393
397
|
updatedAt: now,
|
|
394
|
-
executionHistory: []
|
|
398
|
+
executionHistory: [],
|
|
399
|
+
dependsOn: request.dependsOn || []
|
|
395
400
|
};
|
|
396
401
|
prd.tasks.push(task);
|
|
397
402
|
writePRD(projectPath, prd);
|
|
@@ -434,23 +439,40 @@ function addExecutionEntry(projectPath, taskId, entry) {
|
|
|
434
439
|
writePRD(projectPath, prd);
|
|
435
440
|
return prd.tasks[taskIndex];
|
|
436
441
|
}
|
|
442
|
+
function areDependenciesMet(projectPath, task) {
|
|
443
|
+
if (!task.dependsOn || task.dependsOn.length === 0) {
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
const prd = readPRD(projectPath);
|
|
447
|
+
for (const depId of task.dependsOn) {
|
|
448
|
+
const depTask = prd.tasks.find((t) => t.id === depId);
|
|
449
|
+
if (!depTask || depTask.status !== "completed") {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
437
455
|
function getNextReadyTask(projectPath) {
|
|
438
456
|
const readyTasks = getTasksByStatus(projectPath, "ready");
|
|
439
457
|
if (readyTasks.length === 0) {
|
|
440
458
|
return null;
|
|
441
459
|
}
|
|
460
|
+
const executableTasks = readyTasks.filter((task) => areDependenciesMet(projectPath, task));
|
|
461
|
+
if (executableTasks.length === 0) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
442
464
|
const priorityOrder = {
|
|
443
465
|
critical: 0,
|
|
444
466
|
high: 1,
|
|
445
467
|
medium: 2,
|
|
446
468
|
low: 3
|
|
447
469
|
};
|
|
448
|
-
|
|
470
|
+
executableTasks.sort((a, b) => {
|
|
449
471
|
const priorityDiff = (priorityOrder[a.priority] || 2) - (priorityOrder[b.priority] || 2);
|
|
450
472
|
if (priorityDiff !== 0) return priorityDiff;
|
|
451
473
|
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
|
452
474
|
});
|
|
453
|
-
return
|
|
475
|
+
return executableTasks[0];
|
|
454
476
|
}
|
|
455
477
|
function getTaskCounts(projectPath) {
|
|
456
478
|
const tasks = getAllTasks(projectPath);
|
|
@@ -534,6 +556,11 @@ var TaskExecutor = class extends EventEmitter {
|
|
|
534
556
|
projectPath;
|
|
535
557
|
runningTask = null;
|
|
536
558
|
planningSession = null;
|
|
559
|
+
qaSession = null;
|
|
560
|
+
pendingQATaskId = null;
|
|
561
|
+
// Task waiting for QA
|
|
562
|
+
qaRetryCount = /* @__PURE__ */ new Map();
|
|
563
|
+
// Track QA retries per task
|
|
537
564
|
afkMode = false;
|
|
538
565
|
afkIteration = 0;
|
|
539
566
|
afkMaxIterations = 0;
|
|
@@ -666,10 +693,16 @@ ${summary}
|
|
|
666
693
|
return this.planningSession !== null;
|
|
667
694
|
}
|
|
668
695
|
/**
|
|
669
|
-
* Check if executor is busy (task or
|
|
696
|
+
* Check if executor is busy (task, planning, or QA running)
|
|
670
697
|
*/
|
|
671
698
|
isBusy() {
|
|
672
|
-
return this.runningTask !== null || this.planningSession !== null;
|
|
699
|
+
return this.runningTask !== null || this.planningSession !== null || this.qaSession !== null;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Check if QA is running
|
|
703
|
+
*/
|
|
704
|
+
isQARunning() {
|
|
705
|
+
return this.qaSession !== null;
|
|
673
706
|
}
|
|
674
707
|
/**
|
|
675
708
|
* Get planning session output
|
|
@@ -1092,27 +1125,52 @@ When done, output: <promise>PLANNING_COMPLETE</promise>`;
|
|
|
1092
1125
|
const endedAt = /* @__PURE__ */ new Date();
|
|
1093
1126
|
const duration = endedAt.getTime() - startedAt.getTime();
|
|
1094
1127
|
const output = this.runningTask.output.join("");
|
|
1128
|
+
const config = getConfig(this.projectPath);
|
|
1095
1129
|
const isComplete = output.includes("<promise>COMPLETE</promise>");
|
|
1096
1130
|
const task = getTaskById(this.projectPath, taskId);
|
|
1097
1131
|
if (isComplete || exitCode === 0) {
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1132
|
+
if (config.execution.enableQA) {
|
|
1133
|
+
updateTask(this.projectPath, taskId, {
|
|
1134
|
+
status: "in_progress",
|
|
1135
|
+
// Keep in progress until QA passes
|
|
1136
|
+
passes: false
|
|
1137
|
+
});
|
|
1138
|
+
addExecutionEntry(this.projectPath, taskId, {
|
|
1139
|
+
startedAt: startedAt.toISOString(),
|
|
1140
|
+
endedAt: endedAt.toISOString(),
|
|
1141
|
+
status: "completed",
|
|
1142
|
+
duration
|
|
1143
|
+
});
|
|
1144
|
+
this.runningTask = null;
|
|
1145
|
+
this.pendingQATaskId = taskId;
|
|
1146
|
+
this.runQAVerification(taskId).catch((error) => {
|
|
1147
|
+
console.error("QA verification error:", error);
|
|
1148
|
+
this.handleQAComplete(taskId, false, ["QA process failed to start"]);
|
|
1149
|
+
});
|
|
1150
|
+
} else {
|
|
1151
|
+
updateTask(this.projectPath, taskId, {
|
|
1152
|
+
status: "completed",
|
|
1153
|
+
passes: true
|
|
1154
|
+
});
|
|
1155
|
+
addExecutionEntry(this.projectPath, taskId, {
|
|
1156
|
+
startedAt: startedAt.toISOString(),
|
|
1157
|
+
endedAt: endedAt.toISOString(),
|
|
1158
|
+
status: "completed",
|
|
1159
|
+
duration
|
|
1160
|
+
});
|
|
1161
|
+
logTaskExecution(this.projectPath, {
|
|
1162
|
+
taskId,
|
|
1163
|
+
taskTitle: task?.title || "Unknown",
|
|
1164
|
+
status: "completed",
|
|
1165
|
+
duration
|
|
1166
|
+
});
|
|
1167
|
+
this.emit("task:completed", { taskId, duration });
|
|
1168
|
+
this.afkTasksCompleted++;
|
|
1169
|
+
this.runningTask = null;
|
|
1170
|
+
if (this.afkMode) {
|
|
1171
|
+
this.continueAFKMode();
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1116
1174
|
} else {
|
|
1117
1175
|
updateTask(this.projectPath, taskId, {
|
|
1118
1176
|
status: "failed",
|
|
@@ -1134,12 +1192,397 @@ When done, output: <promise>PLANNING_COMPLETE</promise>`;
|
|
|
1134
1192
|
error
|
|
1135
1193
|
});
|
|
1136
1194
|
this.emit("task:failed", { taskId, error });
|
|
1195
|
+
this.runningTask = null;
|
|
1196
|
+
if (this.afkMode) {
|
|
1197
|
+
this.continueAFKMode();
|
|
1198
|
+
}
|
|
1137
1199
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Build the QA verification prompt
|
|
1203
|
+
*/
|
|
1204
|
+
buildQAPrompt(task, config) {
|
|
1205
|
+
const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
|
|
1206
|
+
const prdPath = join4(kanbanDir, "prd.json");
|
|
1207
|
+
const stepsText = task.steps.length > 0 ? `
|
|
1208
|
+
Verification steps:
|
|
1209
|
+
${task.steps.map((s, i) => `${i + 1}. ${s}`).join("\n")}` : "";
|
|
1210
|
+
const verifyCommands = [];
|
|
1211
|
+
if (config.project.typecheckCommand) {
|
|
1212
|
+
verifyCommands.push(config.project.typecheckCommand);
|
|
1213
|
+
}
|
|
1214
|
+
if (config.project.testCommand) {
|
|
1215
|
+
verifyCommands.push(config.project.testCommand);
|
|
1216
|
+
}
|
|
1217
|
+
return `You are a QA reviewer. Verify that the following task has been correctly implemented.
|
|
1218
|
+
|
|
1219
|
+
## TASK TO VERIFY
|
|
1220
|
+
Title: ${task.title}
|
|
1221
|
+
Category: ${task.category}
|
|
1222
|
+
Priority: ${task.priority}
|
|
1223
|
+
|
|
1224
|
+
${task.description}
|
|
1225
|
+
${stepsText}
|
|
1226
|
+
|
|
1227
|
+
## YOUR JOB
|
|
1228
|
+
|
|
1229
|
+
1. Review the recent git commits and changes made for this task.
|
|
1230
|
+
|
|
1231
|
+
2. Check that all verification steps (if any) have been satisfied.
|
|
1232
|
+
|
|
1233
|
+
3. Run the following quality checks:
|
|
1234
|
+
${verifyCommands.length > 0 ? verifyCommands.map((cmd) => ` - ${cmd}`).join("\n") : " (No verification commands configured)"}
|
|
1235
|
+
|
|
1236
|
+
4. Check that the implementation meets the task requirements.
|
|
1237
|
+
|
|
1238
|
+
5. If everything passes, output: <qa>PASSED</qa>
|
|
1239
|
+
|
|
1240
|
+
6. If there are issues, output: <qa>FAILED</qa>
|
|
1241
|
+
Then list the issues that need to be fixed:
|
|
1242
|
+
<issues>
|
|
1243
|
+
- Issue 1
|
|
1244
|
+
- Issue 2
|
|
1245
|
+
</issues>
|
|
1246
|
+
|
|
1247
|
+
Be thorough but fair. Only fail the QA if there are real issues that affect functionality or quality.`;
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Run QA verification for a task
|
|
1251
|
+
*/
|
|
1252
|
+
async runQAVerification(taskId) {
|
|
1253
|
+
const config = getConfig(this.projectPath);
|
|
1254
|
+
const task = getTaskById(this.projectPath, taskId);
|
|
1255
|
+
if (!task) {
|
|
1256
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
1257
|
+
}
|
|
1258
|
+
const currentRetries = this.qaRetryCount.get(taskId) || 0;
|
|
1259
|
+
const attempt = currentRetries + 1;
|
|
1260
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
1261
|
+
const prompt = this.buildQAPrompt(task, config);
|
|
1262
|
+
const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
|
|
1263
|
+
const promptFile = join4(kanbanDir, `qa-${taskId}.txt`);
|
|
1264
|
+
writeFileSync4(promptFile, prompt);
|
|
1265
|
+
const args = [];
|
|
1266
|
+
if (config.agent.model) {
|
|
1267
|
+
args.push("--model", config.agent.model);
|
|
1268
|
+
}
|
|
1269
|
+
args.push("--permission-mode", config.agent.permissionMode);
|
|
1270
|
+
args.push("-p");
|
|
1271
|
+
args.push("--verbose");
|
|
1272
|
+
args.push("--output-format", "stream-json");
|
|
1273
|
+
args.push(`@${promptFile}`);
|
|
1274
|
+
const fullCommand = `${config.agent.command} ${args.join(" ")}`;
|
|
1275
|
+
console.log("[executor] QA command:", fullCommand);
|
|
1276
|
+
const childProcess = spawn("bash", ["-c", fullCommand], {
|
|
1277
|
+
cwd: this.projectPath,
|
|
1278
|
+
env: {
|
|
1279
|
+
...process.env,
|
|
1280
|
+
TERM: "xterm-256color",
|
|
1281
|
+
FORCE_COLOR: "0",
|
|
1282
|
+
NO_COLOR: "1"
|
|
1283
|
+
},
|
|
1284
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1285
|
+
});
|
|
1286
|
+
this.qaSession = {
|
|
1287
|
+
taskId,
|
|
1288
|
+
process: childProcess,
|
|
1289
|
+
startedAt,
|
|
1290
|
+
output: [],
|
|
1291
|
+
attempt
|
|
1292
|
+
};
|
|
1293
|
+
this.emit("qa:started", { taskId, attempt });
|
|
1294
|
+
const logOutput = (line) => {
|
|
1295
|
+
this.qaSession?.output.push(line);
|
|
1296
|
+
this.emit("qa:output", { taskId, line });
|
|
1297
|
+
};
|
|
1298
|
+
logOutput(`[claude-kanban] Starting QA verification (attempt ${attempt})
|
|
1299
|
+
`);
|
|
1300
|
+
let stdoutBuffer = "";
|
|
1301
|
+
childProcess.stdout?.on("data", (data) => {
|
|
1302
|
+
stdoutBuffer += data.toString();
|
|
1303
|
+
const lines = stdoutBuffer.split("\n");
|
|
1304
|
+
stdoutBuffer = lines.pop() || "";
|
|
1305
|
+
for (const line of lines) {
|
|
1306
|
+
if (!line.trim()) continue;
|
|
1307
|
+
try {
|
|
1308
|
+
const json = JSON.parse(line);
|
|
1309
|
+
let text = "";
|
|
1310
|
+
if (json.type === "assistant" && json.message?.content) {
|
|
1311
|
+
for (const block of json.message.content) {
|
|
1312
|
+
if (block.type === "text") {
|
|
1313
|
+
text += block.text;
|
|
1314
|
+
} else if (block.type === "tool_use") {
|
|
1315
|
+
text += this.formatToolUse(block.name, block.input);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
} else if (json.type === "content_block_delta" && json.delta?.text) {
|
|
1319
|
+
text = json.delta.text;
|
|
1320
|
+
}
|
|
1321
|
+
if (text) {
|
|
1322
|
+
logOutput(text);
|
|
1323
|
+
}
|
|
1324
|
+
} catch {
|
|
1325
|
+
const cleanText = line.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "");
|
|
1326
|
+
if (cleanText.trim()) {
|
|
1327
|
+
logOutput(cleanText + "\n");
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
childProcess.stderr?.on("data", (data) => {
|
|
1333
|
+
const text = data.toString();
|
|
1334
|
+
logOutput(`[stderr] ${text}`);
|
|
1335
|
+
});
|
|
1336
|
+
childProcess.on("error", (error) => {
|
|
1337
|
+
console.log("[executor] QA spawn error:", error.message);
|
|
1338
|
+
try {
|
|
1339
|
+
unlinkSync(promptFile);
|
|
1340
|
+
} catch {
|
|
1341
|
+
}
|
|
1342
|
+
this.handleQAComplete(taskId, false, [`QA process error: ${error.message}`]);
|
|
1343
|
+
});
|
|
1344
|
+
childProcess.on("close", (code) => {
|
|
1345
|
+
console.log("[executor] QA process closed with code:", code);
|
|
1346
|
+
try {
|
|
1347
|
+
unlinkSync(promptFile);
|
|
1348
|
+
} catch {
|
|
1349
|
+
}
|
|
1350
|
+
this.parseQAResult(taskId);
|
|
1351
|
+
});
|
|
1352
|
+
const timeoutMs = 5 * 60 * 1e3;
|
|
1353
|
+
setTimeout(() => {
|
|
1354
|
+
if (this.qaSession?.taskId === taskId) {
|
|
1355
|
+
this.cancelQA("QA timeout exceeded");
|
|
1356
|
+
}
|
|
1357
|
+
}, timeoutMs);
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Parse QA result from output
|
|
1361
|
+
*/
|
|
1362
|
+
parseQAResult(taskId) {
|
|
1363
|
+
if (!this.qaSession || this.qaSession.taskId !== taskId) return;
|
|
1364
|
+
const output = this.qaSession.output.join("");
|
|
1365
|
+
const passedMatch = output.includes("<qa>PASSED</qa>");
|
|
1366
|
+
const failedMatch = output.includes("<qa>FAILED</qa>");
|
|
1367
|
+
if (passedMatch) {
|
|
1368
|
+
this.handleQAComplete(taskId, true, []);
|
|
1369
|
+
} else if (failedMatch) {
|
|
1370
|
+
const issuesMatch = output.match(/<issues>([\s\S]*?)<\/issues>/);
|
|
1371
|
+
const issues = [];
|
|
1372
|
+
if (issuesMatch) {
|
|
1373
|
+
const issueLines = issuesMatch[1].split("\n");
|
|
1374
|
+
for (const line of issueLines) {
|
|
1375
|
+
const trimmed = line.trim();
|
|
1376
|
+
if (trimmed.startsWith("-")) {
|
|
1377
|
+
issues.push(trimmed.substring(1).trim());
|
|
1378
|
+
} else if (trimmed) {
|
|
1379
|
+
issues.push(trimmed);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
this.handleQAComplete(taskId, false, issues.length > 0 ? issues : ["QA verification failed"]);
|
|
1384
|
+
} else {
|
|
1385
|
+
this.handleQAComplete(taskId, false, ["QA did not provide a clear PASSED/FAILED result"]);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Handle QA completion
|
|
1390
|
+
*/
|
|
1391
|
+
handleQAComplete(taskId, passed, issues) {
|
|
1392
|
+
const config = getConfig(this.projectPath);
|
|
1393
|
+
const task = getTaskById(this.projectPath, taskId);
|
|
1394
|
+
const currentRetries = this.qaRetryCount.get(taskId) || 0;
|
|
1395
|
+
this.qaSession = null;
|
|
1396
|
+
this.pendingQATaskId = null;
|
|
1397
|
+
if (passed) {
|
|
1398
|
+
updateTask(this.projectPath, taskId, {
|
|
1399
|
+
status: "completed",
|
|
1400
|
+
passes: true
|
|
1401
|
+
});
|
|
1402
|
+
logTaskExecution(this.projectPath, {
|
|
1403
|
+
taskId,
|
|
1404
|
+
taskTitle: task?.title || "Unknown",
|
|
1405
|
+
status: "completed",
|
|
1406
|
+
duration: 0
|
|
1407
|
+
});
|
|
1408
|
+
this.emit("qa:passed", { taskId });
|
|
1409
|
+
this.emit("task:completed", { taskId, duration: 0 });
|
|
1410
|
+
this.afkTasksCompleted++;
|
|
1411
|
+
this.qaRetryCount.delete(taskId);
|
|
1412
|
+
} else {
|
|
1413
|
+
const willRetry = currentRetries < config.execution.qaMaxRetries;
|
|
1414
|
+
this.emit("qa:failed", { taskId, issues, willRetry });
|
|
1415
|
+
if (willRetry) {
|
|
1416
|
+
this.qaRetryCount.set(taskId, currentRetries + 1);
|
|
1417
|
+
console.log(`[executor] QA failed, retrying (${currentRetries + 1}/${config.execution.qaMaxRetries})`);
|
|
1418
|
+
this.runTaskWithQAFix(taskId, issues).catch((error) => {
|
|
1419
|
+
console.error("QA fix error:", error);
|
|
1420
|
+
this.handleQAComplete(taskId, false, ["Failed to run QA fix"]);
|
|
1421
|
+
});
|
|
1422
|
+
} else {
|
|
1423
|
+
updateTask(this.projectPath, taskId, {
|
|
1424
|
+
status: "failed",
|
|
1425
|
+
passes: false
|
|
1426
|
+
});
|
|
1427
|
+
logTaskExecution(this.projectPath, {
|
|
1428
|
+
taskId,
|
|
1429
|
+
taskTitle: task?.title || "Unknown",
|
|
1430
|
+
status: "failed",
|
|
1431
|
+
duration: 0,
|
|
1432
|
+
error: `QA failed after ${config.execution.qaMaxRetries} retries: ${issues.join(", ")}`
|
|
1433
|
+
});
|
|
1434
|
+
this.emit("task:failed", { taskId, error: `QA failed: ${issues.join(", ")}` });
|
|
1435
|
+
this.qaRetryCount.delete(taskId);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if (this.afkMode && !this.isBusy()) {
|
|
1140
1439
|
this.continueAFKMode();
|
|
1141
1440
|
}
|
|
1142
1441
|
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Run task again with QA fix instructions
|
|
1444
|
+
*/
|
|
1445
|
+
async runTaskWithQAFix(taskId, issues) {
|
|
1446
|
+
const config = getConfig(this.projectPath);
|
|
1447
|
+
const task = getTaskById(this.projectPath, taskId);
|
|
1448
|
+
if (!task) {
|
|
1449
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
1450
|
+
}
|
|
1451
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
1452
|
+
const issuesText = issues.map((i, idx) => `${idx + 1}. ${i}`).join("\n");
|
|
1453
|
+
const prompt = `You are fixing issues found during QA verification for a task.
|
|
1454
|
+
|
|
1455
|
+
## ORIGINAL TASK
|
|
1456
|
+
Title: ${task.title}
|
|
1457
|
+
${task.description}
|
|
1458
|
+
|
|
1459
|
+
## QA ISSUES TO FIX
|
|
1460
|
+
${issuesText}
|
|
1461
|
+
|
|
1462
|
+
## INSTRUCTIONS
|
|
1463
|
+
1. Review the issues reported by QA.
|
|
1464
|
+
2. Fix each issue.
|
|
1465
|
+
3. Run verification commands to ensure fixes work.
|
|
1466
|
+
4. When done, output: <promise>COMPLETE</promise>
|
|
1467
|
+
|
|
1468
|
+
Focus only on fixing the reported issues, don't make unrelated changes.`;
|
|
1469
|
+
const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
|
|
1470
|
+
const promptFile = join4(kanbanDir, `qafix-${taskId}.txt`);
|
|
1471
|
+
writeFileSync4(promptFile, prompt);
|
|
1472
|
+
const args = [];
|
|
1473
|
+
if (config.agent.model) {
|
|
1474
|
+
args.push("--model", config.agent.model);
|
|
1475
|
+
}
|
|
1476
|
+
args.push("--permission-mode", config.agent.permissionMode);
|
|
1477
|
+
args.push("-p");
|
|
1478
|
+
args.push("--verbose");
|
|
1479
|
+
args.push("--output-format", "stream-json");
|
|
1480
|
+
args.push(`@${promptFile}`);
|
|
1481
|
+
const fullCommand = `${config.agent.command} ${args.join(" ")}`;
|
|
1482
|
+
const childProcess = spawn("bash", ["-c", fullCommand], {
|
|
1483
|
+
cwd: this.projectPath,
|
|
1484
|
+
env: {
|
|
1485
|
+
...process.env,
|
|
1486
|
+
TERM: "xterm-256color",
|
|
1487
|
+
FORCE_COLOR: "0",
|
|
1488
|
+
NO_COLOR: "1"
|
|
1489
|
+
},
|
|
1490
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1491
|
+
});
|
|
1492
|
+
this.runningTask = {
|
|
1493
|
+
taskId,
|
|
1494
|
+
process: childProcess,
|
|
1495
|
+
startedAt,
|
|
1496
|
+
output: []
|
|
1497
|
+
};
|
|
1498
|
+
this.emit("task:started", { taskId, timestamp: startedAt.toISOString() });
|
|
1499
|
+
let stdoutBuffer = "";
|
|
1500
|
+
childProcess.stdout?.on("data", (data) => {
|
|
1501
|
+
stdoutBuffer += data.toString();
|
|
1502
|
+
const lines = stdoutBuffer.split("\n");
|
|
1503
|
+
stdoutBuffer = lines.pop() || "";
|
|
1504
|
+
for (const line of lines) {
|
|
1505
|
+
if (!line.trim()) continue;
|
|
1506
|
+
try {
|
|
1507
|
+
const json = JSON.parse(line);
|
|
1508
|
+
let text = "";
|
|
1509
|
+
if (json.type === "assistant" && json.message?.content) {
|
|
1510
|
+
for (const block of json.message.content) {
|
|
1511
|
+
if (block.type === "text") {
|
|
1512
|
+
text += block.text;
|
|
1513
|
+
} else if (block.type === "tool_use") {
|
|
1514
|
+
text += this.formatToolUse(block.name, block.input);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
} else if (json.type === "content_block_delta" && json.delta?.text) {
|
|
1518
|
+
text = json.delta.text;
|
|
1519
|
+
}
|
|
1520
|
+
if (text) {
|
|
1521
|
+
this.runningTask?.output.push(text);
|
|
1522
|
+
this.emit("task:output", { taskId, line: text, lineType: "stdout" });
|
|
1523
|
+
}
|
|
1524
|
+
} catch {
|
|
1525
|
+
const cleanText = line.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "");
|
|
1526
|
+
if (cleanText.trim()) {
|
|
1527
|
+
this.runningTask?.output.push(cleanText + "\n");
|
|
1528
|
+
this.emit("task:output", { taskId, line: cleanText + "\n", lineType: "stdout" });
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
});
|
|
1533
|
+
childProcess.stderr?.on("data", (data) => {
|
|
1534
|
+
const text = data.toString();
|
|
1535
|
+
this.runningTask?.output.push(`[stderr] ${text}`);
|
|
1536
|
+
this.emit("task:output", { taskId, line: `[stderr] ${text}`, lineType: "stderr" });
|
|
1537
|
+
});
|
|
1538
|
+
childProcess.on("error", (error) => {
|
|
1539
|
+
try {
|
|
1540
|
+
unlinkSync(promptFile);
|
|
1541
|
+
} catch {
|
|
1542
|
+
}
|
|
1543
|
+
this.handleQAComplete(taskId, false, [`Fix process error: ${error.message}`]);
|
|
1544
|
+
});
|
|
1545
|
+
childProcess.on("close", (code) => {
|
|
1546
|
+
try {
|
|
1547
|
+
unlinkSync(promptFile);
|
|
1548
|
+
} catch {
|
|
1549
|
+
}
|
|
1550
|
+
const output = this.runningTask?.output.join("") || "";
|
|
1551
|
+
const isComplete = output.includes("<promise>COMPLETE</promise>");
|
|
1552
|
+
this.runningTask = null;
|
|
1553
|
+
if (isComplete || code === 0) {
|
|
1554
|
+
this.runQAVerification(taskId).catch((error) => {
|
|
1555
|
+
console.error("QA verification error after fix:", error);
|
|
1556
|
+
this.handleQAComplete(taskId, false, ["QA verification failed after fix"]);
|
|
1557
|
+
});
|
|
1558
|
+
} else {
|
|
1559
|
+
this.handleQAComplete(taskId, false, [`Fix process exited with code ${code}`]);
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Cancel QA verification
|
|
1565
|
+
*/
|
|
1566
|
+
cancelQA(reason = "Cancelled") {
|
|
1567
|
+
if (!this.qaSession) return false;
|
|
1568
|
+
const { taskId, process: childProcess } = this.qaSession;
|
|
1569
|
+
try {
|
|
1570
|
+
childProcess.kill("SIGTERM");
|
|
1571
|
+
setTimeout(() => {
|
|
1572
|
+
try {
|
|
1573
|
+
if (!childProcess.killed) {
|
|
1574
|
+
childProcess.kill("SIGKILL");
|
|
1575
|
+
}
|
|
1576
|
+
} catch {
|
|
1577
|
+
}
|
|
1578
|
+
}, 2e3);
|
|
1579
|
+
} catch {
|
|
1580
|
+
}
|
|
1581
|
+
this.emit("qa:failed", { taskId, issues: [reason], willRetry: false });
|
|
1582
|
+
this.qaSession = null;
|
|
1583
|
+
this.pendingQATaskId = null;
|
|
1584
|
+
return true;
|
|
1585
|
+
}
|
|
1143
1586
|
/**
|
|
1144
1587
|
* Cancel the running task
|
|
1145
1588
|
*/
|
|
@@ -1219,53 +1662,502 @@ When done, output: <promise>PLANNING_COMPLETE</promise>`;
|
|
|
1219
1662
|
this.emitAFKStatus();
|
|
1220
1663
|
}
|
|
1221
1664
|
/**
|
|
1222
|
-
* Stop AFK mode
|
|
1665
|
+
* Stop AFK mode
|
|
1666
|
+
*/
|
|
1667
|
+
stopAFKMode() {
|
|
1668
|
+
this.afkMode = false;
|
|
1669
|
+
this.emitAFKStatus();
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Emit AFK status
|
|
1673
|
+
*/
|
|
1674
|
+
emitAFKStatus() {
|
|
1675
|
+
this.emit("afk:status", {
|
|
1676
|
+
running: this.afkMode,
|
|
1677
|
+
currentIteration: this.afkIteration,
|
|
1678
|
+
maxIterations: this.afkMaxIterations,
|
|
1679
|
+
tasksCompleted: this.afkTasksCompleted
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Get AFK status
|
|
1684
|
+
*/
|
|
1685
|
+
getAFKStatus() {
|
|
1686
|
+
return {
|
|
1687
|
+
running: this.afkMode,
|
|
1688
|
+
currentIteration: this.afkIteration,
|
|
1689
|
+
maxIterations: this.afkMaxIterations,
|
|
1690
|
+
tasksCompleted: this.afkTasksCompleted
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Cancel running task/planning/QA and stop AFK mode
|
|
1695
|
+
*/
|
|
1696
|
+
cancelAll() {
|
|
1697
|
+
if (this.runningTask) {
|
|
1698
|
+
try {
|
|
1699
|
+
this.runningTask.process.kill("SIGKILL");
|
|
1700
|
+
} catch {
|
|
1701
|
+
}
|
|
1702
|
+
this.runningTask = null;
|
|
1703
|
+
}
|
|
1704
|
+
if (this.planningSession) {
|
|
1705
|
+
try {
|
|
1706
|
+
this.planningSession.process.kill("SIGKILL");
|
|
1707
|
+
} catch {
|
|
1708
|
+
}
|
|
1709
|
+
this.planningSession = null;
|
|
1710
|
+
}
|
|
1711
|
+
if (this.qaSession) {
|
|
1712
|
+
try {
|
|
1713
|
+
this.qaSession.process.kill("SIGKILL");
|
|
1714
|
+
} catch {
|
|
1715
|
+
}
|
|
1716
|
+
this.qaSession = null;
|
|
1717
|
+
}
|
|
1718
|
+
this.pendingQATaskId = null;
|
|
1719
|
+
this.qaRetryCount.clear();
|
|
1720
|
+
this.stopAFKMode();
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
|
|
1724
|
+
// src/server/services/roadmap.ts
|
|
1725
|
+
import { spawn as spawn2 } from "child_process";
|
|
1726
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
1727
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
1728
|
+
import { join as join5, basename as basename2 } from "path";
|
|
1729
|
+
import { nanoid as nanoid2 } from "nanoid";
|
|
1730
|
+
var ROADMAP_DIR = ".claude-kanban";
|
|
1731
|
+
var ROADMAP_FILE = "roadmap.json";
|
|
1732
|
+
var RoadmapService = class extends EventEmitter2 {
|
|
1733
|
+
projectPath;
|
|
1734
|
+
session = null;
|
|
1735
|
+
config = null;
|
|
1736
|
+
constructor(projectPath) {
|
|
1737
|
+
super();
|
|
1738
|
+
this.projectPath = projectPath;
|
|
1739
|
+
this.loadConfig();
|
|
1740
|
+
}
|
|
1741
|
+
loadConfig() {
|
|
1742
|
+
const configPath = join5(this.projectPath, ROADMAP_DIR, "config.json");
|
|
1743
|
+
if (existsSync3(configPath)) {
|
|
1744
|
+
this.config = JSON.parse(readFileSync5(configPath, "utf-8"));
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Check if roadmap generation is in progress
|
|
1749
|
+
*/
|
|
1750
|
+
isRunning() {
|
|
1751
|
+
return this.session !== null;
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Get existing roadmap if it exists
|
|
1755
|
+
*/
|
|
1756
|
+
getRoadmap() {
|
|
1757
|
+
const roadmapPath = join5(this.projectPath, ROADMAP_DIR, ROADMAP_FILE);
|
|
1758
|
+
if (!existsSync3(roadmapPath)) {
|
|
1759
|
+
return null;
|
|
1760
|
+
}
|
|
1761
|
+
try {
|
|
1762
|
+
return JSON.parse(readFileSync5(roadmapPath, "utf-8"));
|
|
1763
|
+
} catch {
|
|
1764
|
+
return null;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Save roadmap to file
|
|
1769
|
+
*/
|
|
1770
|
+
saveRoadmap(roadmap) {
|
|
1771
|
+
const roadmapDir = join5(this.projectPath, ROADMAP_DIR);
|
|
1772
|
+
if (!existsSync3(roadmapDir)) {
|
|
1773
|
+
mkdirSync3(roadmapDir, { recursive: true });
|
|
1774
|
+
}
|
|
1775
|
+
const roadmapPath = join5(roadmapDir, ROADMAP_FILE);
|
|
1776
|
+
writeFileSync5(roadmapPath, JSON.stringify(roadmap, null, 2));
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Generate a new roadmap using Claude
|
|
1780
|
+
*/
|
|
1781
|
+
async generate(request = {}) {
|
|
1782
|
+
if (this.session) {
|
|
1783
|
+
throw new Error("Roadmap generation already in progress");
|
|
1784
|
+
}
|
|
1785
|
+
this.emit("roadmap:started", {
|
|
1786
|
+
type: "roadmap:started",
|
|
1787
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1788
|
+
});
|
|
1789
|
+
try {
|
|
1790
|
+
this.emit("roadmap:progress", {
|
|
1791
|
+
type: "roadmap:progress",
|
|
1792
|
+
phase: "analyzing",
|
|
1793
|
+
message: "Analyzing project structure..."
|
|
1794
|
+
});
|
|
1795
|
+
const projectInfo = await this.analyzeProject();
|
|
1796
|
+
let competitors;
|
|
1797
|
+
if (request.enableCompetitorResearch) {
|
|
1798
|
+
this.emit("roadmap:progress", {
|
|
1799
|
+
type: "roadmap:progress",
|
|
1800
|
+
phase: "researching",
|
|
1801
|
+
message: "Researching competitors..."
|
|
1802
|
+
});
|
|
1803
|
+
competitors = await this.researchCompetitors(projectInfo);
|
|
1804
|
+
}
|
|
1805
|
+
this.emit("roadmap:progress", {
|
|
1806
|
+
type: "roadmap:progress",
|
|
1807
|
+
phase: "generating",
|
|
1808
|
+
message: "Generating feature roadmap..."
|
|
1809
|
+
});
|
|
1810
|
+
const roadmap = await this.generateRoadmap(projectInfo, competitors, request);
|
|
1811
|
+
this.saveRoadmap(roadmap);
|
|
1812
|
+
this.emit("roadmap:completed", {
|
|
1813
|
+
type: "roadmap:completed",
|
|
1814
|
+
roadmap
|
|
1815
|
+
});
|
|
1816
|
+
} catch (error) {
|
|
1817
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1818
|
+
this.emit("roadmap:failed", {
|
|
1819
|
+
type: "roadmap:failed",
|
|
1820
|
+
error: errorMessage
|
|
1821
|
+
});
|
|
1822
|
+
throw error;
|
|
1823
|
+
} finally {
|
|
1824
|
+
this.session = null;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Cancel ongoing roadmap generation
|
|
1829
|
+
*/
|
|
1830
|
+
cancel() {
|
|
1831
|
+
if (this.session) {
|
|
1832
|
+
this.session.process.kill("SIGTERM");
|
|
1833
|
+
this.session = null;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Analyze the project structure
|
|
1838
|
+
*/
|
|
1839
|
+
async analyzeProject() {
|
|
1840
|
+
const projectName = basename2(this.projectPath);
|
|
1841
|
+
let description = "";
|
|
1842
|
+
let stack = [];
|
|
1843
|
+
let existingFeatures = [];
|
|
1844
|
+
const packageJsonPath = join5(this.projectPath, "package.json");
|
|
1845
|
+
if (existsSync3(packageJsonPath)) {
|
|
1846
|
+
try {
|
|
1847
|
+
const pkg = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
|
|
1848
|
+
description = pkg.description || "";
|
|
1849
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1850
|
+
if (deps.react) stack.push("React");
|
|
1851
|
+
if (deps.vue) stack.push("Vue");
|
|
1852
|
+
if (deps.angular) stack.push("Angular");
|
|
1853
|
+
if (deps.next) stack.push("Next.js");
|
|
1854
|
+
if (deps.express) stack.push("Express");
|
|
1855
|
+
if (deps.fastify) stack.push("Fastify");
|
|
1856
|
+
if (deps.typescript) stack.push("TypeScript");
|
|
1857
|
+
if (deps.tailwindcss) stack.push("Tailwind CSS");
|
|
1858
|
+
} catch {
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
const readmePaths = ["README.md", "readme.md", "README.txt", "readme.txt"];
|
|
1862
|
+
for (const readmePath of readmePaths) {
|
|
1863
|
+
const fullPath = join5(this.projectPath, readmePath);
|
|
1864
|
+
if (existsSync3(fullPath)) {
|
|
1865
|
+
const readme = readFileSync5(fullPath, "utf-8");
|
|
1866
|
+
if (!description) {
|
|
1867
|
+
const lines = readme.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
1868
|
+
if (lines.length > 0) {
|
|
1869
|
+
description = lines[0].trim().slice(0, 500);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
break;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
const prdPath = join5(this.projectPath, ROADMAP_DIR, "prd.json");
|
|
1876
|
+
if (existsSync3(prdPath)) {
|
|
1877
|
+
try {
|
|
1878
|
+
const prd = JSON.parse(readFileSync5(prdPath, "utf-8"));
|
|
1879
|
+
existingFeatures = prd.tasks?.map((t) => t.title) || [];
|
|
1880
|
+
} catch {
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
return {
|
|
1884
|
+
name: projectName,
|
|
1885
|
+
description,
|
|
1886
|
+
stack,
|
|
1887
|
+
existingFeatures
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
/**
|
|
1891
|
+
* Research competitors using Claude with web search
|
|
1892
|
+
*/
|
|
1893
|
+
async researchCompetitors(projectInfo) {
|
|
1894
|
+
const prompt = this.buildCompetitorResearchPrompt(projectInfo);
|
|
1895
|
+
const result = await this.runClaudeCommand(prompt);
|
|
1896
|
+
try {
|
|
1897
|
+
const jsonMatch = result.match(/```json\n([\s\S]*?)\n```/);
|
|
1898
|
+
if (jsonMatch) {
|
|
1899
|
+
return JSON.parse(jsonMatch[1]);
|
|
1900
|
+
}
|
|
1901
|
+
return JSON.parse(result);
|
|
1902
|
+
} catch {
|
|
1903
|
+
console.error("[roadmap] Failed to parse competitor research:", result);
|
|
1904
|
+
return [];
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Generate the roadmap using Claude
|
|
1909
|
+
*/
|
|
1910
|
+
async generateRoadmap(projectInfo, competitors, request) {
|
|
1911
|
+
const prompt = this.buildRoadmapPrompt(projectInfo, competitors, request);
|
|
1912
|
+
const result = await this.runClaudeCommand(prompt);
|
|
1913
|
+
try {
|
|
1914
|
+
const jsonMatch = result.match(/```json\n([\s\S]*?)\n```/);
|
|
1915
|
+
let roadmapData;
|
|
1916
|
+
if (jsonMatch) {
|
|
1917
|
+
roadmapData = JSON.parse(jsonMatch[1]);
|
|
1918
|
+
} else {
|
|
1919
|
+
roadmapData = JSON.parse(result);
|
|
1920
|
+
}
|
|
1921
|
+
const roadmap = {
|
|
1922
|
+
id: `roadmap_${nanoid2(8)}`,
|
|
1923
|
+
projectName: projectInfo.name,
|
|
1924
|
+
projectDescription: roadmapData.projectDescription || projectInfo.description,
|
|
1925
|
+
targetAudience: roadmapData.targetAudience || "Developers",
|
|
1926
|
+
phases: roadmapData.phases.map((p, i) => ({
|
|
1927
|
+
id: `phase_${nanoid2(8)}`,
|
|
1928
|
+
name: p.name,
|
|
1929
|
+
description: p.description,
|
|
1930
|
+
order: i
|
|
1931
|
+
})),
|
|
1932
|
+
features: roadmapData.features.map((f) => ({
|
|
1933
|
+
id: `feature_${nanoid2(8)}`,
|
|
1934
|
+
title: f.title,
|
|
1935
|
+
description: f.description,
|
|
1936
|
+
priority: f.priority,
|
|
1937
|
+
category: f.category || "functional",
|
|
1938
|
+
effort: f.effort || "medium",
|
|
1939
|
+
impact: f.impact || "medium",
|
|
1940
|
+
phase: f.phase,
|
|
1941
|
+
rationale: f.rationale || "",
|
|
1942
|
+
steps: f.steps,
|
|
1943
|
+
addedToKanban: false
|
|
1944
|
+
})),
|
|
1945
|
+
competitors,
|
|
1946
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1947
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1948
|
+
};
|
|
1949
|
+
return roadmap;
|
|
1950
|
+
} catch (error) {
|
|
1951
|
+
console.error("[roadmap] Failed to parse roadmap:", result);
|
|
1952
|
+
throw new Error("Failed to parse roadmap from Claude response");
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
/**
|
|
1956
|
+
* Build the competitor research prompt
|
|
1223
1957
|
*/
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1958
|
+
buildCompetitorResearchPrompt(projectInfo) {
|
|
1959
|
+
return `You are a product research analyst. Research competitors for the following project:
|
|
1960
|
+
|
|
1961
|
+
Project: ${projectInfo.name}
|
|
1962
|
+
Description: ${projectInfo.description}
|
|
1963
|
+
Tech Stack: ${projectInfo.stack.join(", ") || "Unknown"}
|
|
1964
|
+
|
|
1965
|
+
Your task:
|
|
1966
|
+
1. Use web search to find 3-5 competitors or similar projects
|
|
1967
|
+
2. Analyze their strengths and weaknesses
|
|
1968
|
+
3. Identify differentiating features
|
|
1969
|
+
|
|
1970
|
+
Return your findings as JSON in this format:
|
|
1971
|
+
\`\`\`json
|
|
1972
|
+
[
|
|
1973
|
+
{
|
|
1974
|
+
"name": "Competitor Name",
|
|
1975
|
+
"url": "https://example.com",
|
|
1976
|
+
"strengths": ["strength 1", "strength 2"],
|
|
1977
|
+
"weaknesses": ["weakness 1", "weakness 2"],
|
|
1978
|
+
"differentiators": ["feature 1", "feature 2"]
|
|
1979
|
+
}
|
|
1980
|
+
]
|
|
1981
|
+
\`\`\`
|
|
1982
|
+
|
|
1983
|
+
Only return the JSON, no other text.`;
|
|
1227
1984
|
}
|
|
1228
1985
|
/**
|
|
1229
|
-
*
|
|
1986
|
+
* Build the roadmap generation prompt
|
|
1230
1987
|
*/
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1988
|
+
buildRoadmapPrompt(projectInfo, competitors, request) {
|
|
1989
|
+
let prompt = `You are a product strategist creating a feature roadmap for a software project.
|
|
1990
|
+
|
|
1991
|
+
## Project Information
|
|
1992
|
+
Name: ${projectInfo.name}
|
|
1993
|
+
Description: ${projectInfo.description}
|
|
1994
|
+
Tech Stack: ${projectInfo.stack.join(", ") || "Unknown"}
|
|
1995
|
+
${projectInfo.existingFeatures.length > 0 ? `
|
|
1996
|
+
Existing Features/Tasks:
|
|
1997
|
+
${projectInfo.existingFeatures.map((f) => `- ${f}`).join("\n")}` : ""}
|
|
1998
|
+
`;
|
|
1999
|
+
if (competitors && competitors.length > 0) {
|
|
2000
|
+
prompt += `
|
|
2001
|
+
## Competitor Analysis
|
|
2002
|
+
${competitors.map((c) => `
|
|
2003
|
+
### ${c.name}
|
|
2004
|
+
- Strengths: ${c.strengths.join(", ")}
|
|
2005
|
+
- Weaknesses: ${c.weaknesses.join(", ")}
|
|
2006
|
+
- Key Features: ${c.differentiators.join(", ")}
|
|
2007
|
+
`).join("\n")}
|
|
2008
|
+
`;
|
|
2009
|
+
}
|
|
2010
|
+
if (request.focusAreas && request.focusAreas.length > 0) {
|
|
2011
|
+
prompt += `
|
|
2012
|
+
## Focus Areas
|
|
2013
|
+
The user wants to focus on: ${request.focusAreas.join(", ")}
|
|
2014
|
+
`;
|
|
2015
|
+
}
|
|
2016
|
+
if (request.customPrompt) {
|
|
2017
|
+
prompt += `
|
|
2018
|
+
## Additional Context
|
|
2019
|
+
${request.customPrompt}
|
|
2020
|
+
`;
|
|
2021
|
+
}
|
|
2022
|
+
prompt += `
|
|
2023
|
+
## Your Task
|
|
2024
|
+
Create a comprehensive product roadmap with features organized into phases.
|
|
2025
|
+
|
|
2026
|
+
Use the MoSCoW prioritization framework:
|
|
2027
|
+
- must: Critical features that must be implemented
|
|
2028
|
+
- should: Important features that should be implemented
|
|
2029
|
+
- could: Nice-to-have features that could be implemented
|
|
2030
|
+
- wont: Features that won't be implemented in the near term
|
|
2031
|
+
|
|
2032
|
+
For each feature, estimate:
|
|
2033
|
+
- effort: low, medium, or high
|
|
2034
|
+
- impact: low, medium, or high
|
|
2035
|
+
|
|
2036
|
+
Categories should be one of: functional, ui, bug, enhancement, testing, refactor
|
|
2037
|
+
|
|
2038
|
+
Return your roadmap as JSON:
|
|
2039
|
+
\`\`\`json
|
|
2040
|
+
{
|
|
2041
|
+
"projectDescription": "Brief description of the project",
|
|
2042
|
+
"targetAudience": "Who this project is for",
|
|
2043
|
+
"phases": [
|
|
2044
|
+
{
|
|
2045
|
+
"name": "Phase 1: MVP",
|
|
2046
|
+
"description": "Core features for minimum viable product"
|
|
2047
|
+
},
|
|
2048
|
+
{
|
|
2049
|
+
"name": "Phase 2: Enhancement",
|
|
2050
|
+
"description": "Features to improve user experience"
|
|
2051
|
+
}
|
|
2052
|
+
],
|
|
2053
|
+
"features": [
|
|
2054
|
+
{
|
|
2055
|
+
"title": "Feature title",
|
|
2056
|
+
"description": "What this feature does",
|
|
2057
|
+
"priority": "must",
|
|
2058
|
+
"category": "functional",
|
|
2059
|
+
"effort": "medium",
|
|
2060
|
+
"impact": "high",
|
|
2061
|
+
"phase": "Phase 1: MVP",
|
|
2062
|
+
"rationale": "Why this feature is important",
|
|
2063
|
+
"steps": ["Step 1", "Step 2"]
|
|
2064
|
+
}
|
|
2065
|
+
]
|
|
2066
|
+
}
|
|
2067
|
+
\`\`\`
|
|
2068
|
+
|
|
2069
|
+
Generate 10-20 strategic features across 3-4 phases. Be specific and actionable.
|
|
2070
|
+
Don't duplicate features that already exist in the project.
|
|
2071
|
+
Only return the JSON, no other text.`;
|
|
2072
|
+
return prompt;
|
|
1238
2073
|
}
|
|
1239
2074
|
/**
|
|
1240
|
-
*
|
|
2075
|
+
* Run a Claude command and return the output
|
|
1241
2076
|
*/
|
|
1242
|
-
|
|
1243
|
-
return {
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
2077
|
+
runClaudeCommand(prompt) {
|
|
2078
|
+
return new Promise((resolve, reject) => {
|
|
2079
|
+
const command = this.config?.agent.command || "claude";
|
|
2080
|
+
const permissionMode = this.config?.agent.permissionMode || "auto";
|
|
2081
|
+
const model = this.config?.agent.model;
|
|
2082
|
+
const promptPath = join5(this.projectPath, ROADMAP_DIR, "prompt-roadmap.txt");
|
|
2083
|
+
writeFileSync5(promptPath, prompt);
|
|
2084
|
+
const args = [
|
|
2085
|
+
"--permission-mode",
|
|
2086
|
+
permissionMode,
|
|
2087
|
+
"-p",
|
|
2088
|
+
"--verbose",
|
|
2089
|
+
"--output-format",
|
|
2090
|
+
"text",
|
|
2091
|
+
`@${promptPath}`
|
|
2092
|
+
];
|
|
2093
|
+
if (model) {
|
|
2094
|
+
args.unshift("--model", model);
|
|
2095
|
+
}
|
|
2096
|
+
console.log(`[roadmap] Command: ${command} ${args.join(" ")}`);
|
|
2097
|
+
const childProcess = spawn2(command, args, {
|
|
2098
|
+
cwd: this.projectPath,
|
|
2099
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2100
|
+
env: {
|
|
2101
|
+
...process.env,
|
|
2102
|
+
FORCE_COLOR: "0"
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
this.session = {
|
|
2106
|
+
process: childProcess,
|
|
2107
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
2108
|
+
output: []
|
|
2109
|
+
};
|
|
2110
|
+
let stdout = "";
|
|
2111
|
+
let stderr = "";
|
|
2112
|
+
childProcess.stdout?.on("data", (data) => {
|
|
2113
|
+
const chunk = data.toString();
|
|
2114
|
+
stdout += chunk;
|
|
2115
|
+
this.session?.output.push(chunk);
|
|
2116
|
+
});
|
|
2117
|
+
childProcess.stderr?.on("data", (data) => {
|
|
2118
|
+
stderr += data.toString();
|
|
2119
|
+
});
|
|
2120
|
+
childProcess.on("close", (code) => {
|
|
2121
|
+
this.session = null;
|
|
2122
|
+
if (code === 0) {
|
|
2123
|
+
resolve(stdout);
|
|
2124
|
+
} else {
|
|
2125
|
+
reject(new Error(`Claude command failed with code ${code}: ${stderr}`));
|
|
2126
|
+
}
|
|
2127
|
+
});
|
|
2128
|
+
childProcess.on("error", (error) => {
|
|
2129
|
+
this.session = null;
|
|
2130
|
+
reject(error);
|
|
2131
|
+
});
|
|
2132
|
+
});
|
|
1249
2133
|
}
|
|
1250
2134
|
/**
|
|
1251
|
-
*
|
|
2135
|
+
* Mark a feature as added to kanban
|
|
1252
2136
|
*/
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
2137
|
+
markFeatureAdded(featureId) {
|
|
2138
|
+
const roadmap = this.getRoadmap();
|
|
2139
|
+
if (!roadmap) return null;
|
|
2140
|
+
const feature = roadmap.features.find((f) => f.id === featureId);
|
|
2141
|
+
if (feature) {
|
|
2142
|
+
feature.addedToKanban = true;
|
|
2143
|
+
roadmap.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2144
|
+
this.saveRoadmap(roadmap);
|
|
1260
2145
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
2146
|
+
return roadmap;
|
|
2147
|
+
}
|
|
2148
|
+
/**
|
|
2149
|
+
* Delete a feature from the roadmap
|
|
2150
|
+
*/
|
|
2151
|
+
deleteFeature(featureId) {
|
|
2152
|
+
const roadmap = this.getRoadmap();
|
|
2153
|
+
if (!roadmap) return null;
|
|
2154
|
+
const index = roadmap.features.findIndex((f) => f.id === featureId);
|
|
2155
|
+
if (index !== -1) {
|
|
2156
|
+
roadmap.features.splice(index, 1);
|
|
2157
|
+
roadmap.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2158
|
+
this.saveRoadmap(roadmap);
|
|
1267
2159
|
}
|
|
1268
|
-
|
|
2160
|
+
return roadmap;
|
|
1269
2161
|
}
|
|
1270
2162
|
};
|
|
1271
2163
|
|
|
@@ -1572,7 +2464,7 @@ function getTemplateById(id) {
|
|
|
1572
2464
|
}
|
|
1573
2465
|
|
|
1574
2466
|
// src/server/services/ai.ts
|
|
1575
|
-
import { spawn as
|
|
2467
|
+
import { spawn as spawn3 } from "child_process";
|
|
1576
2468
|
async function generateTaskFromPrompt(projectPath, userPrompt) {
|
|
1577
2469
|
const config = getConfig(projectPath);
|
|
1578
2470
|
const systemPrompt = `You are a task generator for a Kanban board used in software development.
|
|
@@ -1611,7 +2503,7 @@ User request: ${userPrompt}`;
|
|
|
1611
2503
|
}
|
|
1612
2504
|
let output = "";
|
|
1613
2505
|
let errorOutput = "";
|
|
1614
|
-
const proc =
|
|
2506
|
+
const proc = spawn3(config.agent.command, args, {
|
|
1615
2507
|
cwd: projectPath,
|
|
1616
2508
|
shell: true,
|
|
1617
2509
|
env: { ...process.env }
|
|
@@ -1683,6 +2575,7 @@ async function createServer(projectPath, port) {
|
|
|
1683
2575
|
});
|
|
1684
2576
|
app.use(express.json());
|
|
1685
2577
|
const executor = new TaskExecutor(projectPath);
|
|
2578
|
+
const roadmapService = new RoadmapService(projectPath);
|
|
1686
2579
|
executor.on("task:started", (data) => io.emit("task:started", data));
|
|
1687
2580
|
executor.on("task:output", (data) => io.emit("task:output", data));
|
|
1688
2581
|
executor.on("task:completed", (data) => io.emit("task:completed", data));
|
|
@@ -1694,6 +2587,10 @@ async function createServer(projectPath, port) {
|
|
|
1694
2587
|
executor.on("planning:completed", (data) => io.emit("planning:completed", data));
|
|
1695
2588
|
executor.on("planning:failed", (data) => io.emit("planning:failed", data));
|
|
1696
2589
|
executor.on("planning:cancelled", (data) => io.emit("planning:cancelled", data));
|
|
2590
|
+
roadmapService.on("roadmap:started", (data) => io.emit("roadmap:started", data));
|
|
2591
|
+
roadmapService.on("roadmap:progress", (data) => io.emit("roadmap:progress", data));
|
|
2592
|
+
roadmapService.on("roadmap:completed", (data) => io.emit("roadmap:completed", data));
|
|
2593
|
+
roadmapService.on("roadmap:failed", (data) => io.emit("roadmap:failed", data));
|
|
1697
2594
|
app.get("/api/tasks", (_req, res) => {
|
|
1698
2595
|
try {
|
|
1699
2596
|
const tasks = getAllTasks(projectPath);
|
|
@@ -1961,8 +2858,98 @@ async function createServer(projectPath, port) {
|
|
|
1961
2858
|
res.status(500).json({ error: String(error) });
|
|
1962
2859
|
}
|
|
1963
2860
|
});
|
|
1964
|
-
|
|
1965
|
-
|
|
2861
|
+
app.get("/api/roadmap", (_req, res) => {
|
|
2862
|
+
try {
|
|
2863
|
+
const roadmap = roadmapService.getRoadmap();
|
|
2864
|
+
res.json({ roadmap });
|
|
2865
|
+
} catch (error) {
|
|
2866
|
+
res.status(500).json({ error: String(error) });
|
|
2867
|
+
}
|
|
2868
|
+
});
|
|
2869
|
+
app.post("/api/roadmap/generate", async (req, res) => {
|
|
2870
|
+
try {
|
|
2871
|
+
const request = req.body;
|
|
2872
|
+
if (roadmapService.isRunning()) {
|
|
2873
|
+
res.status(400).json({ error: "Roadmap generation already in progress" });
|
|
2874
|
+
return;
|
|
2875
|
+
}
|
|
2876
|
+
roadmapService.generate(request).catch(console.error);
|
|
2877
|
+
res.json({ success: true, message: "Roadmap generation started" });
|
|
2878
|
+
} catch (error) {
|
|
2879
|
+
res.status(500).json({ error: String(error) });
|
|
2880
|
+
}
|
|
2881
|
+
});
|
|
2882
|
+
app.post("/api/roadmap/cancel", (_req, res) => {
|
|
2883
|
+
try {
|
|
2884
|
+
if (!roadmapService.isRunning()) {
|
|
2885
|
+
res.status(400).json({ error: "No roadmap generation in progress" });
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
roadmapService.cancel();
|
|
2889
|
+
res.json({ success: true });
|
|
2890
|
+
} catch (error) {
|
|
2891
|
+
res.status(500).json({ error: String(error) });
|
|
2892
|
+
}
|
|
2893
|
+
});
|
|
2894
|
+
app.post("/api/roadmap/features/:id/add-to-kanban", (req, res) => {
|
|
2895
|
+
try {
|
|
2896
|
+
const roadmap = roadmapService.getRoadmap();
|
|
2897
|
+
if (!roadmap) {
|
|
2898
|
+
res.status(404).json({ error: "No roadmap found" });
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
const feature = roadmap.features.find((f) => f.id === req.params.id);
|
|
2902
|
+
if (!feature) {
|
|
2903
|
+
res.status(404).json({ error: "Feature not found" });
|
|
2904
|
+
return;
|
|
2905
|
+
}
|
|
2906
|
+
if (feature.addedToKanban) {
|
|
2907
|
+
res.status(400).json({ error: "Feature already added to kanban" });
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
const priorityMap = {
|
|
2911
|
+
must: "critical",
|
|
2912
|
+
should: "high",
|
|
2913
|
+
could: "medium",
|
|
2914
|
+
wont: "low"
|
|
2915
|
+
};
|
|
2916
|
+
const task = createTask(projectPath, {
|
|
2917
|
+
title: feature.title,
|
|
2918
|
+
description: feature.description,
|
|
2919
|
+
category: feature.category,
|
|
2920
|
+
priority: priorityMap[feature.priority] || "medium",
|
|
2921
|
+
steps: feature.steps || [],
|
|
2922
|
+
status: "draft"
|
|
2923
|
+
});
|
|
2924
|
+
roadmapService.markFeatureAdded(req.params.id);
|
|
2925
|
+
io.emit("task:created", task);
|
|
2926
|
+
res.json({ task });
|
|
2927
|
+
} catch (error) {
|
|
2928
|
+
res.status(500).json({ error: String(error) });
|
|
2929
|
+
}
|
|
2930
|
+
});
|
|
2931
|
+
app.delete("/api/roadmap/features/:id", (req, res) => {
|
|
2932
|
+
try {
|
|
2933
|
+
const roadmap = roadmapService.deleteFeature(req.params.id);
|
|
2934
|
+
if (!roadmap) {
|
|
2935
|
+
res.status(404).json({ error: "Roadmap or feature not found" });
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
io.emit("roadmap:updated", roadmap);
|
|
2939
|
+
res.json({ success: true });
|
|
2940
|
+
} catch (error) {
|
|
2941
|
+
res.status(500).json({ error: String(error) });
|
|
2942
|
+
}
|
|
2943
|
+
});
|
|
2944
|
+
app.get("/api/roadmap/status", (_req, res) => {
|
|
2945
|
+
try {
|
|
2946
|
+
res.json({ generating: roadmapService.isRunning() });
|
|
2947
|
+
} catch (error) {
|
|
2948
|
+
res.status(500).json({ error: String(error) });
|
|
2949
|
+
}
|
|
2950
|
+
});
|
|
2951
|
+
const clientPath = join6(__dirname2, "..", "client");
|
|
2952
|
+
if (existsSync4(clientPath)) {
|
|
1966
2953
|
app.use(express.static(clientPath));
|
|
1967
2954
|
}
|
|
1968
2955
|
app.get("*", (_req, res) => {
|
|
@@ -2535,6 +3522,16 @@ let state = {
|
|
|
2535
3522
|
planningOutput: [],
|
|
2536
3523
|
sidePanelTab: 'logs', // 'logs' or 'details'
|
|
2537
3524
|
darkMode: localStorage.getItem('darkMode') === 'true', // Add dark mode state
|
|
3525
|
+
// View state (board or roadmap)
|
|
3526
|
+
currentView: 'board', // 'board' or 'roadmap'
|
|
3527
|
+
// Roadmap state
|
|
3528
|
+
roadmap: null,
|
|
3529
|
+
roadmapGenerating: false,
|
|
3530
|
+
roadmapProgress: null,
|
|
3531
|
+
roadmapError: null,
|
|
3532
|
+
roadmapSelectedFeature: null,
|
|
3533
|
+
roadmapEnableCompetitors: false,
|
|
3534
|
+
roadmapCustomPrompt: '',
|
|
2538
3535
|
};
|
|
2539
3536
|
|
|
2540
3537
|
// Toast notifications
|
|
@@ -2781,6 +3778,41 @@ socket.on('planning:cancelled', () => {
|
|
|
2781
3778
|
render();
|
|
2782
3779
|
});
|
|
2783
3780
|
|
|
3781
|
+
// Roadmap events
|
|
3782
|
+
socket.on('roadmap:started', () => {
|
|
3783
|
+
state.roadmapGenerating = true;
|
|
3784
|
+
state.roadmapProgress = { phase: 'analyzing', message: 'Starting roadmap generation...' };
|
|
3785
|
+
state.roadmapError = null;
|
|
3786
|
+
render();
|
|
3787
|
+
});
|
|
3788
|
+
|
|
3789
|
+
socket.on('roadmap:progress', ({ phase, message }) => {
|
|
3790
|
+
state.roadmapProgress = { phase, message };
|
|
3791
|
+
render();
|
|
3792
|
+
});
|
|
3793
|
+
|
|
3794
|
+
socket.on('roadmap:completed', ({ roadmap }) => {
|
|
3795
|
+
state.roadmap = roadmap;
|
|
3796
|
+
state.roadmapGenerating = false;
|
|
3797
|
+
state.roadmapProgress = null;
|
|
3798
|
+
state.showModal = null;
|
|
3799
|
+
showToast('Roadmap generated successfully!', 'success');
|
|
3800
|
+
render();
|
|
3801
|
+
});
|
|
3802
|
+
|
|
3803
|
+
socket.on('roadmap:failed', ({ error }) => {
|
|
3804
|
+
state.roadmapGenerating = false;
|
|
3805
|
+
state.roadmapProgress = null;
|
|
3806
|
+
state.roadmapError = error;
|
|
3807
|
+
showToast('Roadmap generation failed: ' + error, 'error');
|
|
3808
|
+
render();
|
|
3809
|
+
});
|
|
3810
|
+
|
|
3811
|
+
socket.on('roadmap:updated', (roadmap) => {
|
|
3812
|
+
state.roadmap = roadmap;
|
|
3813
|
+
render();
|
|
3814
|
+
});
|
|
3815
|
+
|
|
2784
3816
|
// Load templates
|
|
2785
3817
|
fetch('/api/templates').then(r => r.json()).then(data => {
|
|
2786
3818
|
state.templates = data.templates;
|
|
@@ -2872,6 +3904,69 @@ async function cancelPlanning() {
|
|
|
2872
3904
|
await fetch('/api/plan/cancel', { method: 'POST' });
|
|
2873
3905
|
}
|
|
2874
3906
|
|
|
3907
|
+
// Roadmap API functions
|
|
3908
|
+
async function loadRoadmap() {
|
|
3909
|
+
const res = await fetch('/api/roadmap');
|
|
3910
|
+
const data = await res.json();
|
|
3911
|
+
state.roadmap = data.roadmap;
|
|
3912
|
+
render();
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
async function generateRoadmap() {
|
|
3916
|
+
state.roadmapGenerating = true;
|
|
3917
|
+
state.roadmapError = null;
|
|
3918
|
+
render();
|
|
3919
|
+
try {
|
|
3920
|
+
await fetch('/api/roadmap/generate', {
|
|
3921
|
+
method: 'POST',
|
|
3922
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3923
|
+
body: JSON.stringify({
|
|
3924
|
+
enableCompetitorResearch: state.roadmapEnableCompetitors,
|
|
3925
|
+
customPrompt: state.roadmapCustomPrompt || undefined
|
|
3926
|
+
})
|
|
3927
|
+
});
|
|
3928
|
+
} catch (e) {
|
|
3929
|
+
state.roadmapGenerating = false;
|
|
3930
|
+
state.roadmapError = e.message;
|
|
3931
|
+
render();
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3935
|
+
async function cancelRoadmap() {
|
|
3936
|
+
await fetch('/api/roadmap/cancel', { method: 'POST' });
|
|
3937
|
+
state.roadmapGenerating = false;
|
|
3938
|
+
state.roadmapProgress = null;
|
|
3939
|
+
render();
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3942
|
+
async function addFeatureToKanban(featureId) {
|
|
3943
|
+
try {
|
|
3944
|
+
const res = await fetch('/api/roadmap/features/' + featureId + '/add-to-kanban', {
|
|
3945
|
+
method: 'POST'
|
|
3946
|
+
});
|
|
3947
|
+
const data = await res.json();
|
|
3948
|
+
if (data.task) {
|
|
3949
|
+
showToast('Feature added to kanban!', 'success');
|
|
3950
|
+
// Reload roadmap to update addedToKanban status
|
|
3951
|
+
await loadRoadmap();
|
|
3952
|
+
}
|
|
3953
|
+
} catch (e) {
|
|
3954
|
+
showToast('Failed to add feature: ' + e.message, 'error');
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
async function deleteRoadmapFeature(featureId) {
|
|
3959
|
+
try {
|
|
3960
|
+
await fetch('/api/roadmap/features/' + featureId, { method: 'DELETE' });
|
|
3961
|
+
showToast('Feature removed', 'info');
|
|
3962
|
+
} catch (e) {
|
|
3963
|
+
showToast('Failed to remove feature: ' + e.message, 'error');
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
// Load roadmap on init
|
|
3968
|
+
loadRoadmap();
|
|
3969
|
+
|
|
2875
3970
|
// Enhanced Drag and drop
|
|
2876
3971
|
let draggedTask = null;
|
|
2877
3972
|
let draggedElement = null;
|
|
@@ -3044,6 +4139,185 @@ function showTaskMenu(taskId) {
|
|
|
3044
4139
|
openSidePanel(taskId);
|
|
3045
4140
|
}
|
|
3046
4141
|
|
|
4142
|
+
// Roadmap rendering functions
|
|
4143
|
+
function renderRoadmap() {
|
|
4144
|
+
if (state.roadmapGenerating) {
|
|
4145
|
+
return \`
|
|
4146
|
+
<div class="flex-1 flex items-center justify-center">
|
|
4147
|
+
<div class="text-center">
|
|
4148
|
+
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-accent mx-auto mb-4"></div>
|
|
4149
|
+
<h3 class="text-lg font-medium text-canvas-800 mb-2">Generating Roadmap...</h3>
|
|
4150
|
+
<p class="text-canvas-500">\${state.roadmapProgress?.message || 'Please wait...'}</p>
|
|
4151
|
+
<button onclick="cancelRoadmap()" class="btn btn-ghost mt-4 text-sm">Cancel</button>
|
|
4152
|
+
</div>
|
|
4153
|
+
</div>
|
|
4154
|
+
\`;
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4157
|
+
if (!state.roadmap) {
|
|
4158
|
+
return \`
|
|
4159
|
+
<div class="flex-1 flex items-center justify-center">
|
|
4160
|
+
<div class="text-center max-w-md">
|
|
4161
|
+
<div class="text-6xl mb-4">\u{1F5FA}\uFE0F</div>
|
|
4162
|
+
<h3 class="text-xl font-semibold text-canvas-800 mb-2">No Roadmap Yet</h3>
|
|
4163
|
+
<p class="text-canvas-500 mb-6">Generate a strategic feature roadmap using AI to analyze your project and suggest features.</p>
|
|
4164
|
+
<button onclick="state.showModal = 'roadmap'; render();" class="btn btn-primary px-6 py-2">
|
|
4165
|
+
\u{1F680} Generate Roadmap
|
|
4166
|
+
</button>
|
|
4167
|
+
</div>
|
|
4168
|
+
</div>
|
|
4169
|
+
\`;
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
const roadmap = state.roadmap;
|
|
4173
|
+
const phases = roadmap.phases || [];
|
|
4174
|
+
const features = roadmap.features || [];
|
|
4175
|
+
|
|
4176
|
+
return \`
|
|
4177
|
+
<div class="flex-1 overflow-y-auto p-6">
|
|
4178
|
+
<!-- Roadmap Header -->
|
|
4179
|
+
<div class="mb-6 flex items-start justify-between">
|
|
4180
|
+
<div>
|
|
4181
|
+
<h2 class="text-2xl font-semibold text-canvas-900">\${escapeHtml(roadmap.projectName)} Roadmap</h2>
|
|
4182
|
+
<p class="text-canvas-500 mt-1">\${escapeHtml(roadmap.projectDescription || '')}</p>
|
|
4183
|
+
<p class="text-sm text-canvas-400 mt-2">Target: \${escapeHtml(roadmap.targetAudience || 'Developers')}</p>
|
|
4184
|
+
</div>
|
|
4185
|
+
<div class="flex gap-2">
|
|
4186
|
+
<button onclick="state.showModal = 'roadmap'; render();" class="btn btn-ghost text-sm">
|
|
4187
|
+
\u{1F504} Regenerate
|
|
4188
|
+
</button>
|
|
4189
|
+
</div>
|
|
4190
|
+
</div>
|
|
4191
|
+
|
|
4192
|
+
<!-- Competitors (if available) -->
|
|
4193
|
+
\${roadmap.competitors && roadmap.competitors.length > 0 ? \`
|
|
4194
|
+
<div class="mb-6">
|
|
4195
|
+
<h3 class="text-sm font-medium text-canvas-700 mb-2">Competitor Insights</h3>
|
|
4196
|
+
<div class="flex gap-2 flex-wrap">
|
|
4197
|
+
\${roadmap.competitors.map(c => \`
|
|
4198
|
+
<span class="px-3 py-1 bg-canvas-100 rounded-full text-sm text-canvas-600">
|
|
4199
|
+
\${escapeHtml(c.name)}
|
|
4200
|
+
</span>
|
|
4201
|
+
\`).join('')}
|
|
4202
|
+
</div>
|
|
4203
|
+
</div>
|
|
4204
|
+
\` : ''}
|
|
4205
|
+
|
|
4206
|
+
<!-- Phases -->
|
|
4207
|
+
<div class="space-y-8">
|
|
4208
|
+
\${phases.map(phase => {
|
|
4209
|
+
const phaseFeatures = features.filter(f => f.phase === phase.name);
|
|
4210
|
+
return \`
|
|
4211
|
+
<div class="phase-section">
|
|
4212
|
+
<div class="flex items-center gap-3 mb-4">
|
|
4213
|
+
<h3 class="text-lg font-semibold text-canvas-800">\${escapeHtml(phase.name)}</h3>
|
|
4214
|
+
<span class="text-xs bg-canvas-100 px-2 py-0.5 rounded-full text-canvas-500">\${phaseFeatures.length} features</span>
|
|
4215
|
+
</div>
|
|
4216
|
+
<p class="text-sm text-canvas-500 mb-4">\${escapeHtml(phase.description || '')}</p>
|
|
4217
|
+
<div class="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
|
4218
|
+
\${phaseFeatures.map(f => renderRoadmapFeature(f)).join('')}
|
|
4219
|
+
</div>
|
|
4220
|
+
</div>
|
|
4221
|
+
\`;
|
|
4222
|
+
}).join('')}
|
|
4223
|
+
</div>
|
|
4224
|
+
</div>
|
|
4225
|
+
\`;
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
function renderRoadmapFeature(feature) {
|
|
4229
|
+
const priorityColors = {
|
|
4230
|
+
must: 'bg-red-100 text-red-700',
|
|
4231
|
+
should: 'bg-orange-100 text-orange-700',
|
|
4232
|
+
could: 'bg-blue-100 text-blue-700',
|
|
4233
|
+
wont: 'bg-gray-100 text-gray-500'
|
|
4234
|
+
};
|
|
4235
|
+
const priorityLabels = {
|
|
4236
|
+
must: 'Must Have',
|
|
4237
|
+
should: 'Should Have',
|
|
4238
|
+
could: 'Could Have',
|
|
4239
|
+
wont: "Won't Have"
|
|
4240
|
+
};
|
|
4241
|
+
const effortIcons = {
|
|
4242
|
+
low: '\u26A1',
|
|
4243
|
+
medium: '\u23F1\uFE0F',
|
|
4244
|
+
high: '\u{1F3CB}\uFE0F'
|
|
4245
|
+
};
|
|
4246
|
+
const impactIcons = {
|
|
4247
|
+
low: '\u{1F4C9}',
|
|
4248
|
+
medium: '\u{1F4CA}',
|
|
4249
|
+
high: '\u{1F4C8}'
|
|
4250
|
+
};
|
|
4251
|
+
|
|
4252
|
+
return \`
|
|
4253
|
+
<div class="card p-4 \${feature.addedToKanban ? 'opacity-60' : ''}" onclick="state.roadmapSelectedFeature = '\${feature.id}'; render();">
|
|
4254
|
+
<div class="flex items-start justify-between mb-2">
|
|
4255
|
+
<h4 class="font-medium text-canvas-800 text-sm">\${escapeHtml(feature.title)}</h4>
|
|
4256
|
+
<span class="text-xs px-2 py-0.5 rounded-full \${priorityColors[feature.priority] || 'bg-gray-100'}">\${priorityLabels[feature.priority] || feature.priority}</span>
|
|
4257
|
+
</div>
|
|
4258
|
+
<p class="text-xs text-canvas-500 mb-3 line-clamp-2">\${escapeHtml(feature.description)}</p>
|
|
4259
|
+
<div class="flex items-center justify-between">
|
|
4260
|
+
<div class="flex gap-2 text-xs text-canvas-400">
|
|
4261
|
+
<span title="Effort">\${effortIcons[feature.effort] || '\u23F1\uFE0F'} \${feature.effort}</span>
|
|
4262
|
+
<span title="Impact">\${impactIcons[feature.impact] || '\u{1F4CA}'} \${feature.impact}</span>
|
|
4263
|
+
</div>
|
|
4264
|
+
\${feature.addedToKanban ? \`
|
|
4265
|
+
<span class="text-xs text-green-600">\u2713 Added</span>
|
|
4266
|
+
\` : \`
|
|
4267
|
+
<button onclick="event.stopPropagation(); addFeatureToKanban('\${feature.id}')" class="text-xs text-accent hover:underline">+ Add to Kanban</button>
|
|
4268
|
+
\`}
|
|
4269
|
+
</div>
|
|
4270
|
+
</div>
|
|
4271
|
+
\`;
|
|
4272
|
+
}
|
|
4273
|
+
|
|
4274
|
+
function renderRoadmapModal() {
|
|
4275
|
+
return \`
|
|
4276
|
+
<div class="modal-backdrop" onclick="state.showModal = null; render();">
|
|
4277
|
+
<div class="modal-content max-w-lg" onclick="event.stopPropagation();">
|
|
4278
|
+
<div class="modal-header">
|
|
4279
|
+
<h2 class="modal-title">Generate Roadmap</h2>
|
|
4280
|
+
<button onclick="state.showModal = null; render();" class="modal-close">\xD7</button>
|
|
4281
|
+
</div>
|
|
4282
|
+
<div class="modal-body">
|
|
4283
|
+
<p class="text-sm text-canvas-500 mb-4">
|
|
4284
|
+
Generate a strategic feature roadmap by analyzing your project structure and optionally researching competitors.
|
|
4285
|
+
</p>
|
|
4286
|
+
|
|
4287
|
+
<div class="space-y-4">
|
|
4288
|
+
<label class="flex items-center gap-3 cursor-pointer">
|
|
4289
|
+
<input type="checkbox"
|
|
4290
|
+
\${state.roadmapEnableCompetitors ? 'checked' : ''}
|
|
4291
|
+
onchange="state.roadmapEnableCompetitors = this.checked; render();"
|
|
4292
|
+
class="w-4 h-4 accent-accent">
|
|
4293
|
+
<div>
|
|
4294
|
+
<span class="text-sm font-medium text-canvas-700">Enable competitor research</span>
|
|
4295
|
+
<p class="text-xs text-canvas-400">Use web search to analyze competitors (takes longer)</p>
|
|
4296
|
+
</div>
|
|
4297
|
+
</label>
|
|
4298
|
+
|
|
4299
|
+
<div>
|
|
4300
|
+
<label class="text-sm font-medium text-canvas-700">Additional context (optional)</label>
|
|
4301
|
+
<textarea
|
|
4302
|
+
class="input w-full mt-1 text-sm"
|
|
4303
|
+
rows="3"
|
|
4304
|
+
placeholder="E.g., Focus on mobile features, target enterprise users..."
|
|
4305
|
+
oninput="state.roadmapCustomPrompt = this.value;"
|
|
4306
|
+
>\${escapeHtml(state.roadmapCustomPrompt)}</textarea>
|
|
4307
|
+
</div>
|
|
4308
|
+
</div>
|
|
4309
|
+
</div>
|
|
4310
|
+
<div class="modal-footer">
|
|
4311
|
+
<button onclick="state.showModal = null; render();" class="btn btn-ghost">Cancel</button>
|
|
4312
|
+
<button onclick="generateRoadmap(); state.showModal = null; render();" class="btn btn-primary">
|
|
4313
|
+
\u{1F680} Generate Roadmap
|
|
4314
|
+
</button>
|
|
4315
|
+
</div>
|
|
4316
|
+
</div>
|
|
4317
|
+
</div>
|
|
4318
|
+
\`;
|
|
4319
|
+
}
|
|
4320
|
+
|
|
3047
4321
|
function renderColumn(status, title, tasks) {
|
|
3048
4322
|
const columnTasks = tasks.filter(t => t.status === status);
|
|
3049
4323
|
const statusLabels = {
|
|
@@ -3390,6 +4664,11 @@ function renderModal() {
|
|
|
3390
4664
|
\`;
|
|
3391
4665
|
}
|
|
3392
4666
|
|
|
4667
|
+
// Roadmap generation modal
|
|
4668
|
+
if (state.showModal === 'roadmap') {
|
|
4669
|
+
return renderRoadmapModal();
|
|
4670
|
+
}
|
|
4671
|
+
|
|
3393
4672
|
return '';
|
|
3394
4673
|
}
|
|
3395
4674
|
|
|
@@ -3709,7 +4988,7 @@ function escapeHtml(str) {
|
|
|
3709
4988
|
// Main render
|
|
3710
4989
|
function render() {
|
|
3711
4990
|
const app = document.getElementById('app');
|
|
3712
|
-
const hasSidePanel = state.sidePanel !== null;
|
|
4991
|
+
const hasSidePanel = state.sidePanel !== null && state.currentView === 'board';
|
|
3713
4992
|
|
|
3714
4993
|
app.innerHTML = \`
|
|
3715
4994
|
<div class="min-h-screen flex flex-col bg-canvas-50">
|
|
@@ -3718,51 +4997,76 @@ function render() {
|
|
|
3718
4997
|
<div class="px-6 py-3 flex items-center justify-between">
|
|
3719
4998
|
<div class="flex items-center gap-6">
|
|
3720
4999
|
<h1 class="text-lg font-semibold text-canvas-900">Claude Kanban</h1>
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
5000
|
+
<!-- Navigation Tabs -->
|
|
5001
|
+
<nav class="flex items-center gap-1 bg-canvas-100 rounded-lg p-1">
|
|
5002
|
+
<button onclick="state.currentView = 'board'; render();"
|
|
5003
|
+
class="px-3 py-1.5 text-sm rounded-md transition-colors \${state.currentView === 'board' ? 'bg-white shadow-sm text-canvas-900 font-medium' : 'text-canvas-500 hover:text-canvas-700'}">
|
|
5004
|
+
\u{1F4CB} Board
|
|
5005
|
+
</button>
|
|
5006
|
+
<button onclick="state.currentView = 'roadmap'; render();"
|
|
5007
|
+
class="px-3 py-1.5 text-sm rounded-md transition-colors \${state.currentView === 'roadmap' ? 'bg-white shadow-sm text-canvas-900 font-medium' : 'text-canvas-500 hover:text-canvas-700'}">
|
|
5008
|
+
\u{1F5FA}\uFE0F Roadmap
|
|
5009
|
+
</button>
|
|
5010
|
+
</nav>
|
|
5011
|
+
\${state.currentView === 'board' ? \`
|
|
5012
|
+
<div class="flex items-center gap-2">
|
|
5013
|
+
<input type="text"
|
|
5014
|
+
placeholder="Search tasks..."
|
|
5015
|
+
value="\${escapeHtml(state.searchQuery)}"
|
|
5016
|
+
oninput="state.searchQuery = this.value; render();"
|
|
5017
|
+
class="input text-sm py-1.5 w-48">
|
|
5018
|
+
</div>
|
|
5019
|
+
\` : ''}
|
|
3728
5020
|
</div>
|
|
3729
5021
|
<div class="flex items-center gap-2">
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
5022
|
+
\${state.currentView === 'board' ? \`
|
|
5023
|
+
<button onclick="openPlanningModal();"
|
|
5024
|
+
class="btn px-4 py-2 text-sm bg-blue-500 hover:bg-blue-600 text-white \${state.planning ? 'opacity-50 cursor-not-allowed' : ''}"
|
|
5025
|
+
\${state.planning ? 'disabled' : ''}
|
|
5026
|
+
title="AI Task Planner">
|
|
5027
|
+
\u{1F3AF} \${state.planning ? 'Planning...' : 'Plan'}
|
|
5028
|
+
</button>
|
|
5029
|
+
<button onclick="state.showModal = 'new'; render();"
|
|
5030
|
+
class="btn btn-primary px-4 py-2 text-sm">
|
|
5031
|
+
+ Add Task
|
|
5032
|
+
</button>
|
|
5033
|
+
<button onclick="state.showModal = 'afk'; render();"
|
|
5034
|
+
class="btn btn-ghost px-3 py-2 text-sm \${state.afk.running ? 'text-status-success' : ''}"
|
|
5035
|
+
title="AFK Mode">
|
|
5036
|
+
\u{1F504} \${state.afk.running ? 'AFK On' : 'AFK'}
|
|
5037
|
+
</button>
|
|
5038
|
+
\` : \`
|
|
5039
|
+
<button onclick="state.showModal = 'roadmap'; render();"
|
|
5040
|
+
class="btn btn-primary px-4 py-2 text-sm \${state.roadmapGenerating ? 'opacity-50 cursor-not-allowed' : ''}"
|
|
5041
|
+
\${state.roadmapGenerating ? 'disabled' : ''}>
|
|
5042
|
+
\${state.roadmapGenerating ? '\u23F3 Generating...' : '\u{1F680} Generate Roadmap'}
|
|
5043
|
+
</button>
|
|
5044
|
+
\`}
|
|
3745
5045
|
</div>
|
|
3746
5046
|
</div>
|
|
3747
5047
|
</header>
|
|
3748
5048
|
|
|
3749
|
-
\${renderAFKBar()}
|
|
5049
|
+
\${state.currentView === 'board' ? renderAFKBar() : ''}
|
|
3750
5050
|
|
|
3751
|
-
<!-- Main Content Area
|
|
5051
|
+
<!-- Main Content Area -->
|
|
3752
5052
|
<div class="flex flex-1 overflow-hidden">
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
<
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
5053
|
+
\${state.currentView === 'board' ? \`
|
|
5054
|
+
<!-- Kanban Board -->
|
|
5055
|
+
<main class="main-content overflow-x-auto p-6">
|
|
5056
|
+
<div class="flex gap-4">
|
|
5057
|
+
\${renderColumn('draft', 'To Do', filterTasks(state.tasks))}
|
|
5058
|
+
\${renderColumn('ready', 'Ready', filterTasks(state.tasks))}
|
|
5059
|
+
\${renderColumn('in_progress', 'In Progress', filterTasks(state.tasks))}
|
|
5060
|
+
\${renderColumn('completed', 'Done', filterTasks(state.tasks))}
|
|
5061
|
+
\${renderColumn('failed', 'Failed', filterTasks(state.tasks))}
|
|
5062
|
+
</div>
|
|
5063
|
+
</main>
|
|
5064
|
+
<!-- Side Panel (pushes content when open) -->
|
|
5065
|
+
\${hasSidePanel ? renderSidePanel() : ''}
|
|
5066
|
+
\` : \`
|
|
5067
|
+
<!-- Roadmap View -->
|
|
5068
|
+
\${renderRoadmap()}
|
|
5069
|
+
\`}
|
|
3766
5070
|
</div>
|
|
3767
5071
|
|
|
3768
5072
|
\${renderModal()}
|
|
@@ -3824,6 +5128,11 @@ window.closeSidePanel = closeSidePanel;
|
|
|
3824
5128
|
window.showTaskMenu = showTaskMenu;
|
|
3825
5129
|
window.filterTasks = filterTasks;
|
|
3826
5130
|
window.scrollSidePanelLog = scrollSidePanelLog;
|
|
5131
|
+
window.generateRoadmap = generateRoadmap;
|
|
5132
|
+
window.cancelRoadmap = cancelRoadmap;
|
|
5133
|
+
window.addFeatureToKanban = addFeatureToKanban;
|
|
5134
|
+
window.deleteRoadmapFeature = deleteRoadmapFeature;
|
|
5135
|
+
window.loadRoadmap = loadRoadmap;
|
|
3827
5136
|
|
|
3828
5137
|
// Keyboard shortcuts
|
|
3829
5138
|
document.addEventListener('keydown', (e) => {
|
|
@@ -3899,7 +5208,7 @@ async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
|
3899
5208
|
|
|
3900
5209
|
// src/bin/cli.ts
|
|
3901
5210
|
function isGitRepo(dir) {
|
|
3902
|
-
return
|
|
5211
|
+
return existsSync5(join7(dir, ".git"));
|
|
3903
5212
|
}
|
|
3904
5213
|
function hasGitRemote(dir) {
|
|
3905
5214
|
try {
|