ralphctl 0.1.4 → 0.2.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/README.md +23 -14
- package/dist/{add-7LBVENXM.mjs → add-SEDQ3VK7.mjs} +4 -4
- package/dist/{add-DVEYDCTR.mjs → add-TGJTRHIF.mjs} +3 -3
- package/dist/{chunk-M7JV6MKD.mjs → chunk-AXNZMHFQ.mjs} +384 -96
- package/dist/{chunk-LFDW6MWF.mjs → chunk-KPTPKLXY.mjs} +16 -3
- package/dist/{chunk-PDI6HBZ7.mjs → chunk-LG6B7QVO.mjs} +1 -1
- package/dist/{chunk-YIB7QYU4.mjs → chunk-Q3VWJARJ.mjs} +2 -2
- package/dist/{chunk-F2MMCTB5.mjs → chunk-XPDI4SYI.mjs} +5 -4
- package/dist/{chunk-DZ6HHTM5.mjs → chunk-XQHEKKDN.mjs} +1 -1
- package/dist/{chunk-W3TY22IS.mjs → chunk-ZDEVRTGY.mjs} +10 -3
- package/dist/cli.mjs +174 -65
- package/dist/{create-MQ4OHZAX.mjs → create-DJHCP7LN.mjs} +3 -3
- package/dist/{handle-K2AZLTKU.mjs → handle-CCTBNAJZ.mjs} +1 -1
- package/dist/{project-Q4LKML42.mjs → project-ZYGNPVGL.mjs} +2 -2
- package/dist/prompts/ideate-auto.md +3 -2
- package/dist/prompts/ideate.md +2 -2
- package/dist/prompts/plan-auto.md +11 -8
- package/dist/prompts/plan-common.md +13 -8
- package/dist/prompts/plan-interactive.md +11 -10
- package/dist/prompts/task-evaluation.md +54 -0
- package/dist/prompts/task-execution.md +7 -5
- package/dist/{resolver-NH34HTB6.mjs → resolver-L52KR4GY.mjs} +2 -2
- package/dist/{sprint-UHYXSEBJ.mjs → sprint-LUXAV3Q3.mjs} +2 -2
- package/dist/{wizard-MCDDXLGE.mjs → wizard-TFJXEYD2.mjs} +6 -6
- package/package.json +17 -14
- package/schemas/config.schema.json +10 -0
- package/schemas/projects.schema.json +5 -0
- package/schemas/tasks.schema.json +9 -0
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
getPendingRequirements,
|
|
12
12
|
groupTicketsByProject,
|
|
13
13
|
listTickets
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-XPDI4SYI.mjs";
|
|
15
15
|
import {
|
|
16
16
|
EXIT_ALL_BLOCKED,
|
|
17
17
|
EXIT_ERROR,
|
|
@@ -23,13 +23,14 @@ import {
|
|
|
23
23
|
import {
|
|
24
24
|
getProject,
|
|
25
25
|
listProjects
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-LG6B7QVO.mjs";
|
|
27
27
|
import {
|
|
28
28
|
activateSprint,
|
|
29
29
|
assertSprintStatus,
|
|
30
30
|
closeSprint,
|
|
31
31
|
generateUuid8,
|
|
32
32
|
getAiProvider,
|
|
33
|
+
getEvaluationIterations,
|
|
33
34
|
getProgress,
|
|
34
35
|
getSprint,
|
|
35
36
|
listSprints,
|
|
@@ -39,7 +40,7 @@ import {
|
|
|
39
40
|
setAiProvider,
|
|
40
41
|
summarizeProgressForContext,
|
|
41
42
|
withFileLock
|
|
42
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-KPTPKLXY.mjs";
|
|
43
44
|
import {
|
|
44
45
|
ensureError,
|
|
45
46
|
unwrapOrThrow,
|
|
@@ -60,7 +61,7 @@ import {
|
|
|
60
61
|
getTasksFilePath,
|
|
61
62
|
readValidatedJson,
|
|
62
63
|
writeValidatedJson
|
|
63
|
-
} from "./chunk-
|
|
64
|
+
} from "./chunk-ZDEVRTGY.mjs";
|
|
64
65
|
import {
|
|
65
66
|
DependencyCycleError,
|
|
66
67
|
IOError,
|
|
@@ -106,7 +107,7 @@ import {
|
|
|
106
107
|
import { mkdir, readFile } from "fs/promises";
|
|
107
108
|
import { join as join4 } from "path";
|
|
108
109
|
import { confirm } from "@inquirer/prompts";
|
|
109
|
-
import { Result as
|
|
110
|
+
import { Result as Result4 } from "typescript-result";
|
|
110
111
|
|
|
111
112
|
// src/ai/prompts/index.ts
|
|
112
113
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -138,7 +139,7 @@ function buildTaskExecutionPrompt(progressFilePath, noCommit, contextFileName) {
|
|
|
138
139
|
const template = loadTemplate("task-execution");
|
|
139
140
|
const commitStep = noCommit ? "" : "\n> **Before continuing:** Create a git commit with a descriptive message for the changes made.\n";
|
|
140
141
|
const commitConstraint = noCommit ? "" : "- **Must commit** \u2014 Create a git commit before signaling completion.\n";
|
|
141
|
-
return template.
|
|
142
|
+
return template.replaceAll("{{PROGRESS_FILE}}", progressFilePath).replaceAll("{{COMMIT_STEP}}", commitStep).replaceAll("{{COMMIT_CONSTRAINT}}", commitConstraint).replaceAll("{{CONTEXT_FILE}}", contextFileName);
|
|
142
143
|
}
|
|
143
144
|
function buildTicketRefinePrompt(ticketContent, outputFile, schema, issueContext = "") {
|
|
144
145
|
const template = loadTemplate("ticket-refine");
|
|
@@ -154,6 +155,18 @@ function buildIdeateAutoPrompt(ideaTitle, ideaDescription, projectName, reposito
|
|
|
154
155
|
const common = loadTemplate("plan-common");
|
|
155
156
|
return template.replace("{{IDEA_TITLE}}", ideaTitle).replace("{{IDEA_DESCRIPTION}}", ideaDescription).replace("{{PROJECT_NAME}}", projectName).replace("{{REPOSITORIES}}", repositories).replace("{{SCHEMA}}", schema).replace("{{COMMON}}", common);
|
|
156
157
|
}
|
|
158
|
+
function buildEvaluatorPrompt(ctx) {
|
|
159
|
+
const template = loadTemplate("task-evaluation");
|
|
160
|
+
const descriptionSection = ctx.taskDescription ? `
|
|
161
|
+
**Description:** ${ctx.taskDescription}` : "";
|
|
162
|
+
const stepsSection = ctx.taskSteps.length > 0 ? `
|
|
163
|
+
**Implementation Steps:**
|
|
164
|
+
${ctx.taskSteps.map((s) => `- ${s}`).join("\n")}` : "";
|
|
165
|
+
const checkSection = ctx.checkScriptSection ? `
|
|
166
|
+
|
|
167
|
+
${ctx.checkScriptSection}` : "";
|
|
168
|
+
return template.replaceAll("{{TASK_NAME}}", ctx.taskName).replace("{{TASK_DESCRIPTION_SECTION}}", descriptionSection).replace("{{TASK_STEPS_SECTION}}", stepsSection).replace("{{PROJECT_PATH}}", ctx.projectPath).replace("{{CHECK_SCRIPT_SECTION}}", checkSection);
|
|
169
|
+
}
|
|
157
170
|
|
|
158
171
|
// src/utils/requirements-export.ts
|
|
159
172
|
import { writeFile } from "fs/promises";
|
|
@@ -223,7 +236,7 @@ function providerDisplayName(provider) {
|
|
|
223
236
|
// src/commands/ticket/refine-utils.ts
|
|
224
237
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
225
238
|
import { join as join3 } from "path";
|
|
226
|
-
import { Result as
|
|
239
|
+
import { Result as Result3 } from "typescript-result";
|
|
227
240
|
|
|
228
241
|
// src/ai/session.ts
|
|
229
242
|
import { spawn, spawnSync } from "child_process";
|
|
@@ -243,6 +256,9 @@ var ProcessManager = class _ProcessManager {
|
|
|
243
256
|
handlersInstalled = false;
|
|
244
257
|
/** Timestamp of first SIGINT (for double-signal detection) */
|
|
245
258
|
firstSigintAt = null;
|
|
259
|
+
/** Stored signal handler references for cleanup */
|
|
260
|
+
sigintHandler = null;
|
|
261
|
+
sigtermHandler = null;
|
|
246
262
|
constructor() {
|
|
247
263
|
}
|
|
248
264
|
/**
|
|
@@ -331,9 +347,9 @@ var ProcessManager = class _ProcessManager {
|
|
|
331
347
|
if (error2.code === "ESRCH") {
|
|
332
348
|
this.children.delete(child);
|
|
333
349
|
} else if (error2.code === "EPERM") {
|
|
334
|
-
|
|
350
|
+
log.warn(`Permission denied killing process ${String(child.pid)}`);
|
|
335
351
|
} else {
|
|
336
|
-
|
|
352
|
+
log.error(`Error killing process ${String(child.pid)}: ${error2.message}`);
|
|
337
353
|
}
|
|
338
354
|
}
|
|
339
355
|
}
|
|
@@ -352,7 +368,7 @@ var ProcessManager = class _ProcessManager {
|
|
|
352
368
|
if (signal === "SIGINT" && this.firstSigintAt) {
|
|
353
369
|
const now = Date.now();
|
|
354
370
|
if (now - this.firstSigintAt < FORCE_QUIT_WINDOW_MS) {
|
|
355
|
-
|
|
371
|
+
log.warn("\n\nForce quit (double signal) \u2014 killing all processes immediately...");
|
|
356
372
|
this.killAll("SIGKILL");
|
|
357
373
|
process.exit(1);
|
|
358
374
|
return;
|
|
@@ -365,12 +381,12 @@ var ProcessManager = class _ProcessManager {
|
|
|
365
381
|
if (signal === "SIGINT") {
|
|
366
382
|
this.firstSigintAt = Date.now();
|
|
367
383
|
}
|
|
368
|
-
|
|
384
|
+
log.dim("\n\nShutting down gracefully... (press Ctrl+C again to force-quit)");
|
|
369
385
|
for (const callback of this.cleanupCallbacks) {
|
|
370
386
|
try {
|
|
371
387
|
callback();
|
|
372
388
|
} catch (err) {
|
|
373
|
-
|
|
389
|
+
log.error(`Error in cleanup callback: ${err instanceof Error ? err.message : String(err)}`);
|
|
374
390
|
}
|
|
375
391
|
}
|
|
376
392
|
this.cleanupCallbacks.clear();
|
|
@@ -380,7 +396,7 @@ var ProcessManager = class _ProcessManager {
|
|
|
380
396
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
381
397
|
}
|
|
382
398
|
if (this.children.size > 0) {
|
|
383
|
-
|
|
399
|
+
log.warn(`Force-killing ${String(this.children.size)} remaining process(es)...`);
|
|
384
400
|
this.killAll("SIGKILL");
|
|
385
401
|
}
|
|
386
402
|
process.exit(signal === "SIGINT" ? EXIT_INTERRUPTED : 1);
|
|
@@ -390,6 +406,14 @@ var ProcessManager = class _ProcessManager {
|
|
|
390
406
|
* @internal
|
|
391
407
|
*/
|
|
392
408
|
dispose() {
|
|
409
|
+
if (this.sigintHandler) {
|
|
410
|
+
process.removeListener("SIGINT", this.sigintHandler);
|
|
411
|
+
this.sigintHandler = null;
|
|
412
|
+
}
|
|
413
|
+
if (this.sigtermHandler) {
|
|
414
|
+
process.removeListener("SIGTERM", this.sigtermHandler);
|
|
415
|
+
this.sigtermHandler = null;
|
|
416
|
+
}
|
|
393
417
|
this.children.clear();
|
|
394
418
|
this.cleanupCallbacks.clear();
|
|
395
419
|
this.exiting = false;
|
|
@@ -399,14 +423,17 @@ var ProcessManager = class _ProcessManager {
|
|
|
399
423
|
/**
|
|
400
424
|
* Install signal handlers for SIGINT and SIGTERM.
|
|
401
425
|
* Uses process.on() (persistent) not process.once() (one-shot).
|
|
426
|
+
* Stores handler references so dispose() can remove them.
|
|
402
427
|
*/
|
|
403
428
|
installSignalHandlers() {
|
|
404
|
-
|
|
429
|
+
this.sigintHandler = () => {
|
|
405
430
|
void this.shutdown("SIGINT");
|
|
406
|
-
}
|
|
407
|
-
|
|
431
|
+
};
|
|
432
|
+
this.sigtermHandler = () => {
|
|
408
433
|
void this.shutdown("SIGTERM");
|
|
409
|
-
}
|
|
434
|
+
};
|
|
435
|
+
process.on("SIGINT", this.sigintHandler);
|
|
436
|
+
process.on("SIGTERM", this.sigtermHandler);
|
|
410
437
|
}
|
|
411
438
|
};
|
|
412
439
|
|
|
@@ -427,14 +454,21 @@ var claudeAdapter = {
|
|
|
427
454
|
parseJsonOutput(stdout) {
|
|
428
455
|
const jsonResult = Result.try(() => JSON.parse(stdout));
|
|
429
456
|
if (!jsonResult.ok) {
|
|
430
|
-
return { result: stdout, sessionId: null };
|
|
457
|
+
return { result: stdout, sessionId: null, model: null };
|
|
431
458
|
}
|
|
432
459
|
const parsed = jsonResult.value;
|
|
433
460
|
return {
|
|
434
461
|
result: parsed.result ?? stdout,
|
|
435
|
-
sessionId: parsed.session_id ?? null
|
|
462
|
+
sessionId: parsed.session_id ?? null,
|
|
463
|
+
model: parsed.model ?? null
|
|
436
464
|
};
|
|
437
465
|
},
|
|
466
|
+
buildResumeArgs(sessionId) {
|
|
467
|
+
if (!/^[a-zA-Z0-9_][a-zA-Z0-9_-]{0,127}$/.test(sessionId)) {
|
|
468
|
+
throw new Error("Invalid session ID format");
|
|
469
|
+
}
|
|
470
|
+
return ["--resume", sessionId];
|
|
471
|
+
},
|
|
438
472
|
detectRateLimit(stderr) {
|
|
439
473
|
const patterns = [/rate.?limit/i, /\b429\b/, /too many requests/i, /overloaded/i, /\b529\b/];
|
|
440
474
|
const isRateLimited = patterns.some((p) => p.test(stderr));
|
|
@@ -453,6 +487,7 @@ var claudeAdapter = {
|
|
|
453
487
|
// src/providers/copilot.ts
|
|
454
488
|
import { lstat, readdir, unlink } from "fs/promises";
|
|
455
489
|
import { join as join2 } from "path";
|
|
490
|
+
import { Result as Result2 } from "typescript-result";
|
|
456
491
|
var copilotAdapter = {
|
|
457
492
|
name: "copilot",
|
|
458
493
|
displayName: "Copilot",
|
|
@@ -463,10 +498,30 @@ var copilotAdapter = {
|
|
|
463
498
|
return [...this.baseArgs, ...extraArgs, "-i", prompt];
|
|
464
499
|
},
|
|
465
500
|
buildHeadlessArgs(extraArgs = []) {
|
|
466
|
-
return ["-p", "-
|
|
501
|
+
return ["-p", "--output-format", "json", "--autopilot", "--no-ask-user", "--share", ...this.baseArgs, ...extraArgs];
|
|
467
502
|
},
|
|
468
503
|
parseJsonOutput(stdout) {
|
|
469
|
-
|
|
504
|
+
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
505
|
+
if (lines.length === 0) {
|
|
506
|
+
return { result: "", sessionId: null, model: null };
|
|
507
|
+
}
|
|
508
|
+
const lastLine = lines.at(-1) ?? "";
|
|
509
|
+
const jsonResult = Result2.try(() => JSON.parse(lastLine));
|
|
510
|
+
if (jsonResult.ok) {
|
|
511
|
+
const parsed = jsonResult.value;
|
|
512
|
+
return {
|
|
513
|
+
result: parsed.result ?? parsed.result_text ?? lastLine,
|
|
514
|
+
sessionId: parsed.session_id ?? null,
|
|
515
|
+
model: null
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
return { result: stdout.trim(), sessionId: null, model: null };
|
|
519
|
+
},
|
|
520
|
+
buildResumeArgs(sessionId) {
|
|
521
|
+
if (!/^[a-zA-Z0-9_][a-zA-Z0-9_-]{0,127}$/.test(sessionId)) {
|
|
522
|
+
throw new Error("Invalid session ID format");
|
|
523
|
+
}
|
|
524
|
+
return [`--resume=${sessionId}`];
|
|
470
525
|
},
|
|
471
526
|
async extractSessionId(cwd) {
|
|
472
527
|
const filesResult = await wrapAsync(
|
|
@@ -511,11 +566,13 @@ function getProvider(provider) {
|
|
|
511
566
|
return copilotAdapter;
|
|
512
567
|
}
|
|
513
568
|
}
|
|
569
|
+
var experimentalWarningShown = false;
|
|
514
570
|
async function getActiveProvider() {
|
|
515
571
|
const provider = await resolveProvider();
|
|
516
572
|
const adapter = getProvider(provider);
|
|
517
|
-
if (adapter.experimental) {
|
|
573
|
+
if (adapter.experimental && !experimentalWarningShown) {
|
|
518
574
|
showWarning(`${adapter.displayName} provider is in public preview \u2014 some features may not work as expected.`);
|
|
575
|
+
experimentalWarningShown = true;
|
|
519
576
|
}
|
|
520
577
|
return adapter;
|
|
521
578
|
}
|
|
@@ -523,25 +580,15 @@ async function getActiveProvider() {
|
|
|
523
580
|
// src/ai/session.ts
|
|
524
581
|
function spawnInteractive(prompt, options, provider) {
|
|
525
582
|
assertSafeCwd(options.cwd);
|
|
526
|
-
const
|
|
527
|
-
binary: "claude",
|
|
528
|
-
baseArgs: ["--permission-mode", "acceptEdits"],
|
|
529
|
-
buildInteractiveArgs: (pr, extra = []) => [
|
|
530
|
-
...["--permission-mode", "acceptEdits"],
|
|
531
|
-
...extra,
|
|
532
|
-
"--",
|
|
533
|
-
pr
|
|
534
|
-
]
|
|
535
|
-
};
|
|
536
|
-
const args = prompt ? p.buildInteractiveArgs(prompt, options.args ?? []) : [...p.baseArgs, ...options.args ?? []];
|
|
583
|
+
const args = prompt ? provider.buildInteractiveArgs(prompt, options.args ?? []) : [...provider.baseArgs, ...options.args ?? []];
|
|
537
584
|
const env = options.env ? { ...process.env, ...options.env } : void 0;
|
|
538
|
-
const result = spawnSync(
|
|
585
|
+
const result = spawnSync(provider.binary, args, {
|
|
539
586
|
cwd: options.cwd,
|
|
540
587
|
stdio: "inherit",
|
|
541
588
|
env
|
|
542
589
|
});
|
|
543
590
|
if (result.error) {
|
|
544
|
-
return { code: 1, error: `Failed to spawn ${
|
|
591
|
+
return { code: 1, error: `Failed to spawn ${provider.binary} CLI: ${result.error.message}` };
|
|
545
592
|
}
|
|
546
593
|
return { code: result.status ?? 1 };
|
|
547
594
|
}
|
|
@@ -555,11 +602,12 @@ async function spawnHeadlessRaw(options, provider) {
|
|
|
555
602
|
return new Promise((resolve, reject) => {
|
|
556
603
|
const allArgs = p.buildHeadlessArgs(options.args ?? []);
|
|
557
604
|
if (options.resumeSessionId) {
|
|
558
|
-
|
|
605
|
+
try {
|
|
606
|
+
allArgs.push(...p.buildResumeArgs(options.resumeSessionId));
|
|
607
|
+
} catch {
|
|
559
608
|
reject(new SpawnError("Invalid session ID format", "", 1));
|
|
560
609
|
return;
|
|
561
610
|
}
|
|
562
|
-
allArgs.push("--resume", options.resumeSessionId);
|
|
563
611
|
}
|
|
564
612
|
const child = spawn(p.binary, allArgs, {
|
|
565
613
|
cwd: options.cwd,
|
|
@@ -573,6 +621,7 @@ async function spawnHeadlessRaw(options, provider) {
|
|
|
573
621
|
reject(new SpawnError("Cannot spawn during shutdown", "", 1));
|
|
574
622
|
return;
|
|
575
623
|
}
|
|
624
|
+
const MAX_STDOUT_SIZE = 1e7;
|
|
576
625
|
const MAX_PROMPT_SIZE = 1e6;
|
|
577
626
|
if (options.prompt) {
|
|
578
627
|
if (options.prompt.length > MAX_PROMPT_SIZE) {
|
|
@@ -585,7 +634,9 @@ async function spawnHeadlessRaw(options, provider) {
|
|
|
585
634
|
let rawStdout = "";
|
|
586
635
|
let stderr = "";
|
|
587
636
|
child.stdout.on("data", (data) => {
|
|
588
|
-
rawStdout
|
|
637
|
+
if (rawStdout.length < MAX_STDOUT_SIZE) {
|
|
638
|
+
rawStdout += data.toString();
|
|
639
|
+
}
|
|
589
640
|
});
|
|
590
641
|
child.stderr.on("data", (data) => {
|
|
591
642
|
stderr += data.toString();
|
|
@@ -593,7 +644,7 @@ async function spawnHeadlessRaw(options, provider) {
|
|
|
593
644
|
child.on("close", (code) => {
|
|
594
645
|
void (async () => {
|
|
595
646
|
const exitCode = code ?? 1;
|
|
596
|
-
const { result, sessionId: parsedSessionId } = p.parseJsonOutput(rawStdout);
|
|
647
|
+
const { result, sessionId: parsedSessionId, model: parsedModel } = p.parseJsonOutput(rawStdout);
|
|
597
648
|
const sessionId = parsedSessionId ?? await p.extractSessionId?.(options.cwd) ?? null;
|
|
598
649
|
if (exitCode !== 0) {
|
|
599
650
|
reject(
|
|
@@ -605,7 +656,7 @@ async function spawnHeadlessRaw(options, provider) {
|
|
|
605
656
|
)
|
|
606
657
|
);
|
|
607
658
|
} else {
|
|
608
|
-
resolve({ stdout: result, stderr, exitCode: 0, sessionId });
|
|
659
|
+
resolve({ stdout: result, stderr, exitCode: 0, sessionId, model: parsedModel });
|
|
609
660
|
}
|
|
610
661
|
})().catch((err) => {
|
|
611
662
|
reject(new SpawnError(`Unexpected error in close handler: ${String(err)}`, "", 1));
|
|
@@ -716,7 +767,7 @@ function formatTicketForPrompt(ticket) {
|
|
|
716
767
|
}
|
|
717
768
|
function parseRequirementsFile(content) {
|
|
718
769
|
const jsonStr = extractJsonArray(content);
|
|
719
|
-
const parseR =
|
|
770
|
+
const parseR = Result3.try(() => JSON.parse(jsonStr));
|
|
720
771
|
if (!parseR.ok) {
|
|
721
772
|
throw new Error(`Invalid JSON: ${parseR.error.message}`, { cause: parseR.error });
|
|
722
773
|
}
|
|
@@ -743,7 +794,8 @@ async function runAiSession(workingDir, prompt, ticketTitle) {
|
|
|
743
794
|
const result = spawnInteractive(
|
|
744
795
|
startPrompt,
|
|
745
796
|
{
|
|
746
|
-
cwd: workingDir
|
|
797
|
+
cwd: workingDir,
|
|
798
|
+
env: provider.getSpawnEnv()
|
|
747
799
|
},
|
|
748
800
|
provider
|
|
749
801
|
);
|
|
@@ -864,7 +916,7 @@ async function sprintRefineCommand(args) {
|
|
|
864
916
|
const fetchSpinner = createSpinner("Fetching issue data...");
|
|
865
917
|
fetchSpinner.start();
|
|
866
918
|
const link = ticket.link;
|
|
867
|
-
const issueR =
|
|
919
|
+
const issueR = Result4.try(() => fetchIssueFromUrl(link));
|
|
868
920
|
if (issueR.ok && issueR.value) {
|
|
869
921
|
issueContext = formatIssueContext(issueR.value);
|
|
870
922
|
fetchSpinner.succeed(`Issue data fetched (${String(issueR.value.comments.length)} comment(s))`);
|
|
@@ -907,7 +959,7 @@ async function sprintRefineCommand(args) {
|
|
|
907
959
|
skipped++;
|
|
908
960
|
continue;
|
|
909
961
|
}
|
|
910
|
-
const parseR =
|
|
962
|
+
const parseR = Result4.try(() => parseRequirementsFile(contentR.value));
|
|
911
963
|
if (!parseR.ok) {
|
|
912
964
|
showError(`Failed to parse requirements file: ${parseR.error.message}`);
|
|
913
965
|
log.newline();
|
|
@@ -1000,7 +1052,7 @@ ${text}`;
|
|
|
1000
1052
|
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
1001
1053
|
import { join as join5 } from "path";
|
|
1002
1054
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
1003
|
-
import { Result as
|
|
1055
|
+
import { Result as Result5 } from "typescript-result";
|
|
1004
1056
|
|
|
1005
1057
|
// src/store/task.ts
|
|
1006
1058
|
async function getTasks(sprintId) {
|
|
@@ -1040,7 +1092,8 @@ async function addTask(input3, sprintId) {
|
|
|
1040
1092
|
ticketId: input3.ticketId,
|
|
1041
1093
|
blockedBy: input3.blockedBy ?? [],
|
|
1042
1094
|
projectPath: input3.projectPath,
|
|
1043
|
-
verified: false
|
|
1095
|
+
verified: false,
|
|
1096
|
+
evaluated: false
|
|
1044
1097
|
};
|
|
1045
1098
|
tasks.push(task);
|
|
1046
1099
|
await saveTasks(tasks, id);
|
|
@@ -1100,6 +1153,12 @@ async function updateTask(taskId, updates, sprintId) {
|
|
|
1100
1153
|
if (updates.verificationOutput !== void 0) {
|
|
1101
1154
|
task.verificationOutput = updates.verificationOutput;
|
|
1102
1155
|
}
|
|
1156
|
+
if (updates.evaluated !== void 0) {
|
|
1157
|
+
task.evaluated = updates.evaluated;
|
|
1158
|
+
}
|
|
1159
|
+
if (updates.evaluationOutput !== void 0) {
|
|
1160
|
+
task.evaluationOutput = updates.evaluationOutput;
|
|
1161
|
+
}
|
|
1103
1162
|
await saveTasks(tasks, id);
|
|
1104
1163
|
return task;
|
|
1105
1164
|
});
|
|
@@ -1269,7 +1328,8 @@ function validateImportTasks(importTasks2, existingTasks, ticketIds) {
|
|
|
1269
1328
|
}),
|
|
1270
1329
|
projectPath: "/tmp",
|
|
1271
1330
|
// Placeholder for validation only
|
|
1272
|
-
verified: false
|
|
1331
|
+
verified: false,
|
|
1332
|
+
evaluated: false
|
|
1273
1333
|
}))
|
|
1274
1334
|
];
|
|
1275
1335
|
try {
|
|
@@ -1295,7 +1355,7 @@ async function selectProject(message = "Select project:") {
|
|
|
1295
1355
|
default: true
|
|
1296
1356
|
});
|
|
1297
1357
|
if (create) {
|
|
1298
|
-
const { projectAddCommand } = await import("./add-
|
|
1358
|
+
const { projectAddCommand } = await import("./add-TGJTRHIF.mjs");
|
|
1299
1359
|
await projectAddCommand({ interactive: true });
|
|
1300
1360
|
const updated = await listProjects();
|
|
1301
1361
|
if (updated.length === 0) return null;
|
|
@@ -1368,7 +1428,7 @@ async function selectSprint(message = "Select sprint:", filter) {
|
|
|
1368
1428
|
default: true
|
|
1369
1429
|
});
|
|
1370
1430
|
if (create) {
|
|
1371
|
-
const { sprintCreateCommand } = await import("./create-
|
|
1431
|
+
const { sprintCreateCommand } = await import("./create-DJHCP7LN.mjs");
|
|
1372
1432
|
await sprintCreateCommand({ interactive: true });
|
|
1373
1433
|
const updated = await listSprints();
|
|
1374
1434
|
const refiltered = filter ? updated.filter((s) => filter.includes(s.status)) : updated;
|
|
@@ -1403,7 +1463,7 @@ async function selectTicket(message = "Select ticket:", filter) {
|
|
|
1403
1463
|
default: true
|
|
1404
1464
|
});
|
|
1405
1465
|
if (create) {
|
|
1406
|
-
const { ticketAddCommand } = await import("./add-
|
|
1466
|
+
const { ticketAddCommand } = await import("./add-SEDQ3VK7.mjs");
|
|
1407
1467
|
await ticketAddCommand({ interactive: true });
|
|
1408
1468
|
const updated = await listTickets();
|
|
1409
1469
|
const refiltered = filter ? updated.filter(filter) : updated;
|
|
@@ -1491,6 +1551,12 @@ function parsePlanningBlocked(output) {
|
|
|
1491
1551
|
const match = /<planning-blocked>([\s\S]*?)<\/planning-blocked>/.exec(output);
|
|
1492
1552
|
return match?.[1]?.trim() ?? null;
|
|
1493
1553
|
}
|
|
1554
|
+
function buildHeadlessAiRequest(repoPaths, prompt) {
|
|
1555
|
+
return {
|
|
1556
|
+
args: repoPaths.flatMap((path) => ["--add-dir", path]),
|
|
1557
|
+
prompt
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1494
1560
|
function parseTasksJson(output) {
|
|
1495
1561
|
const jsonStr = extractJsonArray(output);
|
|
1496
1562
|
let parsed;
|
|
@@ -1598,6 +1664,7 @@ async function importTasksReplace(tasks, sprintId) {
|
|
|
1598
1664
|
blockedBy: [],
|
|
1599
1665
|
// Set in second pass
|
|
1600
1666
|
projectPath: taskInput.projectPath,
|
|
1667
|
+
evaluated: false,
|
|
1601
1668
|
verified: false
|
|
1602
1669
|
});
|
|
1603
1670
|
log.itemSuccess(`${realId}: ${taskInput.name}`);
|
|
@@ -1694,7 +1761,7 @@ async function invokeAiInteractive(prompt, repoPaths, planDir) {
|
|
|
1694
1761
|
const provider = await getActiveProvider();
|
|
1695
1762
|
const ticketCount = (prompt.match(/^####/gm) ?? []).length;
|
|
1696
1763
|
const startPrompt = `I need help planning tasks for a sprint. The full planning context is in planning-context.md (${String(ticketCount)} tickets). Please read that file now and follow the instructions to help me plan implementation tasks.`;
|
|
1697
|
-
const args = ["--add-dir",
|
|
1764
|
+
const args = repoPaths.flatMap((path) => ["--add-dir", path]);
|
|
1698
1765
|
const result = spawnInteractive(
|
|
1699
1766
|
startPrompt,
|
|
1700
1767
|
{
|
|
@@ -1710,15 +1777,12 @@ async function invokeAiInteractive(prompt, repoPaths, planDir) {
|
|
|
1710
1777
|
}
|
|
1711
1778
|
async function invokeAiAuto(prompt, repoPaths, planDir) {
|
|
1712
1779
|
const provider = await getActiveProvider();
|
|
1713
|
-
const
|
|
1714
|
-
for (const path of repoPaths) {
|
|
1715
|
-
args.push("--add-dir", path);
|
|
1716
|
-
}
|
|
1717
|
-
args.push("-p", prompt);
|
|
1780
|
+
const request = buildHeadlessAiRequest(repoPaths, prompt);
|
|
1718
1781
|
return spawnHeadless(
|
|
1719
1782
|
{
|
|
1720
1783
|
cwd: planDir,
|
|
1721
|
-
args,
|
|
1784
|
+
args: request.args,
|
|
1785
|
+
prompt: request.prompt,
|
|
1722
1786
|
env: provider.getSpawnEnv()
|
|
1723
1787
|
},
|
|
1724
1788
|
provider
|
|
@@ -1881,7 +1945,7 @@ async function sprintPlanCommand(args) {
|
|
|
1881
1945
|
return;
|
|
1882
1946
|
}
|
|
1883
1947
|
console.log(muted("Parsing response..."));
|
|
1884
|
-
const parsedR =
|
|
1948
|
+
const parsedR = Result5.try(() => parseTasksJson(output));
|
|
1885
1949
|
if (!parsedR.ok) {
|
|
1886
1950
|
showError(`Failed to parse ${providerName} output: ${parsedR.error.message}`);
|
|
1887
1951
|
log.dim("Raw output:");
|
|
@@ -1946,7 +2010,7 @@ async function sprintPlanCommand(args) {
|
|
|
1946
2010
|
log.newline();
|
|
1947
2011
|
return;
|
|
1948
2012
|
}
|
|
1949
|
-
const parsedR =
|
|
2013
|
+
const parsedR = Result5.try(() => parseTasksJson(contentR.value));
|
|
1950
2014
|
if (!parsedR.ok) {
|
|
1951
2015
|
showError(`Failed to parse task file: ${parsedR.error.message}`);
|
|
1952
2016
|
log.newline();
|
|
@@ -1989,11 +2053,11 @@ async function sprintPlanCommand(args) {
|
|
|
1989
2053
|
}
|
|
1990
2054
|
|
|
1991
2055
|
// src/commands/sprint/start.ts
|
|
1992
|
-
import { Result as
|
|
2056
|
+
import { Result as Result9 } from "typescript-result";
|
|
1993
2057
|
|
|
1994
2058
|
// src/ai/runner.ts
|
|
1995
2059
|
import { confirm as confirm5, input as input2, select as select2 } from "@inquirer/prompts";
|
|
1996
|
-
import { Result as
|
|
2060
|
+
import { Result as Result8 } from "typescript-result";
|
|
1997
2061
|
|
|
1998
2062
|
// src/ai/executor.ts
|
|
1999
2063
|
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
@@ -2095,13 +2159,13 @@ var RateLimitCoordinator = class {
|
|
|
2095
2159
|
import { execSync } from "child_process";
|
|
2096
2160
|
import { writeFile as writeFile4 } from "fs/promises";
|
|
2097
2161
|
import { join as join7 } from "path";
|
|
2098
|
-
import { Result as
|
|
2162
|
+
import { Result as Result7 } from "typescript-result";
|
|
2099
2163
|
|
|
2100
2164
|
// src/ai/permissions.ts
|
|
2101
2165
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
2102
2166
|
import { join as join6 } from "path";
|
|
2103
2167
|
import { homedir } from "os";
|
|
2104
|
-
import { Result as
|
|
2168
|
+
import { Result as Result6 } from "typescript-result";
|
|
2105
2169
|
function getProviderPermissions(projectPath, provider) {
|
|
2106
2170
|
const permissions = {
|
|
2107
2171
|
allow: [],
|
|
@@ -2112,7 +2176,7 @@ function getProviderPermissions(projectPath, provider) {
|
|
|
2112
2176
|
}
|
|
2113
2177
|
const projectSettingsPath = join6(projectPath, ".claude", "settings.local.json");
|
|
2114
2178
|
if (existsSync2(projectSettingsPath)) {
|
|
2115
|
-
const projectResult =
|
|
2179
|
+
const projectResult = Result6.try(() => {
|
|
2116
2180
|
const content = readFileSync2(projectSettingsPath, "utf-8");
|
|
2117
2181
|
return JSON.parse(content);
|
|
2118
2182
|
});
|
|
@@ -2128,7 +2192,7 @@ function getProviderPermissions(projectPath, provider) {
|
|
|
2128
2192
|
}
|
|
2129
2193
|
const userSettingsPath = join6(homedir(), ".claude", "settings.json");
|
|
2130
2194
|
if (existsSync2(userSettingsPath)) {
|
|
2131
|
-
const userResult =
|
|
2195
|
+
const userResult = Result6.try(() => {
|
|
2132
2196
|
const content = readFileSync2(userSettingsPath, "utf-8");
|
|
2133
2197
|
return JSON.parse(content);
|
|
2134
2198
|
});
|
|
@@ -2185,6 +2249,9 @@ function matchesPattern(pattern, tool, specifier) {
|
|
|
2185
2249
|
}
|
|
2186
2250
|
function checkTaskPermissions(projectPath, options) {
|
|
2187
2251
|
const warnings = [];
|
|
2252
|
+
if (options.provider === "copilot") {
|
|
2253
|
+
return warnings;
|
|
2254
|
+
}
|
|
2188
2255
|
const permissions = getProviderPermissions(projectPath, options.provider);
|
|
2189
2256
|
if (options.needsCommit !== false) {
|
|
2190
2257
|
const commitAllowed = isToolAllowed(permissions, "Bash", "git commit");
|
|
@@ -2211,7 +2278,7 @@ function checkTaskPermissions(projectPath, options) {
|
|
|
2211
2278
|
|
|
2212
2279
|
// src/ai/task-context.ts
|
|
2213
2280
|
function getRecentGitHistory(projectPath, count = 20) {
|
|
2214
|
-
const r =
|
|
2281
|
+
const r = Result7.try(() => {
|
|
2215
2282
|
assertSafeCwd(projectPath);
|
|
2216
2283
|
const result = execSync(`git log -${String(count)} --oneline --no-decorate`, {
|
|
2217
2284
|
cwd: projectPath,
|
|
@@ -2279,7 +2346,8 @@ function buildFullTaskContext(ctx, progressSummary, gitHistory, checkScript, che
|
|
|
2279
2346
|
lines.push("");
|
|
2280
2347
|
lines.push("Your task is NOT marked done unless this command passes after completion.");
|
|
2281
2348
|
} else {
|
|
2282
|
-
lines.push("No check script is configured.
|
|
2349
|
+
lines.push("No check script is configured. Check the project root for instruction files");
|
|
2350
|
+
lines.push("(CLAUDE.md, .github/copilot-instructions.md, README) to find verification commands.");
|
|
2283
2351
|
}
|
|
2284
2352
|
if (checkStatus) {
|
|
2285
2353
|
lines.push("");
|
|
@@ -2290,7 +2358,7 @@ function buildFullTaskContext(ctx, progressSummary, gitHistory, checkScript, che
|
|
|
2290
2358
|
lines.push("Do not re-run the install portion unless you encounter dependency errors.");
|
|
2291
2359
|
} else {
|
|
2292
2360
|
lines.push(
|
|
2293
|
-
"No check script is configured for this repository.
|
|
2361
|
+
"No check script is configured for this repository. Check project instruction files (CLAUDE.md, .github/copilot-instructions.md, README) or configuration files (package.json, pyproject.toml, etc.) to discover build, test, and lint commands."
|
|
2294
2362
|
);
|
|
2295
2363
|
}
|
|
2296
2364
|
}
|
|
@@ -2381,9 +2449,9 @@ function getHookTimeoutMs() {
|
|
|
2381
2449
|
}
|
|
2382
2450
|
return DEFAULT_HOOK_TIMEOUT_MS;
|
|
2383
2451
|
}
|
|
2384
|
-
function runLifecycleHook(projectPath, script, event) {
|
|
2452
|
+
function runLifecycleHook(projectPath, script, event, timeoutOverrideMs) {
|
|
2385
2453
|
assertSafeCwd(projectPath);
|
|
2386
|
-
const timeoutMs = getHookTimeoutMs();
|
|
2454
|
+
const timeoutMs = timeoutOverrideMs ?? getHookTimeoutMs();
|
|
2387
2455
|
const result = spawnSync2(script, {
|
|
2388
2456
|
cwd: projectPath,
|
|
2389
2457
|
shell: true,
|
|
@@ -2396,7 +2464,81 @@ function runLifecycleHook(projectPath, script, event) {
|
|
|
2396
2464
|
return { passed: result.status === 0, output };
|
|
2397
2465
|
}
|
|
2398
2466
|
|
|
2467
|
+
// src/ai/evaluator.ts
|
|
2468
|
+
function getEvaluatorModel(generatorModel, provider) {
|
|
2469
|
+
if (provider.name !== "claude" || !generatorModel) return null;
|
|
2470
|
+
const modelLower = generatorModel.toLowerCase();
|
|
2471
|
+
if (modelLower.includes("opus")) return "claude-sonnet-4-6";
|
|
2472
|
+
if (modelLower.includes("sonnet")) return "claude-haiku-4-5";
|
|
2473
|
+
return "claude-haiku-4-5";
|
|
2474
|
+
}
|
|
2475
|
+
function parseEvaluationResult(output) {
|
|
2476
|
+
if (output.includes("<evaluation-passed>")) {
|
|
2477
|
+
return { passed: true, output };
|
|
2478
|
+
}
|
|
2479
|
+
const failedMatch = /<evaluation-failed>([\s\S]*?)<\/evaluation-failed>/.exec(output);
|
|
2480
|
+
if (failedMatch) {
|
|
2481
|
+
return { passed: false, output: failedMatch[1]?.trim() ?? output };
|
|
2482
|
+
}
|
|
2483
|
+
return { passed: false, output };
|
|
2484
|
+
}
|
|
2485
|
+
function buildEvaluatorContext(task, checkScript) {
|
|
2486
|
+
const checkScriptSection = checkScript ? `## Check Script
|
|
2487
|
+
|
|
2488
|
+
You can run the following check script to verify the changes:
|
|
2489
|
+
|
|
2490
|
+
\`\`\`
|
|
2491
|
+
${checkScript}
|
|
2492
|
+
\`\`\`
|
|
2493
|
+
|
|
2494
|
+
Run it to gain additional insight into whether the implementation is correct.` : null;
|
|
2495
|
+
return {
|
|
2496
|
+
taskName: task.name,
|
|
2497
|
+
taskDescription: task.description ?? "",
|
|
2498
|
+
taskSteps: task.steps,
|
|
2499
|
+
projectPath: task.projectPath,
|
|
2500
|
+
checkScriptSection
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
async function runEvaluation(task, generatorModel, checkScript, sprintId, provider) {
|
|
2504
|
+
const p = provider ?? await getActiveProvider();
|
|
2505
|
+
const evaluatorModel = getEvaluatorModel(generatorModel, p);
|
|
2506
|
+
const sprintDir = getSprintDir(sprintId);
|
|
2507
|
+
const ctx = buildEvaluatorContext(task, checkScript);
|
|
2508
|
+
const prompt = buildEvaluatorPrompt(ctx);
|
|
2509
|
+
const providerArgs = ["--add-dir", sprintDir];
|
|
2510
|
+
if (evaluatorModel && p.name === "claude") {
|
|
2511
|
+
providerArgs.push("--model", evaluatorModel);
|
|
2512
|
+
}
|
|
2513
|
+
const result = await spawnWithRetry({
|
|
2514
|
+
cwd: task.projectPath,
|
|
2515
|
+
args: providerArgs,
|
|
2516
|
+
prompt,
|
|
2517
|
+
env: p.getSpawnEnv()
|
|
2518
|
+
});
|
|
2519
|
+
return parseEvaluationResult(result.stdout);
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2399
2522
|
// src/ai/executor.ts
|
|
2523
|
+
function buildProviderArgs(options, provider) {
|
|
2524
|
+
if (provider.name !== "claude") {
|
|
2525
|
+
if (options.maxBudgetUsd != null) {
|
|
2526
|
+
console.log(warning(`--max-budget-usd is only supported with the Claude provider \u2014 ignored`));
|
|
2527
|
+
}
|
|
2528
|
+
if (options.fallbackModel) {
|
|
2529
|
+
console.log(warning(`--fallback-model is only supported with the Claude provider \u2014 ignored`));
|
|
2530
|
+
}
|
|
2531
|
+
return [];
|
|
2532
|
+
}
|
|
2533
|
+
const args = [];
|
|
2534
|
+
if (options.maxBudgetUsd != null) {
|
|
2535
|
+
args.push("--max-budget-usd", String(options.maxBudgetUsd));
|
|
2536
|
+
}
|
|
2537
|
+
if (options.fallbackModel) {
|
|
2538
|
+
args.push("--fallback-model", options.fallbackModel);
|
|
2539
|
+
}
|
|
2540
|
+
return args;
|
|
2541
|
+
}
|
|
2400
2542
|
async function executeTask(ctx, options, sprintId, resumeSessionId, provider, checkStatus) {
|
|
2401
2543
|
const p = provider ?? await getActiveProvider();
|
|
2402
2544
|
const label = p.displayName;
|
|
@@ -2417,21 +2559,23 @@ async function executeTask(ctx, options, sprintId, resumeSessionId, provider, ch
|
|
|
2417
2559
|
`Read ${contextFileName} and follow the instructions`,
|
|
2418
2560
|
{
|
|
2419
2561
|
cwd: projectPath,
|
|
2420
|
-
args: ["--add-dir", sprintDir]
|
|
2562
|
+
args: ["--add-dir", sprintDir],
|
|
2563
|
+
env: p.getSpawnEnv()
|
|
2421
2564
|
},
|
|
2422
2565
|
p
|
|
2423
2566
|
);
|
|
2424
2567
|
if (result.error) {
|
|
2425
|
-
return { success: false, output: "", blockedReason: result.error, sessionId: null };
|
|
2568
|
+
return { success: false, output: "", blockedReason: result.error, sessionId: null, model: null };
|
|
2426
2569
|
}
|
|
2427
2570
|
if (result.code === 0) {
|
|
2428
|
-
return { success: true, output: "", verified: true, sessionId: null };
|
|
2571
|
+
return { success: true, output: "", verified: true, sessionId: null, model: null };
|
|
2429
2572
|
}
|
|
2430
2573
|
return {
|
|
2431
2574
|
success: false,
|
|
2432
2575
|
output: "",
|
|
2433
2576
|
blockedReason: `${label} exited with code ${String(result.code)}`,
|
|
2434
|
-
sessionId: null
|
|
2577
|
+
sessionId: null,
|
|
2578
|
+
model: null
|
|
2435
2579
|
};
|
|
2436
2580
|
} finally {
|
|
2437
2581
|
await unlink2(contextFile).catch(() => void 0);
|
|
@@ -2448,9 +2592,10 @@ async function executeTask(ctx, options, sprintId, resumeSessionId, provider, ch
|
|
|
2448
2592
|
spawnResult = await spawnWithRetry(
|
|
2449
2593
|
{
|
|
2450
2594
|
cwd: projectPath,
|
|
2451
|
-
args: ["--add-dir", sprintDir],
|
|
2595
|
+
args: ["--add-dir", sprintDir, ...buildProviderArgs(options, p)],
|
|
2452
2596
|
prompt: "Continue where you left off. Complete the task and signal completion.",
|
|
2453
|
-
resumeSessionId
|
|
2597
|
+
resumeSessionId,
|
|
2598
|
+
env: p.getSpawnEnv()
|
|
2454
2599
|
},
|
|
2455
2600
|
{
|
|
2456
2601
|
maxRetries: options.maxRetries,
|
|
@@ -2487,8 +2632,9 @@ async function executeTask(ctx, options, sprintId, resumeSessionId, provider, ch
|
|
|
2487
2632
|
spawnResult = await spawnWithRetry(
|
|
2488
2633
|
{
|
|
2489
2634
|
cwd: projectPath,
|
|
2490
|
-
args: ["--add-dir", sprintDir],
|
|
2491
|
-
prompt: contextContent
|
|
2635
|
+
args: ["--add-dir", sprintDir, ...buildProviderArgs(options, p)],
|
|
2636
|
+
prompt: contextContent,
|
|
2637
|
+
env: p.getSpawnEnv()
|
|
2492
2638
|
},
|
|
2493
2639
|
{
|
|
2494
2640
|
maxRetries: options.maxRetries,
|
|
@@ -2508,7 +2654,81 @@ async function executeTask(ctx, options, sprintId, resumeSessionId, provider, ch
|
|
|
2508
2654
|
}
|
|
2509
2655
|
}
|
|
2510
2656
|
const parsed = parseExecutionResult(spawnResult.stdout);
|
|
2511
|
-
return { ...parsed, sessionId: spawnResult.sessionId };
|
|
2657
|
+
return { ...parsed, sessionId: spawnResult.sessionId, model: spawnResult.model };
|
|
2658
|
+
}
|
|
2659
|
+
var MAX_EVAL_OUTPUT = 2e3;
|
|
2660
|
+
async function runEvaluationLoop(params) {
|
|
2661
|
+
const {
|
|
2662
|
+
task,
|
|
2663
|
+
result,
|
|
2664
|
+
project,
|
|
2665
|
+
sprintId,
|
|
2666
|
+
provider,
|
|
2667
|
+
options,
|
|
2668
|
+
evalIterations,
|
|
2669
|
+
checkTimeout,
|
|
2670
|
+
useSpinner = false
|
|
2671
|
+
} = params;
|
|
2672
|
+
const evalCheckScript = getEffectiveCheckScript(project, task.projectPath);
|
|
2673
|
+
const sprintDir = getSprintDir(sprintId);
|
|
2674
|
+
let evalResult = await runEvaluation(task, result.model, evalCheckScript, sprintId, provider);
|
|
2675
|
+
for (let i = 0; i < evalIterations && !evalResult.passed; i++) {
|
|
2676
|
+
console.log(warning(`Evaluation failed for ${task.name} (iteration ${String(i + 1)}/${String(evalIterations)})`));
|
|
2677
|
+
console.log(muted(evalResult.output.slice(0, 500)));
|
|
2678
|
+
const resumeSpinner = useSpinner ? createSpinner(`Fixing evaluation issues: ${task.name}`).start() : null;
|
|
2679
|
+
const resumeResult = await spawnWithRetry(
|
|
2680
|
+
{
|
|
2681
|
+
cwd: task.projectPath,
|
|
2682
|
+
args: ["--add-dir", sprintDir, ...buildProviderArgs(options, provider)],
|
|
2683
|
+
prompt: `The evaluator found issues with your work:
|
|
2684
|
+
|
|
2685
|
+
${evalResult.output}
|
|
2686
|
+
|
|
2687
|
+
Fix these issues, then verify${options.noCommit ? "" : ", commit your fix,"} and signal completion.`,
|
|
2688
|
+
resumeSessionId: result.sessionId ?? void 0,
|
|
2689
|
+
env: provider.getSpawnEnv()
|
|
2690
|
+
},
|
|
2691
|
+
{
|
|
2692
|
+
maxRetries: options.maxRetries,
|
|
2693
|
+
...resumeSpinner ? {
|
|
2694
|
+
onRetry: (attempt, delayMs) => {
|
|
2695
|
+
resumeSpinner.text = `Rate limited, retrying in ${String(Math.round(delayMs / 1e3))}s (attempt ${String(attempt)})...`;
|
|
2696
|
+
}
|
|
2697
|
+
} : {}
|
|
2698
|
+
},
|
|
2699
|
+
provider
|
|
2700
|
+
);
|
|
2701
|
+
resumeSpinner?.succeed(`Fix attempt completed: ${task.name}`);
|
|
2702
|
+
const fixResult = parseExecutionResult(resumeResult.stdout);
|
|
2703
|
+
if (!fixResult.success) {
|
|
2704
|
+
console.log(warning(`Generator could not fix issues after feedback: ${task.name}`));
|
|
2705
|
+
break;
|
|
2706
|
+
}
|
|
2707
|
+
const recheckScript = getEffectiveCheckScript(project, task.projectPath);
|
|
2708
|
+
if (recheckScript) {
|
|
2709
|
+
const recheckResult = runLifecycleHook(task.projectPath, recheckScript, "taskComplete", checkTimeout);
|
|
2710
|
+
if (!recheckResult.passed) {
|
|
2711
|
+
console.log(warning(`Post-task check failed after generator fix: ${task.name}`));
|
|
2712
|
+
break;
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
evalResult = await runEvaluation(task, resumeResult.model ?? result.model, evalCheckScript, sprintId, provider);
|
|
2716
|
+
}
|
|
2717
|
+
await updateTask(
|
|
2718
|
+
task.id,
|
|
2719
|
+
{
|
|
2720
|
+
evaluated: true,
|
|
2721
|
+
evaluationOutput: evalResult.output.slice(0, MAX_EVAL_OUTPUT)
|
|
2722
|
+
},
|
|
2723
|
+
sprintId
|
|
2724
|
+
);
|
|
2725
|
+
if (!evalResult.passed) {
|
|
2726
|
+
console.log(
|
|
2727
|
+
warning(`Evaluation did not pass after ${String(evalIterations)} iteration(s) \u2014 marking done: ${task.name}`)
|
|
2728
|
+
);
|
|
2729
|
+
} else {
|
|
2730
|
+
console.log(success(`Evaluation passed: ${task.name}`));
|
|
2731
|
+
}
|
|
2512
2732
|
}
|
|
2513
2733
|
async function areAllRemainingBlocked(sprintId) {
|
|
2514
2734
|
const remaining = await getRemainingTasks(sprintId);
|
|
@@ -2524,6 +2744,7 @@ async function executeTaskLoop(sprintId, options, checkResults) {
|
|
|
2524
2744
|
ProcessManager.getInstance().ensureHandlers();
|
|
2525
2745
|
const provider = await getActiveProvider();
|
|
2526
2746
|
const label = provider.displayName;
|
|
2747
|
+
const evalIterations = await getEvaluationIterations();
|
|
2527
2748
|
const sprint = await getSprint(sprintId);
|
|
2528
2749
|
let completedCount = 0;
|
|
2529
2750
|
const targetCount = options.count ?? Infinity;
|
|
@@ -2674,6 +2895,18 @@ Post-task check failed for: ${task.name}`));
|
|
|
2674
2895
|
}
|
|
2675
2896
|
console.log(success("Post-task check: passed"));
|
|
2676
2897
|
}
|
|
2898
|
+
if (evalIterations > 0 && !options.noEvaluate && !options.session) {
|
|
2899
|
+
await runEvaluationLoop({
|
|
2900
|
+
task,
|
|
2901
|
+
result,
|
|
2902
|
+
project,
|
|
2903
|
+
sprintId,
|
|
2904
|
+
provider,
|
|
2905
|
+
options,
|
|
2906
|
+
evalIterations,
|
|
2907
|
+
useSpinner: true
|
|
2908
|
+
});
|
|
2909
|
+
}
|
|
2677
2910
|
await updateTaskStatus(task.id, "done", sprintId);
|
|
2678
2911
|
console.log(success("Status updated to: done"));
|
|
2679
2912
|
await logProgress(
|
|
@@ -2720,8 +2953,10 @@ ${String(remaining2.length)} task(s) remaining.`));
|
|
|
2720
2953
|
exitCode: EXIT_SUCCESS
|
|
2721
2954
|
};
|
|
2722
2955
|
}
|
|
2723
|
-
function pickTasksToLaunch(readyTasks, inFlightPaths, concurrencyLimit, currentInFlight) {
|
|
2724
|
-
const available = readyTasks.filter(
|
|
2956
|
+
function pickTasksToLaunch(readyTasks, inFlightPaths, concurrencyLimit, currentInFlight, failedPaths) {
|
|
2957
|
+
const available = readyTasks.filter(
|
|
2958
|
+
(t) => !inFlightPaths.has(t.projectPath) && !(failedPaths?.has(t.projectPath) ?? false)
|
|
2959
|
+
);
|
|
2725
2960
|
const byPath = /* @__PURE__ */ new Map();
|
|
2726
2961
|
for (const task of available) {
|
|
2727
2962
|
if (!byPath.has(task.projectPath)) {
|
|
@@ -2736,6 +2971,7 @@ async function executeTaskLoopParallel(sprintId, options, checkResults) {
|
|
|
2736
2971
|
ProcessManager.getInstance().ensureHandlers();
|
|
2737
2972
|
const provider = await getActiveProvider();
|
|
2738
2973
|
const label = provider.displayName;
|
|
2974
|
+
const evalIterations = await getEvaluationIterations();
|
|
2739
2975
|
const sprint = await getSprint(sprintId);
|
|
2740
2976
|
let completedCount = 0;
|
|
2741
2977
|
const targetCount = options.count ?? Infinity;
|
|
@@ -2761,6 +2997,7 @@ Rate limited. Pausing new launches for ${String(Math.round(delayMs / 1e3))}s...`
|
|
|
2761
2997
|
const running = /* @__PURE__ */ new Map();
|
|
2762
2998
|
const taskSessionIds = /* @__PURE__ */ new Map();
|
|
2763
2999
|
const branchRetries = /* @__PURE__ */ new Map();
|
|
3000
|
+
const failedPaths = /* @__PURE__ */ new Set();
|
|
2764
3001
|
const MAX_BRANCH_RETRIES = 3;
|
|
2765
3002
|
let permissionCheckDone = false;
|
|
2766
3003
|
try {
|
|
@@ -2805,17 +3042,22 @@ Resuming ${String(inProgressTasks.length)} in-progress task(s):`));
|
|
|
2805
3042
|
exitCode: EXIT_SUCCESS
|
|
2806
3043
|
};
|
|
2807
3044
|
}
|
|
3045
|
+
const hasFailures = hasFailed || failedPaths.size > 0;
|
|
3046
|
+
if (failedPaths.size > 0) {
|
|
3047
|
+
console.log(warning(`
|
|
3048
|
+
Repos with failed checks: ${[...failedPaths].join(", ")}`));
|
|
3049
|
+
}
|
|
2808
3050
|
return {
|
|
2809
3051
|
completed: completedCount,
|
|
2810
3052
|
remaining: remaining.length,
|
|
2811
|
-
stopReason:
|
|
3053
|
+
stopReason: hasFailures ? "task_blocked" : "all_blocked",
|
|
2812
3054
|
blockedTask: firstBlockedTask,
|
|
2813
3055
|
blockedReason: firstBlockedReason ?? "All remaining tasks are blocked by dependencies",
|
|
2814
|
-
exitCode:
|
|
3056
|
+
exitCode: hasFailures ? EXIT_ERROR : EXIT_ALL_BLOCKED
|
|
2815
3057
|
};
|
|
2816
3058
|
}
|
|
2817
3059
|
if (!hasFailed || !failFast) {
|
|
2818
|
-
const toStart = pickTasksToLaunch(launchCandidates, inFlightPaths, concurrencyLimit, running.size);
|
|
3060
|
+
const toStart = pickTasksToLaunch(launchCandidates, inFlightPaths, concurrencyLimit, running.size, failedPaths);
|
|
2819
3061
|
for (const task of toStart) {
|
|
2820
3062
|
if (completedCount + running.size >= targetCount) break;
|
|
2821
3063
|
const project = await getProjectForTask(task, sprint);
|
|
@@ -2965,23 +3207,39 @@ Task not completed: ${settled.task.name}`));
|
|
|
2965
3207
|
const taskProject = await getProjectForTask(settled.task, sprint);
|
|
2966
3208
|
const taskCheckScript = getEffectiveCheckScript(taskProject, settled.task.projectPath);
|
|
2967
3209
|
if (taskCheckScript) {
|
|
2968
|
-
const
|
|
3210
|
+
const taskRepo = taskProject?.repositories.find((r) => r.path === settled.task.projectPath);
|
|
3211
|
+
const hookResult = runLifecycleHook(
|
|
3212
|
+
settled.task.projectPath,
|
|
3213
|
+
taskCheckScript,
|
|
3214
|
+
"taskComplete",
|
|
3215
|
+
taskRepo?.checkTimeout
|
|
3216
|
+
);
|
|
2969
3217
|
if (!hookResult.passed) {
|
|
2970
3218
|
console.log(warning(`
|
|
2971
3219
|
Post-task check failed for: ${settled.task.name}`));
|
|
2972
|
-
console.log(muted(`Task ${settled.task.id} remains in_progress.`));
|
|
2973
|
-
|
|
3220
|
+
console.log(muted(`Task ${settled.task.id} remains in_progress. Repo ${settled.task.projectPath} paused.`));
|
|
3221
|
+
failedPaths.add(settled.task.projectPath);
|
|
2974
3222
|
if (!firstBlockedTask) {
|
|
2975
3223
|
firstBlockedTask = settled.task;
|
|
2976
3224
|
firstBlockedReason = `Post-task check failed: ${hookResult.output.slice(0, 500)}`;
|
|
2977
3225
|
}
|
|
2978
|
-
if (failFast) {
|
|
2979
|
-
console.log(muted("Fail-fast: waiting for running tasks to finish..."));
|
|
2980
|
-
}
|
|
2981
3226
|
continue;
|
|
2982
3227
|
}
|
|
2983
3228
|
console.log(success(`Post-task check passed: ${settled.task.name}`));
|
|
2984
3229
|
}
|
|
3230
|
+
if (evalIterations > 0 && !options.noEvaluate && !options.session) {
|
|
3231
|
+
const taskRepo = taskProject?.repositories.find((r) => r.path === settled.task.projectPath);
|
|
3232
|
+
await runEvaluationLoop({
|
|
3233
|
+
task: settled.task,
|
|
3234
|
+
result: settled.result,
|
|
3235
|
+
project: taskProject,
|
|
3236
|
+
sprintId,
|
|
3237
|
+
provider,
|
|
3238
|
+
options,
|
|
3239
|
+
evalIterations,
|
|
3240
|
+
checkTimeout: taskRepo?.checkTimeout
|
|
3241
|
+
});
|
|
3242
|
+
}
|
|
2985
3243
|
await updateTaskStatus(settled.task.id, "done", sprintId);
|
|
2986
3244
|
console.log(success(`Completed: ${settled.task.name}`));
|
|
2987
3245
|
taskSessionIds.delete(settled.task.id);
|
|
@@ -3012,7 +3270,13 @@ Waiting for ${String(running.size)} remaining task(s)...`));
|
|
|
3012
3270
|
const drainProject = await getProjectForTask(r.value.task, sprint);
|
|
3013
3271
|
const drainCheckScript = getEffectiveCheckScript(drainProject, r.value.task.projectPath);
|
|
3014
3272
|
if (drainCheckScript) {
|
|
3015
|
-
const
|
|
3273
|
+
const drainRepo = drainProject?.repositories.find((repo) => repo.path === r.value.task.projectPath);
|
|
3274
|
+
const hookResult = runLifecycleHook(
|
|
3275
|
+
r.value.task.projectPath,
|
|
3276
|
+
drainCheckScript,
|
|
3277
|
+
"taskComplete",
|
|
3278
|
+
drainRepo?.checkTimeout
|
|
3279
|
+
);
|
|
3016
3280
|
if (!hookResult.passed) {
|
|
3017
3281
|
console.log(warning(`Post-task check failed for: ${r.value.task.name}`));
|
|
3018
3282
|
continue;
|
|
@@ -3220,7 +3484,7 @@ async function ensureSprintBranches(sprintId, sprint, branchName) {
|
|
|
3220
3484
|
const uniquePaths = [...new Set(remainingTasks.map((t) => t.projectPath))];
|
|
3221
3485
|
if (uniquePaths.length === 0) return;
|
|
3222
3486
|
for (const projectPath of uniquePaths) {
|
|
3223
|
-
const uncommittedR =
|
|
3487
|
+
const uncommittedR = Result8.try(() => hasUncommittedChanges(projectPath));
|
|
3224
3488
|
if (!uncommittedR.ok) {
|
|
3225
3489
|
log.dim(` Skipping ${projectPath} \u2014 not a git repository`);
|
|
3226
3490
|
continue;
|
|
@@ -3232,7 +3496,7 @@ async function ensureSprintBranches(sprintId, sprint, branchName) {
|
|
|
3232
3496
|
}
|
|
3233
3497
|
}
|
|
3234
3498
|
for (const projectPath of uniquePaths) {
|
|
3235
|
-
const branchR =
|
|
3499
|
+
const branchR = Result8.try(() => {
|
|
3236
3500
|
const currentBranch = getCurrentBranch(projectPath);
|
|
3237
3501
|
if (currentBranch === branchName) {
|
|
3238
3502
|
log.dim(` Already on branch '${branchName}' in ${projectPath}`);
|
|
@@ -3253,7 +3517,7 @@ async function ensureSprintBranches(sprintId, sprint, branchName) {
|
|
|
3253
3517
|
}
|
|
3254
3518
|
}
|
|
3255
3519
|
function verifySprintBranch(projectPath, expectedBranch) {
|
|
3256
|
-
const r =
|
|
3520
|
+
const r = Result8.try(() => {
|
|
3257
3521
|
if (verifyCurrentBranch(projectPath, expectedBranch)) return true;
|
|
3258
3522
|
log.dim(` Branch mismatch in ${projectPath} \u2014 checking out '${expectedBranch}'`);
|
|
3259
3523
|
createAndCheckoutBranch(projectPath, expectedBranch);
|
|
@@ -3289,7 +3553,7 @@ async function runCheckScripts(sprintId, sprint, refreshCheck = false) {
|
|
|
3289
3553
|
}
|
|
3290
3554
|
log.info(`
|
|
3291
3555
|
Running check for ${repoName}: ${checkScript}`);
|
|
3292
|
-
const hookResult = runLifecycleHook(projectPath, checkScript, "sprintStart");
|
|
3556
|
+
const hookResult = runLifecycleHook(projectPath, checkScript, "sprintStart", repo?.checkTimeout);
|
|
3293
3557
|
if (!hookResult.passed) {
|
|
3294
3558
|
return {
|
|
3295
3559
|
success: false,
|
|
@@ -3514,6 +3778,27 @@ function parseArgs3(args) {
|
|
|
3514
3778
|
throw new Error("--branch-name requires a value");
|
|
3515
3779
|
}
|
|
3516
3780
|
options.branchName = nameStr;
|
|
3781
|
+
} else if (arg === "--max-budget-usd") {
|
|
3782
|
+
const budgetStr = args[++i];
|
|
3783
|
+
if (!budgetStr) {
|
|
3784
|
+
throw new Error("--max-budget-usd requires a number");
|
|
3785
|
+
}
|
|
3786
|
+
const budget = parseFloat(budgetStr);
|
|
3787
|
+
if (isNaN(budget) || budget <= 0) {
|
|
3788
|
+
throw new Error("--max-budget-usd must be a positive number");
|
|
3789
|
+
}
|
|
3790
|
+
options.maxBudgetUsd = budget;
|
|
3791
|
+
} else if (arg === "--fallback-model") {
|
|
3792
|
+
const modelStr = args[++i];
|
|
3793
|
+
if (!modelStr) {
|
|
3794
|
+
throw new Error("--fallback-model requires a model name");
|
|
3795
|
+
}
|
|
3796
|
+
if (!/^[a-zA-Z0-9._-]{1,100}$/.test(modelStr)) {
|
|
3797
|
+
throw new Error("Invalid model name \u2014 must be 1-100 alphanumeric characters, dots, hyphens, or underscores");
|
|
3798
|
+
}
|
|
3799
|
+
options.fallbackModel = modelStr;
|
|
3800
|
+
} else if (arg === "--no-evaluate") {
|
|
3801
|
+
options.noEvaluate = true;
|
|
3517
3802
|
} else if (!arg?.startsWith("-")) {
|
|
3518
3803
|
sprintId = arg;
|
|
3519
3804
|
}
|
|
@@ -3521,7 +3806,7 @@ function parseArgs3(args) {
|
|
|
3521
3806
|
return { sprintId, options };
|
|
3522
3807
|
}
|
|
3523
3808
|
async function sprintStartCommand(args) {
|
|
3524
|
-
const parseR =
|
|
3809
|
+
const parseR = Result9.try(() => parseArgs3(args));
|
|
3525
3810
|
if (!parseR.ok) {
|
|
3526
3811
|
showError(parseR.error.message);
|
|
3527
3812
|
log.newline();
|
|
@@ -3566,6 +3851,7 @@ export {
|
|
|
3566
3851
|
reorderTask,
|
|
3567
3852
|
listTasks,
|
|
3568
3853
|
areAllTasksDone,
|
|
3854
|
+
reorderByDependencies,
|
|
3569
3855
|
validateImportTasks,
|
|
3570
3856
|
selectProject,
|
|
3571
3857
|
selectProjectRepository,
|
|
@@ -3584,6 +3870,7 @@ export {
|
|
|
3584
3870
|
getActiveProvider,
|
|
3585
3871
|
spawnInteractive,
|
|
3586
3872
|
spawnHeadless,
|
|
3873
|
+
extractJsonArray,
|
|
3587
3874
|
extractJsonObject,
|
|
3588
3875
|
formatTicketForPrompt,
|
|
3589
3876
|
parseRequirementsFile,
|
|
@@ -3591,6 +3878,7 @@ export {
|
|
|
3591
3878
|
sprintRefineCommand,
|
|
3592
3879
|
getTaskImportSchema,
|
|
3593
3880
|
parsePlanningBlocked,
|
|
3881
|
+
buildHeadlessAiRequest,
|
|
3594
3882
|
parseTasksJson,
|
|
3595
3883
|
renderParsedTasksTable,
|
|
3596
3884
|
importTasks,
|