ralphctl 0.2.2 → 0.2.3

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 +3 -3
  2. package/dist/{add-SEDQ3VK7.mjs → add-DWNLZQ7Q.mjs} +4 -4
  3. package/dist/{add-TGJTRHIF.mjs → add-K7LNOYQ4.mjs} +3 -3
  4. package/dist/{chunk-LG6B7QVO.mjs → chunk-7TBO6GOT.mjs} +1 -1
  5. package/dist/{chunk-ZDEVRTGY.mjs → chunk-GLDPHKEW.mjs} +9 -0
  6. package/dist/{chunk-KPTPKLXY.mjs → chunk-ITRZMBLJ.mjs} +1 -1
  7. package/dist/{chunk-Q3VWJARJ.mjs → chunk-LAERLCL5.mjs} +2 -2
  8. package/dist/{chunk-XXIHDQOH.mjs → chunk-ORVGM6EV.mjs} +74 -16
  9. package/dist/{chunk-XPDI4SYI.mjs → chunk-QYF7QIZJ.mjs} +3 -3
  10. package/dist/{chunk-XQHEKKDN.mjs → chunk-V4ZUDZCG.mjs} +1 -1
  11. package/dist/cli.mjs +105 -16
  12. package/dist/{create-DJHCP7LN.mjs → create-5MILNF7E.mjs} +3 -3
  13. package/dist/{handle-CCTBNAJZ.mjs → handle-2BACSJLR.mjs} +1 -1
  14. package/dist/{project-ZYGNPVGL.mjs → project-XC7AXA4B.mjs} +2 -2
  15. package/dist/prompts/ideate-auto.md +9 -5
  16. package/dist/prompts/ideate.md +28 -12
  17. package/dist/prompts/plan-auto.md +26 -16
  18. package/dist/prompts/plan-common.md +67 -22
  19. package/dist/prompts/plan-interactive.md +26 -27
  20. package/dist/prompts/task-evaluation.md +144 -24
  21. package/dist/prompts/task-execution.md +58 -36
  22. package/dist/prompts/ticket-refine.md +24 -20
  23. package/dist/{resolver-L52KR4GY.mjs → resolver-CFY6DIOP.mjs} +2 -2
  24. package/dist/{sprint-LUXAV3Q3.mjs → sprint-F4VRAEWZ.mjs} +2 -2
  25. package/dist/{wizard-D7N5WZ5H.mjs → wizard-RCQ4QQOL.mjs} +6 -6
  26. package/package.json +6 -6
  27. package/schemas/task-import.schema.json +7 -0
  28. package/schemas/tasks.schema.json +8 -0
package/README.md CHANGED
@@ -206,9 +206,9 @@ Run `ralphctl <command> --help` for details on any command.
206
206
  | [Contributing](./CONTRIBUTING.md) | Dev setup, code style, PR process |
207
207
  | [Changelog](./CHANGELOG.md) | Version history |
208
208
 
