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.
- package/README.md +3 -3
- package/dist/{add-SEDQ3VK7.mjs → add-DWNLZQ7Q.mjs} +4 -4
- package/dist/{add-TGJTRHIF.mjs → add-K7LNOYQ4.mjs} +3 -3
- package/dist/{chunk-LG6B7QVO.mjs → chunk-7TBO6GOT.mjs} +1 -1
- package/dist/{chunk-ZDEVRTGY.mjs → chunk-GLDPHKEW.mjs} +9 -0
- package/dist/{chunk-KPTPKLXY.mjs → chunk-ITRZMBLJ.mjs} +1 -1
- package/dist/{chunk-Q3VWJARJ.mjs → chunk-LAERLCL5.mjs} +2 -2
- package/dist/{chunk-XXIHDQOH.mjs → chunk-ORVGM6EV.mjs} +74 -16
- package/dist/{chunk-XPDI4SYI.mjs → chunk-QYF7QIZJ.mjs} +3 -3
- package/dist/{chunk-XQHEKKDN.mjs → chunk-V4ZUDZCG.mjs} +1 -1
- package/dist/cli.mjs +105 -16
- package/dist/{create-DJHCP7LN.mjs → create-5MILNF7E.mjs} +3 -3
- package/dist/{handle-CCTBNAJZ.mjs → handle-2BACSJLR.mjs} +1 -1
- package/dist/{project-ZYGNPVGL.mjs → project-XC7AXA4B.mjs} +2 -2
- package/dist/prompts/ideate-auto.md +9 -5
- package/dist/prompts/ideate.md +28 -12
- package/dist/prompts/plan-auto.md +26 -16
- package/dist/prompts/plan-common.md +67 -22
- package/dist/prompts/plan-interactive.md +26 -27
- package/dist/prompts/task-evaluation.md +144 -24
- package/dist/prompts/task-execution.md +58 -36
- package/dist/prompts/ticket-refine.md +24 -20
- package/dist/{resolver-L52KR4GY.mjs → resolver-CFY6DIOP.mjs} +2 -2
- package/dist/{sprint-LUXAV3Q3.mjs → sprint-F4VRAEWZ.mjs} +2 -2
- package/dist/{wizard-D7N5WZ5H.mjs → wizard-RCQ4QQOL.mjs} +6 -6
- package/package.json +6 -6
- package/schemas/task-import.schema.json +7 -0
- 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
|
-
|
|
211
|
-
|
|
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-
|
|
5
|
+
} from "./chunk-QYF7QIZJ.mjs";
|
|
6
6
|
import "./chunk-7TG3EAQ2.mjs";
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-7TBO6GOT.mjs";
|
|
8
|
+
import "./chunk-ITRZMBLJ.mjs";
|
|
9
9
|
import "./chunk-OEUJDSHY.mjs";
|
|
10
|
-
import "./chunk-
|
|
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-
|
|
5
|
+
} from "./chunk-LAERLCL5.mjs";
|
|
6
6
|
import "./chunk-7LZ6GOGN.mjs";
|
|
7
7
|
import "./chunk-7TG3EAQ2.mjs";
|
|
8
|
-
import "./chunk-
|
|
8
|
+
import "./chunk-7TBO6GOT.mjs";
|
|
9
9
|
import "./chunk-OEUJDSHY.mjs";
|
|
10
|
-
import "./chunk-
|
|
10
|
+
import "./chunk-GLDPHKEW.mjs";
|
|
11
11
|
import "./chunk-EDJX7TT6.mjs";
|
|
12
12
|
import "./chunk-QBXHAXHI.mjs";
|
|
13
13
|
export {
|
|
@@ -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)
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "./chunk-7TG3EAQ2.mjs";
|
|
9
9
|
import {
|
|
10
10
|
createProject
|
|
11
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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-
|
|
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-
|
|
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-
|
|
27
|
+
} from "./chunk-GLDPHKEW.mjs";
|
|
28
28
|
import {
|
|
29
29
|
IOError,
|
|
30
30
|
IssueFetchError,
|
package/dist/cli.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
addCheckScriptToRepository,
|
|
4
4
|
projectAddCommand
|
|
5
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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("
|
|
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.
|
|
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.
|
|
4391
|
+
"@types/node": "^25.5.2",
|
|
4303
4392
|
"@types/tabtab": "^3.0.4",
|
|
4304
|
-
"@vitest/coverage-v8": "^4.1.
|
|
4305
|
-
eslint: "^10.
|
|
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.
|
|
4315
|
-
vitest: "^4.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-
|
|
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-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-V4ZUDZCG.mjs";
|
|
5
|
+
import "./chunk-ITRZMBLJ.mjs";
|
|
6
6
|
import "./chunk-OEUJDSHY.mjs";
|
|
7
|
-
import "./chunk-
|
|
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-
|
|
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,
|