@lnilluv/pi-ralph-loop 0.1.4-dev.0 → 0.1.4-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -12
- package/package.json +1 -1
- package/src/index.ts +1034 -168
- package/src/ralph-draft-llm.ts +35 -7
- package/src/ralph-draft.ts +1 -1
- package/src/ralph.ts +708 -51
- package/src/runner-rpc.ts +434 -0
- package/src/runner-state.ts +822 -0
- package/src/runner.ts +957 -0
- package/tests/fixtures/parity/migrate/OPEN_QUESTIONS.md +3 -0
- package/tests/fixtures/parity/migrate/RALPH.md +27 -0
- package/tests/fixtures/parity/migrate/golden/MIGRATED.md +15 -0
- package/tests/fixtures/parity/migrate/legacy/source.md +6 -0
- package/tests/fixtures/parity/migrate/legacy/source.yaml +3 -0
- package/tests/fixtures/parity/migrate/scripts/show-legacy.sh +10 -0
- package/tests/fixtures/parity/migrate/scripts/verify.sh +15 -0
- package/tests/fixtures/parity/research/OPEN_QUESTIONS.md +3 -0
- package/tests/fixtures/parity/research/RALPH.md +45 -0
- package/tests/fixtures/parity/research/claim-evidence-checklist.md +15 -0
- package/tests/fixtures/parity/research/expected-outputs.md +22 -0
- package/tests/fixtures/parity/research/scripts/show-snapshots.sh +13 -0
- package/tests/fixtures/parity/research/scripts/verify.sh +55 -0
- package/tests/fixtures/parity/research/snapshots/app-factory-ai-cli.md +11 -0
- package/tests/fixtures/parity/research/snapshots/docs-factory-ai-cli-features-missions.md +11 -0
- package/tests/fixtures/parity/research/snapshots/factory-ai-news-missions.md +11 -0
- package/tests/fixtures/parity/research/source-manifest.md +20 -0
- package/tests/index.test.ts +3169 -104
- package/tests/parity/README.md +9 -0
- package/tests/parity/harness.py +526 -0
- package/tests/parity-harness.test.ts +42 -0
- package/tests/parity-research-fixture.test.ts +34 -0
- package/tests/ralph-draft-llm.test.ts +82 -9
- package/tests/ralph-draft.test.ts +1 -1
- package/tests/ralph.test.ts +1265 -36
- package/tests/runner-event-contract.test.ts +235 -0
- package/tests/runner-rpc.test.ts +358 -0
- package/tests/runner-state.test.ts +553 -0
- package/tests/runner.test.ts +1347 -0
package/src/ralph.ts
CHANGED
|
@@ -7,13 +7,19 @@ export type CommandDef = { name: string; run: string; timeout: number };
|
|
|
7
7
|
export type DraftSource = "deterministic" | "llm-strengthened" | "fallback";
|
|
8
8
|
export type DraftStrengtheningScope = "body-only" | "body-and-commands";
|
|
9
9
|
export type CommandIntent = CommandDef & { source: "heuristic" | "repo-signal" };
|
|
10
|
+
export type RuntimeArg = { name: string; value: string };
|
|
11
|
+
export type RuntimeArgs = Record<string, string>;
|
|
10
12
|
export type Frontmatter = {
|
|
11
13
|
commands: CommandDef[];
|
|
14
|
+
args?: string[];
|
|
12
15
|
maxIterations: number;
|
|
16
|
+
interIterationDelay: number;
|
|
13
17
|
timeout: number;
|
|
14
18
|
completionPromise?: string;
|
|
19
|
+
requiredOutputs?: string[];
|
|
15
20
|
guardrails: { blockCommands: string[]; protectedFiles: string[] };
|
|
16
21
|
invalidCommandEntries?: number[];
|
|
22
|
+
invalidArgEntries?: number[];
|
|
17
23
|
};
|
|
18
24
|
export type ParsedRalph = { frontmatter: Frontmatter; body: string };
|
|
19
25
|
export type CommandOutput = { name: string; output: string };
|
|
@@ -22,9 +28,12 @@ export type RalphTargetResolution = {
|
|
|
22
28
|
absoluteTarget: string;
|
|
23
29
|
markdownPath: string;
|
|
24
30
|
};
|
|
25
|
-
export type CommandArgs =
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
export type CommandArgs = {
|
|
32
|
+
mode: "path" | "task" | "auto";
|
|
33
|
+
value: string;
|
|
34
|
+
runtimeArgs: RuntimeArg[];
|
|
35
|
+
error?: string;
|
|
36
|
+
};
|
|
28
37
|
export type ExistingTargetInspection =
|
|
29
38
|
| { kind: "run"; ralphPath: string }
|
|
30
39
|
| { kind: "invalid-markdown"; path: string }
|
|
@@ -131,6 +140,28 @@ function toStringArray(value: unknown): string[] {
|
|
|
131
140
|
return Array.isArray(value) ? value.map((item) => String(item)) : [];
|
|
132
141
|
}
|
|
133
142
|
|
|
143
|
+
function parseStringArray(value: unknown): { values: string[]; invalidEntries?: number[] } {
|
|
144
|
+
if (!Array.isArray(value)) return { values: [] };
|
|
145
|
+
|
|
146
|
+
const invalidEntries: number[] = [];
|
|
147
|
+
const values = value.flatMap((item, index) => {
|
|
148
|
+
if (typeof item !== "string") {
|
|
149
|
+
invalidEntries.push(index);
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
return [item];
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return { values, invalidEntries: invalidEntries.length > 0 ? invalidEntries : undefined };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isUniversalProtectedGlob(pattern: string): boolean {
|
|
159
|
+
const trimmed = pattern.trim().replace(/\/+$/, "");
|
|
160
|
+
if (!trimmed) return true;
|
|
161
|
+
if (/^\*+$/.test(trimmed)) return true;
|
|
162
|
+
return /^(?:\*\*?\/)+\*\*?$/.test(trimmed);
|
|
163
|
+
}
|
|
164
|
+
|
|
134
165
|
function normalizeRawRalph(raw: string): string {
|
|
135
166
|
return raw.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
136
167
|
}
|
|
@@ -139,8 +170,159 @@ function matchRalphMarkdown(raw: string): RegExpMatchArray | null {
|
|
|
139
170
|
return normalizeRawRalph(raw).match(/^(?:\s*<!--[\s\S]*?-->\s*)*---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
140
171
|
}
|
|
141
172
|
|
|
142
|
-
|
|
143
|
-
|
|
173
|
+
|
|
174
|
+
function validateRawGuardrailsShape(rawFrontmatter: UnknownRecord): string | null {
|
|
175
|
+
if (!Object.prototype.hasOwnProperty.call(rawFrontmatter, "guardrails")) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const guardrails = rawFrontmatter.guardrails;
|
|
180
|
+
if (!isRecord(guardrails)) {
|
|
181
|
+
return "Invalid RALPH frontmatter: guardrails must be a YAML mapping";
|
|
182
|
+
}
|
|
183
|
+
if (
|
|
184
|
+
Object.prototype.hasOwnProperty.call(guardrails, "block_commands") &&
|
|
185
|
+
!Array.isArray(guardrails.block_commands)
|
|
186
|
+
) {
|
|
187
|
+
return "Invalid RALPH frontmatter: guardrails.block_commands must be a YAML sequence";
|
|
188
|
+
}
|
|
189
|
+
if (
|
|
190
|
+
Object.prototype.hasOwnProperty.call(guardrails, "protected_files") &&
|
|
191
|
+
!Array.isArray(guardrails.protected_files)
|
|
192
|
+
) {
|
|
193
|
+
return "Invalid RALPH frontmatter: guardrails.protected_files must be a YAML sequence";
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function validateRawRequiredOutputsShape(rawFrontmatter: UnknownRecord): string | null {
|
|
199
|
+
if (!Object.prototype.hasOwnProperty.call(rawFrontmatter, "required_outputs")) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const requiredOutputs = rawFrontmatter.required_outputs;
|
|
204
|
+
if (!Array.isArray(requiredOutputs)) {
|
|
205
|
+
return "Invalid RALPH frontmatter: required_outputs must be a YAML sequence";
|
|
206
|
+
}
|
|
207
|
+
for (const [index, output] of requiredOutputs.entries()) {
|
|
208
|
+
if (typeof output !== "string") {
|
|
209
|
+
return `Invalid RALPH frontmatter: required_outputs[${index}] must be a YAML string`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function validateRawArgsShape(rawFrontmatter: UnknownRecord): string | null {
|
|
216
|
+
if (!Object.prototype.hasOwnProperty.call(rawFrontmatter, "args")) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const args = rawFrontmatter.args;
|
|
221
|
+
if (!Array.isArray(args)) {
|
|
222
|
+
return "Invalid RALPH frontmatter: args must be a YAML sequence";
|
|
223
|
+
}
|
|
224
|
+
for (const [index, arg] of args.entries()) {
|
|
225
|
+
if (typeof arg !== "string") {
|
|
226
|
+
return `Invalid RALPH frontmatter: args[${index}] must be a YAML string`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function validateRawCommandEntryShape(command: unknown, index: number): string | null {
|
|
233
|
+
if (!isRecord(command)) {
|
|
234
|
+
return `Invalid RALPH frontmatter: commands[${index}] must be a YAML mapping`;
|
|
235
|
+
}
|
|
236
|
+
if (Object.prototype.hasOwnProperty.call(command, "name") && typeof command.name !== "string") {
|
|
237
|
+
return `Invalid RALPH frontmatter: commands[${index}].name must be a YAML string`;
|
|
238
|
+
}
|
|
239
|
+
if (Object.prototype.hasOwnProperty.call(command, "run") && typeof command.run !== "string") {
|
|
240
|
+
return `Invalid RALPH frontmatter: commands[${index}].run must be a YAML string`;
|
|
241
|
+
}
|
|
242
|
+
if (Object.prototype.hasOwnProperty.call(command, "timeout") && typeof command.timeout !== "number") {
|
|
243
|
+
return `Invalid RALPH frontmatter: commands[${index}].timeout must be a YAML number`;
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function validateRawFrontmatterShape(rawFrontmatter: UnknownRecord): string | null {
|
|
249
|
+
if (Object.prototype.hasOwnProperty.call(rawFrontmatter, "commands")) {
|
|
250
|
+
const commands = rawFrontmatter.commands;
|
|
251
|
+
if (!Array.isArray(commands)) {
|
|
252
|
+
return "Invalid RALPH frontmatter: commands must be a YAML sequence";
|
|
253
|
+
}
|
|
254
|
+
for (const [index, command] of commands.entries()) {
|
|
255
|
+
const commandError = validateRawCommandEntryShape(command, index);
|
|
256
|
+
if (commandError) return commandError;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (
|
|
261
|
+
Object.prototype.hasOwnProperty.call(rawFrontmatter, "required_outputs")
|
|
262
|
+
) {
|
|
263
|
+
const requiredOutputsError = validateRawRequiredOutputsShape(rawFrontmatter);
|
|
264
|
+
if (requiredOutputsError) {
|
|
265
|
+
return requiredOutputsError;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (Object.prototype.hasOwnProperty.call(rawFrontmatter, "args")) {
|
|
270
|
+
const argsError = validateRawArgsShape(rawFrontmatter);
|
|
271
|
+
if (argsError) {
|
|
272
|
+
return argsError;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (
|
|
277
|
+
Object.prototype.hasOwnProperty.call(rawFrontmatter, "max_iterations") &&
|
|
278
|
+
(typeof rawFrontmatter.max_iterations !== "number" || !Number.isFinite(rawFrontmatter.max_iterations))
|
|
279
|
+
) {
|
|
280
|
+
return "Invalid RALPH frontmatter: max_iterations must be a YAML number";
|
|
281
|
+
}
|
|
282
|
+
if (
|
|
283
|
+
Object.prototype.hasOwnProperty.call(rawFrontmatter, "inter_iteration_delay") &&
|
|
284
|
+
(typeof rawFrontmatter.inter_iteration_delay !== "number" || !Number.isFinite(rawFrontmatter.inter_iteration_delay))
|
|
285
|
+
) {
|
|
286
|
+
return "Invalid RALPH frontmatter: inter_iteration_delay must be a YAML number";
|
|
287
|
+
}
|
|
288
|
+
if (
|
|
289
|
+
Object.prototype.hasOwnProperty.call(rawFrontmatter, "timeout") &&
|
|
290
|
+
(typeof rawFrontmatter.timeout !== "number" || !Number.isFinite(rawFrontmatter.timeout))
|
|
291
|
+
) {
|
|
292
|
+
return "Invalid RALPH frontmatter: timeout must be a YAML number";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function parseStrictRalphMarkdown(raw: string): { parsed: ParsedRalph; rawFrontmatter: UnknownRecord } | { error: string } {
|
|
299
|
+
const normalized = normalizeRawRalph(raw);
|
|
300
|
+
const match = matchRalphMarkdown(normalized);
|
|
301
|
+
if (!match) return { error: "Missing RALPH frontmatter" };
|
|
302
|
+
|
|
303
|
+
let parsedYaml: unknown;
|
|
304
|
+
try {
|
|
305
|
+
parsedYaml = parseYaml(match[1]);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
308
|
+
return { error: `Invalid RALPH frontmatter: ${message}` };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!isRecord(parsedYaml)) {
|
|
312
|
+
return { error: "Invalid RALPH frontmatter: Frontmatter must be a YAML mapping" };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const guardrailsError = validateRawGuardrailsShape(parsedYaml);
|
|
316
|
+
if (guardrailsError) {
|
|
317
|
+
return { error: guardrailsError };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const rawShapeError = validateRawFrontmatterShape(parsedYaml);
|
|
321
|
+
if (rawShapeError) {
|
|
322
|
+
return { error: rawShapeError };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return { parsed: parseRalphMarkdown(normalized), rawFrontmatter: parsedYaml };
|
|
144
326
|
}
|
|
145
327
|
|
|
146
328
|
function normalizeMissingMarkdownTarget(absoluteTarget: string): { dirPath: string; ralphPath: string } {
|
|
@@ -167,8 +349,55 @@ function summarizeSafetyLabel(guardrails: Frontmatter["guardrails"]): string {
|
|
|
167
349
|
return labels.length > 0 ? labels.join(" and ") : "No extra safety rules";
|
|
168
350
|
}
|
|
169
351
|
|
|
170
|
-
function summarizeFinishLabel(
|
|
171
|
-
|
|
352
|
+
function summarizeFinishLabel(frontmatter: Frontmatter): string {
|
|
353
|
+
const requiredOutputs = frontmatter.requiredOutputs ?? [];
|
|
354
|
+
const labels = [`Stop after ${frontmatter.maxIterations} iterations or /ralph-stop`];
|
|
355
|
+
if (requiredOutputs.length > 0) {
|
|
356
|
+
labels.push(`required outputs: ${requiredOutputs.join(", ")}`);
|
|
357
|
+
}
|
|
358
|
+
return labels.join("; ");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function summarizeFinishBehavior(frontmatter: Frontmatter): string[] {
|
|
362
|
+
const requiredOutputs = frontmatter.requiredOutputs ?? [];
|
|
363
|
+
const lines = [
|
|
364
|
+
`- Stop after ${frontmatter.maxIterations} iterations or /ralph-stop`,
|
|
365
|
+
`- Stop if an iteration exceeds ${frontmatter.timeout}s`,
|
|
366
|
+
];
|
|
367
|
+
|
|
368
|
+
if (frontmatter.completionPromise) {
|
|
369
|
+
if (requiredOutputs.length > 0) {
|
|
370
|
+
lines.push(`- Required outputs must exist before stopping: ${requiredOutputs.join(", ")}`);
|
|
371
|
+
}
|
|
372
|
+
lines.push("- OPEN_QUESTIONS.md must have no remaining P0/P1 items before stopping.");
|
|
373
|
+
lines.push(`- Stop early on <promise>${frontmatter.completionPromise}</promise>`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return lines;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function isSafeCompletionPromise(value: string): boolean {
|
|
380
|
+
return !/[\r\n<>]/.test(value);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function validateRequiredOutputEntry(value: string): string | null {
|
|
384
|
+
const trimmed = value.trim();
|
|
385
|
+
if (
|
|
386
|
+
!trimmed ||
|
|
387
|
+
trimmed !== value ||
|
|
388
|
+
/[\u0000-\u001f\u007f]/.test(value) ||
|
|
389
|
+
trimmed === "." ||
|
|
390
|
+
trimmed === ".." ||
|
|
391
|
+
trimmed.startsWith("/") ||
|
|
392
|
+
/^[A-Za-z]:[\\/]/.test(trimmed) ||
|
|
393
|
+
trimmed.includes("\\") ||
|
|
394
|
+
trimmed.endsWith("/") ||
|
|
395
|
+
trimmed.endsWith("\\") ||
|
|
396
|
+
trimmed.split("/").some((segment) => segment === "." || segment === "..")
|
|
397
|
+
) {
|
|
398
|
+
return `Invalid required_outputs entry: ${value} must be a relative file path`;
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
172
401
|
}
|
|
173
402
|
|
|
174
403
|
function isRalphMarkdownPath(path: string): boolean {
|
|
@@ -254,7 +483,7 @@ function escapeHtmlCommentMarkers(text: string): string {
|
|
|
254
483
|
}
|
|
255
484
|
|
|
256
485
|
export function defaultFrontmatter(): Frontmatter {
|
|
257
|
-
return { commands: [], maxIterations: 50, timeout: 300, guardrails: { blockCommands: [], protectedFiles: [] } };
|
|
486
|
+
return { commands: [], maxIterations: 50, interIterationDelay: 0, timeout: 300, requiredOutputs: [], guardrails: { blockCommands: [], protectedFiles: [] } };
|
|
258
487
|
}
|
|
259
488
|
|
|
260
489
|
export function parseRalphMarkdown(raw: string): ParsedRalph {
|
|
@@ -272,20 +501,25 @@ export function parseRalphMarkdown(raw: string): ParsedRalph {
|
|
|
272
501
|
}
|
|
273
502
|
return [parsed];
|
|
274
503
|
});
|
|
504
|
+
const parsedArgs = parseStringArray(yaml.args);
|
|
275
505
|
const guardrails = isRecord(yaml.guardrails) ? yaml.guardrails : {};
|
|
276
506
|
|
|
277
507
|
return {
|
|
278
508
|
frontmatter: {
|
|
279
509
|
commands,
|
|
510
|
+
...(parsedArgs.values.length > 0 ? { args: parsedArgs.values } : {}),
|
|
280
511
|
maxIterations: Number(yaml.max_iterations ?? 50),
|
|
512
|
+
interIterationDelay: Number(yaml.inter_iteration_delay ?? 0),
|
|
281
513
|
timeout: Number(yaml.timeout ?? 300),
|
|
282
514
|
completionPromise:
|
|
283
515
|
typeof yaml.completion_promise === "string" && yaml.completion_promise.trim() ? yaml.completion_promise : undefined,
|
|
516
|
+
requiredOutputs: toStringArray(yaml.required_outputs),
|
|
284
517
|
guardrails: {
|
|
285
518
|
blockCommands: toStringArray(guardrails.block_commands),
|
|
286
519
|
protectedFiles: toStringArray(guardrails.protected_files),
|
|
287
520
|
},
|
|
288
521
|
invalidCommandEntries: invalidCommandEntries.length > 0 ? invalidCommandEntries : undefined,
|
|
522
|
+
...(parsedArgs.invalidEntries ? { invalidArgEntries: parsedArgs.invalidEntries } : {}),
|
|
289
523
|
},
|
|
290
524
|
body: match[2] ?? "",
|
|
291
525
|
};
|
|
@@ -295,11 +529,40 @@ export function validateFrontmatter(fm: Frontmatter): string | null {
|
|
|
295
529
|
if ((fm.invalidCommandEntries?.length ?? 0) > 0) {
|
|
296
530
|
return `Invalid command entry at index ${fm.invalidCommandEntries![0]}`;
|
|
297
531
|
}
|
|
298
|
-
if (
|
|
299
|
-
return
|
|
532
|
+
if ((fm.invalidArgEntries?.length ?? 0) > 0) {
|
|
533
|
+
return `Invalid args entry at index ${fm.invalidArgEntries![0]}`;
|
|
300
534
|
}
|
|
301
|
-
if (!Number.isFinite(fm.
|
|
302
|
-
return "Invalid
|
|
535
|
+
if (!Number.isFinite(fm.maxIterations) || !Number.isInteger(fm.maxIterations) || fm.maxIterations < 1 || fm.maxIterations > 50) {
|
|
536
|
+
return "Invalid max_iterations: must be between 1 and 50";
|
|
537
|
+
}
|
|
538
|
+
if (!Number.isFinite(fm.interIterationDelay) || !Number.isInteger(fm.interIterationDelay) || fm.interIterationDelay < 0) {
|
|
539
|
+
return "Invalid inter_iteration_delay: must be a non-negative integer";
|
|
540
|
+
}
|
|
541
|
+
if (!Number.isFinite(fm.timeout) || fm.timeout <= 0 || fm.timeout > 300) {
|
|
542
|
+
return "Invalid timeout: must be greater than 0 and at most 300";
|
|
543
|
+
}
|
|
544
|
+
if (fm.completionPromise !== undefined && !isSafeCompletionPromise(fm.completionPromise)) {
|
|
545
|
+
return "Invalid completion_promise: must be a single-line string without line breaks or angle brackets";
|
|
546
|
+
}
|
|
547
|
+
const args = fm.args ?? [];
|
|
548
|
+
const seenArgNames = new Set<string>();
|
|
549
|
+
for (const arg of args) {
|
|
550
|
+
if (!arg.trim()) {
|
|
551
|
+
return "Invalid arg: name is required";
|
|
552
|
+
}
|
|
553
|
+
if (!/^\w[\w-]*$/.test(arg)) {
|
|
554
|
+
return `Invalid arg name: ${arg} must match ^\\w[\\w-]*$`;
|
|
555
|
+
}
|
|
556
|
+
if (seenArgNames.has(arg)) {
|
|
557
|
+
return "Invalid args: names must be unique";
|
|
558
|
+
}
|
|
559
|
+
seenArgNames.add(arg);
|
|
560
|
+
}
|
|
561
|
+
for (const output of fm.requiredOutputs ?? []) {
|
|
562
|
+
const requiredOutputError = validateRequiredOutputEntry(output);
|
|
563
|
+
if (requiredOutputError) {
|
|
564
|
+
return requiredOutputError;
|
|
565
|
+
}
|
|
303
566
|
}
|
|
304
567
|
for (const pattern of fm.guardrails.blockCommands) {
|
|
305
568
|
try {
|
|
@@ -308,20 +571,118 @@ export function validateFrontmatter(fm: Frontmatter): string | null {
|
|
|
308
571
|
return `Invalid block_commands regex: ${pattern}`;
|
|
309
572
|
}
|
|
310
573
|
}
|
|
574
|
+
for (const pattern of fm.guardrails.protectedFiles) {
|
|
575
|
+
if (isUniversalProtectedGlob(pattern)) {
|
|
576
|
+
return `Invalid protected_files glob: ${pattern}`;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
311
579
|
for (const cmd of fm.commands) {
|
|
312
580
|
if (!cmd.name.trim()) {
|
|
313
581
|
return "Invalid command: name is required";
|
|
314
582
|
}
|
|
583
|
+
if (!/^\w[\w-]*$/.test(cmd.name)) {
|
|
584
|
+
return `Invalid command name: ${cmd.name} must match ^\\w[\\w-]*$`;
|
|
585
|
+
}
|
|
315
586
|
if (!cmd.run.trim()) {
|
|
316
587
|
return `Invalid command ${cmd.name}: run is required`;
|
|
317
588
|
}
|
|
318
|
-
if (!Number.isFinite(cmd.timeout) || cmd.timeout <= 0) {
|
|
319
|
-
return `Invalid command ${cmd.name}: timeout must be
|
|
589
|
+
if (!Number.isFinite(cmd.timeout) || cmd.timeout <= 0 || cmd.timeout > 300) {
|
|
590
|
+
return `Invalid command ${cmd.name}: timeout must be greater than 0 and at most 300`;
|
|
591
|
+
}
|
|
592
|
+
if (cmd.timeout > fm.timeout) {
|
|
593
|
+
return `Invalid command ${cmd.name}: timeout must not exceed top-level timeout`;
|
|
320
594
|
}
|
|
321
595
|
}
|
|
322
596
|
return null;
|
|
323
597
|
}
|
|
324
598
|
|
|
599
|
+
function parseCompletionPromiseValue(yaml: UnknownRecord): { present: boolean; value?: string; invalid: boolean } {
|
|
600
|
+
if (!Object.prototype.hasOwnProperty.call(yaml, "completion_promise")) {
|
|
601
|
+
return { present: false, invalid: false };
|
|
602
|
+
}
|
|
603
|
+
const value = yaml.completion_promise;
|
|
604
|
+
if (typeof value !== "string" || !value.trim() || !isSafeCompletionPromise(value)) {
|
|
605
|
+
return { present: true, invalid: true };
|
|
606
|
+
}
|
|
607
|
+
return { present: true, value, invalid: false };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export function acceptStrengthenedDraft(request: DraftRequest, strengthenedDraft: string): DraftPlan | null {
|
|
611
|
+
const baseline = parseStrictRalphMarkdown(request.baselineDraft);
|
|
612
|
+
const strengthened = parseStrictRalphMarkdown(strengthenedDraft);
|
|
613
|
+
if ("error" in baseline || "error" in strengthened) {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const validationError = validateFrontmatter(strengthened.parsed.frontmatter);
|
|
618
|
+
if (validationError) {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const baselineRequiredOutputs = baseline.parsed.frontmatter.requiredOutputs ?? [];
|
|
623
|
+
const strengthenedRequiredOutputs = strengthened.parsed.frontmatter.requiredOutputs ?? [];
|
|
624
|
+
if (baselineRequiredOutputs.join("\n") !== strengthenedRequiredOutputs.join("\n")) {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const baselineArgs = baseline.parsed.frontmatter.args ?? [];
|
|
629
|
+
const strengthenedArgs = strengthened.parsed.frontmatter.args ?? [];
|
|
630
|
+
if (baselineArgs.join("\n") !== strengthenedArgs.join("\n")) {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const baselineCompletion = parseCompletionPromiseValue(baseline.rawFrontmatter);
|
|
635
|
+
const strengthenedCompletion = parseCompletionPromiseValue(strengthened.rawFrontmatter);
|
|
636
|
+
if (baselineCompletion.invalid || strengthenedCompletion.invalid) {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
if (baselineCompletion.present !== strengthenedCompletion.present || baselineCompletion.value !== strengthenedCompletion.value) {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (baseline.parsed.frontmatter.maxIterations < strengthened.parsed.frontmatter.maxIterations) {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
if (baseline.parsed.frontmatter.timeout < strengthened.parsed.frontmatter.timeout) {
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
if (
|
|
650
|
+
baseline.parsed.frontmatter.guardrails.blockCommands.join("\n") !== strengthened.parsed.frontmatter.guardrails.blockCommands.join("\n") ||
|
|
651
|
+
baseline.parsed.frontmatter.guardrails.protectedFiles.join("\n") !== strengthened.parsed.frontmatter.guardrails.protectedFiles.join("\n")
|
|
652
|
+
) {
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const baselineCommands = new Map(baseline.parsed.frontmatter.commands.map((command) => [command.name, command]));
|
|
657
|
+
const seenCommands = new Set<string>();
|
|
658
|
+
for (const command of strengthened.parsed.frontmatter.commands) {
|
|
659
|
+
if (seenCommands.has(command.name)) {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
seenCommands.add(command.name);
|
|
663
|
+
|
|
664
|
+
const baselineCommand = baselineCommands.get(command.name);
|
|
665
|
+
if (!baselineCommand || baselineCommand.run !== command.run) {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
if (command.timeout > baselineCommand.timeout || command.timeout > strengthened.parsed.frontmatter.timeout) {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
for (const placeholder of strengthened.parsed.body.matchAll(/\{\{\s*commands\.(\w[\w-]*)\s*\}\}/g)) {
|
|
674
|
+
if (!seenCommands.has(placeholder[1])) {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (collectArgPlaceholderNames(strengthened.parsed.body).length > 0) {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return renderDraftPlan(request.task, request.mode, request.target, strengthened.parsed.frontmatter, "llm-strengthened", strengthened.parsed.body);
|
|
684
|
+
}
|
|
685
|
+
|
|
325
686
|
export function findBlockedCommandPattern(command: string, blockPatterns: string[]): string | undefined {
|
|
326
687
|
for (const pattern of blockPatterns) {
|
|
327
688
|
try {
|
|
@@ -333,13 +694,231 @@ export function findBlockedCommandPattern(command: string, blockPatterns: string
|
|
|
333
694
|
return undefined;
|
|
334
695
|
}
|
|
335
696
|
|
|
697
|
+
function hasRuntimeArgToken(text: string): boolean {
|
|
698
|
+
return /(?:^|\s)--arg(?:\s|=)/.test(text);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function parseRuntimeArgEntry(token: string): { entry?: RuntimeArg; error?: string } {
|
|
702
|
+
const equalsIndex = token.indexOf("=");
|
|
703
|
+
if (equalsIndex < 0) {
|
|
704
|
+
return { error: "Invalid --arg entry: name=value is required" };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const name = token.slice(0, equalsIndex).trim();
|
|
708
|
+
const value = token.slice(equalsIndex + 1);
|
|
709
|
+
if (!name) {
|
|
710
|
+
return { error: "Invalid --arg entry: name is required" };
|
|
711
|
+
}
|
|
712
|
+
if (!value) {
|
|
713
|
+
return { error: "Invalid --arg entry: value is required" };
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return { entry: { name, value } };
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function parseExplicitPathRuntimeArgs(rawTail: string): { runtimeArgs: RuntimeArg[]; error?: string } {
|
|
720
|
+
const runtimeArgs: RuntimeArg[] = [];
|
|
721
|
+
const trimmed = rawTail.trim();
|
|
722
|
+
if (!trimmed) {
|
|
723
|
+
return { runtimeArgs };
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const syntaxError = "Invalid --arg syntax: values must be a single token and no trailing text is allowed";
|
|
727
|
+
let index = 0;
|
|
728
|
+
|
|
729
|
+
while (index < trimmed.length) {
|
|
730
|
+
while (index < trimmed.length && /\s/.test(trimmed[index])) {
|
|
731
|
+
index += 1;
|
|
732
|
+
}
|
|
733
|
+
if (index >= trimmed.length) {
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (!trimmed.startsWith("--arg", index) || (trimmed[index + 5] !== undefined && !/\s/.test(trimmed[index + 5]))) {
|
|
738
|
+
return { runtimeArgs, error: syntaxError };
|
|
739
|
+
}
|
|
740
|
+
index += 5;
|
|
741
|
+
|
|
742
|
+
while (index < trimmed.length && /\s/.test(trimmed[index])) {
|
|
743
|
+
index += 1;
|
|
744
|
+
}
|
|
745
|
+
if (index >= trimmed.length) {
|
|
746
|
+
return { runtimeArgs, error: "Invalid --arg entry: name=value is required" };
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const nameStart = index;
|
|
750
|
+
while (index < trimmed.length && trimmed[index] !== "=" && !/\s/.test(trimmed[index])) {
|
|
751
|
+
index += 1;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const name = trimmed.slice(nameStart, index).trim();
|
|
755
|
+
if (!name) {
|
|
756
|
+
return { runtimeArgs, error: "Invalid --arg entry: name is required" };
|
|
757
|
+
}
|
|
758
|
+
if (index >= trimmed.length || trimmed[index] !== "=") {
|
|
759
|
+
return { runtimeArgs, error: "Invalid --arg entry: name=value is required" };
|
|
760
|
+
}
|
|
761
|
+
index += 1;
|
|
762
|
+
|
|
763
|
+
if (index >= trimmed.length || /\s/.test(trimmed[index])) {
|
|
764
|
+
return { runtimeArgs, error: "Invalid --arg entry: value is required" };
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
let value = "";
|
|
768
|
+
const quote = trimmed[index];
|
|
769
|
+
if (quote === "'" || quote === '"') {
|
|
770
|
+
index += 1;
|
|
771
|
+
while (index < trimmed.length && trimmed[index] !== quote) {
|
|
772
|
+
value += trimmed[index];
|
|
773
|
+
index += 1;
|
|
774
|
+
}
|
|
775
|
+
if (index >= trimmed.length) {
|
|
776
|
+
return { runtimeArgs, error: syntaxError };
|
|
777
|
+
}
|
|
778
|
+
if (index + 1 < trimmed.length && !/\s/.test(trimmed[index + 1])) {
|
|
779
|
+
return { runtimeArgs, error: syntaxError };
|
|
780
|
+
}
|
|
781
|
+
index += 1;
|
|
782
|
+
} else {
|
|
783
|
+
while (index < trimmed.length && !/\s/.test(trimmed[index])) {
|
|
784
|
+
const char = trimmed[index];
|
|
785
|
+
if (char === "'" || char === '"') {
|
|
786
|
+
return { runtimeArgs, error: syntaxError };
|
|
787
|
+
}
|
|
788
|
+
value += char;
|
|
789
|
+
index += 1;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const parsed = parseRuntimeArgEntry(`${name}=${value}`);
|
|
794
|
+
if (parsed.error) {
|
|
795
|
+
return { runtimeArgs, error: parsed.error };
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const entry = parsed.entry;
|
|
799
|
+
if (!entry) {
|
|
800
|
+
return { runtimeArgs, error: "Invalid --arg entry: name=value is required" };
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (runtimeArgs.some((existing) => existing.name === entry.name)) {
|
|
804
|
+
return { runtimeArgs, error: `Duplicate --arg: ${entry.name}` };
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
runtimeArgs.push(entry);
|
|
808
|
+
|
|
809
|
+
while (index < trimmed.length && /\s/.test(trimmed[index])) {
|
|
810
|
+
index += 1;
|
|
811
|
+
}
|
|
812
|
+
if (index < trimmed.length && !trimmed.startsWith("--arg", index)) {
|
|
813
|
+
return { runtimeArgs, error: syntaxError };
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return { runtimeArgs };
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function parseExplicitPathCommandArgs(valueWithArgs: string): CommandArgs {
|
|
821
|
+
const argMatch = valueWithArgs.match(/(?:^|\s)--arg(?:\s|=|[^\s=]*=|$)/);
|
|
822
|
+
const argIndex = argMatch?.index ?? valueWithArgs.length;
|
|
823
|
+
const value = argMatch ? valueWithArgs.slice(0, argIndex).trim() : valueWithArgs.trim();
|
|
824
|
+
const parsedArgs = parseExplicitPathRuntimeArgs(argMatch ? valueWithArgs.slice(argIndex).trim() : "");
|
|
825
|
+
return { mode: "path", value, runtimeArgs: parsedArgs.runtimeArgs, error: parsedArgs.error ?? undefined };
|
|
826
|
+
}
|
|
827
|
+
|
|
336
828
|
export function parseCommandArgs(raw: string): CommandArgs {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
if (
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
829
|
+
const cleaned = raw.trim();
|
|
830
|
+
|
|
831
|
+
if (cleaned.startsWith("--task=")) {
|
|
832
|
+
const value = cleaned.slice("--task=".length).trim();
|
|
833
|
+
if (hasRuntimeArgToken(value)) {
|
|
834
|
+
return { mode: "task", value, runtimeArgs: [], error: "--arg is only supported with /ralph --path" };
|
|
835
|
+
}
|
|
836
|
+
return { mode: "task", value, runtimeArgs: [], error: undefined };
|
|
837
|
+
}
|
|
838
|
+
if (cleaned.startsWith("--task ")) {
|
|
839
|
+
const value = cleaned.slice("--task ".length).trim();
|
|
840
|
+
if (hasRuntimeArgToken(value)) {
|
|
841
|
+
return { mode: "task", value, runtimeArgs: [], error: "--arg is only supported with /ralph --path" };
|
|
842
|
+
}
|
|
843
|
+
return { mode: "task", value, runtimeArgs: [], error: undefined };
|
|
844
|
+
}
|
|
845
|
+
if (cleaned.startsWith("--path=")) {
|
|
846
|
+
return parseExplicitPathCommandArgs(cleaned.slice("--path=".length).trimStart());
|
|
847
|
+
}
|
|
848
|
+
if (cleaned.startsWith("--path ")) {
|
|
849
|
+
return parseExplicitPathCommandArgs(cleaned.slice("--path ".length).trimStart());
|
|
850
|
+
}
|
|
851
|
+
return { mode: "auto", value: cleaned, runtimeArgs: [], error: undefined };
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
export function runtimeArgEntriesToMap(entries: RuntimeArg[]): { runtimeArgs: RuntimeArgs; error?: string } {
|
|
855
|
+
const runtimeArgs = Object.create(null) as RuntimeArgs;
|
|
856
|
+
for (const entry of entries) {
|
|
857
|
+
if (!entry.name.trim()) {
|
|
858
|
+
return { runtimeArgs, error: "Invalid --arg entry: name is required" };
|
|
859
|
+
}
|
|
860
|
+
if (!entry.value.trim()) {
|
|
861
|
+
return { runtimeArgs, error: "Invalid --arg entry: value is required" };
|
|
862
|
+
}
|
|
863
|
+
if (!/^\w[\w-]*$/.test(entry.name)) {
|
|
864
|
+
return { runtimeArgs, error: `Invalid --arg name: ${entry.name} must match ^\\w[\\w-]*$` };
|
|
865
|
+
}
|
|
866
|
+
if (Object.prototype.hasOwnProperty.call(runtimeArgs, entry.name)) {
|
|
867
|
+
return { runtimeArgs, error: `Duplicate --arg: ${entry.name}` };
|
|
868
|
+
}
|
|
869
|
+
runtimeArgs[entry.name] = entry.value;
|
|
870
|
+
}
|
|
871
|
+
return { runtimeArgs };
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function collectArgPlaceholderNames(source: string): string[] {
|
|
875
|
+
const names = new Set<string>();
|
|
876
|
+
for (const match of source.matchAll(/\{\{\s*args\.(\w[\w-]*)\s*\}\}/g)) {
|
|
877
|
+
names.add(match[1]);
|
|
878
|
+
}
|
|
879
|
+
return [...names];
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function validateBodyArgsAgainstContract(body: string, declaredArgs: string[] | undefined): string | null {
|
|
883
|
+
const declaredSet = new Set(declaredArgs ?? []);
|
|
884
|
+
for (const name of collectArgPlaceholderNames(body)) {
|
|
885
|
+
if (!declaredSet.has(name)) {
|
|
886
|
+
return `Undeclared arg placeholder: ${name}`;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
export function validateRuntimeArgs(frontmatter: Frontmatter, body: string, commands: CommandDef[], runtimeArgs: RuntimeArgs): string | null {
|
|
893
|
+
const declaredArgs = frontmatter.args ?? [];
|
|
894
|
+
const declaredSet = new Set(declaredArgs);
|
|
895
|
+
|
|
896
|
+
for (const name of Object.keys(runtimeArgs)) {
|
|
897
|
+
if (!declaredSet.has(name)) {
|
|
898
|
+
return `Undeclared arg: ${name}`;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
for (const name of declaredArgs) {
|
|
903
|
+
if (!Object.prototype.hasOwnProperty.call(runtimeArgs, name)) {
|
|
904
|
+
return `Missing required arg: ${name}`;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
for (const name of collectArgPlaceholderNames(body)) {
|
|
909
|
+
if (!declaredSet.has(name)) {
|
|
910
|
+
return `Undeclared arg placeholder: ${name}`;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
for (const command of commands) {
|
|
914
|
+
for (const name of collectArgPlaceholderNames(command.run)) {
|
|
915
|
+
if (!declaredSet.has(name)) {
|
|
916
|
+
return `Undeclared arg placeholder: ${name}`;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
return null;
|
|
343
922
|
}
|
|
344
923
|
|
|
345
924
|
export function looksLikePath(value: string): boolean {
|
|
@@ -555,7 +1134,9 @@ function buildDraftFrontmatter(mode: DraftMode, commands: CommandDef[]): Frontma
|
|
|
555
1134
|
return {
|
|
556
1135
|
commands,
|
|
557
1136
|
maxIterations: mode === "analysis" ? 12 : mode === "migration" ? 30 : 25,
|
|
1137
|
+
interIterationDelay: 0,
|
|
558
1138
|
timeout: 300,
|
|
1139
|
+
requiredOutputs: [],
|
|
559
1140
|
guardrails,
|
|
560
1141
|
};
|
|
561
1142
|
}
|
|
@@ -590,15 +1171,23 @@ function commandIntentsToCommands(commandIntents: CommandIntent[]): CommandDef[]
|
|
|
590
1171
|
|
|
591
1172
|
function renderDraftPlan(task: string, mode: DraftMode, target: DraftTarget, frontmatter: Frontmatter, source: DraftSource, body: string): DraftPlan {
|
|
592
1173
|
const metadata: DraftMetadata = { generator: "pi-ralph-loop", version: 2, source, task, mode };
|
|
1174
|
+
const requiredOutputs = frontmatter.requiredOutputs ?? [];
|
|
593
1175
|
const frontmatterLines = [
|
|
594
1176
|
...renderCommandsYaml(frontmatter.commands),
|
|
595
1177
|
`max_iterations: ${frontmatter.maxIterations}`,
|
|
1178
|
+
`inter_iteration_delay: ${frontmatter.interIterationDelay}`,
|
|
596
1179
|
`timeout: ${frontmatter.timeout}`,
|
|
1180
|
+
...(requiredOutputs.length > 0
|
|
1181
|
+
? ["required_outputs:", ...requiredOutputs.map((output) => ` - ${yamlQuote(output)}`)]
|
|
1182
|
+
: []),
|
|
1183
|
+
...(frontmatter.completionPromise ? [`completion_promise: ${yamlQuote(frontmatter.completionPromise)}`] : []),
|
|
597
1184
|
"guardrails:",
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
...frontmatter.guardrails.protectedFiles.
|
|
1185
|
+
...(frontmatter.guardrails.blockCommands.length > 0
|
|
1186
|
+
? [" block_commands:", ...frontmatter.guardrails.blockCommands.map((pattern) => ` - ${yamlQuote(pattern)}`)]
|
|
1187
|
+
: [" block_commands: []"]),
|
|
1188
|
+
...(frontmatter.guardrails.protectedFiles.length > 0
|
|
1189
|
+
? [" protected_files:", ...frontmatter.guardrails.protectedFiles.map((pattern) => ` - ${yamlQuote(pattern)}`)]
|
|
1190
|
+
: [" protected_files: []"]),
|
|
602
1191
|
];
|
|
603
1192
|
|
|
604
1193
|
return {
|
|
@@ -609,7 +1198,7 @@ function renderDraftPlan(task: string, mode: DraftMode, target: DraftTarget, fro
|
|
|
609
1198
|
content: `${metadataComment(metadata)}\n${yamlBlock(frontmatterLines)}\n\n${body}`,
|
|
610
1199
|
commandLabels: frontmatter.commands.map(formatCommandLabel),
|
|
611
1200
|
safetyLabel: summarizeSafetyLabel(frontmatter.guardrails),
|
|
612
|
-
finishLabel: summarizeFinishLabel(frontmatter
|
|
1201
|
+
finishLabel: summarizeFinishLabel(frontmatter),
|
|
613
1202
|
};
|
|
614
1203
|
}
|
|
615
1204
|
|
|
@@ -635,16 +1224,29 @@ export function buildDraftRequest(task: string, target: DraftTarget, repoSignals
|
|
|
635
1224
|
|
|
636
1225
|
export function normalizeStrengthenedDraft(request: DraftRequest, strengthenedDraft: string, scope: DraftStrengtheningScope): DraftPlan {
|
|
637
1226
|
const baseline = parseRalphMarkdown(request.baselineDraft);
|
|
638
|
-
const strengthened =
|
|
1227
|
+
const strengthened = parseStrictRalphMarkdown(strengthenedDraft);
|
|
639
1228
|
|
|
640
1229
|
if (scope === "body-only") {
|
|
641
|
-
|
|
1230
|
+
if (
|
|
1231
|
+
"error" in strengthened ||
|
|
1232
|
+
validateFrontmatter(strengthened.parsed.frontmatter) ||
|
|
1233
|
+
validateBodyArgsAgainstContract(strengthened.parsed.body, baseline.frontmatter.args)
|
|
1234
|
+
) {
|
|
1235
|
+
return renderDraftPlan(request.task, request.mode, request.target, baseline.frontmatter, "llm-strengthened", baseline.body);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
return renderDraftPlan(request.task, request.mode, request.target, baseline.frontmatter, "llm-strengthened", strengthened.parsed.body);
|
|
642
1239
|
}
|
|
643
1240
|
|
|
644
|
-
|
|
1241
|
+
const accepted = acceptStrengthenedDraft(request, strengthenedDraft);
|
|
1242
|
+
if (accepted) {
|
|
1243
|
+
return accepted;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
return renderDraftPlan(request.task, request.mode, request.target, baseline.frontmatter, "llm-strengthened", baseline.body);
|
|
645
1247
|
}
|
|
646
1248
|
|
|
647
|
-
function hasFakeRuntimeEnforcementClaim(text: string): boolean {
|
|
1249
|
+
export function hasFakeRuntimeEnforcementClaim(text: string): boolean {
|
|
648
1250
|
return /read[-\s]?only enforced|write protection is enforced/i.test(text);
|
|
649
1251
|
}
|
|
650
1252
|
|
|
@@ -692,20 +1294,19 @@ export type DraftContentInspection = {
|
|
|
692
1294
|
|
|
693
1295
|
export function inspectDraftContent(raw: string): DraftContentInspection {
|
|
694
1296
|
const metadata = extractDraftMetadata(raw);
|
|
695
|
-
const
|
|
1297
|
+
const parsed = parseStrictRalphMarkdown(raw);
|
|
696
1298
|
|
|
697
|
-
if (
|
|
698
|
-
return { metadata, error:
|
|
1299
|
+
if ("error" in parsed) {
|
|
1300
|
+
return { metadata, error: parsed.error };
|
|
699
1301
|
}
|
|
700
1302
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
return error ? { metadata, parsed, error } : { metadata, parsed };
|
|
705
|
-
} catch (err) {
|
|
706
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
707
|
-
return { metadata, error: `Invalid RALPH frontmatter: ${message}` };
|
|
1303
|
+
const rawCompletionPromise = parseCompletionPromiseValue(parsed.rawFrontmatter);
|
|
1304
|
+
if (rawCompletionPromise.invalid) {
|
|
1305
|
+
return { metadata, parsed: parsed.parsed, error: "Invalid completion_promise: must be a single-line string without line breaks or angle brackets" };
|
|
708
1306
|
}
|
|
1307
|
+
|
|
1308
|
+
const error = validateFrontmatter(parsed.parsed.frontmatter);
|
|
1309
|
+
return error ? { metadata, parsed: parsed.parsed, error } : { metadata, parsed: parsed.parsed };
|
|
709
1310
|
}
|
|
710
1311
|
|
|
711
1312
|
export function validateDraftContent(raw: string): string | null {
|
|
@@ -734,9 +1335,8 @@ export function buildMissionBrief(plan: DraftPlan): string {
|
|
|
734
1335
|
}
|
|
735
1336
|
|
|
736
1337
|
const parsed = inspection.parsed!;
|
|
737
|
-
const mode = inspection.metadata?.mode ?? "general";
|
|
738
1338
|
const commandLabels = parsed.frontmatter.commands.map(formatCommandLabel);
|
|
739
|
-
const
|
|
1339
|
+
const finishBehavior = summarizeFinishBehavior(parsed.frontmatter);
|
|
740
1340
|
const safetyLabel = summarizeSafetyLabel(parsed.frontmatter.guardrails);
|
|
741
1341
|
|
|
742
1342
|
return [
|
|
@@ -753,7 +1353,7 @@ export function buildMissionBrief(plan: DraftPlan): string {
|
|
|
753
1353
|
...commandLabels.map((label) => `- ${label}`),
|
|
754
1354
|
"",
|
|
755
1355
|
"Finish behavior",
|
|
756
|
-
|
|
1356
|
+
...finishBehavior,
|
|
757
1357
|
"",
|
|
758
1358
|
"Safety",
|
|
759
1359
|
`- ${safetyLabel}`,
|
|
@@ -769,20 +1369,77 @@ export function shouldStopForCompletionPromise(text: string, expected: string):
|
|
|
769
1369
|
return extractCompletionPromise(text) === expected.trim();
|
|
770
1370
|
}
|
|
771
1371
|
|
|
772
|
-
|
|
1372
|
+
function shellQuote(value: string): string {
|
|
1373
|
+
return "'" + value.split("'").join("'\\''") + "'";
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
export function replaceArgsPlaceholders(text: string, runtimeArgs: RuntimeArgs, shellSafe = false): string {
|
|
1377
|
+
return text.replace(/\{\{\s*args\.(\w[\w-]*)\s*\}\}/g, (_, name) => {
|
|
1378
|
+
if (!Object.prototype.hasOwnProperty.call(runtimeArgs, name)) {
|
|
1379
|
+
throw new Error(`Missing required arg: ${name}`);
|
|
1380
|
+
}
|
|
1381
|
+
const value = runtimeArgs[name];
|
|
1382
|
+
return shellSafe ? shellQuote(value) : value;
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
export function resolvePlaceholders(
|
|
1387
|
+
body: string,
|
|
1388
|
+
outputs: CommandOutput[],
|
|
1389
|
+
ralph: { iteration: number; name: string; maxIterations: number },
|
|
1390
|
+
runtimeArgs: RuntimeArgs = {},
|
|
1391
|
+
): string {
|
|
773
1392
|
const map = new Map(outputs.map((o) => [o.name, o.output]));
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1393
|
+
const resolved = replaceArgsPlaceholders(
|
|
1394
|
+
body
|
|
1395
|
+
.replace(/\{\{\s*ralph\.iteration\s*\}\}/g, String(ralph.iteration))
|
|
1396
|
+
.replace(/\{\{\s*ralph\.name\s*\}\}/g, ralph.name)
|
|
1397
|
+
.replace(/\{\{\s*ralph\.max_iterations\s*\}\}/g, String(ralph.maxIterations)),
|
|
1398
|
+
runtimeArgs,
|
|
1399
|
+
);
|
|
1400
|
+
return resolved.replace(/\{\{\s*commands\.(\w[\w-]*)\s*\}\}/g, (_, name) => map.get(name) ?? "");
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
export function resolveCommandRun(run: string, runtimeArgs: RuntimeArgs): string {
|
|
1404
|
+
return replaceArgsPlaceholders(run, runtimeArgs, true);
|
|
778
1405
|
}
|
|
779
1406
|
|
|
780
|
-
export function renderRalphBody(
|
|
781
|
-
|
|
1407
|
+
export function renderRalphBody(
|
|
1408
|
+
body: string,
|
|
1409
|
+
outputs: CommandOutput[],
|
|
1410
|
+
ralph: { iteration: number; name: string; maxIterations: number },
|
|
1411
|
+
runtimeArgs: RuntimeArgs = {},
|
|
1412
|
+
): string {
|
|
1413
|
+
return resolvePlaceholders(body, outputs, ralph, runtimeArgs).replace(/<!--[\s\S]*?-->/g, "");
|
|
782
1414
|
}
|
|
783
1415
|
|
|
784
|
-
export function renderIterationPrompt(
|
|
785
|
-
|
|
1416
|
+
export function renderIterationPrompt(
|
|
1417
|
+
body: string,
|
|
1418
|
+
iteration: number,
|
|
1419
|
+
maxIterations: number,
|
|
1420
|
+
completionGate?: { completionPromise?: string; requiredOutputs?: string[]; failureReasons?: string[]; rejectionReasons?: string[] },
|
|
1421
|
+
): string {
|
|
1422
|
+
if (!completionGate) {
|
|
1423
|
+
return `[ralph: iteration ${iteration}/${maxIterations}]\n\n${body}`;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
const requiredOutputs = completionGate.requiredOutputs ?? [];
|
|
1427
|
+
const failureReasons = completionGate.failureReasons ?? [];
|
|
1428
|
+
const rejectionReasons = completionGate.rejectionReasons ?? [];
|
|
1429
|
+
const completionPromise = completionGate.completionPromise ?? "DONE";
|
|
1430
|
+
const gateLines = [
|
|
1431
|
+
"[completion gate]",
|
|
1432
|
+
`- Required outputs must exist before stopping${requiredOutputs.length > 0 ? `: ${requiredOutputs.join(", ")}` : "."}`,
|
|
1433
|
+
"- OPEN_QUESTIONS.md must have no remaining P0/P1 items before stopping.",
|
|
1434
|
+
"- Label inferred claims as HYPOTHESIS.",
|
|
1435
|
+
...(rejectionReasons.length > 0
|
|
1436
|
+
? ["[completion gate rejection]", `- Still missing: ${rejectionReasons.join("; ")}`]
|
|
1437
|
+
: []),
|
|
1438
|
+
...(failureReasons.length > 0 ? [`- Previous gate failures: ${failureReasons.join("; ")}`] : []),
|
|
1439
|
+
`- Emit <promise>${completionPromise}</promise> only when the gate is truly satisfied.`,
|
|
1440
|
+
];
|
|
1441
|
+
|
|
1442
|
+
return `[ralph: iteration ${iteration}/${maxIterations}]\n\n${body}\n\n${gateLines.join("\n")}`;
|
|
786
1443
|
}
|
|
787
1444
|
|
|
788
1445
|
export function shouldWarnForBashFailure(output: string): boolean {
|