claude-kanban 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +120 -651
- package/dist/bin/cli.js.map +1 -1
- package/dist/server/index.js +114 -645
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -7,9 +7,9 @@ import { fileURLToPath } from "url";
|
|
|
7
7
|
import { existsSync as existsSync3 } from "fs";
|
|
8
8
|
|
|
9
9
|
// src/server/services/executor.ts
|
|
10
|
-
import { spawn
|
|
10
|
+
import { spawn } from "child_process";
|
|
11
11
|
import { join as join4 } from "path";
|
|
12
|
-
import { writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync2, existsSync as existsSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync4
|
|
12
|
+
import { writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync2, existsSync as existsSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync4 } from "fs";
|
|
13
13
|
import { EventEmitter } from "events";
|
|
14
14
|
|
|
15
15
|
// src/server/services/project.ts
|
|
@@ -134,7 +134,6 @@ function getTaskCounts(projectPath) {
|
|
|
134
134
|
draft: 0,
|
|
135
135
|
ready: 0,
|
|
136
136
|
in_progress: 0,
|
|
137
|
-
in_review: 0,
|
|
138
137
|
completed: 0,
|
|
139
138
|
failed: 0
|
|
140
139
|
};
|
|
@@ -207,11 +206,9 @@ function getRecentProgress(projectPath, lines = 100) {
|
|
|
207
206
|
// src/server/services/executor.ts
|
|
208
207
|
var KANBAN_DIR4 = ".claude-kanban";
|
|
209
208
|
var LOGS_DIR = "logs";
|
|
210
|
-
var WORKTREES_DIR = "worktrees";
|
|
211
209
|
var TaskExecutor = class extends EventEmitter {
|
|
212
210
|
projectPath;
|
|
213
|
-
|
|
214
|
-
reviewingTasks = /* @__PURE__ */ new Map();
|
|
211
|
+
runningTask = null;
|
|
215
212
|
afkMode = false;
|
|
216
213
|
afkIteration = 0;
|
|
217
214
|
afkMaxIterations = 0;
|
|
@@ -317,186 +314,47 @@ ${summary}
|
|
|
317
314
|
}
|
|
318
315
|
}
|
|
319
316
|
/**
|
|
320
|
-
* Check if
|
|
317
|
+
* Check if a task is currently running
|
|
321
318
|
*/
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
execSync("git rev-parse --is-inside-work-tree", {
|
|
325
|
-
cwd: this.projectPath,
|
|
326
|
-
stdio: "pipe"
|
|
327
|
-
});
|
|
328
|
-
return true;
|
|
329
|
-
} catch {
|
|
330
|
-
return false;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
|
-
* Get worktrees directory path
|
|
335
|
-
*/
|
|
336
|
-
getWorktreesDir() {
|
|
337
|
-
return join4(this.projectPath, KANBAN_DIR4, WORKTREES_DIR);
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* Get worktree path for a task
|
|
341
|
-
*/
|
|
342
|
-
getWorktreePath(taskId) {
|
|
343
|
-
return join4(this.getWorktreesDir(), taskId);
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Get branch name for a task
|
|
347
|
-
*/
|
|
348
|
-
getBranchName(taskId) {
|
|
349
|
-
return `task/${taskId}`;
|
|
350
|
-
}
|
|
351
|
-
/**
|
|
352
|
-
* Create a git worktree for isolated task execution
|
|
353
|
-
*/
|
|
354
|
-
createWorktree(taskId) {
|
|
355
|
-
if (!this.isGitRepo()) {
|
|
356
|
-
console.log("[executor] Not a git repo, skipping worktree creation");
|
|
357
|
-
return null;
|
|
358
|
-
}
|
|
359
|
-
const worktreePath = this.getWorktreePath(taskId);
|
|
360
|
-
const branchName = this.getBranchName(taskId);
|
|
361
|
-
try {
|
|
362
|
-
const worktreesDir = this.getWorktreesDir();
|
|
363
|
-
if (!existsSync2(worktreesDir)) {
|
|
364
|
-
mkdirSync2(worktreesDir, { recursive: true });
|
|
365
|
-
}
|
|
366
|
-
if (existsSync2(worktreePath)) {
|
|
367
|
-
this.removeWorktree(taskId);
|
|
368
|
-
}
|
|
369
|
-
try {
|
|
370
|
-
execSync(`git rev-parse --verify ${branchName}`, {
|
|
371
|
-
cwd: this.projectPath,
|
|
372
|
-
stdio: "pipe"
|
|
373
|
-
});
|
|
374
|
-
execSync(`git branch -D ${branchName}`, {
|
|
375
|
-
cwd: this.projectPath,
|
|
376
|
-
stdio: "pipe"
|
|
377
|
-
});
|
|
378
|
-
} catch {
|
|
379
|
-
}
|
|
380
|
-
execSync(`git worktree add -b ${branchName} "${worktreePath}"`, {
|
|
381
|
-
cwd: this.projectPath,
|
|
382
|
-
stdio: "pipe"
|
|
383
|
-
});
|
|
384
|
-
console.log(`[executor] Created worktree at ${worktreePath} on branch ${branchName}`);
|
|
385
|
-
return { worktreePath, branchName };
|
|
386
|
-
} catch (error) {
|
|
387
|
-
console.error("[executor] Failed to create worktree:", error);
|
|
388
|
-
return null;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Remove a git worktree
|
|
393
|
-
*/
|
|
394
|
-
removeWorktree(taskId) {
|
|
395
|
-
const worktreePath = this.getWorktreePath(taskId);
|
|
396
|
-
const branchName = this.getBranchName(taskId);
|
|
397
|
-
try {
|
|
398
|
-
if (existsSync2(worktreePath)) {
|
|
399
|
-
execSync(`git worktree remove "${worktreePath}" --force`, {
|
|
400
|
-
cwd: this.projectPath,
|
|
401
|
-
stdio: "pipe"
|
|
402
|
-
});
|
|
403
|
-
console.log(`[executor] Removed worktree at ${worktreePath}`);
|
|
404
|
-
}
|
|
405
|
-
} catch (error) {
|
|
406
|
-
console.error("[executor] Failed to remove worktree via git:", error);
|
|
407
|
-
try {
|
|
408
|
-
if (existsSync2(worktreePath)) {
|
|
409
|
-
rmSync(worktreePath, { recursive: true, force: true });
|
|
410
|
-
execSync("git worktree prune", {
|
|
411
|
-
cwd: this.projectPath,
|
|
412
|
-
stdio: "pipe"
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
} catch {
|
|
416
|
-
console.error("[executor] Failed to force remove worktree directory");
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
try {
|
|
420
|
-
execSync(`git branch -D ${branchName}`, {
|
|
421
|
-
cwd: this.projectPath,
|
|
422
|
-
stdio: "pipe"
|
|
423
|
-
});
|
|
424
|
-
console.log(`[executor] Deleted branch ${branchName}`);
|
|
425
|
-
} catch {
|
|
426
|
-
}
|
|
319
|
+
isRunning() {
|
|
320
|
+
return this.runningTask !== null;
|
|
427
321
|
}
|
|
428
322
|
/**
|
|
429
|
-
*
|
|
323
|
+
* Get the currently running task ID
|
|
430
324
|
*/
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
try {
|
|
434
|
-
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
435
|
-
cwd: this.projectPath,
|
|
436
|
-
encoding: "utf-8"
|
|
437
|
-
}).trim();
|
|
438
|
-
execSync(`git merge ${branchName} --no-edit`, {
|
|
439
|
-
cwd: this.projectPath,
|
|
440
|
-
stdio: "pipe"
|
|
441
|
-
});
|
|
442
|
-
console.log(`[executor] Merged ${branchName} into ${currentBranch}`);
|
|
443
|
-
return true;
|
|
444
|
-
} catch (error) {
|
|
445
|
-
console.error("[executor] Failed to merge branch:", error);
|
|
446
|
-
return false;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
/**
|
|
450
|
-
* Get number of currently running tasks
|
|
451
|
-
*/
|
|
452
|
-
getRunningCount() {
|
|
453
|
-
return this.runningTasks.size;
|
|
454
|
-
}
|
|
455
|
-
/**
|
|
456
|
-
* Check if a task is running
|
|
457
|
-
*/
|
|
458
|
-
isTaskRunning(taskId) {
|
|
459
|
-
return this.runningTasks.has(taskId);
|
|
460
|
-
}
|
|
461
|
-
/**
|
|
462
|
-
* Get all running task IDs
|
|
463
|
-
*/
|
|
464
|
-
getRunningTaskIds() {
|
|
465
|
-
return Array.from(this.runningTasks.keys());
|
|
325
|
+
getRunningTaskId() {
|
|
326
|
+
return this.runningTask?.taskId || null;
|
|
466
327
|
}
|
|
467
328
|
/**
|
|
468
329
|
* Get running task output
|
|
469
330
|
*/
|
|
470
331
|
getTaskOutput(taskId) {
|
|
471
|
-
|
|
332
|
+
if (this.runningTask?.taskId === taskId) {
|
|
333
|
+
return this.runningTask.output;
|
|
334
|
+
}
|
|
335
|
+
return void 0;
|
|
472
336
|
}
|
|
473
337
|
/**
|
|
474
|
-
* Build the prompt for a
|
|
338
|
+
* Build the prompt for a task - simplified Ralph-style
|
|
475
339
|
*/
|
|
476
|
-
buildTaskPrompt(task, config
|
|
340
|
+
buildTaskPrompt(task, config) {
|
|
477
341
|
const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
|
|
478
342
|
const prdPath = join4(kanbanDir, "prd.json");
|
|
479
343
|
const progressPath = join4(kanbanDir, "progress.txt");
|
|
480
344
|
const stepsText = task.steps.length > 0 ? `
|
|
481
345
|
Verification steps:
|
|
482
346
|
${task.steps.map((s, i) => `${i + 1}. ${s}`).join("\n")}` : "";
|
|
483
|
-
const
|
|
347
|
+
const verifyCommands = [];
|
|
484
348
|
if (config.project.typecheckCommand) {
|
|
485
|
-
|
|
349
|
+
verifyCommands.push(config.project.typecheckCommand);
|
|
486
350
|
}
|
|
487
351
|
if (config.project.testCommand) {
|
|
488
|
-
|
|
352
|
+
verifyCommands.push(config.project.testCommand);
|
|
489
353
|
}
|
|
490
|
-
const verifySection =
|
|
491
|
-
${
|
|
354
|
+
const verifySection = verifyCommands.length > 0 ? `3. Run quality checks:
|
|
355
|
+
${verifyCommands.map((cmd) => ` - ${cmd}`).join("\n")}
|
|
492
356
|
|
|
493
|
-
|
|
494
|
-
const worktreeSection = worktreeInfo ? `## ENVIRONMENT
|
|
495
|
-
You are running in an isolated git worktree on branch "${worktreeInfo.branchName}".
|
|
496
|
-
This is a fresh checkout - dependencies (node_modules, vendor, etc.) are NOT installed.
|
|
497
|
-
Before running any build/test commands, install dependencies first (e.g., npm install, composer install, pip install).
|
|
498
|
-
|
|
499
|
-
` : "";
|
|
357
|
+
4.` : "3.";
|
|
500
358
|
return `You are an AI coding agent. Complete the following task:
|
|
501
359
|
|
|
502
360
|
## TASK
|
|
@@ -507,48 +365,41 @@ Priority: ${task.priority}
|
|
|
507
365
|
${task.description}
|
|
508
366
|
${stepsText}
|
|
509
367
|
|
|
510
|
-
|
|
511
|
-
|
|
368
|
+
## INSTRUCTIONS
|
|
369
|
+
|
|
370
|
+
1. Implement this task as described above.
|
|
512
371
|
|
|
513
|
-
2.
|
|
372
|
+
2. Make sure your changes work correctly.
|
|
514
373
|
|
|
515
|
-
${verifySection}
|
|
374
|
+
${verifySection} When complete, update the PRD file at ${prdPath}:
|
|
516
375
|
- Find the task with id "${task.id}"
|
|
517
376
|
- Set "passes": true
|
|
518
|
-
|
|
377
|
+
- Set "status": "completed"
|
|
519
378
|
|
|
520
|
-
${
|
|
521
|
-
- What you implemented
|
|
522
|
-
- Key decisions made
|
|
523
|
-
-
|
|
524
|
-
- Useful patterns or approaches that worked well
|
|
525
|
-
- Anything a future agent should know about this area of the codebase
|
|
379
|
+
${verifyCommands.length > 0 ? "5" : "4"}. Document your work in ${progressPath}:
|
|
380
|
+
- What you implemented
|
|
381
|
+
- Key decisions made
|
|
382
|
+
- Any gotchas or important notes for future work
|
|
526
383
|
|
|
527
|
-
${
|
|
384
|
+
${verifyCommands.length > 0 ? "6" : "5"}. Commit your changes with a descriptive message.
|
|
528
385
|
|
|
529
|
-
Focus only on this task. When
|
|
386
|
+
Focus only on this task. When done, output: <promise>COMPLETE</promise>`;
|
|
530
387
|
}
|
|
531
388
|
/**
|
|
532
|
-
* Run a
|
|
389
|
+
* Run a task
|
|
533
390
|
*/
|
|
534
391
|
async runTask(taskId) {
|
|
392
|
+
if (this.isRunning()) {
|
|
393
|
+
throw new Error("A task is already running. Wait for it to complete or cancel it first.");
|
|
394
|
+
}
|
|
535
395
|
const config = getConfig(this.projectPath);
|
|
536
396
|
const task = getTaskById(this.projectPath, taskId);
|
|
537
397
|
if (!task) {
|
|
538
398
|
throw new Error(`Task not found: ${taskId}`);
|
|
539
399
|
}
|
|
540
|
-
if (this.isTaskRunning(taskId)) {
|
|
541
|
-
throw new Error(`Task already running: ${taskId}`);
|
|
542
|
-
}
|
|
543
|
-
const maxConcurrent = config.execution.maxConcurrent || 3;
|
|
544
|
-
if (this.getRunningCount() >= maxConcurrent) {
|
|
545
|
-
throw new Error(`Maximum concurrent tasks (${maxConcurrent}) reached`);
|
|
546
|
-
}
|
|
547
400
|
updateTask(this.projectPath, taskId, { status: "in_progress" });
|
|
548
401
|
const startedAt = /* @__PURE__ */ new Date();
|
|
549
|
-
const
|
|
550
|
-
const executionPath = worktreeInfo?.worktreePath || this.projectPath;
|
|
551
|
-
const prompt = this.buildTaskPrompt(task, config, worktreeInfo);
|
|
402
|
+
const prompt = this.buildTaskPrompt(task, config);
|
|
552
403
|
const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
|
|
553
404
|
const promptFile = join4(kanbanDir, `prompt-${taskId}.txt`);
|
|
554
405
|
writeFileSync4(promptFile, prompt);
|
|
@@ -564,48 +415,32 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
564
415
|
const commandDisplay = `${config.agent.command} ${args.join(" ")}`;
|
|
565
416
|
const fullCommand = `${config.agent.command} ${args.join(" ")}`;
|
|
566
417
|
console.log("[executor] Command:", fullCommand);
|
|
567
|
-
console.log("[executor] CWD:",
|
|
568
|
-
if (worktreeInfo) {
|
|
569
|
-
console.log("[executor] Using worktree:", worktreeInfo.worktreePath);
|
|
570
|
-
console.log("[executor] Branch:", worktreeInfo.branchName);
|
|
571
|
-
}
|
|
418
|
+
console.log("[executor] CWD:", this.projectPath);
|
|
572
419
|
const childProcess = spawn("bash", ["-c", fullCommand], {
|
|
573
|
-
cwd:
|
|
420
|
+
cwd: this.projectPath,
|
|
574
421
|
env: {
|
|
575
422
|
...process.env,
|
|
576
423
|
TERM: "xterm-256color",
|
|
577
424
|
FORCE_COLOR: "0",
|
|
578
|
-
// Disable colors to avoid escape codes
|
|
579
425
|
NO_COLOR: "1"
|
|
580
|
-
// Standard way to disable colors
|
|
581
426
|
},
|
|
582
427
|
stdio: ["ignore", "pipe", "pipe"]
|
|
583
|
-
// Close stdin since we don't need interactive input
|
|
584
428
|
});
|
|
585
|
-
|
|
429
|
+
this.runningTask = {
|
|
586
430
|
taskId,
|
|
587
431
|
process: childProcess,
|
|
588
432
|
startedAt,
|
|
589
|
-
output: []
|
|
590
|
-
worktreePath: worktreeInfo?.worktreePath,
|
|
591
|
-
branchName: worktreeInfo?.branchName
|
|
433
|
+
output: []
|
|
592
434
|
};
|
|
593
|
-
this.runningTasks.set(taskId, runningTask);
|
|
594
435
|
this.initLogFile(taskId);
|
|
595
436
|
const logOutput = (line) => {
|
|
596
437
|
this.appendToLog(taskId, line);
|
|
597
|
-
runningTask
|
|
438
|
+
this.runningTask?.output.push(line);
|
|
598
439
|
this.emit("task:output", { taskId, line, lineType: "stdout" });
|
|
599
440
|
};
|
|
600
441
|
this.emit("task:started", { taskId, timestamp: startedAt.toISOString() });
|
|
601
442
|
logOutput(`[claude-kanban] Starting task: ${task.title}
|
|
602
443
|
`);
|
|
603
|
-
if (worktreeInfo) {
|
|
604
|
-
logOutput(`[claude-kanban] Worktree: ${worktreeInfo.worktreePath}
|
|
605
|
-
`);
|
|
606
|
-
logOutput(`[claude-kanban] Branch: ${worktreeInfo.branchName}
|
|
607
|
-
`);
|
|
608
|
-
}
|
|
609
444
|
logOutput(`[claude-kanban] Command: ${commandDisplay}
|
|
610
445
|
`);
|
|
611
446
|
logOutput(`[claude-kanban] Process spawned (PID: ${childProcess.pid})
|
|
@@ -650,9 +485,6 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
650
485
|
const text = data.toString();
|
|
651
486
|
logOutput(`[stderr] ${text}`);
|
|
652
487
|
});
|
|
653
|
-
childProcess.on("spawn", () => {
|
|
654
|
-
console.log("[executor] Process spawned successfully");
|
|
655
|
-
});
|
|
656
488
|
childProcess.on("error", (error) => {
|
|
657
489
|
console.log("[executor] Spawn error:", error.message);
|
|
658
490
|
this.emit("task:output", { taskId, line: `[claude-kanban] Error: ${error.message}
|
|
@@ -661,9 +493,6 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
661
493
|
unlinkSync(promptFile);
|
|
662
494
|
} catch {
|
|
663
495
|
}
|
|
664
|
-
if (worktreeInfo) {
|
|
665
|
-
this.removeWorktree(taskId);
|
|
666
|
-
}
|
|
667
496
|
updateTask(this.projectPath, taskId, { status: "failed", passes: false });
|
|
668
497
|
const endedAt = /* @__PURE__ */ new Date();
|
|
669
498
|
addExecutionEntry(this.projectPath, taskId, {
|
|
@@ -674,7 +503,7 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
674
503
|
error: error.message
|
|
675
504
|
});
|
|
676
505
|
this.emit("task:failed", { taskId, error: error.message });
|
|
677
|
-
this.
|
|
506
|
+
this.runningTask = null;
|
|
678
507
|
});
|
|
679
508
|
childProcess.on("close", (code, signal) => {
|
|
680
509
|
console.log("[executor] Process closed with code:", code, "signal:", signal);
|
|
@@ -688,48 +517,41 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
688
517
|
});
|
|
689
518
|
const timeoutMs = (config.execution.timeout || 30) * 60 * 1e3;
|
|
690
519
|
setTimeout(() => {
|
|
691
|
-
if (this.
|
|
692
|
-
this.cancelTask(
|
|
520
|
+
if (this.runningTask?.taskId === taskId) {
|
|
521
|
+
this.cancelTask("Timeout exceeded");
|
|
693
522
|
}
|
|
694
523
|
}, timeoutMs);
|
|
695
524
|
}
|
|
696
525
|
/**
|
|
697
|
-
* Handle task completion
|
|
526
|
+
* Handle task completion
|
|
698
527
|
*/
|
|
699
528
|
handleTaskComplete(taskId, exitCode, startedAt) {
|
|
700
|
-
|
|
701
|
-
if (!runningTask) return;
|
|
529
|
+
if (!this.runningTask || this.runningTask.taskId !== taskId) return;
|
|
702
530
|
const endedAt = /* @__PURE__ */ new Date();
|
|
703
531
|
const duration = endedAt.getTime() - startedAt.getTime();
|
|
704
|
-
const output = runningTask.output.join("");
|
|
532
|
+
const output = this.runningTask.output.join("");
|
|
705
533
|
const isComplete = output.includes("<promise>COMPLETE</promise>");
|
|
706
534
|
const task = getTaskById(this.projectPath, taskId);
|
|
707
535
|
if (isComplete || exitCode === 0) {
|
|
708
|
-
if (runningTask.worktreePath && runningTask.branchName) {
|
|
709
|
-
this.reviewingTasks.set(taskId, {
|
|
710
|
-
worktreePath: runningTask.worktreePath,
|
|
711
|
-
branchName: runningTask.branchName,
|
|
712
|
-
startedAt,
|
|
713
|
-
endedAt
|
|
714
|
-
});
|
|
715
|
-
console.log(`[executor] Task ${taskId} ready for review at ${runningTask.worktreePath}`);
|
|
716
|
-
}
|
|
717
536
|
updateTask(this.projectPath, taskId, {
|
|
718
|
-
status: "
|
|
537
|
+
status: "completed",
|
|
719
538
|
passes: true
|
|
720
539
|
});
|
|
540
|
+
addExecutionEntry(this.projectPath, taskId, {
|
|
541
|
+
startedAt: startedAt.toISOString(),
|
|
542
|
+
endedAt: endedAt.toISOString(),
|
|
543
|
+
status: "completed",
|
|
544
|
+
duration
|
|
545
|
+
});
|
|
721
546
|
logTaskExecution(this.projectPath, {
|
|
722
547
|
taskId,
|
|
723
548
|
taskTitle: task?.title || "Unknown",
|
|
724
|
-
status: "
|
|
549
|
+
status: "completed",
|
|
725
550
|
duration
|
|
726
551
|
});
|
|
727
|
-
this.emit("task:completed", { taskId, duration
|
|
552
|
+
this.emit("task:completed", { taskId, duration });
|
|
728
553
|
this.afkTasksCompleted++;
|
|
729
554
|
} else {
|
|
730
|
-
if (runningTask.worktreePath) {
|
|
731
|
-
this.removeWorktree(taskId);
|
|
732
|
-
}
|
|
733
555
|
updateTask(this.projectPath, taskId, {
|
|
734
556
|
status: "failed",
|
|
735
557
|
passes: false
|
|
@@ -751,184 +573,33 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
751
573
|
});
|
|
752
574
|
this.emit("task:failed", { taskId, error });
|
|
753
575
|
}
|
|
754
|
-
this.
|
|
576
|
+
this.runningTask = null;
|
|
755
577
|
if (this.afkMode) {
|
|
756
578
|
this.continueAFKMode();
|
|
757
579
|
}
|
|
758
580
|
}
|
|
759
581
|
/**
|
|
760
|
-
*
|
|
761
|
-
*/
|
|
762
|
-
getReviewingTask(taskId) {
|
|
763
|
-
return this.reviewingTasks.get(taskId);
|
|
764
|
-
}
|
|
765
|
-
/**
|
|
766
|
-
* Merge a reviewed task into the base branch
|
|
767
|
-
*/
|
|
768
|
-
mergeTask(taskId) {
|
|
769
|
-
const reviewInfo = this.reviewingTasks.get(taskId);
|
|
770
|
-
if (!reviewInfo) {
|
|
771
|
-
return { success: false, error: "Task not in review or no worktree found" };
|
|
772
|
-
}
|
|
773
|
-
const merged = this.mergeWorktreeBranch(taskId);
|
|
774
|
-
if (!merged) {
|
|
775
|
-
return { success: false, error: "Merge failed - may have conflicts" };
|
|
776
|
-
}
|
|
777
|
-
this.removeWorktree(taskId);
|
|
778
|
-
this.reviewingTasks.delete(taskId);
|
|
779
|
-
updateTask(this.projectPath, taskId, {
|
|
780
|
-
status: "completed"
|
|
781
|
-
});
|
|
782
|
-
addExecutionEntry(this.projectPath, taskId, {
|
|
783
|
-
startedAt: reviewInfo.startedAt.toISOString(),
|
|
784
|
-
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
785
|
-
status: "completed",
|
|
786
|
-
duration: (/* @__PURE__ */ new Date()).getTime() - reviewInfo.startedAt.getTime()
|
|
787
|
-
});
|
|
788
|
-
console.log(`[executor] Task ${taskId} merged successfully`);
|
|
789
|
-
return { success: true };
|
|
790
|
-
}
|
|
791
|
-
/**
|
|
792
|
-
* Create a PR for a reviewed task
|
|
793
|
-
*/
|
|
794
|
-
createPR(taskId) {
|
|
795
|
-
const reviewInfo = this.reviewingTasks.get(taskId);
|
|
796
|
-
const task = getTaskById(this.projectPath, taskId);
|
|
797
|
-
if (!reviewInfo) {
|
|
798
|
-
return { success: false, error: "Task not in review or no worktree found" };
|
|
799
|
-
}
|
|
800
|
-
try {
|
|
801
|
-
execSync(`git push -u origin ${reviewInfo.branchName}`, {
|
|
802
|
-
cwd: this.projectPath,
|
|
803
|
-
stdio: "pipe"
|
|
804
|
-
});
|
|
805
|
-
const prTitle = task?.title || `Task ${taskId}`;
|
|
806
|
-
const prBody = task?.description || "";
|
|
807
|
-
const result = execSync(
|
|
808
|
-
`gh pr create --title "${prTitle.replace(/"/g, '\\"')}" --body "${prBody.replace(/"/g, '\\"')}" --head ${reviewInfo.branchName}`,
|
|
809
|
-
{
|
|
810
|
-
cwd: this.projectPath,
|
|
811
|
-
encoding: "utf-8"
|
|
812
|
-
}
|
|
813
|
-
);
|
|
814
|
-
const prUrl = result.trim();
|
|
815
|
-
try {
|
|
816
|
-
execSync(`git worktree remove "${reviewInfo.worktreePath}" --force`, {
|
|
817
|
-
cwd: this.projectPath,
|
|
818
|
-
stdio: "pipe"
|
|
819
|
-
});
|
|
820
|
-
} catch {
|
|
821
|
-
}
|
|
822
|
-
this.reviewingTasks.delete(taskId);
|
|
823
|
-
updateTask(this.projectPath, taskId, {
|
|
824
|
-
status: "completed"
|
|
825
|
-
});
|
|
826
|
-
addExecutionEntry(this.projectPath, taskId, {
|
|
827
|
-
startedAt: reviewInfo.startedAt.toISOString(),
|
|
828
|
-
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
829
|
-
status: "completed",
|
|
830
|
-
duration: (/* @__PURE__ */ new Date()).getTime() - reviewInfo.startedAt.getTime()
|
|
831
|
-
});
|
|
832
|
-
console.log(`[executor] PR created for task ${taskId}: ${prUrl}`);
|
|
833
|
-
return { success: true, prUrl };
|
|
834
|
-
} catch (error) {
|
|
835
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
836
|
-
console.error(`[executor] Failed to create PR:`, errorMsg);
|
|
837
|
-
return { success: false, error: errorMsg };
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
/**
|
|
841
|
-
* Discard a reviewed task - delete worktree and return to ready
|
|
842
|
-
*/
|
|
843
|
-
discardTask(taskId) {
|
|
844
|
-
const reviewInfo = this.reviewingTasks.get(taskId);
|
|
845
|
-
if (!reviewInfo) {
|
|
846
|
-
return { success: false, error: "Task not in review or no worktree found" };
|
|
847
|
-
}
|
|
848
|
-
this.removeWorktree(taskId);
|
|
849
|
-
this.reviewingTasks.delete(taskId);
|
|
850
|
-
updateTask(this.projectPath, taskId, {
|
|
851
|
-
status: "ready",
|
|
852
|
-
passes: false
|
|
853
|
-
});
|
|
854
|
-
addExecutionEntry(this.projectPath, taskId, {
|
|
855
|
-
startedAt: reviewInfo.startedAt.toISOString(),
|
|
856
|
-
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
857
|
-
status: "discarded",
|
|
858
|
-
duration: (/* @__PURE__ */ new Date()).getTime() - reviewInfo.startedAt.getTime()
|
|
859
|
-
});
|
|
860
|
-
console.log(`[executor] Task ${taskId} discarded, returned to ready`);
|
|
861
|
-
return { success: true };
|
|
862
|
-
}
|
|
863
|
-
/**
|
|
864
|
-
* Get diff for a task in review
|
|
865
|
-
*/
|
|
866
|
-
getTaskDiff(taskId) {
|
|
867
|
-
const reviewInfo = this.reviewingTasks.get(taskId);
|
|
868
|
-
if (!reviewInfo) {
|
|
869
|
-
return { success: false, error: "Task not in review or no worktree found" };
|
|
870
|
-
}
|
|
871
|
-
try {
|
|
872
|
-
const diff = execSync(`git diff main...${reviewInfo.branchName}`, {
|
|
873
|
-
cwd: this.projectPath,
|
|
874
|
-
encoding: "utf-8",
|
|
875
|
-
maxBuffer: 10 * 1024 * 1024
|
|
876
|
-
// 10MB buffer for large diffs
|
|
877
|
-
});
|
|
878
|
-
return { success: true, diff };
|
|
879
|
-
} catch (error) {
|
|
880
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
881
|
-
return { success: false, error: errorMsg };
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
/**
|
|
885
|
-
* Get list of changed files for a task in review
|
|
582
|
+
* Cancel the running task
|
|
886
583
|
*/
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
return { success: false, error: "Task not in review or no worktree found" };
|
|
891
|
-
}
|
|
892
|
-
try {
|
|
893
|
-
const result = execSync(`git diff --name-only main...${reviewInfo.branchName}`, {
|
|
894
|
-
cwd: this.projectPath,
|
|
895
|
-
encoding: "utf-8"
|
|
896
|
-
});
|
|
897
|
-
const files = result.trim().split("\n").filter((f) => f);
|
|
898
|
-
return { success: true, files };
|
|
899
|
-
} catch (error) {
|
|
900
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
901
|
-
return { success: false, error: errorMsg };
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
/**
|
|
905
|
-
* Cancel a running task
|
|
906
|
-
*/
|
|
907
|
-
cancelTask(taskId, reason = "Cancelled by user") {
|
|
908
|
-
const runningTask = this.runningTasks.get(taskId);
|
|
909
|
-
if (!runningTask) return false;
|
|
910
|
-
const startedAt = runningTask.startedAt;
|
|
584
|
+
cancelTask(reason = "Cancelled by user") {
|
|
585
|
+
if (!this.runningTask) return false;
|
|
586
|
+
const { taskId, process: childProcess, startedAt } = this.runningTask;
|
|
911
587
|
const endedAt = /* @__PURE__ */ new Date();
|
|
912
588
|
const duration = endedAt.getTime() - startedAt.getTime();
|
|
913
589
|
const task = getTaskById(this.projectPath, taskId);
|
|
914
590
|
try {
|
|
915
|
-
|
|
591
|
+
childProcess.kill("SIGTERM");
|
|
916
592
|
setTimeout(() => {
|
|
917
593
|
try {
|
|
918
|
-
if (!
|
|
919
|
-
|
|
594
|
+
if (!childProcess.killed) {
|
|
595
|
+
childProcess.kill("SIGKILL");
|
|
920
596
|
}
|
|
921
597
|
} catch {
|
|
922
598
|
}
|
|
923
599
|
}, 2e3);
|
|
924
600
|
} catch {
|
|
925
601
|
}
|
|
926
|
-
|
|
927
|
-
this.removeWorktree(taskId);
|
|
928
|
-
}
|
|
929
|
-
updateTask(this.projectPath, taskId, {
|
|
930
|
-
status: "ready"
|
|
931
|
-
});
|
|
602
|
+
updateTask(this.projectPath, taskId, { status: "ready" });
|
|
932
603
|
addExecutionEntry(this.projectPath, taskId, {
|
|
933
604
|
startedAt: startedAt.toISOString(),
|
|
934
605
|
endedAt: endedAt.toISOString(),
|
|
@@ -944,48 +615,46 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
944
615
|
error: reason
|
|
945
616
|
});
|
|
946
617
|
this.emit("task:cancelled", { taskId });
|
|
947
|
-
this.
|
|
618
|
+
this.runningTask = null;
|
|
948
619
|
return true;
|
|
949
620
|
}
|
|
950
621
|
/**
|
|
951
|
-
* Start AFK mode
|
|
622
|
+
* Start AFK mode - run tasks sequentially until done
|
|
952
623
|
*/
|
|
953
|
-
startAFKMode(maxIterations
|
|
624
|
+
startAFKMode(maxIterations) {
|
|
954
625
|
if (this.afkMode) {
|
|
955
626
|
throw new Error("AFK mode already running");
|
|
956
627
|
}
|
|
628
|
+
if (this.isRunning()) {
|
|
629
|
+
throw new Error("Cannot start AFK mode while a task is running");
|
|
630
|
+
}
|
|
957
631
|
this.afkMode = true;
|
|
958
632
|
this.afkIteration = 0;
|
|
959
633
|
this.afkMaxIterations = maxIterations;
|
|
960
634
|
this.afkTasksCompleted = 0;
|
|
961
635
|
this.emitAFKStatus();
|
|
962
|
-
this.continueAFKMode(
|
|
636
|
+
this.continueAFKMode();
|
|
963
637
|
}
|
|
964
638
|
/**
|
|
965
|
-
* Continue AFK mode - pick
|
|
639
|
+
* Continue AFK mode - pick next task
|
|
966
640
|
*/
|
|
967
|
-
continueAFKMode(
|
|
641
|
+
continueAFKMode() {
|
|
968
642
|
if (!this.afkMode) return;
|
|
969
643
|
if (this.afkIteration >= this.afkMaxIterations) {
|
|
970
644
|
this.stopAFKMode();
|
|
971
645
|
return;
|
|
972
646
|
}
|
|
973
|
-
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
if (this.getRunningCount() === 0) {
|
|
979
|
-
this.stopAFKMode();
|
|
980
|
-
}
|
|
981
|
-
break;
|
|
982
|
-
}
|
|
983
|
-
this.afkIteration++;
|
|
984
|
-
this.runTask(nextTask.id).catch((error) => {
|
|
985
|
-
console.error("AFK task error:", error);
|
|
986
|
-
});
|
|
987
|
-
this.emitAFKStatus();
|
|
647
|
+
if (this.isRunning()) return;
|
|
648
|
+
const nextTask = getNextReadyTask(this.projectPath);
|
|
649
|
+
if (!nextTask) {
|
|
650
|
+
this.stopAFKMode();
|
|
651
|
+
return;
|
|
988
652
|
}
|
|
653
|
+
this.afkIteration++;
|
|
654
|
+
this.runTask(nextTask.id).catch((error) => {
|
|
655
|
+
console.error("AFK task error:", error);
|
|
656
|
+
});
|
|
657
|
+
this.emitAFKStatus();
|
|
989
658
|
}
|
|
990
659
|
/**
|
|
991
660
|
* Stop AFK mode
|
|
@@ -1017,18 +686,15 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
1017
686
|
};
|
|
1018
687
|
}
|
|
1019
688
|
/**
|
|
1020
|
-
* Cancel
|
|
689
|
+
* Cancel running task and stop AFK mode
|
|
1021
690
|
*/
|
|
1022
691
|
cancelAll() {
|
|
1023
|
-
|
|
692
|
+
if (this.runningTask) {
|
|
1024
693
|
try {
|
|
1025
|
-
runningTask.process.kill("SIGKILL");
|
|
694
|
+
this.runningTask.process.kill("SIGKILL");
|
|
1026
695
|
} catch {
|
|
1027
696
|
}
|
|
1028
|
-
|
|
1029
|
-
this.removeWorktree(taskId);
|
|
1030
|
-
}
|
|
1031
|
-
this.runningTasks.delete(taskId);
|
|
697
|
+
this.runningTask = null;
|
|
1032
698
|
}
|
|
1033
699
|
this.stopAFKMode();
|
|
1034
700
|
}
|
|
@@ -1538,7 +1204,12 @@ async function createServer(projectPath, port) {
|
|
|
1538
1204
|
});
|
|
1539
1205
|
app.post("/api/tasks/:id/cancel", (req, res) => {
|
|
1540
1206
|
try {
|
|
1541
|
-
const
|
|
1207
|
+
const runningId = executor.getRunningTaskId();
|
|
1208
|
+
if (runningId !== req.params.id) {
|
|
1209
|
+
res.status(404).json({ error: "Task not running" });
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
const cancelled = executor.cancelTask();
|
|
1542
1213
|
if (!cancelled) {
|
|
1543
1214
|
res.status(404).json({ error: "Task not running" });
|
|
1544
1215
|
return;
|
|
@@ -1567,90 +1238,6 @@ async function createServer(projectPath, port) {
|
|
|
1567
1238
|
res.status(500).json({ error: String(error) });
|
|
1568
1239
|
}
|
|
1569
1240
|
});
|
|
1570
|
-
app.post("/api/tasks/:id/merge", (req, res) => {
|
|
1571
|
-
try {
|
|
1572
|
-
const result = executor.mergeTask(req.params.id);
|
|
1573
|
-
if (!result.success) {
|
|
1574
|
-
res.status(400).json({ error: result.error });
|
|
1575
|
-
return;
|
|
1576
|
-
}
|
|
1577
|
-
const task = getTaskById(projectPath, req.params.id);
|
|
1578
|
-
if (task) {
|
|
1579
|
-
io.emit("task:updated", task);
|
|
1580
|
-
}
|
|
1581
|
-
res.json({ success: true });
|
|
1582
|
-
} catch (error) {
|
|
1583
|
-
res.status(500).json({ error: String(error) });
|
|
1584
|
-
}
|
|
1585
|
-
});
|
|
1586
|
-
app.post("/api/tasks/:id/create-pr", (req, res) => {
|
|
1587
|
-
try {
|
|
1588
|
-
const result = executor.createPR(req.params.id);
|
|
1589
|
-
if (!result.success) {
|
|
1590
|
-
res.status(400).json({ error: result.error });
|
|
1591
|
-
return;
|
|
1592
|
-
}
|
|
1593
|
-
const task = getTaskById(projectPath, req.params.id);
|
|
1594
|
-
if (task) {
|
|
1595
|
-
io.emit("task:updated", task);
|
|
1596
|
-
}
|
|
1597
|
-
res.json({ success: true, prUrl: result.prUrl });
|
|
1598
|
-
} catch (error) {
|
|
1599
|
-
res.status(500).json({ error: String(error) });
|
|
1600
|
-
}
|
|
1601
|
-
});
|
|
1602
|
-
app.post("/api/tasks/:id/discard", (req, res) => {
|
|
1603
|
-
try {
|
|
1604
|
-
const result = executor.discardTask(req.params.id);
|
|
1605
|
-
if (!result.success) {
|
|
1606
|
-
res.status(400).json({ error: result.error });
|
|
1607
|
-
return;
|
|
1608
|
-
}
|
|
1609
|
-
const task = getTaskById(projectPath, req.params.id);
|
|
1610
|
-
if (task) {
|
|
1611
|
-
io.emit("task:updated", task);
|
|
1612
|
-
}
|
|
1613
|
-
res.json({ success: true });
|
|
1614
|
-
} catch (error) {
|
|
1615
|
-
res.status(500).json({ error: String(error) });
|
|
1616
|
-
}
|
|
1617
|
-
});
|
|
1618
|
-
app.get("/api/tasks/:id/diff", (req, res) => {
|
|
1619
|
-
try {
|
|
1620
|
-
const result = executor.getTaskDiff(req.params.id);
|
|
1621
|
-
if (!result.success) {
|
|
1622
|
-
res.status(400).json({ error: result.error });
|
|
1623
|
-
return;
|
|
1624
|
-
}
|
|
1625
|
-
res.json({ diff: result.diff });
|
|
1626
|
-
} catch (error) {
|
|
1627
|
-
res.status(500).json({ error: String(error) });
|
|
1628
|
-
}
|
|
1629
|
-
});
|
|
1630
|
-
app.get("/api/tasks/:id/changed-files", (req, res) => {
|
|
1631
|
-
try {
|
|
1632
|
-
const result = executor.getTaskChangedFiles(req.params.id);
|
|
1633
|
-
if (!result.success) {
|
|
1634
|
-
res.status(400).json({ error: result.error });
|
|
1635
|
-
return;
|
|
1636
|
-
}
|
|
1637
|
-
res.json({ files: result.files });
|
|
1638
|
-
} catch (error) {
|
|
1639
|
-
res.status(500).json({ error: String(error) });
|
|
1640
|
-
}
|
|
1641
|
-
});
|
|
1642
|
-
app.get("/api/tasks/:id/review-info", (req, res) => {
|
|
1643
|
-
try {
|
|
1644
|
-
const info = executor.getReviewingTask(req.params.id);
|
|
1645
|
-
if (!info) {
|
|
1646
|
-
res.status(404).json({ error: "Task not in review" });
|
|
1647
|
-
return;
|
|
1648
|
-
}
|
|
1649
|
-
res.json(info);
|
|
1650
|
-
} catch (error) {
|
|
1651
|
-
res.status(500).json({ error: String(error) });
|
|
1652
|
-
}
|
|
1653
|
-
});
|
|
1654
1241
|
app.get("/api/tasks/:id/logs", (req, res) => {
|
|
1655
1242
|
try {
|
|
1656
1243
|
const logs = executor.getTaskLog(req.params.id);
|
|
@@ -1724,8 +1311,8 @@ async function createServer(projectPath, port) {
|
|
|
1724
1311
|
});
|
|
1725
1312
|
app.post("/api/afk/start", (req, res) => {
|
|
1726
1313
|
try {
|
|
1727
|
-
const { maxIterations
|
|
1728
|
-
executor.startAFKMode(maxIterations || 10
|
|
1314
|
+
const { maxIterations } = req.body;
|
|
1315
|
+
executor.startAFKMode(maxIterations || 10);
|
|
1729
1316
|
res.json({ success: true, status: executor.getAFKStatus() });
|
|
1730
1317
|
} catch (error) {
|
|
1731
1318
|
res.status(400).json({ error: String(error) });
|
|
@@ -1749,8 +1336,8 @@ async function createServer(projectPath, port) {
|
|
|
1749
1336
|
});
|
|
1750
1337
|
app.get("/api/running", (_req, res) => {
|
|
1751
1338
|
try {
|
|
1752
|
-
const
|
|
1753
|
-
res.json({ running:
|
|
1339
|
+
const taskId = executor.getRunningTaskId();
|
|
1340
|
+
res.json({ running: taskId ? [taskId] : [], count: taskId ? 1 : 0 });
|
|
1754
1341
|
} catch (error) {
|
|
1755
1342
|
res.status(500).json({ error: String(error) });
|
|
1756
1343
|
}
|
|
@@ -1758,7 +1345,7 @@ async function createServer(projectPath, port) {
|
|
|
1758
1345
|
app.get("/api/stats", (_req, res) => {
|
|
1759
1346
|
try {
|
|
1760
1347
|
const counts = getTaskCounts(projectPath);
|
|
1761
|
-
const running = executor.
|
|
1348
|
+
const running = executor.isRunning() ? 1 : 0;
|
|
1762
1349
|
const afk = executor.getAFKStatus();
|
|
1763
1350
|
res.json({ counts, running, afk });
|
|
1764
1351
|
} catch (error) {
|
|
@@ -1774,12 +1361,13 @@ async function createServer(projectPath, port) {
|
|
|
1774
1361
|
});
|
|
1775
1362
|
io.on("connection", (socket) => {
|
|
1776
1363
|
console.log("Client connected");
|
|
1777
|
-
const
|
|
1364
|
+
const runningId = executor.getRunningTaskId();
|
|
1365
|
+
const runningIds = runningId ? [runningId] : [];
|
|
1778
1366
|
const taskLogs = {};
|
|
1779
|
-
|
|
1780
|
-
const logs = executor.getTaskLog(
|
|
1367
|
+
if (runningId) {
|
|
1368
|
+
const logs = executor.getTaskLog(runningId);
|
|
1781
1369
|
if (logs) {
|
|
1782
|
-
taskLogs[
|
|
1370
|
+
taskLogs[runningId] = logs;
|
|
1783
1371
|
}
|
|
1784
1372
|
}
|
|
1785
1373
|
socket.emit("init", {
|
|
@@ -1787,7 +1375,7 @@ async function createServer(projectPath, port) {
|
|
|
1787
1375
|
running: runningIds,
|
|
1788
1376
|
afk: executor.getAFKStatus(),
|
|
1789
1377
|
taskLogs
|
|
1790
|
-
// Include logs for running
|
|
1378
|
+
// Include logs for running task
|
|
1791
1379
|
});
|
|
1792
1380
|
socket.on("get-logs", (taskId) => {
|
|
1793
1381
|
const logs = executor.getTaskLog(taskId);
|
|
@@ -1960,7 +1548,6 @@ function getClientHTML() {
|
|
|
1960
1548
|
.status-dot-draft { background: #a3a3a3; }
|
|
1961
1549
|
.status-dot-ready { background: #3b82f6; }
|
|
1962
1550
|
.status-dot-in_progress { background: #f97316; }
|
|
1963
|
-
.status-dot-in_review { background: #8b5cf6; }
|
|
1964
1551
|
.status-dot-completed { background: #22c55e; }
|
|
1965
1552
|
.status-dot-failed { background: #ef4444; }
|
|
1966
1553
|
|
|
@@ -2170,7 +1757,6 @@ function getClientHTML() {
|
|
|
2170
1757
|
.status-badge-draft { background: #f5f5f5; color: #737373; }
|
|
2171
1758
|
.status-badge-ready { background: #eff6ff; color: #3b82f6; }
|
|
2172
1759
|
.status-badge-in_progress { background: #fff7ed; color: #f97316; }
|
|
2173
|
-
.status-badge-in_review { background: #f5f3ff; color: #8b5cf6; }
|
|
2174
1760
|
.status-badge-completed { background: #f0fdf4; color: #22c55e; }
|
|
2175
1761
|
.status-badge-failed { background: #fef2f2; color: #ef4444; }
|
|
2176
1762
|
|
|
@@ -2496,16 +2082,14 @@ socket.on('task:output', ({ taskId, line }) => {
|
|
|
2496
2082
|
}
|
|
2497
2083
|
});
|
|
2498
2084
|
|
|
2499
|
-
socket.on('task:completed', ({ taskId
|
|
2085
|
+
socket.on('task:completed', ({ taskId }) => {
|
|
2500
2086
|
state.running = state.running.filter(id => id !== taskId);
|
|
2501
2087
|
const task = state.tasks.find(t => t.id === taskId);
|
|
2502
2088
|
if (task) {
|
|
2503
|
-
|
|
2504
|
-
task.status = status || 'in_review';
|
|
2089
|
+
task.status = 'completed';
|
|
2505
2090
|
task.passes = true;
|
|
2506
2091
|
}
|
|
2507
|
-
|
|
2508
|
-
showToast('Task ' + statusMsg + ': ' + (task?.title || taskId), 'success');
|
|
2092
|
+
showToast('Task completed: ' + (task?.title || taskId), 'success');
|
|
2509
2093
|
render();
|
|
2510
2094
|
});
|
|
2511
2095
|
|
|
@@ -2579,53 +2163,6 @@ async function retryTask(id) {
|
|
|
2579
2163
|
});
|
|
2580
2164
|
}
|
|
2581
2165
|
|
|
2582
|
-
async function mergeTask(id) {
|
|
2583
|
-
const res = await fetch('/api/tasks/' + id + '/merge', { method: 'POST' });
|
|
2584
|
-
const data = await res.json();
|
|
2585
|
-
if (!res.ok) {
|
|
2586
|
-
throw new Error(data.error || 'Merge failed');
|
|
2587
|
-
}
|
|
2588
|
-
return data;
|
|
2589
|
-
}
|
|
2590
|
-
|
|
2591
|
-
async function createPR(id) {
|
|
2592
|
-
const res = await fetch('/api/tasks/' + id + '/create-pr', { method: 'POST' });
|
|
2593
|
-
const data = await res.json();
|
|
2594
|
-
if (!res.ok) {
|
|
2595
|
-
throw new Error(data.error || 'PR creation failed');
|
|
2596
|
-
}
|
|
2597
|
-
return data;
|
|
2598
|
-
}
|
|
2599
|
-
|
|
2600
|
-
async function discardTask(id) {
|
|
2601
|
-
const res = await fetch('/api/tasks/' + id + '/discard', { method: 'POST' });
|
|
2602
|
-
const data = await res.json();
|
|
2603
|
-
if (!res.ok) {
|
|
2604
|
-
throw new Error(data.error || 'Discard failed');
|
|
2605
|
-
}
|
|
2606
|
-
return data;
|
|
2607
|
-
}
|
|
2608
|
-
|
|
2609
|
-
async function getReviewInfo(id) {
|
|
2610
|
-
const res = await fetch('/api/tasks/' + id + '/review-info');
|
|
2611
|
-
const data = await res.json();
|
|
2612
|
-
if (!res.ok) {
|
|
2613
|
-
throw new Error(data.error || 'Failed to get review info');
|
|
2614
|
-
}
|
|
2615
|
-
return data;
|
|
2616
|
-
}
|
|
2617
|
-
|
|
2618
|
-
async function openInVSCode(id) {
|
|
2619
|
-
try {
|
|
2620
|
-
const info = await getReviewInfo(id);
|
|
2621
|
-
// Open VS Code at the worktree path
|
|
2622
|
-
window.open('vscode://file/' + encodeURIComponent(info.worktreePath), '_blank');
|
|
2623
|
-
showToast('Opening in VS Code...', 'info');
|
|
2624
|
-
} catch (e) {
|
|
2625
|
-
showToast('Failed to open in VS Code: ' + e.message, 'error');
|
|
2626
|
-
}
|
|
2627
|
-
}
|
|
2628
|
-
|
|
2629
2166
|
async function generateTask(prompt) {
|
|
2630
2167
|
state.aiGenerating = true;
|
|
2631
2168
|
render();
|
|
@@ -2644,11 +2181,11 @@ async function generateTask(prompt) {
|
|
|
2644
2181
|
}
|
|
2645
2182
|
}
|
|
2646
2183
|
|
|
2647
|
-
async function startAFK(maxIterations
|
|
2184
|
+
async function startAFK(maxIterations) {
|
|
2648
2185
|
await fetch('/api/afk/start', {
|
|
2649
2186
|
method: 'POST',
|
|
2650
2187
|
headers: { 'Content-Type': 'application/json' },
|
|
2651
|
-
body: JSON.stringify({ maxIterations
|
|
2188
|
+
body: JSON.stringify({ maxIterations })
|
|
2652
2189
|
});
|
|
2653
2190
|
}
|
|
2654
2191
|
|
|
@@ -2829,7 +2366,6 @@ function renderColumn(status, title, tasks) {
|
|
|
2829
2366
|
draft: 'To Do',
|
|
2830
2367
|
ready: 'Ready',
|
|
2831
2368
|
in_progress: 'In Progress',
|
|
2832
|
-
in_review: 'In Review',
|
|
2833
2369
|
completed: 'Done',
|
|
2834
2370
|
failed: 'Failed'
|
|
2835
2371
|
};
|
|
@@ -3082,21 +2618,13 @@ function renderModal() {
|
|
|
3082
2618
|
<button onclick="state.showModal = null; render();" class="btn btn-ghost p-1.5 text-canvas-500 hover:text-canvas-700">\u2715</button>
|
|
3083
2619
|
</div>
|
|
3084
2620
|
<div class="p-6">
|
|
3085
|
-
<p class="text-sm text-canvas-600 mb-5">Run the agent in a loop, automatically picking up tasks from the "Ready" column until complete.</p>
|
|
2621
|
+
<p class="text-sm text-canvas-600 mb-5">Run the agent in a loop, automatically picking up tasks from the "Ready" column one at a time until complete.</p>
|
|
3086
2622
|
<div class="space-y-4">
|
|
3087
2623
|
<div>
|
|
3088
2624
|
<label class="block text-sm font-medium text-canvas-700 mb-2">Maximum Iterations</label>
|
|
3089
2625
|
<input type="number" id="afk-iterations" value="10" min="1" max="100"
|
|
3090
2626
|
class="input w-full">
|
|
3091
2627
|
</div>
|
|
3092
|
-
<div>
|
|
3093
|
-
<label class="block text-sm font-medium text-canvas-700 mb-2">Concurrent Tasks</label>
|
|
3094
|
-
<select id="afk-concurrent" class="input w-full">
|
|
3095
|
-
<option value="1">1 (Sequential)</option>
|
|
3096
|
-
<option value="2">2</option>
|
|
3097
|
-
<option value="3">3 (Max)</option>
|
|
3098
|
-
</select>
|
|
3099
|
-
</div>
|
|
3100
2628
|
<div class="bg-status-running/10 border border-status-running/20 rounded-lg p-3">
|
|
3101
2629
|
<p class="text-xs text-status-running">\u26A0\uFE0F You can close this tab - the agent will continue running. Check back later or watch the terminal output.</p>
|
|
3102
2630
|
</div>
|
|
@@ -3211,20 +2739,6 @@ function renderSidePanel() {
|
|
|
3211
2739
|
\u23F9 Stop Attempt
|
|
3212
2740
|
</button>
|
|
3213
2741
|
\` : ''}
|
|
3214
|
-
\${task.status === 'in_review' ? \`
|
|
3215
|
-
<button onclick="handleMerge('\${task.id}')" class="btn btn-primary px-4 py-2 text-sm">
|
|
3216
|
-
\u2713 Merge
|
|
3217
|
-
</button>
|
|
3218
|
-
<button onclick="handleCreatePR('\${task.id}')" class="btn btn-ghost px-4 py-2 text-sm">
|
|
3219
|
-
\u21E1 Create PR
|
|
3220
|
-
</button>
|
|
3221
|
-
<button onclick="openInVSCode('\${task.id}')" class="btn btn-ghost px-4 py-2 text-sm">
|
|
3222
|
-
\u{1F4C2} Open in VS Code
|
|
3223
|
-
</button>
|
|
3224
|
-
<button onclick="handleDiscard('\${task.id}')" class="btn btn-danger px-4 py-2 text-sm">
|
|
3225
|
-
\u2715 Discard
|
|
3226
|
-
</button>
|
|
3227
|
-
\` : ''}
|
|
3228
2742
|
\${task.status === 'failed' ? \`
|
|
3229
2743
|
<button onclick="retryTask('\${task.id}')" class="btn btn-primary px-4 py-2 text-sm">
|
|
3230
2744
|
\u21BB Retry
|
|
@@ -3419,47 +2933,11 @@ function applyTemplate(templateId) {
|
|
|
3419
2933
|
|
|
3420
2934
|
function handleStartAFK() {
|
|
3421
2935
|
const iterations = parseInt(document.getElementById('afk-iterations').value) || 10;
|
|
3422
|
-
|
|
3423
|
-
startAFK(iterations, concurrent);
|
|
2936
|
+
startAFK(iterations);
|
|
3424
2937
|
state.showModal = null;
|
|
3425
2938
|
render();
|
|
3426
2939
|
}
|
|
3427
2940
|
|
|
3428
|
-
async function handleMerge(taskId) {
|
|
3429
|
-
if (!confirm('Merge this task into the main branch?')) return;
|
|
3430
|
-
try {
|
|
3431
|
-
await mergeTask(taskId);
|
|
3432
|
-
showToast('Task merged successfully!', 'success');
|
|
3433
|
-
closeSidePanel();
|
|
3434
|
-
} catch (e) {
|
|
3435
|
-
showToast('Merge failed: ' + e.message, 'error');
|
|
3436
|
-
}
|
|
3437
|
-
}
|
|
3438
|
-
|
|
3439
|
-
async function handleCreatePR(taskId) {
|
|
3440
|
-
try {
|
|
3441
|
-
const result = await createPR(taskId);
|
|
3442
|
-
showToast('PR created successfully!', 'success');
|
|
3443
|
-
if (result.prUrl) {
|
|
3444
|
-
window.open(result.prUrl, '_blank');
|
|
3445
|
-
}
|
|
3446
|
-
closeSidePanel();
|
|
3447
|
-
} catch (e) {
|
|
3448
|
-
showToast('PR creation failed: ' + e.message, 'error');
|
|
3449
|
-
}
|
|
3450
|
-
}
|
|
3451
|
-
|
|
3452
|
-
async function handleDiscard(taskId) {
|
|
3453
|
-
if (!confirm('Discard this work? The task will return to Ready status.')) return;
|
|
3454
|
-
try {
|
|
3455
|
-
await discardTask(taskId);
|
|
3456
|
-
showToast('Task discarded, returned to Ready', 'warning');
|
|
3457
|
-
closeSidePanel();
|
|
3458
|
-
} catch (e) {
|
|
3459
|
-
showToast('Discard failed: ' + e.message, 'error');
|
|
3460
|
-
}
|
|
3461
|
-
}
|
|
3462
|
-
|
|
3463
2941
|
// Filter tasks based on search query
|
|
3464
2942
|
function filterTasks(tasks) {
|
|
3465
2943
|
if (!state.searchQuery) return tasks;
|
|
@@ -3521,7 +2999,6 @@ function render() {
|
|
|
3521
2999
|
\${renderColumn('draft', 'To Do', filterTasks(state.tasks))}
|
|
3522
3000
|
\${renderColumn('ready', 'Ready', filterTasks(state.tasks))}
|
|
3523
3001
|
\${renderColumn('in_progress', 'In Progress', filterTasks(state.tasks))}
|
|
3524
|
-
\${renderColumn('in_review', 'In Review', filterTasks(state.tasks))}
|
|
3525
3002
|
\${renderColumn('completed', 'Done', filterTasks(state.tasks))}
|
|
3526
3003
|
\${renderColumn('failed', 'Failed', filterTasks(state.tasks))}
|
|
3527
3004
|
</div>
|
|
@@ -3586,14 +3063,6 @@ window.closeSidePanel = closeSidePanel;
|
|
|
3586
3063
|
window.showTaskMenu = showTaskMenu;
|
|
3587
3064
|
window.filterTasks = filterTasks;
|
|
3588
3065
|
window.scrollSidePanelLog = scrollSidePanelLog;
|
|
3589
|
-
window.mergeTask = mergeTask;
|
|
3590
|
-
window.createPR = createPR;
|
|
3591
|
-
window.discardTask = discardTask;
|
|
3592
|
-
window.handleMerge = handleMerge;
|
|
3593
|
-
window.handleCreatePR = handleCreatePR;
|
|
3594
|
-
window.handleDiscard = handleDiscard;
|
|
3595
|
-
window.openInVSCode = openInVSCode;
|
|
3596
|
-
window.getReviewInfo = getReviewInfo;
|
|
3597
3066
|
|
|
3598
3067
|
// Keyboard shortcuts
|
|
3599
3068
|
document.addEventListener('keydown', (e) => {
|