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.
Files changed (28) hide show
  1. package/README.md +23 -14
  2. package/dist/{add-7LBVENXM.mjs → add-SEDQ3VK7.mjs} +4 -4
  3. package/dist/{add-DVEYDCTR.mjs → add-TGJTRHIF.mjs} +3 -3
  4. package/dist/{chunk-M7JV6MKD.mjs → chunk-AXNZMHFQ.mjs} +384 -96
  5. package/dist/{chunk-LFDW6MWF.mjs → chunk-KPTPKLXY.mjs} +16 -3
  6. package/dist/{chunk-PDI6HBZ7.mjs → chunk-LG6B7QVO.mjs} +1 -1
  7. package/dist/{chunk-YIB7QYU4.mjs → chunk-Q3VWJARJ.mjs} +2 -2
  8. package/dist/{chunk-F2MMCTB5.mjs → chunk-XPDI4SYI.mjs} +5 -4
  9. package/dist/{chunk-DZ6HHTM5.mjs → chunk-XQHEKKDN.mjs} +1 -1
  10. package/dist/{chunk-W3TY22IS.mjs → chunk-ZDEVRTGY.mjs} +10 -3
  11. package/dist/cli.mjs +174 -65
  12. package/dist/{create-MQ4OHZAX.mjs → create-DJHCP7LN.mjs} +3 -3
  13. package/dist/{handle-K2AZLTKU.mjs → handle-CCTBNAJZ.mjs} +1 -1
  14. package/dist/{project-Q4LKML42.mjs → project-ZYGNPVGL.mjs} +2 -2
  15. package/dist/prompts/ideate-auto.md +3 -2
  16. package/dist/prompts/ideate.md +2 -2
  17. package/dist/prompts/plan-auto.md +11 -8
  18. package/dist/prompts/plan-common.md +13 -8
  19. package/dist/prompts/plan-interactive.md +11 -10
  20. package/dist/prompts/task-evaluation.md +54 -0
  21. package/dist/prompts/task-execution.md +7 -5
  22. package/dist/{resolver-NH34HTB6.mjs → resolver-L52KR4GY.mjs} +2 -2
  23. package/dist/{sprint-UHYXSEBJ.mjs → sprint-LUXAV3Q3.mjs} +2 -2
  24. package/dist/{wizard-MCDDXLGE.mjs → wizard-TFJXEYD2.mjs} +6 -6
  25. package/package.json +17 -14
  26. package/schemas/config.schema.json +10 -0
  27. package/schemas/projects.schema.json +5 -0
  28. package/schemas/tasks.schema.json +9 -0
@@ -11,7 +11,7 @@ import {
11
11
  getPendingRequirements,
12
12
  groupTicketsByProject,
13
13
  listTickets
14
- } from "./chunk-F2MMCTB5.mjs";
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-PDI6HBZ7.mjs";
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-LFDW6MWF.mjs";
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-W3TY22IS.mjs";
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 Result3 } from "typescript-result";
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.replace("{{PROGRESS_FILE}}", progressFilePath).replace("{{COMMIT_STEP}}", commitStep).replace("{{COMMIT_CONSTRAINT}}", commitConstraint).replaceAll("{{CONTEXT_FILE}}", contextFileName);
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 Result2 } from "typescript-result";
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
- console.warn(`Warning: Permission denied killing process ${String(child.pid)}`);
350
+ log.warn(`Permission denied killing process ${String(child.pid)}`);
335
351
  } else {
336
- console.error(`Error killing process ${String(child.pid)}:`, error2.message);
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
- console.log("\n\nForce quit (double signal) \u2014 killing all processes immediately...");
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
- console.log("\n\nShutting down gracefully... (press Ctrl+C again to force-quit)");
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
- console.error("Error in cleanup callback:", err instanceof Error ? err.message : String(err));
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
- console.log(`Force-killing ${String(this.children.size)} remaining process(es)...`);
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
- process.on("SIGINT", () => {
429
+ this.sigintHandler = () => {
405
430
  void this.shutdown("SIGINT");
406
- });
407
- process.on("SIGTERM", () => {
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", "-s", "--autopilot", "--no-ask-user", "--share", ...this.baseArgs, ...extraArgs];
501
+ return ["-p", "--output-format", "json", "--autopilot", "--no-ask-user", "--share", ...this.baseArgs, ...extraArgs];
467
502
  },
468
503
  parseJsonOutput(stdout) {
469
- return { result: stdout.trim(), sessionId: null };
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 p = provider ?? {
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(p.binary, args, {
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 ${p.binary} CLI: ${result.error.message}` };
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
- if (!/^[a-zA-Z0-9_][a-zA-Z0-9_-]{0,127}$/.test(options.resumeSessionId)) {
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 += data.toString();
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 = Result2.try(() => JSON.parse(jsonStr));
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 = Result3.try(() => fetchIssueFromUrl(link));
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 = Result3.try(() => parseRequirementsFile(contentR.value));
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 Result4 } from "typescript-result";
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-DVEYDCTR.mjs");
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-MQ4OHZAX.mjs");
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-7LBVENXM.mjs");
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", ...repoPaths];
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 args = ["--permission-mode", "plan", "--print"];
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 = Result4.try(() => parseTasksJson(output));
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 = Result4.try(() => parseTasksJson(contentR.value));
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 Result8 } from "typescript-result";
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 Result7 } from "typescript-result";
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 Result6 } from "typescript-result";
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 Result5 } from "typescript-result";
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 = Result5.try(() => {
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 = Result5.try(() => {
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 = Result6.try(() => {
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. Read CLAUDE.md in the project root to find verification commands.");
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. Read CLAUDE.md or project configuration files (package.json, pyproject.toml, etc.) to discover build, test, and lint commands."
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((t) => !inFlightPaths.has(t.projectPath));
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: hasFailed ? "task_blocked" : "all_blocked",
3053
+ stopReason: hasFailures ? "task_blocked" : "all_blocked",
2812
3054
  blockedTask: firstBlockedTask,
2813
3055
  blockedReason: firstBlockedReason ?? "All remaining tasks are blocked by dependencies",
2814
- exitCode: hasFailed ? EXIT_ERROR : EXIT_ALL_BLOCKED
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 hookResult = runLifecycleHook(settled.task.projectPath, taskCheckScript, "taskComplete");
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
- hasFailed = true;
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 hookResult = runLifecycleHook(r.value.task.projectPath, drainCheckScript, "taskComplete");
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 = Result7.try(() => hasUncommittedChanges(projectPath));
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 = Result7.try(() => {
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 = Result7.try(() => {
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 = Result8.try(() => parseArgs3(args));
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,