@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/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
+ });