@weppy/ralph 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- package/dist/cli.js +2248 -0
- package/dist/index.js +28 -0
- package/package.json +43 -0
- package/specs/00-/352/260/234/354/232/224.md +40 -0
- package/specs/10-/354/225/204/355/202/244/355/205/215/354/262/230.md +89 -0
- package/specs/20-/354/203/201/355/203/234/354/231/200-/354/213/244/355/226/211.md +161 -0
- package/specs/30-/354/235/270/355/204/260/355/216/230/354/235/264/354/212/244.md +137 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/planning/index.ts
|
|
4
|
+
import { stat } from "node:fs/promises";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
async function createInitialPlan(input) {
|
|
7
|
+
const normalizedInputDocuments = await Promise.all(
|
|
8
|
+
input.inputDocuments.map(async (documentPath) => {
|
|
9
|
+
const resolvedPath = resolve(input.cwd ?? process.cwd(), documentPath);
|
|
10
|
+
const fileStat = await stat(resolvedPath);
|
|
11
|
+
return {
|
|
12
|
+
path: resolvedPath,
|
|
13
|
+
kind: detectInputDocumentKind(resolvedPath, fileStat.isDirectory())
|
|
14
|
+
};
|
|
15
|
+
})
|
|
16
|
+
);
|
|
17
|
+
const planModel = createDefaultPlanningModel({
|
|
18
|
+
title: input.title,
|
|
19
|
+
workspacePath: input.workspacePath,
|
|
20
|
+
inputDocuments: normalizedInputDocuments
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
normalizedInputDocuments,
|
|
24
|
+
specMarkdown: buildSpecMarkdown(input.title, input.workspacePath, normalizedInputDocuments),
|
|
25
|
+
planMarkdown: buildPlanMarkdown(input.title, planModel, normalizedInputDocuments),
|
|
26
|
+
tasks: {
|
|
27
|
+
phases: planModel.phases,
|
|
28
|
+
tasks: planModel.tasks,
|
|
29
|
+
dependencies: planModel.dependencies,
|
|
30
|
+
retryCounts: Object.fromEntries(planModel.tasks.map((task) => [task.id, 0])),
|
|
31
|
+
evidenceLinks: Object.fromEntries(planModel.tasks.map((task) => [task.id, []])),
|
|
32
|
+
phaseGateStatus: Object.fromEntries(planModel.phases.map((phase) => [phase.id, "pending"]))
|
|
33
|
+
},
|
|
34
|
+
runtime: {
|
|
35
|
+
currentPhaseId: planModel.phases[0]?.id ?? null,
|
|
36
|
+
currentTaskId: planModel.tasks[0]?.id ?? null,
|
|
37
|
+
remainingTaskCount: planModel.tasks.length,
|
|
38
|
+
lastRunId: null,
|
|
39
|
+
nextAction: "resume",
|
|
40
|
+
blockedReason: null,
|
|
41
|
+
lastValidationStatus: "pending"
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function buildSpecMarkdown(title, workspacePath, inputDocuments) {
|
|
46
|
+
const sections = ["# Job Spec", "", `Title: ${title}`, "", `Workspace: ${workspacePath}`, ""];
|
|
47
|
+
if (inputDocuments.length === 0) {
|
|
48
|
+
sections.push("No input documents were provided.");
|
|
49
|
+
sections.push("");
|
|
50
|
+
} else {
|
|
51
|
+
sections.push("Referenced inputs:", "");
|
|
52
|
+
sections.push(
|
|
53
|
+
...inputDocuments.map((document) => `- [${document.kind}] ${document.path}`),
|
|
54
|
+
"",
|
|
55
|
+
"Ralph passes these paths to the agent as references.",
|
|
56
|
+
"The agent must inspect the files directly instead of relying on copied content.",
|
|
57
|
+
""
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return `${sections.join("\n").trimEnd()}
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
function buildPlanMarkdown(title, planModel, inputDocuments) {
|
|
64
|
+
const lines = [
|
|
65
|
+
"# Plan",
|
|
66
|
+
"",
|
|
67
|
+
`Job title: ${title}`,
|
|
68
|
+
""
|
|
69
|
+
];
|
|
70
|
+
for (const phase of planModel.phases) {
|
|
71
|
+
lines.push(`## ${phase.id} ${phase.title}`, "");
|
|
72
|
+
const phaseTasks = planModel.tasks.filter((task) => task.phaseId === phase.id);
|
|
73
|
+
if (phaseTasks.length === 0) {
|
|
74
|
+
lines.push("No tasks.", "");
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
for (const task of phaseTasks) {
|
|
78
|
+
const dependencies = planModel.dependencies[task.id] ?? [];
|
|
79
|
+
lines.push(`- ${task.id}: ${task.title}`);
|
|
80
|
+
lines.push(` - ${task.description}`);
|
|
81
|
+
lines.push(
|
|
82
|
+
dependencies.length === 0 ? " - Dependencies: none" : ` - Dependencies: ${dependencies.join(", ")}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
lines.push("");
|
|
86
|
+
}
|
|
87
|
+
if (inputDocuments.length > 0) {
|
|
88
|
+
lines.push(
|
|
89
|
+
"",
|
|
90
|
+
"## Inputs",
|
|
91
|
+
"",
|
|
92
|
+
...inputDocuments.map((value) => `- [${value.kind}] ${value.path}`)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return `${lines.join("\n")}
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
function createDefaultPlanningModel(input) {
|
|
99
|
+
const phaseId = "PHASE-001";
|
|
100
|
+
const taskId = "TASK-001";
|
|
101
|
+
const inputSummary = input.inputDocuments.length === 0 ? "Use the workspace state as the primary source of truth." : `Inspect ${input.inputDocuments.length} referenced input path(s) before making changes.`;
|
|
102
|
+
return {
|
|
103
|
+
phases: [
|
|
104
|
+
{
|
|
105
|
+
id: phaseId,
|
|
106
|
+
title: "Requested Work"
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
tasks: [
|
|
110
|
+
{
|
|
111
|
+
id: taskId,
|
|
112
|
+
phaseId,
|
|
113
|
+
title: input.title,
|
|
114
|
+
description: `${input.title}. ${inputSummary}`,
|
|
115
|
+
status: "pending"
|
|
116
|
+
}
|
|
117
|
+
],
|
|
118
|
+
dependencies: {
|
|
119
|
+
[taskId]: []
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function detectInputDocumentKind(documentPath, isDirectory) {
|
|
124
|
+
if (isDirectory) {
|
|
125
|
+
return "directory";
|
|
126
|
+
}
|
|
127
|
+
const normalizedPath = documentPath.toLowerCase();
|
|
128
|
+
if ([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"].some(
|
|
129
|
+
(extension) => normalizedPath.endsWith(extension)
|
|
130
|
+
)) {
|
|
131
|
+
return "image";
|
|
132
|
+
}
|
|
133
|
+
if (normalizedPath.includes(".")) {
|
|
134
|
+
return "file";
|
|
135
|
+
}
|
|
136
|
+
return "unknown";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/state/index.ts
|
|
140
|
+
import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
141
|
+
import { dirname, join, resolve as resolve2 } from "node:path";
|
|
142
|
+
var stateFeature = {
|
|
143
|
+
rootDirectoryName: ".ralph-cache",
|
|
144
|
+
defaultLocationBase: "workspacePath",
|
|
145
|
+
cliOverrideFlag: "--state-dir"
|
|
146
|
+
};
|
|
147
|
+
function resolveWorkspacePath(workspacePath, cwd = process.cwd()) {
|
|
148
|
+
if (workspacePath.trim().length === 0) {
|
|
149
|
+
throw new Error("`workspacePath` must not be empty.");
|
|
150
|
+
}
|
|
151
|
+
return resolve2(cwd, workspacePath);
|
|
152
|
+
}
|
|
153
|
+
function resolveStateDirectoryPath({
|
|
154
|
+
workspacePath,
|
|
155
|
+
stateDirectoryPath,
|
|
156
|
+
cwd = process.cwd()
|
|
157
|
+
}) {
|
|
158
|
+
if (stateDirectoryPath !== void 0) {
|
|
159
|
+
return resolve2(cwd, stateDirectoryPath);
|
|
160
|
+
}
|
|
161
|
+
if (workspacePath === void 0) {
|
|
162
|
+
throw new Error("`workspacePath` is required when `stateDirectoryPath` is not provided.");
|
|
163
|
+
}
|
|
164
|
+
const resolvedWorkspacePath = resolveWorkspacePath(workspacePath, cwd);
|
|
165
|
+
return join(resolvedWorkspacePath, stateFeature.rootDirectoryName);
|
|
166
|
+
}
|
|
167
|
+
function getJobPaths(jobId, rootDirectoryPath) {
|
|
168
|
+
const jobDirectoryPath = join(rootDirectoryPath, "jobs", jobId);
|
|
169
|
+
return {
|
|
170
|
+
rootDirectoryPath,
|
|
171
|
+
jobsDirectoryPath: join(rootDirectoryPath, "jobs"),
|
|
172
|
+
currentJobPath: join(rootDirectoryPath, "current-job.txt"),
|
|
173
|
+
jobDirectoryPath,
|
|
174
|
+
jobJsonPath: join(jobDirectoryPath, "job.json"),
|
|
175
|
+
inputsJsonPath: join(jobDirectoryPath, "inputs.json"),
|
|
176
|
+
specMarkdownPath: join(jobDirectoryPath, "spec.md"),
|
|
177
|
+
planMarkdownPath: join(jobDirectoryPath, "plan.md"),
|
|
178
|
+
tasksJsonPath: join(jobDirectoryPath, "tasks.json"),
|
|
179
|
+
runtimeJsonPath: join(jobDirectoryPath, "runtime.json"),
|
|
180
|
+
finalSummaryPath: join(jobDirectoryPath, "final-summary.md"),
|
|
181
|
+
userChecksPath: join(jobDirectoryPath, "user-checks.md"),
|
|
182
|
+
validationsDirectoryPath: join(jobDirectoryPath, "validations"),
|
|
183
|
+
runsDirectoryPath: join(jobDirectoryPath, "runs"),
|
|
184
|
+
artifactsDirectoryPath: join(jobDirectoryPath, "artifacts")
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
async function ensureStateRoot(rootDirectoryPath) {
|
|
188
|
+
await mkdir(join(rootDirectoryPath, "jobs"), { recursive: true });
|
|
189
|
+
}
|
|
190
|
+
async function ensureJobDirectories(jobPaths) {
|
|
191
|
+
await ensureStateRoot(jobPaths.rootDirectoryPath);
|
|
192
|
+
await mkdir(jobPaths.jobDirectoryPath, { recursive: true });
|
|
193
|
+
await mkdir(jobPaths.validationsDirectoryPath, { recursive: true });
|
|
194
|
+
await mkdir(jobPaths.runsDirectoryPath, { recursive: true });
|
|
195
|
+
await mkdir(jobPaths.artifactsDirectoryPath, { recursive: true });
|
|
196
|
+
}
|
|
197
|
+
async function ensureRunDirectory(runDirectoryPath) {
|
|
198
|
+
await mkdir(runDirectoryPath, { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
async function atomicWriteFile(filePath, content) {
|
|
201
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
202
|
+
const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
203
|
+
await writeFile(temporaryPath, content, "utf8");
|
|
204
|
+
await rename(temporaryPath, filePath);
|
|
205
|
+
}
|
|
206
|
+
async function atomicWriteJson(filePath, value) {
|
|
207
|
+
await atomicWriteFile(filePath, `${JSON.stringify(value, null, 2)}
|
|
208
|
+
`);
|
|
209
|
+
}
|
|
210
|
+
async function readTextFile(filePath) {
|
|
211
|
+
try {
|
|
212
|
+
return await readFile(filePath, "utf8");
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (isMissingFileError(error)) {
|
|
215
|
+
return void 0;
|
|
216
|
+
}
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function readJsonFile(filePath) {
|
|
221
|
+
const content = await readTextFile(filePath);
|
|
222
|
+
if (content === void 0) {
|
|
223
|
+
return void 0;
|
|
224
|
+
}
|
|
225
|
+
return JSON.parse(content);
|
|
226
|
+
}
|
|
227
|
+
function getNextRunId(lastRunId) {
|
|
228
|
+
const previousNumber = lastRunId === null ? 0 : Number.parseInt(lastRunId.replace("run-", ""), 10);
|
|
229
|
+
const nextNumber = Number.isFinite(previousNumber) ? previousNumber + 1 : 1;
|
|
230
|
+
return `run-${String(nextNumber).padStart(3, "0")}`;
|
|
231
|
+
}
|
|
232
|
+
function getRunPaths(runsDirectoryPath, runId) {
|
|
233
|
+
const runDirectoryPath = join(runsDirectoryPath, runId);
|
|
234
|
+
const resultPath = join(runDirectoryPath, "result.json");
|
|
235
|
+
return {
|
|
236
|
+
runDirectoryPath,
|
|
237
|
+
promptPath: join(runDirectoryPath, "prompt.md"),
|
|
238
|
+
resultPath,
|
|
239
|
+
runRecordPath: join(runDirectoryPath, "run-record.json"),
|
|
240
|
+
stdoutLogPath: join(runDirectoryPath, "stdout.log"),
|
|
241
|
+
stderrLogPath: join(runDirectoryPath, "stderr.log"),
|
|
242
|
+
outputPath: resultPath
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function getValidationPath(validationsDirectoryPath, validationIndex) {
|
|
246
|
+
return join(
|
|
247
|
+
validationsDirectoryPath,
|
|
248
|
+
`validation-${String(validationIndex).padStart(3, "0")}.json`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
async function listDirectoryEntries(directoryPath) {
|
|
252
|
+
try {
|
|
253
|
+
return await readdir(directoryPath);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (isMissingFileError(error)) {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function isMissingFileError(error) {
|
|
262
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/shared/utils/format.ts
|
|
266
|
+
function slugify(value) {
|
|
267
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "job";
|
|
268
|
+
}
|
|
269
|
+
function toIsoTimestamp(date = /* @__PURE__ */ new Date()) {
|
|
270
|
+
return date.toISOString();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/job/rules.ts
|
|
274
|
+
function isTaskCompleted(status) {
|
|
275
|
+
return status === "completed";
|
|
276
|
+
}
|
|
277
|
+
function isTaskTerminal(status) {
|
|
278
|
+
return ["completed", "blocked", "failed", "cancelled"].includes(status);
|
|
279
|
+
}
|
|
280
|
+
function isTaskRemaining(status) {
|
|
281
|
+
return ["pending", "running", "partial"].includes(status);
|
|
282
|
+
}
|
|
283
|
+
function isJobTerminal(status) {
|
|
284
|
+
return ["completed", "failed", "cancelled"].includes(status);
|
|
285
|
+
}
|
|
286
|
+
function derivePhaseGateStatus(phases, tasks) {
|
|
287
|
+
return Object.fromEntries(
|
|
288
|
+
phases.map((phase) => {
|
|
289
|
+
const phaseTasks = tasks.filter((task) => task.phaseId === phase.id);
|
|
290
|
+
if (phaseTasks.some((task) => ["failed", "blocked"].includes(task.status))) {
|
|
291
|
+
return [phase.id, "failed"];
|
|
292
|
+
}
|
|
293
|
+
if (phaseTasks.length > 0 && phaseTasks.every((task) => isTaskCompleted(task.status))) {
|
|
294
|
+
return [phase.id, "passed"];
|
|
295
|
+
}
|
|
296
|
+
return [phase.id, "pending"];
|
|
297
|
+
})
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
function selectRunnableTask(snapshot) {
|
|
301
|
+
const activePhaseId = getActivePhaseId(snapshot.tasks.phases, snapshot.tasks.tasks);
|
|
302
|
+
if (activePhaseId === null) {
|
|
303
|
+
return void 0;
|
|
304
|
+
}
|
|
305
|
+
return snapshot.tasks.tasks.find((task) => {
|
|
306
|
+
if (task.phaseId !== activePhaseId || task.status !== "pending") {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
return areTaskDependenciesSatisfied(task, snapshot.tasks.tasks, snapshot.tasks.dependencies);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
function nextPendingTaskId(phases, tasks, dependencies) {
|
|
313
|
+
const activePhaseId = getActivePhaseId(phases, tasks);
|
|
314
|
+
if (activePhaseId === null) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
return tasks.find((task) => {
|
|
318
|
+
if (task.phaseId !== activePhaseId || task.status !== "pending") {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
return areTaskDependenciesSatisfied(task, tasks, dependencies);
|
|
322
|
+
})?.id ?? null;
|
|
323
|
+
}
|
|
324
|
+
function countRemainingTasks(tasks) {
|
|
325
|
+
return tasks.filter((task) => isTaskRemaining(task.status)).length;
|
|
326
|
+
}
|
|
327
|
+
function buildNoRunnableTaskReason(snapshot) {
|
|
328
|
+
const phaseGateStatus = derivePhaseGateStatus(snapshot.tasks.phases, snapshot.tasks.tasks);
|
|
329
|
+
const activePhaseId = getActivePhaseId(snapshot.tasks.phases, snapshot.tasks.tasks);
|
|
330
|
+
if (activePhaseId === null) {
|
|
331
|
+
return "No runnable task is available.";
|
|
332
|
+
}
|
|
333
|
+
if (phaseGateStatus[activePhaseId] === "failed") {
|
|
334
|
+
return `No runnable task is available because ${activePhaseId} has failed.`;
|
|
335
|
+
}
|
|
336
|
+
const pendingTasks = snapshot.tasks.tasks.filter(
|
|
337
|
+
(task) => task.phaseId === activePhaseId && task.status === "pending"
|
|
338
|
+
);
|
|
339
|
+
const blockedDependencies = pendingTasks.map((task) => {
|
|
340
|
+
const unmetDependencies = (snapshot.tasks.dependencies[task.id] ?? []).filter((dependencyId) => {
|
|
341
|
+
const dependency = snapshot.tasks.tasks.find((entry) => entry.id === dependencyId);
|
|
342
|
+
return dependency === void 0 || !isTaskCompleted(dependency.status);
|
|
343
|
+
});
|
|
344
|
+
return unmetDependencies.length > 0 ? `${task.id} waits for ${unmetDependencies.join(", ")}` : null;
|
|
345
|
+
}).filter((value) => value !== null);
|
|
346
|
+
if (blockedDependencies.length > 0) {
|
|
347
|
+
return `No runnable task is available because dependencies are incomplete: ${blockedDependencies.join("; ")}.`;
|
|
348
|
+
}
|
|
349
|
+
return `No runnable task is available in ${activePhaseId}.`;
|
|
350
|
+
}
|
|
351
|
+
function buildBlockedRuntime(snapshot, reason) {
|
|
352
|
+
return {
|
|
353
|
+
...snapshot.runtime,
|
|
354
|
+
currentTaskId: null,
|
|
355
|
+
currentPhaseId: getActivePhaseId(snapshot.tasks.phases, snapshot.tasks.tasks),
|
|
356
|
+
remainingTaskCount: countRemainingTasks(snapshot.tasks.tasks),
|
|
357
|
+
nextAction: "blocked",
|
|
358
|
+
blockedReason: reason
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function getActivePhaseId(phases, tasks) {
|
|
362
|
+
const phaseGateStatus = derivePhaseGateStatus(phases, tasks);
|
|
363
|
+
for (const phase of phases) {
|
|
364
|
+
if (phaseGateStatus[phase.id] === "passed") {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
return phase.id;
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
function areTaskDependenciesSatisfied(task, tasks, dependencies) {
|
|
372
|
+
const taskDependencies = dependencies[task.id] ?? [];
|
|
373
|
+
return taskDependencies.every((dependencyId) => {
|
|
374
|
+
const dependency = tasks.find((entry) => entry.id === dependencyId);
|
|
375
|
+
return dependency !== void 0 && isTaskCompleted(dependency.status);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/job/index.ts
|
|
380
|
+
async function createJob(options) {
|
|
381
|
+
const cwd = options.cwd ?? process.cwd();
|
|
382
|
+
const workspacePath = resolveWorkspacePath(options.workspacePath, cwd);
|
|
383
|
+
const stateDirectoryPath = resolveStateDirectoryPath({
|
|
384
|
+
workspacePath,
|
|
385
|
+
stateDirectoryPath: options.stateDirectoryPath,
|
|
386
|
+
cwd
|
|
387
|
+
});
|
|
388
|
+
const jobId = buildJobId(options.title);
|
|
389
|
+
const paths = getJobPaths(jobId, stateDirectoryPath);
|
|
390
|
+
const planningResult = await createInitialPlan({
|
|
391
|
+
title: options.title,
|
|
392
|
+
workspacePath,
|
|
393
|
+
inputDocuments: options.inputDocuments,
|
|
394
|
+
cwd
|
|
395
|
+
});
|
|
396
|
+
const timestamp = toIsoTimestamp();
|
|
397
|
+
const job = {
|
|
398
|
+
id: jobId,
|
|
399
|
+
title: options.title,
|
|
400
|
+
requestedAgent: options.agent,
|
|
401
|
+
status: "planned",
|
|
402
|
+
workspacePath,
|
|
403
|
+
stateDirectoryPath,
|
|
404
|
+
inputDocuments: planningResult.normalizedInputDocuments,
|
|
405
|
+
validationProfile: {
|
|
406
|
+
name: options.validateCommands !== void 0 && options.validateCommands.length > 0 ? "commands" : "default",
|
|
407
|
+
commands: options.validateCommands ?? []
|
|
408
|
+
},
|
|
409
|
+
retryPolicy: {
|
|
410
|
+
maxRetriesPerTask: options.maxRetriesPerTask ?? 1
|
|
411
|
+
},
|
|
412
|
+
createdAt: timestamp,
|
|
413
|
+
updatedAt: timestamp
|
|
414
|
+
};
|
|
415
|
+
await ensureJobDirectories(paths);
|
|
416
|
+
await Promise.all([
|
|
417
|
+
atomicWriteJson(paths.jobJsonPath, job),
|
|
418
|
+
atomicWriteJson(paths.inputsJsonPath, planningResult.normalizedInputDocuments),
|
|
419
|
+
atomicWriteJson(paths.tasksJsonPath, planningResult.tasks),
|
|
420
|
+
atomicWriteJson(paths.runtimeJsonPath, planningResult.runtime),
|
|
421
|
+
atomicWriteFile(paths.specMarkdownPath, planningResult.specMarkdown),
|
|
422
|
+
atomicWriteFile(paths.planMarkdownPath, planningResult.planMarkdown),
|
|
423
|
+
atomicWriteFile(paths.userChecksPath, "# User Checks\n\n"),
|
|
424
|
+
atomicWriteFile(paths.finalSummaryPath, "# Final Summary\n\nPending.\n"),
|
|
425
|
+
atomicWriteFile(paths.currentJobPath, `${jobId}
|
|
426
|
+
`)
|
|
427
|
+
]);
|
|
428
|
+
return {
|
|
429
|
+
snapshot: {
|
|
430
|
+
job,
|
|
431
|
+
tasks: planningResult.tasks,
|
|
432
|
+
runtime: planningResult.runtime
|
|
433
|
+
},
|
|
434
|
+
paths
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
async function loadJob(options) {
|
|
438
|
+
const cwd = options.cwd ?? process.cwd();
|
|
439
|
+
const stateDirectoryPath = resolveStateDirectoryPath({
|
|
440
|
+
workspacePath: options.workspacePath,
|
|
441
|
+
stateDirectoryPath: options.stateDirectoryPath,
|
|
442
|
+
cwd
|
|
443
|
+
});
|
|
444
|
+
const currentJobId = options.jobId ?? await loadCurrentJobId(stateDirectoryPath);
|
|
445
|
+
if (currentJobId === void 0) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
`No job id was provided and no current job exists under ${stateDirectoryPath}.`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
const paths = getJobPaths(currentJobId, stateDirectoryPath);
|
|
451
|
+
const [job, tasks, runtime] = await Promise.all([
|
|
452
|
+
readJsonFile(paths.jobJsonPath),
|
|
453
|
+
readJsonFile(paths.tasksJsonPath),
|
|
454
|
+
readJsonFile(paths.runtimeJsonPath)
|
|
455
|
+
]);
|
|
456
|
+
if (job === void 0 || tasks === void 0 || runtime === void 0) {
|
|
457
|
+
throw new Error(`Job ${currentJobId} was not found under ${stateDirectoryPath}.`);
|
|
458
|
+
}
|
|
459
|
+
return { job, tasks, runtime };
|
|
460
|
+
}
|
|
461
|
+
async function saveJobSnapshot(snapshot) {
|
|
462
|
+
const paths = getJobPaths(snapshot.job.id, snapshot.job.stateDirectoryPath);
|
|
463
|
+
await Promise.all([
|
|
464
|
+
atomicWriteJson(paths.jobJsonPath, snapshot.job),
|
|
465
|
+
atomicWriteJson(paths.tasksJsonPath, snapshot.tasks),
|
|
466
|
+
atomicWriteJson(paths.runtimeJsonPath, snapshot.runtime)
|
|
467
|
+
]);
|
|
468
|
+
}
|
|
469
|
+
async function cancelJob(options) {
|
|
470
|
+
const snapshot = await loadJob(options);
|
|
471
|
+
const updatedTasks = snapshot.tasks.tasks.map(
|
|
472
|
+
(task) => isTerminalTaskStatus(task.status) ? task : {
|
|
473
|
+
...task,
|
|
474
|
+
status: "cancelled"
|
|
475
|
+
}
|
|
476
|
+
);
|
|
477
|
+
const nextSnapshot = {
|
|
478
|
+
job: {
|
|
479
|
+
...snapshot.job,
|
|
480
|
+
status: "cancelled",
|
|
481
|
+
updatedAt: toIsoTimestamp()
|
|
482
|
+
},
|
|
483
|
+
tasks: {
|
|
484
|
+
...snapshot.tasks,
|
|
485
|
+
tasks: updatedTasks,
|
|
486
|
+
phaseGateStatus: derivePhaseGateStatus(snapshot.tasks.phases, updatedTasks)
|
|
487
|
+
},
|
|
488
|
+
runtime: {
|
|
489
|
+
...snapshot.runtime,
|
|
490
|
+
currentTaskId: null,
|
|
491
|
+
remainingTaskCount: 0,
|
|
492
|
+
nextAction: "none",
|
|
493
|
+
blockedReason: "Cancelled by user.",
|
|
494
|
+
lastValidationStatus: snapshot.runtime.lastValidationStatus
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
await saveJobSnapshot(nextSnapshot);
|
|
498
|
+
return nextSnapshot;
|
|
499
|
+
}
|
|
500
|
+
async function loadJobDetails(options) {
|
|
501
|
+
const snapshot = await loadJob(options);
|
|
502
|
+
const paths = getJobPaths(snapshot.job.id, snapshot.job.stateDirectoryPath);
|
|
503
|
+
const finalSummary = await readTextFile(paths.finalSummaryPath) ?? null;
|
|
504
|
+
const userChecks = await readTextFile(paths.userChecksPath) ?? null;
|
|
505
|
+
if (snapshot.runtime.lastRunId === null) {
|
|
506
|
+
return {
|
|
507
|
+
snapshot,
|
|
508
|
+
finalSummary,
|
|
509
|
+
userChecks,
|
|
510
|
+
latestAgentResult: null,
|
|
511
|
+
latestRunRecord: null,
|
|
512
|
+
latestValidation: null
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
const runPaths = getRunPaths(paths.runsDirectoryPath, snapshot.runtime.lastRunId);
|
|
516
|
+
const validationIndex = Number.parseInt(snapshot.runtime.lastRunId.replace("run-", ""), 10);
|
|
517
|
+
const validationPath = getValidationPath(paths.validationsDirectoryPath, validationIndex);
|
|
518
|
+
const [latestAgentResult, latestRunRecord, latestValidation] = await Promise.all([
|
|
519
|
+
readJsonFile(runPaths.resultPath),
|
|
520
|
+
readJsonFile(runPaths.runRecordPath),
|
|
521
|
+
readJsonFile(validationPath)
|
|
522
|
+
]);
|
|
523
|
+
return {
|
|
524
|
+
snapshot,
|
|
525
|
+
finalSummary,
|
|
526
|
+
userChecks,
|
|
527
|
+
latestAgentResult: latestAgentResult ?? null,
|
|
528
|
+
latestRunRecord: latestRunRecord ?? null,
|
|
529
|
+
latestValidation: latestValidation ?? null
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
async function getJobOverview(options) {
|
|
533
|
+
const snapshot = await loadJob(options);
|
|
534
|
+
const paths = getJobPaths(snapshot.job.id, snapshot.job.stateDirectoryPath);
|
|
535
|
+
const runIds = (await listDirectoryEntries(paths.runsDirectoryPath)).sort();
|
|
536
|
+
return { snapshot, runIds };
|
|
537
|
+
}
|
|
538
|
+
async function loadCurrentJobId(stateDirectoryPath) {
|
|
539
|
+
const content = await readTextFile(getJobPaths("placeholder", stateDirectoryPath).currentJobPath);
|
|
540
|
+
return content?.trim() || void 0;
|
|
541
|
+
}
|
|
542
|
+
function buildJobId(title) {
|
|
543
|
+
const datePart = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
|
|
544
|
+
const randomPart = Math.random().toString(36).slice(2, 8);
|
|
545
|
+
return `job-${datePart}-${slugify(title)}-${randomPart}`;
|
|
546
|
+
}
|
|
547
|
+
function isTerminalTaskStatus(status) {
|
|
548
|
+
return isTaskTerminal(status);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/shared/utils/cli.ts
|
|
552
|
+
function parseCliArgs(argv) {
|
|
553
|
+
const flags = /* @__PURE__ */ new Map();
|
|
554
|
+
const positionals = [];
|
|
555
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
556
|
+
const token = argv[index];
|
|
557
|
+
if (!token.startsWith("--")) {
|
|
558
|
+
positionals.push(token);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
const [name, inlineValue] = token.split("=", 2);
|
|
562
|
+
if (inlineValue !== void 0) {
|
|
563
|
+
appendFlagValue(flags, name, inlineValue);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const nextToken = argv[index + 1];
|
|
567
|
+
if (nextToken !== void 0 && !nextToken.startsWith("--")) {
|
|
568
|
+
appendFlagValue(flags, name, nextToken);
|
|
569
|
+
index += 1;
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
appendFlagValue(flags, name, "true");
|
|
573
|
+
}
|
|
574
|
+
return { flags, positionals };
|
|
575
|
+
}
|
|
576
|
+
function getSingleFlag(parsedArgs, flagName) {
|
|
577
|
+
return parsedArgs.flags.get(flagName)?.at(-1);
|
|
578
|
+
}
|
|
579
|
+
function getMultiFlag(parsedArgs, flagName) {
|
|
580
|
+
return parsedArgs.flags.get(flagName) ?? [];
|
|
581
|
+
}
|
|
582
|
+
function hasFlag(parsedArgs, flagName) {
|
|
583
|
+
return parsedArgs.flags.has(flagName);
|
|
584
|
+
}
|
|
585
|
+
function appendFlagValue(flags, flagName, value) {
|
|
586
|
+
const currentValues = flags.get(flagName) ?? [];
|
|
587
|
+
currentValues.push(value);
|
|
588
|
+
flags.set(flagName, currentValues);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/shared/utils/output.ts
|
|
592
|
+
function printLine(message) {
|
|
593
|
+
process.stdout.write(`${message}
|
|
594
|
+
`);
|
|
595
|
+
}
|
|
596
|
+
function printError(message) {
|
|
597
|
+
process.stderr.write(`${message}
|
|
598
|
+
`);
|
|
599
|
+
}
|
|
600
|
+
function printJson(value) {
|
|
601
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
602
|
+
`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/cli/cancel/index.ts
|
|
606
|
+
async function runCancelCommand(argv) {
|
|
607
|
+
const parsedArgs = parseCliArgs(argv);
|
|
608
|
+
const jobId = getSingleFlag(parsedArgs, "--job");
|
|
609
|
+
const workspacePath = getSingleFlag(parsedArgs, "--workspace");
|
|
610
|
+
const stateDirectoryPath = getSingleFlag(parsedArgs, "--state-dir");
|
|
611
|
+
const jsonMode = hasFlag(parsedArgs, "--json");
|
|
612
|
+
if (workspacePath === void 0 && stateDirectoryPath === void 0) {
|
|
613
|
+
printError("Either --workspace or --state-dir is required.");
|
|
614
|
+
return 1;
|
|
615
|
+
}
|
|
616
|
+
try {
|
|
617
|
+
const snapshot = await cancelJob({
|
|
618
|
+
jobId,
|
|
619
|
+
workspacePath,
|
|
620
|
+
stateDirectoryPath
|
|
621
|
+
});
|
|
622
|
+
if (jsonMode) {
|
|
623
|
+
printJson({
|
|
624
|
+
job_id: snapshot.job.id,
|
|
625
|
+
status: snapshot.job.status,
|
|
626
|
+
next_action: snapshot.runtime.nextAction
|
|
627
|
+
});
|
|
628
|
+
return 0;
|
|
629
|
+
}
|
|
630
|
+
printLine(`Cancelled job ${snapshot.job.id}`);
|
|
631
|
+
printLine(`Status: ${snapshot.job.status}`);
|
|
632
|
+
return 0;
|
|
633
|
+
} catch (error) {
|
|
634
|
+
printError(error instanceof Error ? error.message : "Failed to cancel job.");
|
|
635
|
+
return 1;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/cli/result/index.ts
|
|
640
|
+
async function runResultCommand(argv) {
|
|
641
|
+
const parsedArgs = parseCliArgs(argv);
|
|
642
|
+
const jobId = getSingleFlag(parsedArgs, "--job");
|
|
643
|
+
const workspacePath = getSingleFlag(parsedArgs, "--workspace");
|
|
644
|
+
const stateDirectoryPath = getSingleFlag(parsedArgs, "--state-dir");
|
|
645
|
+
const jsonMode = hasFlag(parsedArgs, "--json");
|
|
646
|
+
if (workspacePath === void 0 && stateDirectoryPath === void 0) {
|
|
647
|
+
printError("Either --workspace or --state-dir is required.");
|
|
648
|
+
return 1;
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
const details = await loadJobDetails({
|
|
652
|
+
jobId,
|
|
653
|
+
workspacePath,
|
|
654
|
+
stateDirectoryPath
|
|
655
|
+
});
|
|
656
|
+
if (jsonMode) {
|
|
657
|
+
printJson({
|
|
658
|
+
job_id: details.snapshot.job.id,
|
|
659
|
+
status: details.snapshot.job.status,
|
|
660
|
+
final_summary: details.finalSummary,
|
|
661
|
+
user_checks: details.userChecks,
|
|
662
|
+
latest_run_record: details.latestRunRecord,
|
|
663
|
+
latest_agent_result: details.latestAgentResult,
|
|
664
|
+
latest_validation: details.latestValidation
|
|
665
|
+
});
|
|
666
|
+
return 0;
|
|
667
|
+
}
|
|
668
|
+
printLine(`Job: ${details.snapshot.job.id}`);
|
|
669
|
+
printLine(`Status: ${details.snapshot.job.status}`);
|
|
670
|
+
printLine("");
|
|
671
|
+
printLine("Final Summary:");
|
|
672
|
+
printLine(details.finalSummary ?? "No final summary available.");
|
|
673
|
+
printLine("User Checks:");
|
|
674
|
+
printLine(details.userChecks ?? "No user checks available.");
|
|
675
|
+
if (details.latestRunRecord !== null) {
|
|
676
|
+
printLine("Latest Run:");
|
|
677
|
+
printLine(`Run: ${details.latestRunRecord.runId}`);
|
|
678
|
+
printLine(`Status: ${details.latestRunRecord.status}`);
|
|
679
|
+
printLine(`Summary: ${details.latestRunRecord.summary}`);
|
|
680
|
+
}
|
|
681
|
+
return 0;
|
|
682
|
+
} catch (error) {
|
|
683
|
+
printError(error instanceof Error ? error.message : "Failed to load job result.");
|
|
684
|
+
return 1;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// src/adapters/run.ts
|
|
689
|
+
import { spawn } from "node:child_process";
|
|
690
|
+
|
|
691
|
+
// src/adapters/prompts.ts
|
|
692
|
+
function buildExecutionPrompt(context) {
|
|
693
|
+
const lines = [
|
|
694
|
+
"You are executing a single Ralph task in a fresh context.",
|
|
695
|
+
"Return only a JSON object that matches the required schema.",
|
|
696
|
+
"Do not wrap the JSON in markdown fences.",
|
|
697
|
+
"",
|
|
698
|
+
`Workspace: ${context.workspacePath}`,
|
|
699
|
+
`Task ID: ${context.taskId}`,
|
|
700
|
+
`Task Title: ${context.taskTitle}`,
|
|
701
|
+
"",
|
|
702
|
+
"Task Description:",
|
|
703
|
+
context.taskDescription,
|
|
704
|
+
"",
|
|
705
|
+
"Execution Requirements:",
|
|
706
|
+
"- Work only inside the provided workspace.",
|
|
707
|
+
"- Read referenced files directly from their paths before acting.",
|
|
708
|
+
"- Do not assume Ralph copied file or image contents into this prompt.",
|
|
709
|
+
"- If no changes are needed, explain why in the summary.",
|
|
710
|
+
"- `changed_files`, `artifacts`, `follow_up_tasks`, `user_checks`, `validation_hints`, and `blockers` must always be arrays.",
|
|
711
|
+
"- Use `blocked` only when progress cannot continue without an external unblocker.",
|
|
712
|
+
"- Use `partial` when you made progress but more work remains.",
|
|
713
|
+
"",
|
|
714
|
+
"Ralph Reference Files:",
|
|
715
|
+
`- Plan: ${context.planPath}`,
|
|
716
|
+
`- Job spec: ${context.specPath}`,
|
|
717
|
+
`- Input manifest: ${context.inputManifestPath}`,
|
|
718
|
+
"",
|
|
719
|
+
"Referenced Input Paths:",
|
|
720
|
+
...context.inputDocuments.length === 0 ? ["- None."] : context.inputDocuments.map((document) => `- [${document.kind}] ${document.path}`),
|
|
721
|
+
"",
|
|
722
|
+
"Open the referenced files as needed and execute the task."
|
|
723
|
+
];
|
|
724
|
+
return `${lines.join("\n")}
|
|
725
|
+
`;
|
|
726
|
+
}
|
|
727
|
+
function buildResultJsonSchema() {
|
|
728
|
+
return {
|
|
729
|
+
type: "object",
|
|
730
|
+
additionalProperties: false,
|
|
731
|
+
required: [
|
|
732
|
+
"status",
|
|
733
|
+
"task_id",
|
|
734
|
+
"summary",
|
|
735
|
+
"changed_files",
|
|
736
|
+
"artifacts",
|
|
737
|
+
"follow_up_tasks",
|
|
738
|
+
"user_checks",
|
|
739
|
+
"validation_hints",
|
|
740
|
+
"blockers"
|
|
741
|
+
],
|
|
742
|
+
properties: {
|
|
743
|
+
status: {
|
|
744
|
+
type: "string",
|
|
745
|
+
enum: ["completed", "partial", "blocked", "failed"]
|
|
746
|
+
},
|
|
747
|
+
task_id: {
|
|
748
|
+
type: "string"
|
|
749
|
+
},
|
|
750
|
+
summary: {
|
|
751
|
+
type: "string"
|
|
752
|
+
},
|
|
753
|
+
changed_files: {
|
|
754
|
+
type: "array",
|
|
755
|
+
items: { type: "string" }
|
|
756
|
+
},
|
|
757
|
+
artifacts: {
|
|
758
|
+
type: "array",
|
|
759
|
+
items: { type: "string" }
|
|
760
|
+
},
|
|
761
|
+
follow_up_tasks: {
|
|
762
|
+
type: "array",
|
|
763
|
+
items: { type: "string" }
|
|
764
|
+
},
|
|
765
|
+
user_checks: {
|
|
766
|
+
type: "array",
|
|
767
|
+
items: { type: "string" }
|
|
768
|
+
},
|
|
769
|
+
validation_hints: {
|
|
770
|
+
type: "array",
|
|
771
|
+
items: { type: "string" }
|
|
772
|
+
},
|
|
773
|
+
blockers: {
|
|
774
|
+
type: "array",
|
|
775
|
+
items: { type: "string" }
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
function buildOutputFileArgs(context) {
|
|
781
|
+
return ["-o", context.outputPath];
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/adapters/run.ts
|
|
785
|
+
var defaultAgentTimeoutMs = 15 * 60 * 1e3;
|
|
786
|
+
var authRequiredPattern = /(auth(entication)? (required|failed)|not logged in|login required|please log in|please login|unauthorized|invalid api key|missing api key|expired token|sign in)/i;
|
|
787
|
+
async function runAdapter(context) {
|
|
788
|
+
const adapter = getAdapterDefinition(context.agent);
|
|
789
|
+
try {
|
|
790
|
+
return await adapter.execute(context);
|
|
791
|
+
} catch (error) {
|
|
792
|
+
throw adapter.normalizeError(error);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
function getAdapterDefinition(agent) {
|
|
796
|
+
switch (agent) {
|
|
797
|
+
case "codex":
|
|
798
|
+
return {
|
|
799
|
+
preparePrompt: buildExecutionPrompt,
|
|
800
|
+
execute: async (runContext) => executeAdapterProcess(
|
|
801
|
+
runContext,
|
|
802
|
+
createProcessSpec(
|
|
803
|
+
"codex",
|
|
804
|
+
[
|
|
805
|
+
"exec",
|
|
806
|
+
"--skip-git-repo-check",
|
|
807
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
808
|
+
"-C",
|
|
809
|
+
runContext.workspacePath,
|
|
810
|
+
"--output-schema",
|
|
811
|
+
`${runContext.runDirectoryPath}/result.schema.json`,
|
|
812
|
+
...buildOutputFileArgs(runContext),
|
|
813
|
+
runContext.promptText
|
|
814
|
+
],
|
|
815
|
+
"file"
|
|
816
|
+
)
|
|
817
|
+
),
|
|
818
|
+
normalizeError: normalizeAdapterError
|
|
819
|
+
};
|
|
820
|
+
case "claude-code":
|
|
821
|
+
return {
|
|
822
|
+
preparePrompt: buildExecutionPrompt,
|
|
823
|
+
execute: async (runContext) => executeAdapterProcess(
|
|
824
|
+
runContext,
|
|
825
|
+
createProcessSpec(
|
|
826
|
+
"claude",
|
|
827
|
+
[
|
|
828
|
+
"-p",
|
|
829
|
+
"--dangerously-skip-permissions",
|
|
830
|
+
"--output-format",
|
|
831
|
+
"json",
|
|
832
|
+
"--json-schema",
|
|
833
|
+
JSON.stringify(buildResultJsonSchema()),
|
|
834
|
+
runContext.promptText
|
|
835
|
+
],
|
|
836
|
+
"stdout"
|
|
837
|
+
)
|
|
838
|
+
),
|
|
839
|
+
normalizeError: normalizeAdapterError
|
|
840
|
+
};
|
|
841
|
+
case "custom-command":
|
|
842
|
+
return {
|
|
843
|
+
preparePrompt: buildExecutionPrompt,
|
|
844
|
+
execute: async (runContext) => {
|
|
845
|
+
const customCommand = process.env.RALPH_CUSTOM_AGENT_COMMAND;
|
|
846
|
+
if (customCommand === void 0 || customCommand.trim().length === 0) {
|
|
847
|
+
throw createAdapterFailure(
|
|
848
|
+
"execution_failed",
|
|
849
|
+
"custom-command adapter requires RALPH_CUSTOM_AGENT_COMMAND."
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
return executeAdapterProcess(
|
|
853
|
+
runContext,
|
|
854
|
+
createProcessSpec("sh", ["-lc", customCommand], "file")
|
|
855
|
+
);
|
|
856
|
+
},
|
|
857
|
+
normalizeError: normalizeAdapterError
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
function parseAgentResult(rawResultText) {
|
|
862
|
+
if (rawResultText === null || rawResultText.trim().length === 0) {
|
|
863
|
+
throw createAdapterFailure("malformed_result", "Agent did not return a result payload.");
|
|
864
|
+
}
|
|
865
|
+
let parsed;
|
|
866
|
+
try {
|
|
867
|
+
parsed = JSON.parse(rawResultText);
|
|
868
|
+
} catch {
|
|
869
|
+
throw createAdapterFailure("malformed_result", "Agent returned invalid JSON.");
|
|
870
|
+
}
|
|
871
|
+
if (isClaudeWrappedResult(parsed)) {
|
|
872
|
+
return parsed.structured_output;
|
|
873
|
+
}
|
|
874
|
+
if (!isAgentResult(parsed)) {
|
|
875
|
+
throw createAdapterFailure("malformed_result", "Agent result did not match the expected contract.");
|
|
876
|
+
}
|
|
877
|
+
return parsed;
|
|
878
|
+
}
|
|
879
|
+
async function persistRunLogs(context, result) {
|
|
880
|
+
await Promise.all([
|
|
881
|
+
atomicWriteFile(`${context.runDirectoryPath}/stdout.log`, `${result.stdout}`),
|
|
882
|
+
atomicWriteFile(`${context.runDirectoryPath}/stderr.log`, `${result.stderr}`)
|
|
883
|
+
]);
|
|
884
|
+
}
|
|
885
|
+
function createAdapterFailure(code, message, options) {
|
|
886
|
+
return {
|
|
887
|
+
code,
|
|
888
|
+
message,
|
|
889
|
+
stdout: options?.stdout ?? "",
|
|
890
|
+
stderr: options?.stderr ?? "",
|
|
891
|
+
exitCode: options?.exitCode ?? null,
|
|
892
|
+
signal: options?.signal ?? null
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
async function executeAdapterProcess(context, processSpec) {
|
|
896
|
+
await atomicWriteJson(`${context.runDirectoryPath}/result.schema.json`, buildResultJsonSchema());
|
|
897
|
+
return new Promise((resolve3, reject) => {
|
|
898
|
+
const child = spawn(processSpec.command, processSpec.args, {
|
|
899
|
+
cwd: context.workspacePath,
|
|
900
|
+
env: {
|
|
901
|
+
...process.env,
|
|
902
|
+
RALPH_WORKSPACE_PATH: context.workspacePath,
|
|
903
|
+
RALPH_PROMPT_PATH: context.promptPath,
|
|
904
|
+
RALPH_OUTPUT_PATH: context.outputPath,
|
|
905
|
+
RALPH_RUN_DIRECTORY_PATH: context.runDirectoryPath
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
let stdout = "";
|
|
909
|
+
let stderr = "";
|
|
910
|
+
let settled = false;
|
|
911
|
+
let timeoutId;
|
|
912
|
+
const finishReject = (error) => {
|
|
913
|
+
if (settled) {
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
settled = true;
|
|
917
|
+
if (timeoutId !== void 0) {
|
|
918
|
+
clearTimeout(timeoutId);
|
|
919
|
+
}
|
|
920
|
+
reject(error);
|
|
921
|
+
};
|
|
922
|
+
const finishResolve = async (exitCode, signal) => {
|
|
923
|
+
if (settled) {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
settled = true;
|
|
927
|
+
if (timeoutId !== void 0) {
|
|
928
|
+
clearTimeout(timeoutId);
|
|
929
|
+
}
|
|
930
|
+
const rawResultText = processSpec.outputMode === "stdout" ? stdout.trim() || null : await readRawResult(context.outputPath);
|
|
931
|
+
resolve3({
|
|
932
|
+
stdout,
|
|
933
|
+
stderr,
|
|
934
|
+
exitCode,
|
|
935
|
+
signal,
|
|
936
|
+
rawResultText
|
|
937
|
+
});
|
|
938
|
+
};
|
|
939
|
+
child.stdout?.on("data", (chunk) => {
|
|
940
|
+
stdout += chunk.toString();
|
|
941
|
+
});
|
|
942
|
+
child.stderr?.on("data", (chunk) => {
|
|
943
|
+
stderr += chunk.toString();
|
|
944
|
+
});
|
|
945
|
+
child.on("error", (error) => {
|
|
946
|
+
const spawnError = error;
|
|
947
|
+
finishReject({
|
|
948
|
+
kind: "spawn",
|
|
949
|
+
message: spawnError.message,
|
|
950
|
+
stdout,
|
|
951
|
+
stderr,
|
|
952
|
+
exitCode: null,
|
|
953
|
+
signal: null,
|
|
954
|
+
code: spawnError.code === "ENOENT" ? "cli_missing" : "execution_failed"
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
child.on("close", (exitCode, signal) => {
|
|
958
|
+
if (exitCode !== 0) {
|
|
959
|
+
finishReject({
|
|
960
|
+
kind: "process_exit",
|
|
961
|
+
message: "Agent process exited with a non-zero status.",
|
|
962
|
+
stdout,
|
|
963
|
+
stderr,
|
|
964
|
+
exitCode,
|
|
965
|
+
signal
|
|
966
|
+
});
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
void finishResolve(exitCode, signal).catch((error) => finishReject(error));
|
|
970
|
+
});
|
|
971
|
+
timeoutId = setTimeout(() => {
|
|
972
|
+
child.kill("SIGTERM");
|
|
973
|
+
finishReject({
|
|
974
|
+
kind: "timeout",
|
|
975
|
+
message: `Agent process timed out after ${String(processSpec.timeoutMs)}ms.`,
|
|
976
|
+
stdout,
|
|
977
|
+
stderr,
|
|
978
|
+
exitCode: null,
|
|
979
|
+
signal: "SIGTERM"
|
|
980
|
+
});
|
|
981
|
+
}, processSpec.timeoutMs);
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
function createProcessSpec(command, args, outputMode) {
|
|
985
|
+
return {
|
|
986
|
+
command,
|
|
987
|
+
args,
|
|
988
|
+
outputMode,
|
|
989
|
+
timeoutMs: getAgentTimeoutMs()
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
function normalizeAdapterError(error) {
|
|
993
|
+
if (isAdapterExecutionFailure(error)) {
|
|
994
|
+
return error;
|
|
995
|
+
}
|
|
996
|
+
if (isAdapterProcessFailure(error)) {
|
|
997
|
+
if (error.kind === "timeout") {
|
|
998
|
+
return createAdapterFailure("timeout", error.message ?? "Agent process timed out.", error);
|
|
999
|
+
}
|
|
1000
|
+
if (error.kind === "spawn" && error.code === "cli_missing") {
|
|
1001
|
+
return createAdapterFailure("cli_missing", error.message ?? "Agent CLI is missing.", error);
|
|
1002
|
+
}
|
|
1003
|
+
if (looksLikeAuthRequired(error.stdout ?? "", error.stderr ?? "", error.message ?? "")) {
|
|
1004
|
+
return createAdapterFailure(
|
|
1005
|
+
"auth_required",
|
|
1006
|
+
error.message ?? "Agent requires authentication before it can run.",
|
|
1007
|
+
error
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
return createAdapterFailure(
|
|
1011
|
+
error.code === "cli_missing" ? "cli_missing" : "execution_failed",
|
|
1012
|
+
error.message ?? "Agent execution failed.",
|
|
1013
|
+
error
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
1017
|
+
return createAdapterFailure("cli_missing", error.message);
|
|
1018
|
+
}
|
|
1019
|
+
return createAdapterFailure(
|
|
1020
|
+
"execution_failed",
|
|
1021
|
+
error instanceof Error ? error.message : "Execution failed."
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
function isAgentResult(value) {
|
|
1025
|
+
if (typeof value !== "object" || value === null) {
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1028
|
+
const candidate = value;
|
|
1029
|
+
const status = candidate.status;
|
|
1030
|
+
return typeof candidate.task_id === "string" && typeof candidate.summary === "string" && typeof status === "string" && ["completed", "partial", "blocked", "failed"].includes(status) && (status !== "blocked" || isStringArray(candidate.blockers) && candidate.blockers.length > 0) && isStringArray(candidate.changed_files) && isStringArray(candidate.artifacts) && isStringArray(candidate.follow_up_tasks) && isStringArray(candidate.user_checks) && isStringArray(candidate.validation_hints) && isStringArray(candidate.blockers);
|
|
1031
|
+
}
|
|
1032
|
+
function isClaudeWrappedResult(value) {
|
|
1033
|
+
if (typeof value !== "object" || value === null) {
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
const candidate = value;
|
|
1037
|
+
return isAgentResult(candidate.structured_output);
|
|
1038
|
+
}
|
|
1039
|
+
function isStringArray(value) {
|
|
1040
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
1041
|
+
}
|
|
1042
|
+
function isAdapterExecutionFailure(error) {
|
|
1043
|
+
return typeof error === "object" && error !== null && "code" in error && "message" in error && "stdout" in error && "stderr" in error;
|
|
1044
|
+
}
|
|
1045
|
+
function isAdapterProcessFailure(error) {
|
|
1046
|
+
return typeof error === "object" && error !== null && "kind" in error;
|
|
1047
|
+
}
|
|
1048
|
+
function looksLikeAuthRequired(stdout, stderr, message) {
|
|
1049
|
+
return authRequiredPattern.test(`${stdout}
|
|
1050
|
+
${stderr}
|
|
1051
|
+
${message}`);
|
|
1052
|
+
}
|
|
1053
|
+
function getAgentTimeoutMs() {
|
|
1054
|
+
const raw = process.env.RALPH_AGENT_TIMEOUT_MS;
|
|
1055
|
+
if (raw === void 0) {
|
|
1056
|
+
return defaultAgentTimeoutMs;
|
|
1057
|
+
}
|
|
1058
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1059
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultAgentTimeoutMs;
|
|
1060
|
+
}
|
|
1061
|
+
async function readRawResult(outputPath) {
|
|
1062
|
+
try {
|
|
1063
|
+
const { readFile: readFile2 } = await import("node:fs/promises");
|
|
1064
|
+
return await readFile2(outputPath, "utf8");
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
throw error;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// src/adapters/index.ts
|
|
1074
|
+
var adapterFeature = {
|
|
1075
|
+
supported: ["codex", "claude-code", "custom-command"]
|
|
1076
|
+
};
|
|
1077
|
+
function isSupportedAgent(value) {
|
|
1078
|
+
return adapterFeature.supported.includes(value);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// src/reporting/index.ts
|
|
1082
|
+
async function updateUserChecksReport(userChecksPath, userChecks) {
|
|
1083
|
+
const existingContent = await readTextFile(userChecksPath) ?? "# User Checks\n\n";
|
|
1084
|
+
const existingChecks = existingContent.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("- ")).map((line) => line.slice(2));
|
|
1085
|
+
const mergedChecks = Array.from(/* @__PURE__ */ new Set([...existingChecks, ...userChecks]));
|
|
1086
|
+
const lines = ["# User Checks", ""];
|
|
1087
|
+
if (mergedChecks.length === 0) {
|
|
1088
|
+
lines.push("None.");
|
|
1089
|
+
} else {
|
|
1090
|
+
lines.push(...mergedChecks.map((entry) => `- ${entry}`));
|
|
1091
|
+
}
|
|
1092
|
+
lines.push("");
|
|
1093
|
+
await atomicWriteFile(userChecksPath, `${lines.join("\n")}`);
|
|
1094
|
+
}
|
|
1095
|
+
async function updateFinalSummaryReport(options) {
|
|
1096
|
+
const runIds = (await listDirectoryEntries(options.runsDirectoryPath)).sort();
|
|
1097
|
+
const runRecords = (await Promise.all(
|
|
1098
|
+
runIds.map(
|
|
1099
|
+
async (runId) => readJsonFile(`${options.runsDirectoryPath}/${runId}/run-record.json`)
|
|
1100
|
+
)
|
|
1101
|
+
)).filter((record) => record !== void 0);
|
|
1102
|
+
if (runRecords.length === 0) {
|
|
1103
|
+
await atomicWriteFile(
|
|
1104
|
+
options.finalSummaryPath,
|
|
1105
|
+
`# Final Summary
|
|
1106
|
+
|
|
1107
|
+
Title: ${options.title}
|
|
1108
|
+
|
|
1109
|
+
Status: ${options.jobStatus}
|
|
1110
|
+
|
|
1111
|
+
Pending.
|
|
1112
|
+
`
|
|
1113
|
+
);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
const latestRun = runRecords.at(-1);
|
|
1117
|
+
const lines = [
|
|
1118
|
+
"# Final Summary",
|
|
1119
|
+
"",
|
|
1120
|
+
`Title: ${options.title}`,
|
|
1121
|
+
"",
|
|
1122
|
+
`Status: ${options.jobStatus}`,
|
|
1123
|
+
`Latest run: ${latestRun.runId}`,
|
|
1124
|
+
`Latest run status: ${latestRun.status}`,
|
|
1125
|
+
"",
|
|
1126
|
+
"Run history:",
|
|
1127
|
+
""
|
|
1128
|
+
];
|
|
1129
|
+
for (const runRecord of runRecords) {
|
|
1130
|
+
lines.push(`## ${runRecord.runId} ${runRecord.taskId}`, "");
|
|
1131
|
+
lines.push(`- Status: ${runRecord.status}`);
|
|
1132
|
+
lines.push(`- Summary: ${runRecord.summary}`);
|
|
1133
|
+
if (runRecord.changedFiles.length > 0) {
|
|
1134
|
+
lines.push("- Changed files:");
|
|
1135
|
+
lines.push(...runRecord.changedFiles.map((value) => ` - ${value}`));
|
|
1136
|
+
}
|
|
1137
|
+
if (runRecord.followUpTasks.length > 0) {
|
|
1138
|
+
lines.push("- Follow-up tasks:");
|
|
1139
|
+
lines.push(...runRecord.followUpTasks.map((value) => ` - ${value}`));
|
|
1140
|
+
}
|
|
1141
|
+
if (runRecord.blockers.length > 0) {
|
|
1142
|
+
lines.push("- Blockers:");
|
|
1143
|
+
lines.push(...runRecord.blockers.map((value) => ` - ${value}`));
|
|
1144
|
+
}
|
|
1145
|
+
lines.push("");
|
|
1146
|
+
}
|
|
1147
|
+
await atomicWriteFile(options.finalSummaryPath, `${lines.join("\n")}
|
|
1148
|
+
`);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/validation/index.ts
|
|
1152
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1153
|
+
function applyValidationOutcome(agentResult, validationRecord) {
|
|
1154
|
+
if (validationRecord.status !== "failed") {
|
|
1155
|
+
return agentResult;
|
|
1156
|
+
}
|
|
1157
|
+
return {
|
|
1158
|
+
...agentResult,
|
|
1159
|
+
status: "failed",
|
|
1160
|
+
blockers: [...agentResult.blockers, validationRecord.summary]
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
async function runValidation(options) {
|
|
1164
|
+
const commandResults = await Promise.all(
|
|
1165
|
+
options.commands.map((command) => executeValidationCommand(command, options.workspacePath))
|
|
1166
|
+
);
|
|
1167
|
+
const hasFailedCommand = commandResults.some((commandResult) => commandResult.status === "failed");
|
|
1168
|
+
const record = {
|
|
1169
|
+
id: `validation-${String(options.validationIndex).padStart(3, "0")}`,
|
|
1170
|
+
status: hasFailedCommand ? "failed" : "passed",
|
|
1171
|
+
hints: options.validationHints,
|
|
1172
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1173
|
+
summary: commandResults.length === 0 ? options.validationHints.length > 0 ? "Validation hints recorded. No executable validations configured yet." : "No executable validations configured. Passing by default." : hasFailedCommand ? "One or more validation commands failed." : "All validation commands passed.",
|
|
1174
|
+
commandResults
|
|
1175
|
+
};
|
|
1176
|
+
await atomicWriteJson(getValidationPath(options.validationsDirectoryPath, options.validationIndex), record);
|
|
1177
|
+
return record;
|
|
1178
|
+
}
|
|
1179
|
+
async function executeValidationCommand(command, workspacePath) {
|
|
1180
|
+
return new Promise((resolve3, reject) => {
|
|
1181
|
+
const child = spawn2("sh", ["-lc", command], {
|
|
1182
|
+
cwd: workspacePath,
|
|
1183
|
+
env: process.env
|
|
1184
|
+
});
|
|
1185
|
+
let stdout = "";
|
|
1186
|
+
let stderr = "";
|
|
1187
|
+
child.stdout.on("data", (chunk) => {
|
|
1188
|
+
stdout += chunk.toString();
|
|
1189
|
+
});
|
|
1190
|
+
child.stderr.on("data", (chunk) => {
|
|
1191
|
+
stderr += chunk.toString();
|
|
1192
|
+
});
|
|
1193
|
+
child.on("error", (error) => {
|
|
1194
|
+
reject(error);
|
|
1195
|
+
});
|
|
1196
|
+
child.on("close", (exitCode, signal) => {
|
|
1197
|
+
resolve3({
|
|
1198
|
+
command,
|
|
1199
|
+
exitCode,
|
|
1200
|
+
signal,
|
|
1201
|
+
stdout,
|
|
1202
|
+
stderr,
|
|
1203
|
+
status: exitCode === 0 ? "passed" : "failed"
|
|
1204
|
+
});
|
|
1205
|
+
});
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// src/execution/index.ts
|
|
1210
|
+
async function resumeJob(options) {
|
|
1211
|
+
const maxIterations = options.maxIterations ?? Number.POSITIVE_INFINITY;
|
|
1212
|
+
let snapshot = await loadJob(options);
|
|
1213
|
+
const runRecords = [];
|
|
1214
|
+
let lastRunDirectoryPath = "";
|
|
1215
|
+
if (isTerminalJobStatus(snapshot.job.status)) {
|
|
1216
|
+
throw new Error(`Job ${snapshot.job.id} is already ${snapshot.job.status}.`);
|
|
1217
|
+
}
|
|
1218
|
+
while (runRecords.length < maxIterations) {
|
|
1219
|
+
const runnableTask = selectRunnableTask(snapshot);
|
|
1220
|
+
if (runnableTask === void 0) {
|
|
1221
|
+
snapshot = await blockJobOnNoRunnableTask(snapshot);
|
|
1222
|
+
break;
|
|
1223
|
+
}
|
|
1224
|
+
const iterationResult = await executeIteration(snapshot, runnableTask);
|
|
1225
|
+
snapshot = iterationResult.snapshot;
|
|
1226
|
+
runRecords.push(iterationResult.runRecord);
|
|
1227
|
+
lastRunDirectoryPath = iterationResult.runDirectoryPath;
|
|
1228
|
+
if (snapshot.runtime.nextAction !== "resume") {
|
|
1229
|
+
break;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (runRecords.length === 0 && snapshot.job.status !== "blocked") {
|
|
1233
|
+
throw new Error(`Job ${snapshot.job.id} has no runnable task.`);
|
|
1234
|
+
}
|
|
1235
|
+
return {
|
|
1236
|
+
snapshot,
|
|
1237
|
+
runRecord: runRecords.at(-1) ?? null,
|
|
1238
|
+
runRecords,
|
|
1239
|
+
runDirectoryPath: lastRunDirectoryPath || null,
|
|
1240
|
+
iterations: runRecords.length
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
async function executeIteration(snapshot, runnableTask) {
|
|
1244
|
+
const paths = getJobPaths(snapshot.job.id, snapshot.job.stateDirectoryPath);
|
|
1245
|
+
const runId = getNextRunId(snapshot.runtime.lastRunId);
|
|
1246
|
+
const runPaths = getRunPaths(paths.runsDirectoryPath, runId);
|
|
1247
|
+
const adapter = getAdapterDefinition(snapshot.job.requestedAgent);
|
|
1248
|
+
const promptText = adapter.preparePrompt({
|
|
1249
|
+
title: snapshot.job.title,
|
|
1250
|
+
taskId: runnableTask.id,
|
|
1251
|
+
taskTitle: runnableTask.title,
|
|
1252
|
+
taskDescription: runnableTask.description,
|
|
1253
|
+
workspacePath: snapshot.job.workspacePath,
|
|
1254
|
+
specPath: paths.specMarkdownPath,
|
|
1255
|
+
planPath: paths.planMarkdownPath,
|
|
1256
|
+
inputManifestPath: paths.inputsJsonPath,
|
|
1257
|
+
inputDocuments: snapshot.job.inputDocuments
|
|
1258
|
+
});
|
|
1259
|
+
await ensureRunDirectory(runPaths.runDirectoryPath);
|
|
1260
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1261
|
+
const runningSnapshot = buildRunningSnapshot(snapshot, runnableTask, runId);
|
|
1262
|
+
const runningRunRecord = buildRunningRunRecord({
|
|
1263
|
+
runId,
|
|
1264
|
+
taskId: runnableTask.id,
|
|
1265
|
+
agent: snapshot.job.requestedAgent,
|
|
1266
|
+
startedAt
|
|
1267
|
+
});
|
|
1268
|
+
await Promise.all([
|
|
1269
|
+
atomicWriteFile(runPaths.promptPath, promptText),
|
|
1270
|
+
atomicWriteFile(runPaths.stdoutLogPath, ""),
|
|
1271
|
+
atomicWriteFile(runPaths.stderrLogPath, ""),
|
|
1272
|
+
atomicWriteJson(runPaths.runRecordPath, runningRunRecord),
|
|
1273
|
+
saveJobSnapshot(runningSnapshot)
|
|
1274
|
+
]);
|
|
1275
|
+
try {
|
|
1276
|
+
const adapterResult = await runAdapter({
|
|
1277
|
+
agent: runningSnapshot.job.requestedAgent,
|
|
1278
|
+
workspacePath: runningSnapshot.job.workspacePath,
|
|
1279
|
+
promptPath: runPaths.promptPath,
|
|
1280
|
+
promptText,
|
|
1281
|
+
outputPath: runPaths.outputPath,
|
|
1282
|
+
runDirectoryPath: runPaths.runDirectoryPath
|
|
1283
|
+
});
|
|
1284
|
+
await persistRunLogs(buildRunContext(runningSnapshot, runPaths, promptText), adapterResult);
|
|
1285
|
+
const agentResult = parseAgentResult(adapterResult.rawResultText);
|
|
1286
|
+
if (agentResult.task_id !== runnableTask.id) {
|
|
1287
|
+
throw {
|
|
1288
|
+
code: "malformed_result",
|
|
1289
|
+
message: `Agent returned task_id ${agentResult.task_id}, expected ${runnableTask.id}.`,
|
|
1290
|
+
stdout: adapterResult.stdout,
|
|
1291
|
+
stderr: adapterResult.stderr,
|
|
1292
|
+
exitCode: adapterResult.exitCode,
|
|
1293
|
+
signal: adapterResult.signal
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
const validationRecord = await runValidation({
|
|
1297
|
+
jobId: runningSnapshot.job.id,
|
|
1298
|
+
taskId: runnableTask.id,
|
|
1299
|
+
validationHints: agentResult.validation_hints,
|
|
1300
|
+
workspacePath: runningSnapshot.job.workspacePath,
|
|
1301
|
+
commands: runningSnapshot.job.validationProfile.commands,
|
|
1302
|
+
validationsDirectoryPath: paths.validationsDirectoryPath,
|
|
1303
|
+
validationIndex: toRunNumber(runId)
|
|
1304
|
+
});
|
|
1305
|
+
const normalizedResult = applyValidationOutcome(agentResult, validationRecord);
|
|
1306
|
+
const runRecord = buildRunRecord({
|
|
1307
|
+
runId,
|
|
1308
|
+
taskId: runnableTask.id,
|
|
1309
|
+
agent: runningSnapshot.job.requestedAgent,
|
|
1310
|
+
startedAt,
|
|
1311
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1312
|
+
result: normalizedResult,
|
|
1313
|
+
exitCode: adapterResult.exitCode,
|
|
1314
|
+
signal: adapterResult.signal
|
|
1315
|
+
});
|
|
1316
|
+
await Promise.all([
|
|
1317
|
+
atomicWriteJson(runPaths.resultPath, normalizedResult),
|
|
1318
|
+
atomicWriteJson(runPaths.runRecordPath, runRecord)
|
|
1319
|
+
]);
|
|
1320
|
+
const nextSnapshot = applyTaskOutcome({
|
|
1321
|
+
snapshot: runningSnapshot,
|
|
1322
|
+
task: runnableTask,
|
|
1323
|
+
runId,
|
|
1324
|
+
runRecord,
|
|
1325
|
+
result: normalizedResult,
|
|
1326
|
+
validationStatus: validationRecord.status
|
|
1327
|
+
});
|
|
1328
|
+
await saveJobSnapshot(nextSnapshot);
|
|
1329
|
+
await Promise.all([
|
|
1330
|
+
updateUserChecksReport(paths.userChecksPath, collectUserChecks(nextSnapshot, runRecord)),
|
|
1331
|
+
updateFinalSummaryReport({
|
|
1332
|
+
finalSummaryPath: paths.finalSummaryPath,
|
|
1333
|
+
title: runningSnapshot.job.title,
|
|
1334
|
+
jobStatus: nextSnapshot.job.status,
|
|
1335
|
+
runsDirectoryPath: paths.runsDirectoryPath
|
|
1336
|
+
})
|
|
1337
|
+
]);
|
|
1338
|
+
return {
|
|
1339
|
+
snapshot: nextSnapshot,
|
|
1340
|
+
runRecord,
|
|
1341
|
+
runDirectoryPath: runPaths.runDirectoryPath
|
|
1342
|
+
};
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
const failure = normalizeExecutionFailure(error);
|
|
1345
|
+
await persistRunLogs(buildRunContext(runningSnapshot, runPaths, promptText), failure);
|
|
1346
|
+
const failedResult = {
|
|
1347
|
+
status: "failed",
|
|
1348
|
+
task_id: runnableTask.id,
|
|
1349
|
+
summary: failure.message,
|
|
1350
|
+
changed_files: [],
|
|
1351
|
+
artifacts: [],
|
|
1352
|
+
follow_up_tasks: [],
|
|
1353
|
+
user_checks: [],
|
|
1354
|
+
validation_hints: [],
|
|
1355
|
+
blockers: failure.code === "cli_missing" ? [failure.message] : []
|
|
1356
|
+
};
|
|
1357
|
+
const runRecord = buildRunRecord({
|
|
1358
|
+
runId,
|
|
1359
|
+
taskId: runnableTask.id,
|
|
1360
|
+
agent: runningSnapshot.job.requestedAgent,
|
|
1361
|
+
startedAt,
|
|
1362
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1363
|
+
result: failedResult,
|
|
1364
|
+
exitCode: failure.exitCode,
|
|
1365
|
+
signal: failure.signal,
|
|
1366
|
+
errorCode: failure.code
|
|
1367
|
+
});
|
|
1368
|
+
await Promise.all([
|
|
1369
|
+
atomicWriteJson(runPaths.resultPath, failedResult),
|
|
1370
|
+
atomicWriteJson(runPaths.runRecordPath, runRecord)
|
|
1371
|
+
]);
|
|
1372
|
+
const nextSnapshot = applyTaskOutcome({
|
|
1373
|
+
snapshot: runningSnapshot,
|
|
1374
|
+
task: runnableTask,
|
|
1375
|
+
runId,
|
|
1376
|
+
runRecord,
|
|
1377
|
+
result: failedResult,
|
|
1378
|
+
validationStatus: "failed",
|
|
1379
|
+
failureCode: failure.code
|
|
1380
|
+
});
|
|
1381
|
+
await saveJobSnapshot(nextSnapshot);
|
|
1382
|
+
await updateFinalSummaryReport({
|
|
1383
|
+
finalSummaryPath: paths.finalSummaryPath,
|
|
1384
|
+
title: runningSnapshot.job.title,
|
|
1385
|
+
jobStatus: nextSnapshot.job.status,
|
|
1386
|
+
runsDirectoryPath: paths.runsDirectoryPath
|
|
1387
|
+
});
|
|
1388
|
+
return {
|
|
1389
|
+
snapshot: nextSnapshot,
|
|
1390
|
+
runRecord,
|
|
1391
|
+
runDirectoryPath: runPaths.runDirectoryPath
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
function buildRunContext(snapshot, runPaths, promptText) {
|
|
1396
|
+
return {
|
|
1397
|
+
agent: snapshot.job.requestedAgent,
|
|
1398
|
+
workspacePath: snapshot.job.workspacePath,
|
|
1399
|
+
promptPath: runPaths.promptPath,
|
|
1400
|
+
promptText,
|
|
1401
|
+
outputPath: runPaths.outputPath,
|
|
1402
|
+
runDirectoryPath: runPaths.runDirectoryPath
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
function applyTaskOutcome(options) {
|
|
1406
|
+
const { snapshot, task, runId, runRecord, result } = options;
|
|
1407
|
+
const nextRetryCounts = {
|
|
1408
|
+
...snapshot.tasks.retryCounts
|
|
1409
|
+
};
|
|
1410
|
+
const nextEvidenceLinks = {
|
|
1411
|
+
...snapshot.tasks.evidenceLinks,
|
|
1412
|
+
[task.id]: [...snapshot.tasks.evidenceLinks[task.id] ?? [], runId]
|
|
1413
|
+
};
|
|
1414
|
+
const nextDependencies = Object.fromEntries(
|
|
1415
|
+
Object.entries(snapshot.tasks.dependencies).map(([key, value]) => [key, [...value]])
|
|
1416
|
+
);
|
|
1417
|
+
const nextTasks = snapshot.tasks.tasks.map(
|
|
1418
|
+
(entry) => entry.id === task.id ? { ...entry } : entry
|
|
1419
|
+
);
|
|
1420
|
+
const targetTask = nextTasks.find((entry) => entry.id === task.id);
|
|
1421
|
+
const canRetry = nextRetryCounts[task.id] < snapshot.job.retryPolicy.maxRetriesPerTask;
|
|
1422
|
+
if (result.status === "completed") {
|
|
1423
|
+
targetTask.status = "completed";
|
|
1424
|
+
} else if (result.status === "partial") {
|
|
1425
|
+
if (result.follow_up_tasks.length > 0) {
|
|
1426
|
+
targetTask.status = "completed";
|
|
1427
|
+
appendFollowUpTasks({
|
|
1428
|
+
dependencies: nextDependencies,
|
|
1429
|
+
nextTasks,
|
|
1430
|
+
nextRetryCounts,
|
|
1431
|
+
nextEvidenceLinks,
|
|
1432
|
+
sourceTask: task,
|
|
1433
|
+
followUpTasks: result.follow_up_tasks
|
|
1434
|
+
});
|
|
1435
|
+
} else if (canRetry) {
|
|
1436
|
+
targetTask.status = "pending";
|
|
1437
|
+
nextRetryCounts[task.id] += 1;
|
|
1438
|
+
} else {
|
|
1439
|
+
targetTask.status = "blocked";
|
|
1440
|
+
result.blockers = [
|
|
1441
|
+
...result.blockers,
|
|
1442
|
+
"Partial result exhausted retry budget without follow-up tasks."
|
|
1443
|
+
];
|
|
1444
|
+
}
|
|
1445
|
+
} else if (result.status === "failed") {
|
|
1446
|
+
if (canRetry) {
|
|
1447
|
+
targetTask.status = "pending";
|
|
1448
|
+
nextRetryCounts[task.id] += 1;
|
|
1449
|
+
} else {
|
|
1450
|
+
targetTask.status = options.failureCode === "malformed_result" ? "blocked" : "failed";
|
|
1451
|
+
}
|
|
1452
|
+
} else if (result.status === "blocked") {
|
|
1453
|
+
targetTask.status = "blocked";
|
|
1454
|
+
}
|
|
1455
|
+
if (result.status === "completed" && result.follow_up_tasks.length > 0) {
|
|
1456
|
+
appendFollowUpTasks({
|
|
1457
|
+
dependencies: nextDependencies,
|
|
1458
|
+
nextTasks,
|
|
1459
|
+
nextRetryCounts,
|
|
1460
|
+
nextEvidenceLinks,
|
|
1461
|
+
sourceTask: task,
|
|
1462
|
+
followUpTasks: result.follow_up_tasks
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
const phaseGateStatus = derivePhaseGateStatus(snapshot.tasks.phases, nextTasks);
|
|
1466
|
+
const remainingTaskCount = countRemainingTasks(nextTasks);
|
|
1467
|
+
const blockedReason = nextTasks.some((entry) => entry.status === "blocked") ? result.blockers.join("; ") || runRecord.summary : null;
|
|
1468
|
+
const hasFailedTask = nextTasks.some((entry) => entry.status === "failed");
|
|
1469
|
+
const allRequiredWorkCompleted = nextTasks.length > 0 && nextTasks.every((entry) => entry.status === "completed");
|
|
1470
|
+
const allPhaseGatesPassed = Object.values(phaseGateStatus).length > 0 && Object.values(phaseGateStatus).every((status) => status === "passed");
|
|
1471
|
+
const nextAction = blockedReason !== null ? "blocked" : remainingTaskCount > 0 ? "resume" : "none";
|
|
1472
|
+
const nextJobStatus = blockedReason !== null ? "blocked" : hasFailedTask ? "failed" : remainingTaskCount === 0 && allRequiredWorkCompleted && allPhaseGatesPassed ? "completed" : "running";
|
|
1473
|
+
const currentTaskId = nextPendingTaskId(snapshot.tasks.phases, nextTasks, nextDependencies);
|
|
1474
|
+
const currentPhaseId = currentTaskId === null ? snapshot.tasks.phases.find((phase) => phaseGateStatus[phase.id] !== "passed")?.id ?? null : nextTasks.find((entry) => entry.id === currentTaskId)?.phaseId ?? snapshot.runtime.currentPhaseId;
|
|
1475
|
+
return {
|
|
1476
|
+
job: {
|
|
1477
|
+
...snapshot.job,
|
|
1478
|
+
status: nextJobStatus,
|
|
1479
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1480
|
+
},
|
|
1481
|
+
tasks: {
|
|
1482
|
+
...snapshot.tasks,
|
|
1483
|
+
tasks: nextTasks,
|
|
1484
|
+
dependencies: nextDependencies,
|
|
1485
|
+
retryCounts: nextRetryCounts,
|
|
1486
|
+
evidenceLinks: nextEvidenceLinks,
|
|
1487
|
+
phaseGateStatus
|
|
1488
|
+
},
|
|
1489
|
+
runtime: {
|
|
1490
|
+
...snapshot.runtime,
|
|
1491
|
+
currentPhaseId,
|
|
1492
|
+
currentTaskId,
|
|
1493
|
+
remainingTaskCount,
|
|
1494
|
+
lastRunId: runId,
|
|
1495
|
+
nextAction,
|
|
1496
|
+
blockedReason,
|
|
1497
|
+
lastValidationStatus: options.validationStatus
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
function appendFollowUpTasks(options) {
|
|
1502
|
+
const nextTaskNumber = options.nextTasks.length + 1;
|
|
1503
|
+
options.followUpTasks.forEach((followUpTask, index) => {
|
|
1504
|
+
const taskId = `TASK-${String(nextTaskNumber + index).padStart(3, "0")}`;
|
|
1505
|
+
options.nextTasks.push({
|
|
1506
|
+
id: taskId,
|
|
1507
|
+
phaseId: options.sourceTask.phaseId,
|
|
1508
|
+
title: followUpTask,
|
|
1509
|
+
description: followUpTask,
|
|
1510
|
+
status: "pending",
|
|
1511
|
+
sourceTaskId: options.sourceTask.id
|
|
1512
|
+
});
|
|
1513
|
+
options.dependencies[taskId] = [options.sourceTask.id];
|
|
1514
|
+
options.nextRetryCounts[taskId] = 0;
|
|
1515
|
+
options.nextEvidenceLinks[taskId] = [];
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
function buildRunRecord(options) {
|
|
1519
|
+
return {
|
|
1520
|
+
runId: options.runId,
|
|
1521
|
+
taskId: options.taskId,
|
|
1522
|
+
agent: options.agent,
|
|
1523
|
+
startedAt: options.startedAt,
|
|
1524
|
+
finishedAt: options.finishedAt,
|
|
1525
|
+
status: options.result.status,
|
|
1526
|
+
summary: options.result.summary,
|
|
1527
|
+
changedFiles: options.result.changed_files,
|
|
1528
|
+
blockers: options.result.blockers,
|
|
1529
|
+
userChecks: options.result.user_checks,
|
|
1530
|
+
validationHints: options.result.validation_hints,
|
|
1531
|
+
artifacts: options.result.artifacts,
|
|
1532
|
+
followUpTasks: options.result.follow_up_tasks,
|
|
1533
|
+
exitCode: options.exitCode,
|
|
1534
|
+
signal: options.signal,
|
|
1535
|
+
errorCode: options.errorCode
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
function buildRunningRunRecord(options) {
|
|
1539
|
+
return {
|
|
1540
|
+
runId: options.runId,
|
|
1541
|
+
taskId: options.taskId,
|
|
1542
|
+
agent: options.agent,
|
|
1543
|
+
startedAt: options.startedAt,
|
|
1544
|
+
finishedAt: null,
|
|
1545
|
+
status: "running",
|
|
1546
|
+
summary: `Started ${options.taskId}.`,
|
|
1547
|
+
changedFiles: [],
|
|
1548
|
+
blockers: [],
|
|
1549
|
+
userChecks: [],
|
|
1550
|
+
validationHints: [],
|
|
1551
|
+
artifacts: [],
|
|
1552
|
+
followUpTasks: [],
|
|
1553
|
+
exitCode: null,
|
|
1554
|
+
signal: null
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
function isTerminalJobStatus(status) {
|
|
1558
|
+
return isJobTerminal(status);
|
|
1559
|
+
}
|
|
1560
|
+
function buildRunningSnapshot(snapshot, task, runId) {
|
|
1561
|
+
const nextTasks = snapshot.tasks.tasks.map(
|
|
1562
|
+
(entry) => entry.id === task.id ? {
|
|
1563
|
+
...entry,
|
|
1564
|
+
status: "running"
|
|
1565
|
+
} : entry
|
|
1566
|
+
);
|
|
1567
|
+
return {
|
|
1568
|
+
job: {
|
|
1569
|
+
...snapshot.job,
|
|
1570
|
+
status: "running",
|
|
1571
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1572
|
+
},
|
|
1573
|
+
tasks: {
|
|
1574
|
+
...snapshot.tasks,
|
|
1575
|
+
tasks: nextTasks
|
|
1576
|
+
},
|
|
1577
|
+
runtime: {
|
|
1578
|
+
...snapshot.runtime,
|
|
1579
|
+
currentPhaseId: task.phaseId,
|
|
1580
|
+
currentTaskId: task.id,
|
|
1581
|
+
remainingTaskCount: countRemainingTasks(nextTasks),
|
|
1582
|
+
lastRunId: runId,
|
|
1583
|
+
nextAction: "resume",
|
|
1584
|
+
blockedReason: null,
|
|
1585
|
+
lastValidationStatus: "pending"
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
function collectUserChecks(snapshot, runRecord) {
|
|
1590
|
+
return runRecord.userChecks;
|
|
1591
|
+
}
|
|
1592
|
+
function toRunNumber(runId) {
|
|
1593
|
+
return Number.parseInt(runId.replace("run-", ""), 10);
|
|
1594
|
+
}
|
|
1595
|
+
function normalizeExecutionFailure(error) {
|
|
1596
|
+
if (typeof error === "object" && error !== null && "code" in error && "message" in error && "stdout" in error && "stderr" in error) {
|
|
1597
|
+
return error;
|
|
1598
|
+
}
|
|
1599
|
+
return {
|
|
1600
|
+
code: "execution_failed",
|
|
1601
|
+
message: error instanceof Error ? error.message : "Execution failed.",
|
|
1602
|
+
stdout: "",
|
|
1603
|
+
stderr: "",
|
|
1604
|
+
exitCode: null,
|
|
1605
|
+
signal: null
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
async function blockJobOnNoRunnableTask(snapshot) {
|
|
1609
|
+
const reason = buildNoRunnableTaskReason(snapshot);
|
|
1610
|
+
const paths = getJobPaths(snapshot.job.id, snapshot.job.stateDirectoryPath);
|
|
1611
|
+
const nextSnapshot = {
|
|
1612
|
+
job: {
|
|
1613
|
+
...snapshot.job,
|
|
1614
|
+
status: "blocked",
|
|
1615
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1616
|
+
},
|
|
1617
|
+
tasks: {
|
|
1618
|
+
...snapshot.tasks,
|
|
1619
|
+
phaseGateStatus: derivePhaseGateStatus(snapshot.tasks.phases, snapshot.tasks.tasks)
|
|
1620
|
+
},
|
|
1621
|
+
runtime: {
|
|
1622
|
+
...buildBlockedRuntime(snapshot, reason),
|
|
1623
|
+
lastValidationStatus: snapshot.runtime.lastValidationStatus
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
await saveJobSnapshot(nextSnapshot);
|
|
1627
|
+
await updateFinalSummaryReport({
|
|
1628
|
+
finalSummaryPath: paths.finalSummaryPath,
|
|
1629
|
+
title: snapshot.job.title,
|
|
1630
|
+
jobStatus: nextSnapshot.job.status,
|
|
1631
|
+
runsDirectoryPath: paths.runsDirectoryPath
|
|
1632
|
+
});
|
|
1633
|
+
return nextSnapshot;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// src/cli/resume/index.ts
|
|
1637
|
+
async function runResumeCommand(argv) {
|
|
1638
|
+
const parsedArgs = parseCliArgs(argv);
|
|
1639
|
+
const jobId = getSingleFlag(parsedArgs, "--job");
|
|
1640
|
+
const workspacePath = getSingleFlag(parsedArgs, "--workspace");
|
|
1641
|
+
const stateDirectoryPath = getSingleFlag(parsedArgs, "--state-dir");
|
|
1642
|
+
const maxIterationsRaw = getSingleFlag(parsedArgs, "--max-iterations");
|
|
1643
|
+
const jsonMode = hasFlag(parsedArgs, "--json");
|
|
1644
|
+
if (workspacePath === void 0 && stateDirectoryPath === void 0) {
|
|
1645
|
+
printError("Either --workspace or --state-dir is required.");
|
|
1646
|
+
return 1;
|
|
1647
|
+
}
|
|
1648
|
+
try {
|
|
1649
|
+
const parsedMaxIterations = maxIterationsRaw === void 0 ? void 0 : Number.parseInt(maxIterationsRaw, 10);
|
|
1650
|
+
const invalidMaxIterations = parsedMaxIterations === void 0 || !Number.isFinite(parsedMaxIterations) || parsedMaxIterations <= 0;
|
|
1651
|
+
if (maxIterationsRaw !== void 0 && invalidMaxIterations) {
|
|
1652
|
+
printError("`--max-iterations` must be a positive integer.");
|
|
1653
|
+
return 1;
|
|
1654
|
+
}
|
|
1655
|
+
const result = await resumeJob({
|
|
1656
|
+
jobId,
|
|
1657
|
+
workspacePath,
|
|
1658
|
+
stateDirectoryPath,
|
|
1659
|
+
maxIterations: parsedMaxIterations
|
|
1660
|
+
});
|
|
1661
|
+
if (jsonMode) {
|
|
1662
|
+
printJson({
|
|
1663
|
+
job_id: result.snapshot.job.id,
|
|
1664
|
+
job_status: result.snapshot.job.status,
|
|
1665
|
+
run_id: result.runRecord?.runId ?? null,
|
|
1666
|
+
run_status: result.runRecord?.status ?? null,
|
|
1667
|
+
summary: result.runRecord?.summary ?? result.snapshot.runtime.blockedReason,
|
|
1668
|
+
run_directory_path: result.runDirectoryPath,
|
|
1669
|
+
iterations: result.iterations,
|
|
1670
|
+
next_action: result.snapshot.runtime.nextAction,
|
|
1671
|
+
blocked_reason: result.snapshot.runtime.blockedReason
|
|
1672
|
+
});
|
|
1673
|
+
return 0;
|
|
1674
|
+
}
|
|
1675
|
+
printLine(`Job: ${result.snapshot.job.id}`);
|
|
1676
|
+
printLine(`Run: ${result.runRecord?.runId ?? "-"}`);
|
|
1677
|
+
printLine(`Run status: ${result.runRecord?.status ?? "-"}`);
|
|
1678
|
+
printLine(`Job status: ${result.snapshot.job.status}`);
|
|
1679
|
+
printLine(`Summary: ${result.runRecord?.summary ?? result.snapshot.runtime.blockedReason ?? "-"}`);
|
|
1680
|
+
printLine(`Run dir: ${result.runDirectoryPath ?? "-"}`);
|
|
1681
|
+
printLine(`Iterations: ${String(result.iterations)}`);
|
|
1682
|
+
printLine(`Next action: ${result.snapshot.runtime.nextAction}`);
|
|
1683
|
+
return 0;
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
printError(error instanceof Error ? error.message : "Failed to resume job.");
|
|
1686
|
+
return 1;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// src/cli/start/index.ts
|
|
1691
|
+
async function runStartCommand(argv) {
|
|
1692
|
+
const parsedArgs = parseCliArgs(argv);
|
|
1693
|
+
const title = getSingleFlag(parsedArgs, "--title");
|
|
1694
|
+
const agent = getSingleFlag(parsedArgs, "--agent");
|
|
1695
|
+
const workspacePath = getSingleFlag(parsedArgs, "--workspace");
|
|
1696
|
+
const stateDirectoryPath = getSingleFlag(parsedArgs, "--state-dir");
|
|
1697
|
+
const inputDocuments = getMultiFlag(parsedArgs, "--input");
|
|
1698
|
+
const validateCommands = getMultiFlag(parsedArgs, "--validate-cmd");
|
|
1699
|
+
const maxRetriesPerTaskRaw = getSingleFlag(parsedArgs, "--max-retries");
|
|
1700
|
+
const jsonMode = hasFlag(parsedArgs, "--json");
|
|
1701
|
+
if (title === void 0) {
|
|
1702
|
+
printError("Missing required option: --title");
|
|
1703
|
+
return 1;
|
|
1704
|
+
}
|
|
1705
|
+
if (agent === void 0) {
|
|
1706
|
+
printError("Missing required option: --agent");
|
|
1707
|
+
return 1;
|
|
1708
|
+
}
|
|
1709
|
+
if (workspacePath === void 0) {
|
|
1710
|
+
printError("Missing required option: --workspace");
|
|
1711
|
+
return 1;
|
|
1712
|
+
}
|
|
1713
|
+
if (!isSupportedAgent(agent)) {
|
|
1714
|
+
printError(
|
|
1715
|
+
`Unsupported agent: ${agent}. Supported agents: ${adapterFeature.supported.join(", ")}`
|
|
1716
|
+
);
|
|
1717
|
+
return 1;
|
|
1718
|
+
}
|
|
1719
|
+
try {
|
|
1720
|
+
const parsedMaxRetriesPerTask = maxRetriesPerTaskRaw === void 0 ? void 0 : Number.parseInt(maxRetriesPerTaskRaw, 10);
|
|
1721
|
+
const invalidMaxRetries = parsedMaxRetriesPerTask === void 0 || !Number.isFinite(parsedMaxRetriesPerTask) || parsedMaxRetriesPerTask < 0;
|
|
1722
|
+
if (maxRetriesPerTaskRaw !== void 0 && invalidMaxRetries) {
|
|
1723
|
+
printError("`--max-retries` must be a non-negative integer.");
|
|
1724
|
+
return 1;
|
|
1725
|
+
}
|
|
1726
|
+
const { snapshot, paths } = await createJob({
|
|
1727
|
+
title,
|
|
1728
|
+
agent,
|
|
1729
|
+
workspacePath,
|
|
1730
|
+
stateDirectoryPath,
|
|
1731
|
+
inputDocuments,
|
|
1732
|
+
validateCommands,
|
|
1733
|
+
maxRetriesPerTask: parsedMaxRetriesPerTask
|
|
1734
|
+
});
|
|
1735
|
+
if (jsonMode) {
|
|
1736
|
+
printJson({
|
|
1737
|
+
job_id: snapshot.job.id,
|
|
1738
|
+
status: snapshot.job.status,
|
|
1739
|
+
workspace_path: snapshot.job.workspacePath,
|
|
1740
|
+
state_directory_path: snapshot.job.stateDirectoryPath,
|
|
1741
|
+
validation_commands: snapshot.job.validationProfile.commands,
|
|
1742
|
+
max_retries_per_task: snapshot.job.retryPolicy.maxRetriesPerTask,
|
|
1743
|
+
next_action: snapshot.runtime.nextAction,
|
|
1744
|
+
current_task_id: snapshot.runtime.currentTaskId
|
|
1745
|
+
});
|
|
1746
|
+
return 0;
|
|
1747
|
+
}
|
|
1748
|
+
printLine(`Started job ${snapshot.job.id}`);
|
|
1749
|
+
printLine(`Status: ${snapshot.job.status}`);
|
|
1750
|
+
printLine(`Workspace: ${snapshot.job.workspacePath}`);
|
|
1751
|
+
printLine(`State dir: ${snapshot.job.stateDirectoryPath}`);
|
|
1752
|
+
printLine(`Job dir: ${paths.jobDirectoryPath}`);
|
|
1753
|
+
if (snapshot.job.inputDocuments.length > 0) {
|
|
1754
|
+
printLine(`Inputs: ${snapshot.job.inputDocuments.map((entry) => entry.path).join(", ")}`);
|
|
1755
|
+
}
|
|
1756
|
+
if (snapshot.job.validationProfile.commands.length > 0) {
|
|
1757
|
+
printLine(`Validation commands: ${snapshot.job.validationProfile.commands.join(" | ")}`);
|
|
1758
|
+
}
|
|
1759
|
+
printLine(`Max retries per task: ${String(snapshot.job.retryPolicy.maxRetriesPerTask)}`);
|
|
1760
|
+
printLine("Next: `ralph status` or `ralph resume`");
|
|
1761
|
+
return 0;
|
|
1762
|
+
} catch (error) {
|
|
1763
|
+
printError(error instanceof Error ? error.message : "Failed to start job.");
|
|
1764
|
+
return 1;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// src/cli/status/index.ts
|
|
1769
|
+
async function runStatusCommand(argv) {
|
|
1770
|
+
const parsedArgs = parseCliArgs(argv);
|
|
1771
|
+
const jobId = getSingleFlag(parsedArgs, "--job");
|
|
1772
|
+
const workspacePath = getSingleFlag(parsedArgs, "--workspace");
|
|
1773
|
+
const stateDirectoryPath = getSingleFlag(parsedArgs, "--state-dir");
|
|
1774
|
+
const jsonMode = hasFlag(parsedArgs, "--json");
|
|
1775
|
+
if (workspacePath === void 0 && stateDirectoryPath === void 0) {
|
|
1776
|
+
printError("Either --workspace or --state-dir is required.");
|
|
1777
|
+
return 1;
|
|
1778
|
+
}
|
|
1779
|
+
try {
|
|
1780
|
+
const { snapshot, runIds } = await getJobOverview({
|
|
1781
|
+
jobId,
|
|
1782
|
+
workspacePath,
|
|
1783
|
+
stateDirectoryPath
|
|
1784
|
+
});
|
|
1785
|
+
if (jsonMode) {
|
|
1786
|
+
printJson({
|
|
1787
|
+
job_id: snapshot.job.id,
|
|
1788
|
+
title: snapshot.job.title,
|
|
1789
|
+
status: snapshot.job.status,
|
|
1790
|
+
agent: snapshot.job.requestedAgent,
|
|
1791
|
+
workspace_path: snapshot.job.workspacePath,
|
|
1792
|
+
state_directory_path: snapshot.job.stateDirectoryPath,
|
|
1793
|
+
current_phase_id: snapshot.runtime.currentPhaseId,
|
|
1794
|
+
current_task_id: snapshot.runtime.currentTaskId,
|
|
1795
|
+
remaining_task_count: snapshot.runtime.remainingTaskCount,
|
|
1796
|
+
next_action: snapshot.runtime.nextAction,
|
|
1797
|
+
blocked_reason: snapshot.runtime.blockedReason,
|
|
1798
|
+
last_validation_status: snapshot.runtime.lastValidationStatus,
|
|
1799
|
+
max_retries_per_task: snapshot.job.retryPolicy.maxRetriesPerTask,
|
|
1800
|
+
retry_counts: snapshot.tasks.retryCounts,
|
|
1801
|
+
last_run_id: snapshot.runtime.lastRunId,
|
|
1802
|
+
run_ids: runIds
|
|
1803
|
+
});
|
|
1804
|
+
return 0;
|
|
1805
|
+
}
|
|
1806
|
+
printLine(`Job: ${snapshot.job.id}`);
|
|
1807
|
+
printLine(`Title: ${snapshot.job.title}`);
|
|
1808
|
+
printLine(`Status: ${snapshot.job.status}`);
|
|
1809
|
+
printLine(`Agent: ${snapshot.job.requestedAgent}`);
|
|
1810
|
+
printLine(`Workspace: ${snapshot.job.workspacePath}`);
|
|
1811
|
+
printLine(`State dir: ${snapshot.job.stateDirectoryPath}`);
|
|
1812
|
+
printLine(`Current phase: ${snapshot.runtime.currentPhaseId ?? "-"}`);
|
|
1813
|
+
printLine(`Current task: ${snapshot.runtime.currentTaskId ?? "-"}`);
|
|
1814
|
+
printLine(`Remaining tasks: ${String(snapshot.runtime.remainingTaskCount)}`);
|
|
1815
|
+
printLine(`Next action: ${snapshot.runtime.nextAction}`);
|
|
1816
|
+
printLine(`Blocked reason: ${snapshot.runtime.blockedReason ?? "-"}`);
|
|
1817
|
+
printLine(`Validation: ${snapshot.runtime.lastValidationStatus}`);
|
|
1818
|
+
printLine(`Last run: ${snapshot.runtime.lastRunId ?? "-"}`);
|
|
1819
|
+
printLine(`Max retries per task: ${String(snapshot.job.retryPolicy.maxRetriesPerTask)}`);
|
|
1820
|
+
return 0;
|
|
1821
|
+
} catch (error) {
|
|
1822
|
+
printError(error instanceof Error ? error.message : "Failed to load job status.");
|
|
1823
|
+
return 1;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// src/shared/constants/index.ts
|
|
1828
|
+
var packageName = "@weppy/ralph";
|
|
1829
|
+
var version = "0.1.0";
|
|
1830
|
+
|
|
1831
|
+
// src/mcp/tools/index.ts
|
|
1832
|
+
var mcpToolDefinitions = [
|
|
1833
|
+
{
|
|
1834
|
+
name: "start_job",
|
|
1835
|
+
description: "Create a Ralph job in the target workspace.",
|
|
1836
|
+
inputSchema: {
|
|
1837
|
+
type: "object",
|
|
1838
|
+
required: ["title", "agent", "workspace_path"],
|
|
1839
|
+
properties: {
|
|
1840
|
+
title: { type: "string" },
|
|
1841
|
+
agent: { type: "string", enum: ["codex", "claude-code", "custom-command"] },
|
|
1842
|
+
workspace_path: { type: "string" },
|
|
1843
|
+
state_dir: { type: "string" },
|
|
1844
|
+
input_documents: {
|
|
1845
|
+
type: "array",
|
|
1846
|
+
items: { type: "string" }
|
|
1847
|
+
},
|
|
1848
|
+
validation_commands: {
|
|
1849
|
+
type: "array",
|
|
1850
|
+
items: { type: "string" }
|
|
1851
|
+
},
|
|
1852
|
+
max_retries_per_task: { type: "integer", minimum: 0 }
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
},
|
|
1856
|
+
{
|
|
1857
|
+
name: "get_status",
|
|
1858
|
+
description: "Read the current Ralph job status.",
|
|
1859
|
+
inputSchema: {
|
|
1860
|
+
type: "object",
|
|
1861
|
+
properties: {
|
|
1862
|
+
job_id: { type: "string" },
|
|
1863
|
+
workspace_path: { type: "string" },
|
|
1864
|
+
state_dir: { type: "string" }
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
},
|
|
1868
|
+
{
|
|
1869
|
+
name: "get_result",
|
|
1870
|
+
description: "Read the latest Ralph job result and reports.",
|
|
1871
|
+
inputSchema: {
|
|
1872
|
+
type: "object",
|
|
1873
|
+
properties: {
|
|
1874
|
+
job_id: { type: "string" },
|
|
1875
|
+
workspace_path: { type: "string" },
|
|
1876
|
+
state_dir: { type: "string" }
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
},
|
|
1880
|
+
{
|
|
1881
|
+
name: "resume_job",
|
|
1882
|
+
description: "Resume a Ralph job until it reaches a non-resumable state or the iteration limit.",
|
|
1883
|
+
inputSchema: {
|
|
1884
|
+
type: "object",
|
|
1885
|
+
properties: {
|
|
1886
|
+
job_id: { type: "string" },
|
|
1887
|
+
workspace_path: { type: "string" },
|
|
1888
|
+
state_dir: { type: "string" },
|
|
1889
|
+
max_iterations: { type: "integer", minimum: 1 }
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
},
|
|
1893
|
+
{
|
|
1894
|
+
name: "cancel_job",
|
|
1895
|
+
description: "Cancel a Ralph job and mark pending work as cancelled.",
|
|
1896
|
+
inputSchema: {
|
|
1897
|
+
type: "object",
|
|
1898
|
+
properties: {
|
|
1899
|
+
job_id: { type: "string" },
|
|
1900
|
+
workspace_path: { type: "string" },
|
|
1901
|
+
state_dir: { type: "string" }
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
];
|
|
1906
|
+
|
|
1907
|
+
// src/mcp/index.ts
|
|
1908
|
+
async function startMcpServer() {
|
|
1909
|
+
let buffer = Buffer.alloc(0);
|
|
1910
|
+
let expectedBodyLength = null;
|
|
1911
|
+
process.stdin.on("data", (chunk) => {
|
|
1912
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
1913
|
+
void processIncomingBuffer();
|
|
1914
|
+
});
|
|
1915
|
+
process.stdin.resume();
|
|
1916
|
+
return await new Promise((resolve3) => {
|
|
1917
|
+
process.stdin.on("end", () => resolve3(0));
|
|
1918
|
+
});
|
|
1919
|
+
async function processIncomingBuffer() {
|
|
1920
|
+
while (true) {
|
|
1921
|
+
if (expectedBodyLength === null) {
|
|
1922
|
+
const headerEndIndex = buffer.indexOf("\r\n\r\n");
|
|
1923
|
+
if (headerEndIndex === -1) {
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
const headerText = buffer.subarray(0, headerEndIndex).toString("utf8");
|
|
1927
|
+
const contentLengthHeader = headerText.split("\r\n").find((line) => line.toLowerCase().startsWith("content-length:"));
|
|
1928
|
+
if (contentLengthHeader === void 0) {
|
|
1929
|
+
writeJsonRpcResponse({
|
|
1930
|
+
jsonrpc: "2.0",
|
|
1931
|
+
id: null,
|
|
1932
|
+
error: {
|
|
1933
|
+
code: -32600,
|
|
1934
|
+
message: "Missing Content-Length header."
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
buffer = Buffer.alloc(0);
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
expectedBodyLength = Number.parseInt(contentLengthHeader.split(":")[1].trim(), 10);
|
|
1941
|
+
buffer = buffer.subarray(headerEndIndex + 4);
|
|
1942
|
+
}
|
|
1943
|
+
if (expectedBodyLength === null || buffer.length < expectedBodyLength) {
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
const body = buffer.subarray(0, expectedBodyLength).toString("utf8");
|
|
1947
|
+
buffer = buffer.subarray(expectedBodyLength);
|
|
1948
|
+
expectedBodyLength = null;
|
|
1949
|
+
let request;
|
|
1950
|
+
try {
|
|
1951
|
+
request = JSON.parse(body);
|
|
1952
|
+
} catch {
|
|
1953
|
+
writeJsonRpcResponse({
|
|
1954
|
+
jsonrpc: "2.0",
|
|
1955
|
+
id: null,
|
|
1956
|
+
error: {
|
|
1957
|
+
code: -32700,
|
|
1958
|
+
message: "Invalid JSON payload."
|
|
1959
|
+
}
|
|
1960
|
+
});
|
|
1961
|
+
continue;
|
|
1962
|
+
}
|
|
1963
|
+
const response = await handleRequest(request);
|
|
1964
|
+
if (response !== null) {
|
|
1965
|
+
writeJsonRpcResponse(response);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
async function handleRequest(request) {
|
|
1971
|
+
if (request.method === "notifications/initialized") {
|
|
1972
|
+
return null;
|
|
1973
|
+
}
|
|
1974
|
+
try {
|
|
1975
|
+
switch (request.method) {
|
|
1976
|
+
case "initialize":
|
|
1977
|
+
return {
|
|
1978
|
+
jsonrpc: "2.0",
|
|
1979
|
+
id: request.id ?? null,
|
|
1980
|
+
result: {
|
|
1981
|
+
protocolVersion: "2024-11-05",
|
|
1982
|
+
serverInfo: {
|
|
1983
|
+
name: "@weppy/ralph",
|
|
1984
|
+
version
|
|
1985
|
+
},
|
|
1986
|
+
capabilities: {
|
|
1987
|
+
tools: {}
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1991
|
+
case "ping":
|
|
1992
|
+
return {
|
|
1993
|
+
jsonrpc: "2.0",
|
|
1994
|
+
id: request.id ?? null,
|
|
1995
|
+
result: {}
|
|
1996
|
+
};
|
|
1997
|
+
case "tools/list":
|
|
1998
|
+
return {
|
|
1999
|
+
jsonrpc: "2.0",
|
|
2000
|
+
id: request.id ?? null,
|
|
2001
|
+
result: {
|
|
2002
|
+
tools: mcpToolDefinitions
|
|
2003
|
+
}
|
|
2004
|
+
};
|
|
2005
|
+
case "tools/call":
|
|
2006
|
+
return {
|
|
2007
|
+
jsonrpc: "2.0",
|
|
2008
|
+
id: request.id ?? null,
|
|
2009
|
+
result: await callTool(request.params ?? {})
|
|
2010
|
+
};
|
|
2011
|
+
default:
|
|
2012
|
+
return {
|
|
2013
|
+
jsonrpc: "2.0",
|
|
2014
|
+
id: request.id ?? null,
|
|
2015
|
+
error: {
|
|
2016
|
+
code: -32601,
|
|
2017
|
+
message: `Unknown method: ${request.method}`
|
|
2018
|
+
}
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
} catch (error) {
|
|
2022
|
+
return {
|
|
2023
|
+
jsonrpc: "2.0",
|
|
2024
|
+
id: request.id ?? null,
|
|
2025
|
+
error: {
|
|
2026
|
+
code: -32e3,
|
|
2027
|
+
message: error instanceof Error ? error.message : "Unhandled MCP error."
|
|
2028
|
+
}
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
async function callTool(params) {
|
|
2033
|
+
const toolName = asString(params.name);
|
|
2034
|
+
const args = isRecord(params.arguments) ? params.arguments : {};
|
|
2035
|
+
let payload;
|
|
2036
|
+
switch (toolName) {
|
|
2037
|
+
case "start_job": {
|
|
2038
|
+
const result = await createJob({
|
|
2039
|
+
title: requireString(args.title, "title"),
|
|
2040
|
+
agent: requireAgent(args.agent),
|
|
2041
|
+
workspacePath: requireString(args.workspace_path, "workspace_path"),
|
|
2042
|
+
stateDirectoryPath: asOptionalString(args.state_dir),
|
|
2043
|
+
inputDocuments: asStringArray(args.input_documents),
|
|
2044
|
+
validateCommands: asStringArray(args.validation_commands),
|
|
2045
|
+
maxRetriesPerTask: requireOptionalInteger(args.max_retries_per_task, "max_retries_per_task", 0)
|
|
2046
|
+
});
|
|
2047
|
+
payload = {
|
|
2048
|
+
job_id: result.snapshot.job.id,
|
|
2049
|
+
status: result.snapshot.job.status,
|
|
2050
|
+
workspace_path: result.snapshot.job.workspacePath,
|
|
2051
|
+
state_directory_path: result.snapshot.job.stateDirectoryPath,
|
|
2052
|
+
next_action: result.snapshot.runtime.nextAction
|
|
2053
|
+
};
|
|
2054
|
+
break;
|
|
2055
|
+
}
|
|
2056
|
+
case "get_status": {
|
|
2057
|
+
requireOneOf(args, "workspace_path", "state_dir");
|
|
2058
|
+
const result = await getJobOverview({
|
|
2059
|
+
jobId: asOptionalString(args.job_id),
|
|
2060
|
+
workspacePath: asOptionalString(args.workspace_path),
|
|
2061
|
+
stateDirectoryPath: asOptionalString(args.state_dir)
|
|
2062
|
+
});
|
|
2063
|
+
payload = {
|
|
2064
|
+
job: result.snapshot.job,
|
|
2065
|
+
runtime: result.snapshot.runtime,
|
|
2066
|
+
tasks: result.snapshot.tasks,
|
|
2067
|
+
run_ids: result.runIds
|
|
2068
|
+
};
|
|
2069
|
+
break;
|
|
2070
|
+
}
|
|
2071
|
+
case "get_result": {
|
|
2072
|
+
requireOneOf(args, "workspace_path", "state_dir");
|
|
2073
|
+
const result = await loadJobDetails({
|
|
2074
|
+
jobId: asOptionalString(args.job_id),
|
|
2075
|
+
workspacePath: asOptionalString(args.workspace_path),
|
|
2076
|
+
stateDirectoryPath: asOptionalString(args.state_dir)
|
|
2077
|
+
});
|
|
2078
|
+
payload = result;
|
|
2079
|
+
break;
|
|
2080
|
+
}
|
|
2081
|
+
case "resume_job": {
|
|
2082
|
+
requireOneOf(args, "workspace_path", "state_dir");
|
|
2083
|
+
const result = await resumeJob({
|
|
2084
|
+
jobId: asOptionalString(args.job_id),
|
|
2085
|
+
workspacePath: asOptionalString(args.workspace_path),
|
|
2086
|
+
stateDirectoryPath: asOptionalString(args.state_dir),
|
|
2087
|
+
maxIterations: requireOptionalInteger(args.max_iterations, "max_iterations", 1)
|
|
2088
|
+
});
|
|
2089
|
+
payload = result;
|
|
2090
|
+
break;
|
|
2091
|
+
}
|
|
2092
|
+
case "cancel_job": {
|
|
2093
|
+
requireOneOf(args, "workspace_path", "state_dir");
|
|
2094
|
+
const result = await cancelJob({
|
|
2095
|
+
jobId: asOptionalString(args.job_id),
|
|
2096
|
+
workspacePath: asOptionalString(args.workspace_path),
|
|
2097
|
+
stateDirectoryPath: asOptionalString(args.state_dir)
|
|
2098
|
+
});
|
|
2099
|
+
payload = result;
|
|
2100
|
+
break;
|
|
2101
|
+
}
|
|
2102
|
+
default:
|
|
2103
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
2104
|
+
}
|
|
2105
|
+
return {
|
|
2106
|
+
content: [
|
|
2107
|
+
{
|
|
2108
|
+
type: "text",
|
|
2109
|
+
text: JSON.stringify(payload, null, 2)
|
|
2110
|
+
}
|
|
2111
|
+
],
|
|
2112
|
+
structuredContent: payload
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
function writeJsonRpcResponse(response) {
|
|
2116
|
+
const body = JSON.stringify(response);
|
|
2117
|
+
process.stdout.write(`Content-Length: ${Buffer.byteLength(body, "utf8")}\r
|
|
2118
|
+
\r
|
|
2119
|
+
${body}`);
|
|
2120
|
+
}
|
|
2121
|
+
function isRecord(value) {
|
|
2122
|
+
return typeof value === "object" && value !== null;
|
|
2123
|
+
}
|
|
2124
|
+
function asString(value) {
|
|
2125
|
+
if (typeof value !== "string") {
|
|
2126
|
+
throw new Error("Expected a string.");
|
|
2127
|
+
}
|
|
2128
|
+
return value;
|
|
2129
|
+
}
|
|
2130
|
+
function requireString(value, fieldName) {
|
|
2131
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
2132
|
+
throw new Error(`Missing required string field: ${fieldName}`);
|
|
2133
|
+
}
|
|
2134
|
+
return value;
|
|
2135
|
+
}
|
|
2136
|
+
function asOptionalString(value) {
|
|
2137
|
+
return typeof value === "string" ? value : void 0;
|
|
2138
|
+
}
|
|
2139
|
+
function asOptionalNumber(value) {
|
|
2140
|
+
return typeof value === "number" ? value : void 0;
|
|
2141
|
+
}
|
|
2142
|
+
function requireOptionalInteger(value, fieldName, minimum) {
|
|
2143
|
+
const parsed = asOptionalNumber(value);
|
|
2144
|
+
if (parsed === void 0) {
|
|
2145
|
+
return void 0;
|
|
2146
|
+
}
|
|
2147
|
+
if (!Number.isInteger(parsed) || parsed < minimum) {
|
|
2148
|
+
throw new Error(`\`${fieldName}\` must be an integer greater than or equal to ${String(minimum)}.`);
|
|
2149
|
+
}
|
|
2150
|
+
return parsed;
|
|
2151
|
+
}
|
|
2152
|
+
function asStringArray(value) {
|
|
2153
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
2154
|
+
}
|
|
2155
|
+
function requireOneOf(args, ...fields) {
|
|
2156
|
+
const hasAny = fields.some(
|
|
2157
|
+
(field) => typeof args[field] === "string" && args[field].trim().length > 0
|
|
2158
|
+
);
|
|
2159
|
+
if (!hasAny) {
|
|
2160
|
+
throw new Error(`At least one of ${fields.join(", ")} is required.`);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
function requireAgent(value) {
|
|
2164
|
+
if (value === "codex" || value === "claude-code" || value === "custom-command") {
|
|
2165
|
+
return value;
|
|
2166
|
+
}
|
|
2167
|
+
throw new Error("`agent` must be one of: codex, claude-code, custom-command.");
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
// src/cli/index.ts
|
|
2171
|
+
function printHelp() {
|
|
2172
|
+
const helpText = [
|
|
2173
|
+
`${packageName} v${version}`,
|
|
2174
|
+
"",
|
|
2175
|
+
"Usage:",
|
|
2176
|
+
" ralph <command> [options]",
|
|
2177
|
+
"",
|
|
2178
|
+
"Commands:",
|
|
2179
|
+
" start Create a new job",
|
|
2180
|
+
" status Show current job status",
|
|
2181
|
+
" resume Resume job execution",
|
|
2182
|
+
" cancel Cancel a running job",
|
|
2183
|
+
" result Show job result and reports",
|
|
2184
|
+
" mcp Start MCP server (stdin/stdout)",
|
|
2185
|
+
"",
|
|
2186
|
+
"Common options:",
|
|
2187
|
+
" --workspace <path> Working directory for agent execution",
|
|
2188
|
+
" --state-dir <path> State storage root override",
|
|
2189
|
+
" --json Output in JSON format",
|
|
2190
|
+
"",
|
|
2191
|
+
"Start options:",
|
|
2192
|
+
" --title <text> Job title (required)",
|
|
2193
|
+
" --agent <name> Agent adapter: codex, claude-code, custom-command (required)",
|
|
2194
|
+
" --input <path> Input file/image/directory reference (repeatable)",
|
|
2195
|
+
" --validate-cmd <cmd> Validation command (repeatable)",
|
|
2196
|
+
" --max-retries <n> Max retries per task",
|
|
2197
|
+
"",
|
|
2198
|
+
"Resume options:",
|
|
2199
|
+
" --max-iterations <n> Max iterations per resume call",
|
|
2200
|
+
"",
|
|
2201
|
+
"Status/Cancel/Result options:",
|
|
2202
|
+
" --job <id> Job ID (defaults to current job)",
|
|
2203
|
+
"",
|
|
2204
|
+
"Flags:",
|
|
2205
|
+
" --help, -h Show this help",
|
|
2206
|
+
" --version, -v Show version"
|
|
2207
|
+
];
|
|
2208
|
+
process.stdout.write(`${helpText.join("\n")}
|
|
2209
|
+
`);
|
|
2210
|
+
}
|
|
2211
|
+
async function main(argv) {
|
|
2212
|
+
const [command] = argv;
|
|
2213
|
+
const commandArgs = argv.slice(1);
|
|
2214
|
+
switch (command) {
|
|
2215
|
+
case void 0:
|
|
2216
|
+
case "help":
|
|
2217
|
+
case "--help":
|
|
2218
|
+
case "-h":
|
|
2219
|
+
printHelp();
|
|
2220
|
+
return 0;
|
|
2221
|
+
case "version":
|
|
2222
|
+
case "--version":
|
|
2223
|
+
case "-v":
|
|
2224
|
+
process.stdout.write(`${version}
|
|
2225
|
+
`);
|
|
2226
|
+
return 0;
|
|
2227
|
+
case "start":
|
|
2228
|
+
return runStartCommand(commandArgs);
|
|
2229
|
+
case "status":
|
|
2230
|
+
return runStatusCommand(commandArgs);
|
|
2231
|
+
case "resume":
|
|
2232
|
+
return runResumeCommand(commandArgs);
|
|
2233
|
+
case "cancel":
|
|
2234
|
+
return runCancelCommand(commandArgs);
|
|
2235
|
+
case "result":
|
|
2236
|
+
return runResultCommand(commandArgs);
|
|
2237
|
+
case "mcp":
|
|
2238
|
+
return startMcpServer();
|
|
2239
|
+
default:
|
|
2240
|
+
process.stderr.write(`Unknown command: ${command}
|
|
2241
|
+
`);
|
|
2242
|
+
printHelp();
|
|
2243
|
+
return 1;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
void main(process.argv.slice(2)).then((exitCode) => {
|
|
2247
|
+
process.exitCode = exitCode;
|
|
2248
|
+
});
|