209
- **Blog posts:** [Building ralphctl](https://lukasgrigis.dev/blog/building-ralphctl) (
210
- backstory) | [From task CLI to agent harness](https://lukasgrigis.dev/blog/ralphctl-agent-harness/) (evaluator
211
- deep-dive)
209
+ **Blog posts:** [Building ralphctl](https://lukasgrigis.dev/blog/building-ralphctl) (backstory) | [From task CLI to agent harness](https://lukasgrigis.dev/blog/ralphctl-agent-harness/) (evaluator deep-dive)
210
+
211
+ **Further reading:** [Harness Engineering for Coding Agent Users](https://martinfowler.com/articles/harness-engineering.html) — Martin Fowler (April 2026) | [Harness Design for Long-Running Application Development](https://www.anthropic.com/engineering/harness-design-long-running-apps) — Anthropic Engineering
212
212
 
213
213
  ---
214
214
 
@@ -2,12 +2,12 @@
2
2
  import {
3
3
  addSingleTicketInteractive,
4
4
  ticketAddCommand
5
- } from "./chunk-XPDI4SYI.mjs";
5
+ } from "./chunk-QYF7QIZJ.mjs";
6
6
  import "./chunk-7TG3EAQ2.mjs";
7
- import "./chunk-LG6B7QVO.mjs";
8
- import "./chunk-KPTPKLXY.mjs";
7
+ import "./chunk-7TBO6GOT.mjs";
8
+ import "./chunk-ITRZMBLJ.mjs";
9
9
  import "./chunk-OEUJDSHY.mjs";
10
- import "./chunk-ZDEVRTGY.mjs";
10
+ import "./chunk-GLDPHKEW.mjs";
11
11
  import "./chunk-EDJX7TT6.mjs";
12
12
  import "./chunk-QBXHAXHI.mjs";
13
13
  export {
@@ -2,12 +2,12 @@
2
2
  import {
3
3
  addCheckScriptToRepository,
4
4
  projectAddCommand
5
- } from "./chunk-Q3VWJARJ.mjs";
5
+ } from "./chunk-LAERLCL5.mjs";
6
6
  import "./chunk-7LZ6GOGN.mjs";
7
7
  import "./chunk-7TG3EAQ2.mjs";
8
- import "./chunk-LG6B7QVO.mjs";
8
+ import "./chunk-7TBO6GOT.mjs";
9
9
  import "./chunk-OEUJDSHY.mjs";
10
- import "./chunk-ZDEVRTGY.mjs";
10
+ import "./chunk-GLDPHKEW.mjs";
11
11
  import "./chunk-EDJX7TT6.mjs";
12
12
  import "./chunk-QBXHAXHI.mjs";
13
13
  export {
@@ -7,7 +7,7 @@ import {
7
7
  readValidatedJson,
8
8
  validateProjectPath,
9
9
  writeValidatedJson
10
- } from "./chunk-ZDEVRTGY.mjs";
10
+ } from "./chunk-GLDPHKEW.mjs";
11
11
  import {
12
12
  ParseError,
13
13
  ProjectExistsError,
@@ -53,13 +53,20 @@ function getTasksFilePath(sprintId) {
53
53
  function getProgressFilePath(sprintId) {
54
54
  return join(getSprintDir(sprintId), "progress.md");
55
55
  }
56
+ function assertSafeSegment(segment, label) {
57
+ if (!segment || segment.includes("/") || segment.includes("\\") || segment.includes("..") || segment.includes("\0")) {
58
+ throw new Error(`Path traversal detected in ${label}: ${segment}`);
59
+ }
60
+ }
56
61
  function getRefinementDir(sprintId, ticketId) {
62
+ assertSafeSegment(ticketId, "ticket ID");
57
63
  return join(getSprintDir(sprintId), "refinement", ticketId);
58
64
  }
59
65
  function getPlanningDir(sprintId) {
60
66
  return join(getSprintDir(sprintId), "planning");
61
67
  }
62
68
  function getIdeateDir(sprintId, ticketId) {
69
+ assertSafeSegment(ticketId, "ticket ID");
63
70
  return join(getSprintDir(sprintId), "ideation", ticketId);
64
71
  }
65
72
  function getSchemaPath(schemaName) {
@@ -233,6 +240,7 @@ var TaskSchema = z.object({
233
240
  name: z.string().min(1),
234
241
  description: z.string().optional(),
235
242
  steps: z.array(z.string()).default([]),
243
+ verificationCriteria: z.array(z.string()).default([]),
236
244
  status: TaskStatusSchema.default("todo"),
237
245
  order: z.number().int().positive(),
238
246
  ticketId: z.string().optional(),
@@ -257,6 +265,7 @@ var ImportTaskSchema = z.object({
257
265
  // Required
258
266
  description: z.string().optional(),
259
267
  steps: z.array(z.string()).optional(),
268
+ verificationCriteria: z.array(z.string()).optional(),
260
269
  ticketId: z.string().optional(),
261
270
  blockedBy: z.array(z.string()).optional(),
262
271
  projectPath: z.string().min(1)
@@ -21,7 +21,7 @@ import {
21
21
  readValidatedJson,
22
22
  removeDir,
23
23
  writeValidatedJson
24
- } from "./chunk-ZDEVRTGY.mjs";
24
+ } from "./chunk-GLDPHKEW.mjs";
25
25
  import {
26
26
  LockError,
27
27
  NoCurrentSprintError,
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-7TG3EAQ2.mjs";
9
9
  import {
10
10
  createProject
11
- } from "./chunk-LG6B7QVO.mjs";
11
+ } from "./chunk-7TBO6GOT.mjs";
12
12
  import {
13
13
  ensureError,
14
14
  wrapAsync
@@ -16,7 +16,7 @@ import {
16
16
  import {
17
17
  expandTilde,
18
18
  validateProjectPath
19
- } from "./chunk-ZDEVRTGY.mjs";
19
+ } from "./chunk-GLDPHKEW.mjs";
20
20
  import {
21
21
  IOError,
22
22
  ProjectExistsError
@@ -11,7 +11,7 @@ import {
11
11
  getPendingRequirements,
12
12
  groupTicketsByProject,
13
13
  listTickets
14
- } from "./chunk-XPDI4SYI.mjs";
14
+ } from "./chunk-QYF7QIZJ.mjs";
15
15
  import {
16
16
  EXIT_ALL_BLOCKED,
17
17
  EXIT_ERROR,
@@ -23,7 +23,7 @@ import {
23
23
  import {
24
24
  getProject,
25
25
  listProjects
26
- } from "./chunk-LG6B7QVO.mjs";
26
+ } from "./chunk-7TBO6GOT.mjs";
27
27
  import {
28
28
  activateSprint,
29
29
  assertSprintStatus,
@@ -40,7 +40,7 @@ import {
40
40
  setAiProvider,
41
41
  summarizeProgressForContext,
42
42
  withFileLock
43
- } from "./chunk-KPTPKLXY.mjs";
43
+ } from "./chunk-ITRZMBLJ.mjs";
44
44
  import {
45
45
  ensureError,
46
46
  unwrapOrThrow,
@@ -61,7 +61,7 @@ import {
61
61
  getTasksFilePath,
62
62
  readValidatedJson,
63
63
  writeValidatedJson
64
- } from "./chunk-ZDEVRTGY.mjs";
64
+ } from "./chunk-GLDPHKEW.mjs";
65
65
  import {
66
66
  DependencyCycleError,
67
67
  IOError,
@@ -162,10 +162,13 @@ function buildEvaluatorPrompt(ctx) {
162
162
  const stepsSection = ctx.taskSteps.length > 0 ? `
163
163
  **Implementation Steps:**
164
164
  ${ctx.taskSteps.map((s) => `- ${s}`).join("\n")}` : "";
165
+ const criteriaSection = ctx.verificationCriteria.length > 0 ? `
166
+ **Verification Criteria:**
167
+ ${ctx.verificationCriteria.map((c) => `- ${c}`).join("\n")}` : "";
165
168
  const checkSection = ctx.checkScriptSection ? `
166
169
 
167
170
  ${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);
171
+ return template.replaceAll("{{TASK_NAME}}", ctx.taskName).replace("{{TASK_DESCRIPTION_SECTION}}", descriptionSection).replace("{{TASK_STEPS_SECTION}}", stepsSection).replace("{{VERIFICATION_CRITERIA_SECTION}}", criteriaSection).replace("{{PROJECT_PATH}}", ctx.projectPath).replace("{{CHECK_SCRIPT_SECTION}}", checkSection);
169
172
  }
170
173
 
171
174
  // src/utils/requirements-export.ts
@@ -1087,6 +1090,7 @@ async function addTask(input3, sprintId) {
1087
1090
  name: input3.name,
1088
1091
  description: input3.description,
1089
1092
  steps: input3.steps ?? [],
1093
+ verificationCriteria: input3.verificationCriteria ?? [],
1090
1094
  status: "todo",
1091
1095
  order: maxOrder + 1,
1092
1096
  ticketId: input3.ticketId,
@@ -1320,6 +1324,7 @@ function validateImportTasks(importTasks2, existingTasks, ticketIds) {
1320
1324
  name: t.name,
1321
1325
  description: void 0,
1322
1326
  steps: [],
1327
+ verificationCriteria: [],
1323
1328
  status: "todo",
1324
1329
  order: existingTasks.length + i + 1,
1325
1330
  ticketId: void 0,
@@ -1355,7 +1360,7 @@ async function selectProject(message = "Select project:") {
1355
1360
  default: true
1356
1361
  });
1357
1362
  if (create) {
1358
- const { projectAddCommand } = await import("./add-TGJTRHIF.mjs");
1363
+ const { projectAddCommand } = await import("./add-K7LNOYQ4.mjs");
1359
1364
  await projectAddCommand({ interactive: true });
1360
1365
  const updated = await listProjects();
1361
1366
  if (updated.length === 0) return null;
@@ -1428,7 +1433,7 @@ async function selectSprint(message = "Select sprint:", filter) {
1428
1433
  default: true
1429
1434
  });
1430
1435
  if (create) {
1431
- const { sprintCreateCommand } = await import("./create-DJHCP7LN.mjs");
1436
+ const { sprintCreateCommand } = await import("./create-5MILNF7E.mjs");
1432
1437
  await sprintCreateCommand({ interactive: true });
1433
1438
  const updated = await listSprints();
1434
1439
  const refiltered = filter ? updated.filter((s) => filter.includes(s.status)) : updated;
@@ -1463,7 +1468,7 @@ async function selectTicket(message = "Select ticket:", filter) {
1463
1468
  default: true
1464
1469
  });
1465
1470
  if (create) {
1466
- const { ticketAddCommand } = await import("./add-SEDQ3VK7.mjs");
1471
+ const { ticketAddCommand } = await import("./add-DWNLZQ7Q.mjs");
1467
1472
  await ticketAddCommand({ interactive: true });
1468
1473
  const updated = await listTickets();
1469
1474
  const refiltered = filter ? updated.filter(filter) : updated;
@@ -1658,6 +1663,7 @@ async function importTasksReplace(tasks, sprintId) {
1658
1663
  name: taskInput.name,
1659
1664
  description: taskInput.description,
1660
1665
  steps: taskInput.steps ?? [],
1666
+ verificationCriteria: taskInput.verificationCriteria ?? [],
1661
1667
  status: "todo",
1662
1668
  order: newTasks.length + 1,
1663
1669
  ticketId: taskInput.ticketId,
@@ -2321,6 +2327,16 @@ function formatTask(ctx) {
2321
2327
  lines.push(`${String(i + 1)}. ${step}`);
2322
2328
  });
2323
2329
  }
2330
+ if (ctx.task.verificationCriteria.length > 0) {
2331
+ lines.push("");
2332
+ lines.push("## Verification Criteria");
2333
+ lines.push("");
2334
+ lines.push("The task is done when all of the following are true:");
2335
+ lines.push("");
2336
+ ctx.task.verificationCriteria.forEach((criterion) => {
2337
+ lines.push(`- ${criterion}`);
2338
+ });
2339
+ }
2324
2340
  return lines.join("\n");
2325
2341
  }
2326
2342
  function buildFullTaskContext(ctx, progressSummary, gitHistory, checkScript, checkStatus) {
@@ -2472,30 +2488,53 @@ function getEvaluatorModel(generatorModel, provider) {
2472
2488
  if (modelLower.includes("sonnet")) return "claude-haiku-4-5";
2473
2489
  return "claude-haiku-4-5";
2474
2490
  }
2491
+ var DIMENSION_NAMES = ["correctness", "completeness", "safety", "consistency"];
2492
+ var DIMENSION_PATTERNS = {
2493
+ correctness: /\*\*correctness\*\*\s*:\s*(PASS|FAIL)\s*(?:—|-)\s*(.+)/i,
2494
+ completeness: /\*\*completeness\*\*\s*:\s*(PASS|FAIL)\s*(?:—|-)\s*(.+)/i,
2495
+ safety: /\*\*safety\*\*\s*:\s*(PASS|FAIL)\s*(?:—|-)\s*(.+)/i,
2496
+ consistency: /\*\*consistency\*\*\s*:\s*(PASS|FAIL)\s*(?:—|-)\s*(.+)/i
2497
+ };
2498
+ function parseDimensionScores(output) {
2499
+ const scores = [];
2500
+ for (const dim of DIMENSION_NAMES) {
2501
+ const match = DIMENSION_PATTERNS[dim].exec(output);
2502
+ if (match?.[1] && match[2]) {
2503
+ scores.push({
2504
+ dimension: dim,
2505
+ passed: match[1].toUpperCase() === "PASS",
2506
+ finding: match[2].trim()
2507
+ });
2508
+ }
2509
+ }
2510
+ return scores;
2511
+ }
2475
2512
  function parseEvaluationResult(output) {
2513
+ const dimensions = parseDimensionScores(output);
2476
2514
  if (output.includes("<evaluation-passed>")) {
2477
- return { passed: true, output };
2515
+ return { passed: true, output, dimensions };
2478
2516
  }
2479
2517
  const failedMatch = /<evaluation-failed>([\s\S]*?)<\/evaluation-failed>/.exec(output);
2480
2518
  if (failedMatch) {
2481
- return { passed: false, output: failedMatch[1]?.trim() ?? output };
2519
+ return { passed: false, output: failedMatch[1]?.trim() ?? output, dimensions };
2482
2520
  }
2483
- return { passed: false, output };
2521
+ return { passed: false, output, dimensions };
2484
2522
  }
2485
2523
  function buildEvaluatorContext(task, checkScript) {
2486
- const checkScriptSection = checkScript ? `## Check Script
2524
+ const checkScriptSection = checkScript ? `## Check Script (Computational Gate)
2487
2525
 
2488
- You can run the following check script to verify the changes:
2526
+ Run this check script as the **first step** of your review \u2014 it is the same gate the harness uses post-task:
2489
2527
 
2490
2528
  \`\`\`
2491
2529
  ${checkScript}
2492
2530
  \`\`\`
2493
2531
 
2494
- Run it to gain additional insight into whether the implementation is correct.` : null;
2532
+ If this script fails, the implementation fails regardless of code quality. Record the full output.` : null;
2495
2533
  return {
2496
2534
  taskName: task.name,
2497
2535
  taskDescription: task.description ?? "",
2498
2536
  taskSteps: task.steps,
2537
+ verificationCriteria: task.verificationCriteria,
2499
2538
  projectPath: task.projectPath,
2500
2539
  checkScriptSection
2501
2540
  };
@@ -2520,6 +2559,7 @@ async function runEvaluation(task, generatorModel, checkScript, sprintId, provid
2520
2559
  }
2521
2560
 
2522
2561
  // src/ai/executor.ts
2562
+ var DEFAULT_MAX_TURNS = 200;
2523
2563
  function buildProviderArgs(options, provider) {
2524
2564
  if (provider.name !== "claude") {
2525
2565
  if (options.maxBudgetUsd != null) {
@@ -2528,6 +2568,9 @@ function buildProviderArgs(options, provider) {
2528
2568
  if (options.fallbackModel) {
2529
2569
  console.log(warning(`--fallback-model is only supported with the Claude provider \u2014 ignored`));
2530
2570
  }
2571
+ if (options.maxTurns != null) {
2572
+ console.log(warning(`--max-turns is only supported with the Claude provider \u2014 ignored`));
2573
+ }
2531
2574
  return [];
2532
2575
  }
2533
2576
  const args = [];
@@ -2537,6 +2580,7 @@ function buildProviderArgs(options, provider) {
2537
2580
  if (options.fallbackModel) {
2538
2581
  args.push("--fallback-model", options.fallbackModel);
2539
2582
  }
2583
+ args.push("--max-turns", String(options.maxTurns ?? DEFAULT_MAX_TURNS));
2540
2584
  return args;
2541
2585
  }
2542
2586
  async function executeTask(ctx, options, sprintId, resumeSessionId, provider, checkStatus) {
@@ -2672,6 +2716,8 @@ async function runEvaluationLoop(params) {
2672
2716
  const evalCheckScript = getEffectiveCheckScript(project, task.projectPath);
2673
2717
  const sprintDir = getSprintDir(sprintId);
2674
2718
  let evalResult = await runEvaluation(task, result.model, evalCheckScript, sprintId, provider);
2719
+ let currentSessionId = result.sessionId;
2720
+ let currentModel = result.model;
2675
2721
  for (let i = 0; i < evalIterations && !evalResult.passed; i++) {
2676
2722
  console.log(warning(`Evaluation failed for ${task.name} (iteration ${String(i + 1)}/${String(evalIterations)})`));
2677
2723
  console.log(muted(evalResult.output.slice(0, 500)));
@@ -2689,7 +2735,7 @@ Review the critique carefully. Fix each identified issue in the code, then:
2689
2735
  ${options.noCommit ? "" : "2. Commit the fix with a descriptive message\n"}${options.noCommit ? "2" : "3"}. Signal completion with <task-verified> and <task-complete>
2690
2736
 
2691
2737
  If the critique is about something outside your task scope, fix only what is within scope and signal completion.`,
2692
- resumeSessionId: result.sessionId ?? void 0,
2738
+ resumeSessionId: currentSessionId ?? void 0,
2693
2739
  env: provider.getSpawnEnv()
2694
2740
  },
2695
2741
  {
@@ -2703,6 +2749,8 @@ If the critique is about something outside your task scope, fix only what is wit
2703
2749
  provider
2704
2750
  );
2705
2751
  resumeSpinner?.succeed(`Fix attempt completed: ${task.name}`);
2752
+ if (resumeResult.sessionId) currentSessionId = resumeResult.sessionId;
2753
+ if (resumeResult.model) currentModel = resumeResult.model;
2706
2754
  const fixResult = parseExecutionResult(resumeResult.stdout);
2707
2755
  if (!fixResult.success) {
2708
2756
  console.log(warning(`Generator could not fix issues after feedback: ${task.name}`));
@@ -2716,7 +2764,7 @@ If the critique is about something outside your task scope, fix only what is wit
2716
2764
  break;
2717
2765
  }
2718
2766
  }
2719
- evalResult = await runEvaluation(task, resumeResult.model ?? result.model, evalCheckScript, sprintId, provider);
2767
+ evalResult = await runEvaluation(task, currentModel, evalCheckScript, sprintId, provider);
2720
2768
  }
2721
2769
  await updateTask(
2722
2770
  task.id,
@@ -3801,6 +3849,16 @@ function parseArgs3(args) {
3801
3849
  throw new Error("Invalid model name \u2014 must be 1-100 alphanumeric characters, dots, hyphens, or underscores");
3802
3850
  }
3803
3851
  options.fallbackModel = modelStr;
3852
+ } else if (arg === "--max-turns") {
3853
+ const turnsStr = args[++i];
3854
+ if (!turnsStr) {
3855
+ throw new Error("--max-turns requires a number");
3856
+ }
3857
+ const turns = parseInt(turnsStr, 10);
3858
+ if (isNaN(turns) || turns <= 0) {
3859
+ throw new Error("--max-turns must be a positive integer");
3860
+ }
3861
+ options.maxTurns = turns;
3804
3862
  } else if (arg === "--no-evaluate") {
3805
3863
  options.noEvaluate = true;
3806
3864
  } else if (!arg?.startsWith("-")) {
@@ -6,14 +6,14 @@ import {
6
6
  import {
7
7
  listProjects,
8
8
  projectExists
9
- } from "./chunk-LG6B7QVO.mjs";
9
+ } from "./chunk-7TBO6GOT.mjs";
10
10
  import {
11
11
  assertSprintStatus,
12
12
  generateUuid8,
13
13
  getEditor,
14
14
  resolveSprintId,
15
15
  setEditor
16
- } from "./chunk-KPTPKLXY.mjs";
16
+ } from "./chunk-ITRZMBLJ.mjs";
17
17
  import {
18
18
  ensureError,
19
19
  unwrapOrThrow,
@@ -24,7 +24,7 @@ import {
24
24
  getSprintFilePath,
25
25
  readValidatedJson,
26
26
  writeValidatedJson
27
- } from "./chunk-ZDEVRTGY.mjs";
27
+ } from "./chunk-GLDPHKEW.mjs";
28
28
  import {
29
29
  IOError,
30
30
  IssueFetchError,
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  createSprint,
4
4
  setCurrentSprint
5
- } from "./chunk-KPTPKLXY.mjs";
5
+ } from "./chunk-ITRZMBLJ.mjs";
6
6
  import {
7
7
  emoji,
8
8
  field,
package/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  addCheckScriptToRepository,
4
4
  projectAddCommand
5
- } from "./chunk-Q3VWJARJ.mjs";
5
+ } from "./chunk-LAERLCL5.mjs";
6
6
  import {
7
7
  addTask,
8
8
  areAllTasksDone,
@@ -52,13 +52,13 @@ import {
52
52
  sprintStartCommand,
53
53
  updateTaskStatus,
54
54
  validateImportTasks
55
- } from "./chunk-XXIHDQOH.mjs";
55
+ } from "./chunk-ORVGM6EV.mjs";
56
56
  import {
57
57
  escapableSelect
58
58
  } from "./chunk-7LZ6GOGN.mjs";
59
59
  import {
60
60
  sprintCreateCommand
61
- } from "./chunk-XQHEKKDN.mjs";
61
+ } from "./chunk-V4ZUDZCG.mjs";
62
62
  import {
63
63
  addTicket,
64
64
  allRequirementsApproved,
@@ -73,7 +73,7 @@ import {
73
73
  removeTicket,
74
74
  ticketAddCommand,
75
75
  updateTicket
76
- } from "./chunk-XPDI4SYI.mjs";
76
+ } from "./chunk-QYF7QIZJ.mjs";
77
77
  import {
78
78
  EXIT_ERROR,
79
79
  exitWithCode
@@ -84,7 +84,7 @@ import {
84
84
  listProjects,
85
85
  removeProject,
86
86
  removeProjectRepo
87
- } from "./chunk-LG6B7QVO.mjs";
87
+ } from "./chunk-7TBO6GOT.mjs";
88
88
  import {
89
89
  DEFAULT_EVALUATION_ITERATIONS,
90
90
  assertSprintStatus,
@@ -107,7 +107,7 @@ import {
107
107
  setEditor,
108
108
  setEvaluationIterations,
109
109
  withFileLock
110
- } from "./chunk-KPTPKLXY.mjs";
110
+ } from "./chunk-ITRZMBLJ.mjs";
111
111
  import {
112
112
  ensureError,
113
113
  wrapAsync
@@ -122,6 +122,7 @@ import {
122
122
  TaskStatusSchema,
123
123
  TasksSchema,
124
124
  assertSafeCwd,
125
+ ensureDir,
125
126
  expandTilde,
126
127
  fileExists,
127
128
  getDataDir,
@@ -133,7 +134,7 @@ import {
133
134
  getTasksFilePath,
134
135
  readValidatedJson,
135
136
  validateProjectPath
136
- } from "./chunk-ZDEVRTGY.mjs";
137
+ } from "./chunk-GLDPHKEW.mjs";
137
138
  import {
138
139
  DomainError,
139
140
  NoCurrentSprintError,
@@ -3763,7 +3764,7 @@ async function interactiveMode() {
3763
3764
  continue;
3764
3765
  }
3765
3766
  if (command === "wizard") {
3766
- const { runWizard } = await import("./wizard-D7N5WZ5H.mjs");
3767
+ const { runWizard } = await import("./wizard-RCQ4QQOL.mjs");
3767
3768
  await runWizard();
3768
3769
  continue;
3769
3770
  }
@@ -3898,6 +3899,87 @@ async function sprintSwitchCommand() {
3898
3899
  log.newline();
3899
3900
  }
3900
3901
 
3902
+ // src/commands/sprint/insights.ts
3903
+ import { writeFile as writeFile2 } from "fs/promises";
3904
+ import { join as join5 } from "path";
3905
+ async function sprintInsightsCommand(args) {
3906
+ const exportFlag = args.includes("--export");
3907
+ const positionalArgs = args.filter((a) => !a.startsWith("--"));
3908
+ const sprintId = positionalArgs[0];
3909
+ const sprintR = await wrapAsync(async () => {
3910
+ if (sprintId) return getSprint(sprintId);
3911
+ return getCurrentSprintOrThrow();
3912
+ }, ensureError);
3913
+ if (!sprintR.ok) {
3914
+ showError(sprintR.error.message);
3915
+ return;
3916
+ }
3917
+ const sprint = sprintR.value;
3918
+ const tasks = await getTasks(sprint.id);
3919
+ printHeader(`Sprint Insights: ${sprint.name}`, icons.sprint);
3920
+ const evaluatedTasks = tasks.filter((t) => t.evaluated);
3921
+ if (evaluatedTasks.length === 0) {
3922
+ log.info("No evaluation data found for this sprint.");
3923
+ return;
3924
+ }
3925
+ const totalTasks = tasks.length;
3926
+ const evaluatedCount = evaluatedTasks.length;
3927
+ const withOutput = evaluatedTasks.filter((t) => t.evaluationOutput && t.evaluationOutput.trim().length > 0);
3928
+ console.log(` Tasks evaluated: ${colors.accent(String(evaluatedCount))} / ${String(totalTasks)} total`);
3929
+ log.newline();
3930
+ if (withOutput.length > 0) {
3931
+ console.log(` ${colors.accent("Evaluation output:")}`);
3932
+ for (const task of withOutput) {
3933
+ const output = task.evaluationOutput ?? "";
3934
+ const truncated = output.length > 200 ? output.slice(0, 200) + "..." : output;
3935
+ console.log(` ${icons.bullet} ${colors.accent(task.name)}: ${colors.muted(truncated)}`);
3936
+ }
3937
+ log.newline();
3938
+ }
3939
+ console.log(` ${colors.accent("Harness recommendations:")}`);
3940
+ if (withOutput.length > 1) {
3941
+ console.log(
3942
+ ` ${icons.bullet} Consider reviewing evaluation failure patterns and updating CLAUDE.md with lessons learned.`
3943
+ );
3944
+ }
3945
+ if (withOutput.length > 0) {
3946
+ console.log(
3947
+ ` ${icons.bullet} Run: ${colors.muted("ralphctl sprint insights --export")} to save details to $RALPHCTL_ROOT/insights/<sprint-id>.md`
3948
+ );
3949
+ }
3950
+ log.newline();
3951
+ if (exportFlag) {
3952
+ await exportInsights(sprint, tasks);
3953
+ }
3954
+ }
3955
+ async function exportInsights(sprint, tasks) {
3956
+ const dir = join5(getDataDir(), "insights");
3957
+ await ensureDir(dir);
3958
+ const filePath = join5(dir, `${sprint.id}.md`);
3959
+ const evaluatedCount = tasks.filter((t) => t.evaluated).length;
3960
+ const lines = [
3961
+ `# Sprint Insights: ${sprint.name}`,
3962
+ "",
3963
+ `**Date:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
3964
+ `**Sprint ID:** ${sprint.id}`,
3965
+ `**Tasks evaluated:** ${String(evaluatedCount)} / ${String(tasks.length)} total`,
3966
+ "",
3967
+ "## Evaluation Details"
3968
+ ];
3969
+ for (const task of tasks) {
3970
+ lines.push("");
3971
+ lines.push(`### ${task.name} (${task.id})`);
3972
+ lines.push(`**Status:** ${task.status}`);
3973
+ lines.push(`**Evaluated:** ${task.evaluated ? "yes" : "no"}`);
3974
+ lines.push("");
3975
+ lines.push(task.evaluationOutput ?? "No evaluation output");
3976
+ lines.push("");
3977
+ lines.push("---");
3978
+ }
3979
+ await writeFile2(filePath, lines.join("\n"), "utf-8");
3980
+ log.success(`Insights exported to ${colors.accent(filePath)}`);
3981
+ }
3982
+
3901
3983
  // src/commands/sprint/index.ts
3902
3984
  function registerSprintCommands(program2) {
3903
3985
  const sprint = program2.command("sprint").description("Manage sprints");
@@ -3974,7 +4056,13 @@ Examples:
3974
4056
  sprint.command("health").description("Check sprint health").action(async () => {
3975
4057
  await sprintHealthCommand();
3976
4058
  });
3977
- sprint.command("start [id]").description("Run automated implementation loop").option("-s, --session", "Interactive AI session (collaborate with your AI provider)").option("-t, --step", "Step through tasks with approval between each").option("-c, --count <n>", "Limit to N tasks").option("--no-commit", "Skip automatic git commit after each task completes").option("--concurrency <n>", "Max parallel tasks (default: auto based on unique repos)").option("--max-retries <n>", "Max rate-limit retries per task (default: 5)").option("--fail-fast", "Stop launching new tasks on first failure").option("-f, --force", "Skip precondition checks (e.g., unplanned tickets)").option("--refresh-check", "Force re-run check scripts even if they already ran this sprint").option("-b, --branch", "Create sprint branch (ralphctl/<sprint-id>) in all repos").option("--branch-name <name>", "Use a custom branch name for sprint execution").option("--max-budget-usd <amount>", "Max USD budget per AI task (Claude only)").option("--fallback-model <model>", "Fallback model when primary is overloaded (Claude only)").addHelpText(
4059
+ sprint.command("insights [id]").description("Analyze evaluation results and suggest improvements").option("--export", "Export insights to $RALPHCTL_ROOT/insights/<sprint-id>.md").action(async (id, opts) => {
4060
+ const args = [];
4061
+ if (id) args.push(id);
4062
+ if (opts?.export) args.push("--export");
4063
+ await sprintInsightsCommand(args);
4064
+ });
4065
+ sprint.command("start [id]").description("Run automated implementation loop").option("-s, --session", "Interactive AI session (collaborate with your AI provider)").option("-t, --step", "Step through tasks with approval between each").option("-c, --count <n>", "Limit to N tasks").option("--no-commit", "Skip automatic git commit after each task completes").option("--concurrency <n>", "Max parallel tasks (default: auto based on unique repos)").option("--max-retries <n>", "Max rate-limit retries per task (default: 5)").option("--fail-fast", "Stop launching new tasks on first failure").option("-f, --force", "Skip precondition checks (e.g., unplanned tickets)").option("--refresh-check", "Force re-run check scripts even if they already ran this sprint").option("-b, --branch", "Create sprint branch (ralphctl/<sprint-id>) in all repos").option("--branch-name <name>", "Use a custom branch name for sprint execution").option("--max-budget-usd <amount>", "Max USD budget per AI task (Claude only)").option("--fallback-model <model>", "Fallback model when primary is overloaded (Claude only)").option("--max-turns <number>", "Max agentic turns per task (Claude only, default: 200)").addHelpText(
3978
4066
  "after",
3979
4067
  `
3980
4068
  Exit Codes:
@@ -4012,6 +4100,7 @@ Branch Management:
4012
4100
  if (opts?.branchName) args.push("--branch-name", opts.branchName);
4013
4101
  if (opts?.maxBudgetUsd) args.push("--max-budget-usd", opts.maxBudgetUsd);
4014
4102
  if (opts?.fallbackModel) args.push("--fallback-model", opts.fallbackModel);
4103
+ if (opts?.maxTurns) args.push("--max-turns", opts.maxTurns);
4015
4104
  await sprintStartCommand(args);
4016
4105
  }
4017
4106
  );
@@ -4234,7 +4323,7 @@ Checks performed:
4234
4323
  // package.json
4235
4324
  var package_default = {
4236
4325
  name: "ralphctl",
4237
- version: "0.2.2",
4326
+ version: "0.2.3",
4238
4327
  description: "Agent harness for long-running AI coding tasks \u2014 orchestrates Claude Code & GitHub Copilot across repositories",
4239
4328
  homepage: "https://github.com/lukas-grigis/ralphctl",
4240
4329
  type: "module",
@@ -4299,10 +4388,10 @@ var package_default = {
4299
4388
  },
4300
4389
  devDependencies: {
4301
4390
  "@eslint/js": "^10.0.1",
4302
- "@types/node": "^25.5.0",
4391
+ "@types/node": "^25.5.2",
4303
4392
  "@types/tabtab": "^3.0.4",
4304
- "@vitest/coverage-v8": "^4.1.1",
4305
- eslint: "^10.1.0",
4393
+ "@vitest/coverage-v8": "^4.1.2",
4394
+ eslint: "^10.2.0",
4306
4395
  "eslint-config-prettier": "^10.1.8",
4307
4396
  globals: "^17.4.0",
4308
4397
  husky: "^9.1.7",
@@ -4311,8 +4400,8 @@ var package_default = {
4311
4400
  tsup: "^8.5.1",
4312
4401
  tsx: "^4.21.0",
4313
4402
  typescript: "^5.9.3",
4314
- "typescript-eslint": "^8.57.2",
4315
- vitest: "^4.1.1"
4403
+ "typescript-eslint": "^8.58.0",
4404
+ vitest: "^4.1.2"
4316
4405
  },
4317
4406
  "lint-staged": {
4318
4407
  "*.ts": [
@@ -4356,7 +4445,7 @@ registerCompletionCommands(program);
4356
4445
  registerDoctorCommands(program);
4357
4446
  async function main() {
4358
4447
  if (process.env["COMP_CWORD"] && process.env["COMP_POINT"] && process.env["COMP_LINE"]) {
4359
- const { handleCompletionRequest } = await import("./handle-CCTBNAJZ.mjs");
4448
+ const { handleCompletionRequest } = await import("./handle-2BACSJLR.mjs");
4360
4449
  if (await handleCompletionRequest(program)) return;
4361
4450
  }
4362
4451
  if (process.argv.length <= 2 || process.argv[2] === "interactive") {
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  sprintCreateCommand
4
- } from "./chunk-XQHEKKDN.mjs";
5
- import "./chunk-KPTPKLXY.mjs";
4
+ } from "./chunk-V4ZUDZCG.mjs";
5
+ import "./chunk-ITRZMBLJ.mjs";
6
6
  import "./chunk-OEUJDSHY.mjs";
7
- import "./chunk-ZDEVRTGY.mjs";
7
+ import "./chunk-GLDPHKEW.mjs";
8
8
  import "./chunk-EDJX7TT6.mjs";
9
9
  import "./chunk-QBXHAXHI.mjs";
10
10
  export {
@@ -7,7 +7,7 @@ async function handleCompletionRequest(program) {
7
7
  return false;
8
8
  }
9
9
  const tabtab = (await import("tabtab")).default;
10
- const { resolveCompletions } = await import("./resolver-L52KR4GY.mjs");
10
+ const { resolveCompletions } = await import("./resolver-CFY6DIOP.mjs");
11
11
  const tabEnv = tabtab.parseEnv(env);
12
12
  const completions = await resolveCompletions(program, {
13
13
  line: tabEnv.line,