claude-kanban 0.5.1 → 0.6.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 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 existsSync4 } from "fs";
7
+ import { existsSync as existsSync5 } from "fs";
8
8
  import { execSync } from "child_process";
9
9
  import { createInterface } from "readline";
10
- import { join as join6 } from "path";
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 join5, dirname } from "path";
16
+ import { join as join6, dirname } from "path";
17
17
  import { fileURLToPath } from "url";
18
- import { existsSync as existsSync3 } from "fs";
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
- readyTasks.sort((a, b) => {
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 readyTasks[0];
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 planning running)
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
- updateTask(this.projectPath, taskId, {
1099
- status: "completed",
1100
- passes: true
1101
- });
1102
- addExecutionEntry(this.projectPath, taskId, {
1103
- startedAt: startedAt.toISOString(),
1104
- endedAt: endedAt.toISOString(),
1105
- status: "completed",
1106
- duration
1107
- });
1108
- logTaskExecution(this.projectPath, {
1109
- taskId,
1110
- taskTitle: task?.title || "Unknown",
1111
- status: "completed",
1112
- duration
1113
- });
1114
- this.emit("task:completed", { taskId, duration });
1115
- this.afkTasksCompleted++;
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
- this.runningTask = null;
1139
- if (this.afkMode) {
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
  */
@@ -1248,7 +1691,7 @@ When done, output: <promise>PLANNING_COMPLETE</promise>`;
1248
1691
  };
1249
1692
  }
1250
1693
  /**
1251
- * Cancel running task/planning and stop AFK mode
1694
+ * Cancel running task/planning/QA and stop AFK mode
1252
1695
  */
1253
1696
  cancelAll() {
1254
1697
  if (this.runningTask) {
@@ -1265,10 +1708,459 @@ When done, output: <promise>PLANNING_COMPLETE</promise>`;
1265
1708
  }
1266
1709
  this.planningSession = null;
1267
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();
1268
1720
  this.stopAFKMode();
1269
1721
  }
1270
1722
  };
1271
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
1957
+ */
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.`;
1984
+ }
1985
+ /**
1986
+ * Build the roadmap generation prompt
1987
+ */
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;
2073
+ }
2074
+ /**
2075
+ * Run a Claude command and return the output
2076
+ */
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
+ });
2133
+ }
2134
+ /**
2135
+ * Mark a feature as added to kanban
2136
+ */
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);
2145
+ }
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);
2159
+ }
2160
+ return roadmap;
2161
+ }
2162
+ };
2163
+
1272
2164
  // src/server/services/templates.ts
1273
2165
  var taskTemplates = [
1274
2166
  {
@@ -1572,7 +2464,7 @@ function getTemplateById(id) {
1572
2464
  }
1573
2465
 
1574
2466
  // src/server/services/ai.ts
1575
- import { spawn as spawn2 } from "child_process";
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 = spawn2(config.agent.command, args, {
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
- const clientPath = join5(__dirname2, "..", "client");
1965
- if (existsSync3(clientPath)) {
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) => {
@@ -3899,7 +4886,7 @@ async function findAvailablePort(startPort, maxAttempts = 10) {
3899
4886
 
3900
4887
  // src/bin/cli.ts
3901
4888
  function isGitRepo(dir) {
3902
- return existsSync4(join6(dir, ".git"));
4889
+ return existsSync5(join7(dir, ".git"));
3903
4890
  }
3904
4891
  function hasGitRemote(dir) {
3905
4892
  try {