@lnilluv/pi-ralph-loop 0.3.0 → 1.0.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/.github/workflows/release.yml +8 -39
- package/README.md +50 -160
- package/package.json +2 -2
- package/scripts/version-helper.ts +210 -0
- package/src/index.ts +1085 -188
- package/src/ralph-draft-context.ts +618 -0
- package/src/ralph-draft-llm.ts +297 -0
- package/src/ralph-draft.ts +33 -0
- package/src/ralph.ts +917 -102
- package/src/runner-rpc.ts +434 -0
- package/src/runner-state.ts +822 -0
- package/src/runner.ts +957 -0
- package/src/secret-paths.ts +66 -0
- package/src/shims.d.ts +0 -3
- 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 +3529 -0
- 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-context.test.ts +672 -0
- package/tests/ralph-draft-llm.test.ts +434 -0
- package/tests/ralph-draft.test.ts +168 -0
- package/tests/ralph.test.ts +1389 -19
- 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/tests/secret-paths.test.ts +55 -0
- package/tests/version-helper.test.ts +75 -0
package/src/ralph.ts
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
|
+
import { SECRET_PATH_POLICY_TOKEN, filterSecretBearingTopLevelNames, isSecretBearingPath, isSecretBearingTopLevelName } from "./secret-paths.ts";
|
|
4
5
|
|
|
5
6
|
export type CommandDef = { name: string; run: string; timeout: number };
|
|
7
|
+
export type DraftSource = "deterministic" | "llm-strengthened" | "fallback";
|
|
8
|
+
export type DraftStrengtheningScope = "body-only" | "body-and-commands";
|
|
9
|
+
export type CommandIntent = CommandDef & { source: "heuristic" | "repo-signal" };
|
|
10
|
+
export type RuntimeArg = { name: string; value: string };
|
|
11
|
+
export type RuntimeArgs = Record<string, string>;
|
|
6
12
|
export type Frontmatter = {
|
|
7
13
|
commands: CommandDef[];
|
|
14
|
+
args?: string[];
|
|
8
15
|
maxIterations: number;
|
|
16
|
+
interIterationDelay: number;
|
|
9
17
|
timeout: number;
|
|
10
18
|
completionPromise?: string;
|
|
19
|
+
requiredOutputs?: string[];
|
|
11
20
|
guardrails: { blockCommands: string[]; protectedFiles: string[] };
|
|
12
21
|
invalidCommandEntries?: number[];
|
|
22
|
+
invalidArgEntries?: number[];
|
|
13
23
|
};
|
|
14
24
|
export type ParsedRalph = { frontmatter: Frontmatter; body: string };
|
|
15
25
|
export type CommandOutput = { name: string; output: string };
|
|
@@ -18,9 +28,12 @@ export type RalphTargetResolution = {
|
|
|
18
28
|
absoluteTarget: string;
|
|
19
29
|
markdownPath: string;
|
|
20
30
|
};
|
|
21
|
-
export type CommandArgs =
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
export type CommandArgs = {
|
|
32
|
+
mode: "path" | "task" | "auto";
|
|
33
|
+
value: string;
|
|
34
|
+
runtimeArgs: RuntimeArg[];
|
|
35
|
+
error?: string;
|
|
36
|
+
};
|
|
24
37
|
export type ExistingTargetInspection =
|
|
25
38
|
| { kind: "run"; ralphPath: string }
|
|
26
39
|
| { kind: "invalid-markdown"; path: string }
|
|
@@ -29,12 +42,20 @@ export type ExistingTargetInspection =
|
|
|
29
42
|
| { kind: "missing-path"; dirPath: string; ralphPath: string }
|
|
30
43
|
| { kind: "not-path" };
|
|
31
44
|
export type DraftMode = "analysis" | "fix" | "migration" | "general";
|
|
32
|
-
export type DraftMetadata =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
export type DraftMetadata =
|
|
46
|
+
| {
|
|
47
|
+
generator: "pi-ralph-loop";
|
|
48
|
+
version: 1;
|
|
49
|
+
task: string;
|
|
50
|
+
mode: DraftMode;
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
generator: "pi-ralph-loop";
|
|
54
|
+
version: 2;
|
|
55
|
+
source: DraftSource;
|
|
56
|
+
task: string;
|
|
57
|
+
mode: DraftMode;
|
|
58
|
+
};
|
|
38
59
|
export type DraftTarget = {
|
|
39
60
|
slug: string;
|
|
40
61
|
dirPath: string;
|
|
@@ -51,10 +72,29 @@ export type RepoSignals = {
|
|
|
51
72
|
topLevelDirs: string[];
|
|
52
73
|
topLevelFiles: string[];
|
|
53
74
|
};
|
|
75
|
+
export type RepoContextSelectedFile = {
|
|
76
|
+
path: string;
|
|
77
|
+
content: string;
|
|
78
|
+
reason: string;
|
|
79
|
+
};
|
|
80
|
+
export type RepoContext = {
|
|
81
|
+
summaryLines: string[];
|
|
82
|
+
selectedFiles: RepoContextSelectedFile[];
|
|
83
|
+
};
|
|
84
|
+
export type DraftRequest = {
|
|
85
|
+
task: string;
|
|
86
|
+
mode: DraftMode;
|
|
87
|
+
target: DraftTarget;
|
|
88
|
+
repoSignals: RepoSignals;
|
|
89
|
+
repoContext: RepoContext;
|
|
90
|
+
commandIntent: CommandIntent[];
|
|
91
|
+
baselineDraft: string;
|
|
92
|
+
};
|
|
54
93
|
export type DraftPlan = {
|
|
55
94
|
task: string;
|
|
56
95
|
mode: DraftMode;
|
|
57
96
|
target: DraftTarget;
|
|
97
|
+
source: DraftSource;
|
|
58
98
|
content: string;
|
|
59
99
|
commandLabels: string[];
|
|
60
100
|
safetyLabel: string;
|
|
@@ -67,6 +107,17 @@ function isRecord(value: unknown): value is UnknownRecord {
|
|
|
67
107
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
68
108
|
}
|
|
69
109
|
|
|
110
|
+
const draftModes: DraftMode[] = ["analysis", "fix", "migration", "general"];
|
|
111
|
+
const draftSources: DraftSource[] = ["deterministic", "llm-strengthened", "fallback"];
|
|
112
|
+
|
|
113
|
+
function isDraftMode(value: unknown): value is DraftMode {
|
|
114
|
+
return typeof value === "string" && draftModes.includes(value as DraftMode);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isDraftSource(value: unknown): value is DraftSource {
|
|
118
|
+
return typeof value === "string" && draftSources.includes(value as DraftSource);
|
|
119
|
+
}
|
|
120
|
+
|
|
70
121
|
function parseRalphFrontmatter(raw: string): UnknownRecord {
|
|
71
122
|
const parsed: unknown = parseYaml(raw);
|
|
72
123
|
return isRecord(parsed) ? parsed : {};
|
|
@@ -89,6 +140,28 @@ function toStringArray(value: unknown): string[] {
|
|
|
89
140
|
return Array.isArray(value) ? value.map((item) => String(item)) : [];
|
|
90
141
|
}
|
|
91
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
|
+
|
|
92
165
|
function normalizeRawRalph(raw: string): string {
|
|
93
166
|
return raw.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
94
167
|
}
|
|
@@ -97,8 +170,159 @@ function matchRalphMarkdown(raw: string): RegExpMatchArray | null {
|
|
|
97
170
|
return normalizeRawRalph(raw).match(/^(?:\s*<!--[\s\S]*?-->\s*)*---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
98
171
|
}
|
|
99
172
|
|
|
100
|
-
|
|
101
|
-
|
|
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 };
|
|
102
326
|
}
|
|
103
327
|
|
|
104
328
|
function normalizeMissingMarkdownTarget(absoluteTarget: string): { dirPath: string; ralphPath: string } {
|
|
@@ -117,7 +341,7 @@ function summarizeSafetyLabel(guardrails: Frontmatter["guardrails"]): string {
|
|
|
117
341
|
} else if (guardrails.blockCommands.length > 0) {
|
|
118
342
|
labels.push(`blocks ${guardrails.blockCommands.length} command pattern${guardrails.blockCommands.length === 1 ? "" : "s"}`);
|
|
119
343
|
}
|
|
120
|
-
if (guardrails.protectedFiles.some((pattern) => pattern
|
|
344
|
+
if (guardrails.protectedFiles.some((pattern) => pattern === SECRET_PATH_POLICY_TOKEN || isSecretBearingPath(pattern))) {
|
|
121
345
|
labels.push("blocks write/edit to secret files");
|
|
122
346
|
} else if (guardrails.protectedFiles.length > 0) {
|
|
123
347
|
labels.push(`blocks write/edit to ${guardrails.protectedFiles.length} file glob${guardrails.protectedFiles.length === 1 ? "" : "s"}`);
|
|
@@ -125,8 +349,55 @@ function summarizeSafetyLabel(guardrails: Frontmatter["guardrails"]): string {
|
|
|
125
349
|
return labels.length > 0 ? labels.join(" and ") : "No extra safety rules";
|
|
126
350
|
}
|
|
127
351
|
|
|
128
|
-
function summarizeFinishLabel(
|
|
129
|
-
|
|
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;
|
|
130
401
|
}
|
|
131
402
|
|
|
132
403
|
function isRalphMarkdownPath(path: string): boolean {
|
|
@@ -212,7 +483,7 @@ function escapeHtmlCommentMarkers(text: string): string {
|
|
|
212
483
|
}
|
|
213
484
|
|
|
214
485
|
export function defaultFrontmatter(): Frontmatter {
|
|
215
|
-
return { commands: [], maxIterations: 50, timeout: 300, guardrails: { blockCommands: [], protectedFiles: [] } };
|
|
486
|
+
return { commands: [], maxIterations: 50, interIterationDelay: 0, timeout: 300, requiredOutputs: [], guardrails: { blockCommands: [], protectedFiles: [] } };
|
|
216
487
|
}
|
|
217
488
|
|
|
218
489
|
export function parseRalphMarkdown(raw: string): ParsedRalph {
|
|
@@ -230,20 +501,25 @@ export function parseRalphMarkdown(raw: string): ParsedRalph {
|
|
|
230
501
|
}
|
|
231
502
|
return [parsed];
|
|
232
503
|
});
|
|
504
|
+
const parsedArgs = parseStringArray(yaml.args);
|
|
233
505
|
const guardrails = isRecord(yaml.guardrails) ? yaml.guardrails : {};
|
|
234
506
|
|
|
235
507
|
return {
|
|
236
508
|
frontmatter: {
|
|
237
509
|
commands,
|
|
510
|
+
...(parsedArgs.values.length > 0 ? { args: parsedArgs.values } : {}),
|
|
238
511
|
maxIterations: Number(yaml.max_iterations ?? 50),
|
|
512
|
+
interIterationDelay: Number(yaml.inter_iteration_delay ?? 0),
|
|
239
513
|
timeout: Number(yaml.timeout ?? 300),
|
|
240
514
|
completionPromise:
|
|
241
515
|
typeof yaml.completion_promise === "string" && yaml.completion_promise.trim() ? yaml.completion_promise : undefined,
|
|
516
|
+
requiredOutputs: toStringArray(yaml.required_outputs),
|
|
242
517
|
guardrails: {
|
|
243
518
|
blockCommands: toStringArray(guardrails.block_commands),
|
|
244
519
|
protectedFiles: toStringArray(guardrails.protected_files),
|
|
245
520
|
},
|
|
246
521
|
invalidCommandEntries: invalidCommandEntries.length > 0 ? invalidCommandEntries : undefined,
|
|
522
|
+
...(parsedArgs.invalidEntries ? { invalidArgEntries: parsedArgs.invalidEntries } : {}),
|
|
247
523
|
},
|
|
248
524
|
body: match[2] ?? "",
|
|
249
525
|
};
|
|
@@ -253,11 +529,40 @@ export function validateFrontmatter(fm: Frontmatter): string | null {
|
|
|
253
529
|
if ((fm.invalidCommandEntries?.length ?? 0) > 0) {
|
|
254
530
|
return `Invalid command entry at index ${fm.invalidCommandEntries![0]}`;
|
|
255
531
|
}
|
|
256
|
-
if (
|
|
257
|
-
return
|
|
532
|
+
if ((fm.invalidArgEntries?.length ?? 0) > 0) {
|
|
533
|
+
return `Invalid args entry at index ${fm.invalidArgEntries![0]}`;
|
|
534
|
+
}
|
|
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";
|
|
258
540
|
}
|
|
259
|
-
if (!Number.isFinite(fm.timeout) || fm.timeout <= 0) {
|
|
260
|
-
return "Invalid timeout: must be
|
|
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
|
+
}
|
|
261
566
|
}
|
|
262
567
|
for (const pattern of fm.guardrails.blockCommands) {
|
|
263
568
|
try {
|
|
@@ -266,20 +571,118 @@ export function validateFrontmatter(fm: Frontmatter): string | null {
|
|
|
266
571
|
return `Invalid block_commands regex: ${pattern}`;
|
|
267
572
|
}
|
|
268
573
|
}
|
|
574
|
+
for (const pattern of fm.guardrails.protectedFiles) {
|
|
575
|
+
if (isUniversalProtectedGlob(pattern)) {
|
|
576
|
+
return `Invalid protected_files glob: ${pattern}`;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
269
579
|
for (const cmd of fm.commands) {
|
|
270
580
|
if (!cmd.name.trim()) {
|
|
271
581
|
return "Invalid command: name is required";
|
|
272
582
|
}
|
|
583
|
+
if (!/^\w[\w-]*$/.test(cmd.name)) {
|
|
584
|
+
return `Invalid command name: ${cmd.name} must match ^\\w[\\w-]*$`;
|
|
585
|
+
}
|
|
273
586
|
if (!cmd.run.trim()) {
|
|
274
587
|
return `Invalid command ${cmd.name}: run is required`;
|
|
275
588
|
}
|
|
276
|
-
if (!Number.isFinite(cmd.timeout) || cmd.timeout <= 0) {
|
|
277
|
-
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`;
|
|
278
594
|
}
|
|
279
595
|
}
|
|
280
596
|
return null;
|
|
281
597
|
}
|
|
282
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
|
+
|
|
283
686
|
export function findBlockedCommandPattern(command: string, blockPatterns: string[]): string | undefined {
|
|
284
687
|
for (const pattern of blockPatterns) {
|
|
285
688
|
try {
|
|
@@ -291,13 +694,231 @@ export function findBlockedCommandPattern(command: string, blockPatterns: string
|
|
|
291
694
|
return undefined;
|
|
292
695
|
}
|
|
293
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
|
+
|
|
294
828
|
export function parseCommandArgs(raw: string): CommandArgs {
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
if (
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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;
|
|
301
922
|
}
|
|
302
923
|
|
|
303
924
|
export function looksLikePath(value: string): boolean {
|
|
@@ -415,8 +1036,9 @@ export function inspectRepo(cwd: string): RepoSignals {
|
|
|
415
1036
|
|
|
416
1037
|
try {
|
|
417
1038
|
const entries = readdirSync(cwd, { withFileTypes: true }).slice(0, 50);
|
|
418
|
-
|
|
419
|
-
|
|
1039
|
+
const filteredEntries = entries.filter((entry) => !isSecretBearingTopLevelName(entry.name));
|
|
1040
|
+
topLevelDirs = filteredEntries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).slice(0, 10);
|
|
1041
|
+
topLevelFiles = filteredEntries.filter((entry) => entry.isFile()).map((entry) => entry.name).slice(0, 10);
|
|
420
1042
|
} catch {
|
|
421
1043
|
// ignore bounded inspection failures
|
|
422
1044
|
}
|
|
@@ -431,21 +1053,70 @@ export function inspectRepo(cwd: string): RepoSignals {
|
|
|
431
1053
|
};
|
|
432
1054
|
}
|
|
433
1055
|
|
|
434
|
-
export function
|
|
1056
|
+
export function buildRepoContext(signals: RepoSignals): RepoContext {
|
|
1057
|
+
const topLevelDirs = filterSecretBearingTopLevelNames(signals.topLevelDirs);
|
|
1058
|
+
const topLevelFiles = filterSecretBearingTopLevelNames(signals.topLevelFiles);
|
|
1059
|
+
|
|
1060
|
+
return {
|
|
1061
|
+
summaryLines: [
|
|
1062
|
+
`package manager: ${signals.packageManager ?? "unknown"}`,
|
|
1063
|
+
`test command: ${signals.testCommand ?? "none"}`,
|
|
1064
|
+
`lint command: ${signals.lintCommand ?? "none"}`,
|
|
1065
|
+
`git repository: ${signals.hasGit ? "present" : "absent"}`,
|
|
1066
|
+
`top-level dirs: ${topLevelDirs.length > 0 ? topLevelDirs.join(", ") : "none"}`,
|
|
1067
|
+
`top-level files: ${topLevelFiles.length > 0 ? topLevelFiles.join(", ") : "none"}`,
|
|
1068
|
+
],
|
|
1069
|
+
selectedFiles: topLevelFiles.slice(0, 10).map((path) => ({
|
|
1070
|
+
path,
|
|
1071
|
+
content: "",
|
|
1072
|
+
reason: "top-level file",
|
|
1073
|
+
})),
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function normalizeSelectedFile(file: unknown): RepoContextSelectedFile {
|
|
1078
|
+
if (isRecord(file)) {
|
|
1079
|
+
return {
|
|
1080
|
+
path: String(file.path ?? ""),
|
|
1081
|
+
content: String(file.content ?? ""),
|
|
1082
|
+
reason: String(file.reason ?? "selected file"),
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
if (typeof file === "string") {
|
|
1086
|
+
return { path: file, content: "", reason: "selected file" };
|
|
1087
|
+
}
|
|
1088
|
+
return { path: String(file), content: "", reason: "selected file" };
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function normalizeRepoContext(repoContext: RepoContext | undefined, signals: RepoSignals): RepoContext {
|
|
1092
|
+
if (repoContext && Array.isArray(repoContext.summaryLines) && Array.isArray(repoContext.selectedFiles)) {
|
|
1093
|
+
return {
|
|
1094
|
+
summaryLines: repoContext.summaryLines.map((line) => String(line)),
|
|
1095
|
+
selectedFiles: repoContext.selectedFiles.map((file) => normalizeSelectedFile(file)),
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
return buildRepoContext(signals);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
export function buildCommandIntent(mode: DraftMode, signals: RepoSignals): CommandIntent[] {
|
|
435
1102
|
if (mode === "analysis") {
|
|
436
|
-
const commands:
|
|
437
|
-
if (signals.hasGit) commands.unshift({ name: "git-log", run: "git log --oneline -10", timeout: 20 });
|
|
1103
|
+
const commands: CommandIntent[] = [{ name: "repo-map", run: "find . -maxdepth 2 -type f | sort | head -n 120", timeout: 20, source: "heuristic" }];
|
|
1104
|
+
if (signals.hasGit) commands.unshift({ name: "git-log", run: "git log --oneline -10", timeout: 20, source: "heuristic" });
|
|
438
1105
|
return commands;
|
|
439
1106
|
}
|
|
440
1107
|
|
|
441
|
-
const commands:
|
|
442
|
-
if (signals.testCommand) commands.push({ name: "tests", run: signals.testCommand, timeout: 120 });
|
|
443
|
-
if (signals.lintCommand) commands.push({ name: "lint", run: signals.lintCommand, timeout: 90 });
|
|
444
|
-
if (signals.hasGit) commands.push({ name: "git-log", run: "git log --oneline -10", timeout: 20 });
|
|
445
|
-
if (commands.length === 0) commands.push({ name: "repo-map", run: "find . -maxdepth 2 -type f | sort | head -n 120", timeout: 20 });
|
|
1108
|
+
const commands: CommandIntent[] = [];
|
|
1109
|
+
if (signals.testCommand) commands.push({ name: "tests", run: signals.testCommand, timeout: 120, source: "repo-signal" });
|
|
1110
|
+
if (signals.lintCommand) commands.push({ name: "lint", run: signals.lintCommand, timeout: 90, source: "repo-signal" });
|
|
1111
|
+
if (signals.hasGit) commands.push({ name: "git-log", run: "git log --oneline -10", timeout: 20, source: "heuristic" });
|
|
1112
|
+
if (commands.length === 0) commands.push({ name: "repo-map", run: "find . -maxdepth 2 -type f | sort | head -n 120", timeout: 20, source: "heuristic" });
|
|
446
1113
|
return commands;
|
|
447
1114
|
}
|
|
448
1115
|
|
|
1116
|
+
export function suggestedCommandsForMode(mode: DraftMode, signals: RepoSignals): CommandDef[] {
|
|
1117
|
+
return buildCommandIntent(mode, signals).map(({ source: _source, ...command }) => command);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
449
1120
|
function formatCommandLabel(command: CommandDef): string {
|
|
450
1121
|
return `${command.name}: ${command.run}`;
|
|
451
1122
|
}
|
|
@@ -455,68 +1126,157 @@ function extractVisibleTask(body: string): string | undefined {
|
|
|
455
1126
|
return match?.[1]?.trim() || undefined;
|
|
456
1127
|
}
|
|
457
1128
|
|
|
458
|
-
|
|
459
|
-
const mode = classifyTaskMode(task);
|
|
460
|
-
const commands = suggestedCommandsForMode(mode, signals);
|
|
461
|
-
const metadata: DraftMetadata = { generator: "pi-ralph-loop", version: 1, task, mode };
|
|
1129
|
+
function buildDraftFrontmatter(mode: DraftMode, commands: CommandDef[]): Frontmatter {
|
|
462
1130
|
const guardrails = {
|
|
463
1131
|
blockCommands: ["git\\s+push"],
|
|
464
|
-
protectedFiles: mode === "analysis" ? [] : [
|
|
1132
|
+
protectedFiles: mode === "analysis" ? [] : [SECRET_PATH_POLICY_TOKEN],
|
|
1133
|
+
};
|
|
1134
|
+
return {
|
|
1135
|
+
commands,
|
|
1136
|
+
maxIterations: mode === "analysis" ? 12 : mode === "migration" ? 30 : 25,
|
|
1137
|
+
interIterationDelay: 0,
|
|
1138
|
+
timeout: 300,
|
|
1139
|
+
requiredOutputs: [],
|
|
1140
|
+
guardrails,
|
|
465
1141
|
};
|
|
466
|
-
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function renderDraftBody(task: string, mode: DraftMode, commands: CommandDef[]): string {
|
|
1145
|
+
const commandSections = commands.map((command) => bodySection(command.name === "git-log" ? "Recent git history" : `Latest ${command.name} output`, `{{ commands.${command.name} }}`));
|
|
1146
|
+
return mode === "analysis"
|
|
1147
|
+
? [
|
|
1148
|
+
`Task: ${escapeHtmlCommentMarkers(task)}`,
|
|
1149
|
+
"",
|
|
1150
|
+
...commandSections,
|
|
1151
|
+
"",
|
|
1152
|
+
"Start with read-only inspection. Avoid edits and commits until you have a clear plan.",
|
|
1153
|
+
"Map the architecture, identify entry points, and summarize the important moving parts.",
|
|
1154
|
+
"End each iteration with concrete findings, open questions, and the next files to inspect.",
|
|
1155
|
+
"Iteration {{ ralph.iteration }} of {{ ralph.name }}.",
|
|
1156
|
+
].join("\n")
|
|
1157
|
+
: [
|
|
1158
|
+
`Task: ${escapeHtmlCommentMarkers(task)}`,
|
|
1159
|
+
"",
|
|
1160
|
+
...commandSections,
|
|
1161
|
+
"",
|
|
1162
|
+
mode === "fix" ? "If tests or lint are failing, fix those failures before starting new work." : "Make the smallest safe change that moves the task forward.",
|
|
1163
|
+
"Prefer concrete, verifiable progress. Explain why your change works.",
|
|
1164
|
+
"Iteration {{ ralph.iteration }} of {{ ralph.name }}.",
|
|
1165
|
+
].join("\n");
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function commandIntentsToCommands(commandIntents: CommandIntent[]): CommandDef[] {
|
|
1169
|
+
return commandIntents.map(({ source: _source, ...command }) => command);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function renderDraftPlan(task: string, mode: DraftMode, target: DraftTarget, frontmatter: Frontmatter, source: DraftSource, body: string): DraftPlan {
|
|
1173
|
+
const metadata: DraftMetadata = { generator: "pi-ralph-loop", version: 2, source, task, mode };
|
|
1174
|
+
const requiredOutputs = frontmatter.requiredOutputs ?? [];
|
|
467
1175
|
const frontmatterLines = [
|
|
468
|
-
...renderCommandsYaml(commands),
|
|
469
|
-
`max_iterations: ${maxIterations}`,
|
|
470
|
-
|
|
1176
|
+
...renderCommandsYaml(frontmatter.commands),
|
|
1177
|
+
`max_iterations: ${frontmatter.maxIterations}`,
|
|
1178
|
+
`inter_iteration_delay: ${frontmatter.interIterationDelay}`,
|
|
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)}`] : []),
|
|
471
1184
|
"guardrails:",
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
...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: []"]),
|
|
476
1191
|
];
|
|
477
1192
|
|
|
478
|
-
const commandSections = commands.map((command) => bodySection(command.name === "git-log" ? "Recent git history" : `Latest ${command.name} output`, `{{ commands.${command.name} }}`));
|
|
479
|
-
const body =
|
|
480
|
-
mode === "analysis"
|
|
481
|
-
? [
|
|
482
|
-
`Task: ${escapeHtmlCommentMarkers(task)}`,
|
|
483
|
-
"",
|
|
484
|
-
...commandSections,
|
|
485
|
-
"",
|
|
486
|
-
"Start with read-only inspection. Avoid edits and commits until you have a clear plan.",
|
|
487
|
-
"Map the architecture, identify entry points, and summarize the important moving parts.",
|
|
488
|
-
"End each iteration with concrete findings, open questions, and the next files to inspect.",
|
|
489
|
-
"Iteration {{ ralph.iteration }} of {{ ralph.name }}.",
|
|
490
|
-
].join("\n")
|
|
491
|
-
: [
|
|
492
|
-
`Task: ${escapeHtmlCommentMarkers(task)}`,
|
|
493
|
-
"",
|
|
494
|
-
...commandSections,
|
|
495
|
-
"",
|
|
496
|
-
mode === "fix"
|
|
497
|
-
? "If tests or lint are failing, fix those failures before starting new work."
|
|
498
|
-
: "Make the smallest safe change that moves the task forward.",
|
|
499
|
-
"Prefer concrete, verifiable progress. Explain why your change works.",
|
|
500
|
-
"Iteration {{ ralph.iteration }} of {{ ralph.name }}.",
|
|
501
|
-
].join("\n");
|
|
502
|
-
|
|
503
1193
|
return {
|
|
504
1194
|
task,
|
|
505
1195
|
mode,
|
|
506
1196
|
target,
|
|
1197
|
+
source,
|
|
507
1198
|
content: `${metadataComment(metadata)}\n${yamlBlock(frontmatterLines)}\n\n${body}`,
|
|
508
|
-
commandLabels: commands.map(formatCommandLabel),
|
|
509
|
-
safetyLabel: summarizeSafetyLabel(guardrails),
|
|
510
|
-
finishLabel: summarizeFinishLabel(
|
|
1199
|
+
commandLabels: frontmatter.commands.map(formatCommandLabel),
|
|
1200
|
+
safetyLabel: summarizeSafetyLabel(frontmatter.guardrails),
|
|
1201
|
+
finishLabel: summarizeFinishLabel(frontmatter),
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
export function generateDraftFromRequest(request: Omit<DraftRequest, "baselineDraft">, source: DraftSource): DraftPlan {
|
|
1206
|
+
const commands = commandIntentsToCommands(request.commandIntent);
|
|
1207
|
+
const frontmatter = buildDraftFrontmatter(request.mode, commands);
|
|
1208
|
+
return renderDraftPlan(request.task, request.mode, request.target, frontmatter, source, renderDraftBody(request.task, request.mode, commands));
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
export function buildDraftRequest(task: string, target: DraftTarget, repoSignals: RepoSignals, repoContext?: RepoContext): DraftRequest {
|
|
1212
|
+
const mode = classifyTaskMode(task);
|
|
1213
|
+
const commandIntents = buildCommandIntent(mode, repoSignals);
|
|
1214
|
+
const request: Omit<DraftRequest, "baselineDraft"> = {
|
|
1215
|
+
task,
|
|
1216
|
+
mode,
|
|
1217
|
+
target,
|
|
1218
|
+
repoSignals,
|
|
1219
|
+
repoContext: normalizeRepoContext(repoContext, repoSignals),
|
|
1220
|
+
commandIntent: commandIntents,
|
|
511
1221
|
};
|
|
1222
|
+
return { ...request, baselineDraft: generateDraftFromRequest(request, "deterministic").content };
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
export function normalizeStrengthenedDraft(request: DraftRequest, strengthenedDraft: string, scope: DraftStrengtheningScope): DraftPlan {
|
|
1226
|
+
const baseline = parseRalphMarkdown(request.baselineDraft);
|
|
1227
|
+
const strengthened = parseStrictRalphMarkdown(strengthenedDraft);
|
|
1228
|
+
|
|
1229
|
+
if (scope === "body-only") {
|
|
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);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
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);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
export function hasFakeRuntimeEnforcementClaim(text: string): boolean {
|
|
1250
|
+
return /read[-\s]?only enforced|write protection is enforced/i.test(text);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
export function isWeakStrengthenedDraft(baselineBody: string, analysisText: string, strengthenedBody: string): boolean {
|
|
1254
|
+
return baselineBody.trim() === strengthenedBody.trim() || hasFakeRuntimeEnforcementClaim(analysisText) || hasFakeRuntimeEnforcementClaim(strengthenedBody);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
export function generateDraft(task: string, target: DraftTarget, signals: RepoSignals): DraftPlan {
|
|
1258
|
+
const request = buildDraftRequest(task, target, signals);
|
|
1259
|
+
return generateDraftFromRequest(request, "deterministic");
|
|
512
1260
|
}
|
|
513
1261
|
|
|
514
1262
|
export function extractDraftMetadata(raw: string): DraftMetadata | undefined {
|
|
515
1263
|
const match = raw.match(/^<!-- pi-ralph-loop: (.+?) -->/);
|
|
516
1264
|
if (!match) return undefined;
|
|
1265
|
+
|
|
517
1266
|
try {
|
|
518
|
-
const parsed = JSON.parse(decodeDraftMetadata(match[1]))
|
|
519
|
-
|
|
1267
|
+
const parsed: unknown = JSON.parse(decodeDraftMetadata(match[1]));
|
|
1268
|
+
if (!isRecord(parsed) || parsed.generator !== "pi-ralph-loop") return undefined;
|
|
1269
|
+
if (!isDraftMode(parsed.mode) || typeof parsed.task !== "string") return undefined;
|
|
1270
|
+
|
|
1271
|
+
if (parsed.version === 1) {
|
|
1272
|
+
return { generator: "pi-ralph-loop", version: 1, task: parsed.task, mode: parsed.mode };
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
if (parsed.version === 2 && isDraftSource(parsed.source)) {
|
|
1276
|
+
return { generator: "pi-ralph-loop", version: 2, source: parsed.source, task: parsed.task, mode: parsed.mode };
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
return undefined;
|
|
520
1280
|
} catch {
|
|
521
1281
|
return undefined;
|
|
522
1282
|
}
|
|
@@ -534,20 +1294,19 @@ export type DraftContentInspection = {
|
|
|
534
1294
|
|
|
535
1295
|
export function inspectDraftContent(raw: string): DraftContentInspection {
|
|
536
1296
|
const metadata = extractDraftMetadata(raw);
|
|
537
|
-
const
|
|
1297
|
+
const parsed = parseStrictRalphMarkdown(raw);
|
|
538
1298
|
|
|
539
|
-
if (
|
|
540
|
-
return { metadata, error:
|
|
1299
|
+
if ("error" in parsed) {
|
|
1300
|
+
return { metadata, error: parsed.error };
|
|
541
1301
|
}
|
|
542
1302
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
return error ? { metadata, parsed, error } : { metadata, parsed };
|
|
547
|
-
} catch (err) {
|
|
548
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
549
|
-
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" };
|
|
550
1306
|
}
|
|
1307
|
+
|
|
1308
|
+
const error = validateFrontmatter(parsed.parsed.frontmatter);
|
|
1309
|
+
return error ? { metadata, parsed: parsed.parsed, error } : { metadata, parsed: parsed.parsed };
|
|
551
1310
|
}
|
|
552
1311
|
|
|
553
1312
|
export function validateDraftContent(raw: string): string | null {
|
|
@@ -576,9 +1335,8 @@ export function buildMissionBrief(plan: DraftPlan): string {
|
|
|
576
1335
|
}
|
|
577
1336
|
|
|
578
1337
|
const parsed = inspection.parsed!;
|
|
579
|
-
const mode = inspection.metadata?.mode ?? "general";
|
|
580
1338
|
const commandLabels = parsed.frontmatter.commands.map(formatCommandLabel);
|
|
581
|
-
const
|
|
1339
|
+
const finishBehavior = summarizeFinishBehavior(parsed.frontmatter);
|
|
582
1340
|
const safetyLabel = summarizeSafetyLabel(parsed.frontmatter.guardrails);
|
|
583
1341
|
|
|
584
1342
|
return [
|
|
@@ -595,7 +1353,7 @@ export function buildMissionBrief(plan: DraftPlan): string {
|
|
|
595
1353
|
...commandLabels.map((label) => `- ${label}`),
|
|
596
1354
|
"",
|
|
597
1355
|
"Finish behavior",
|
|
598
|
-
|
|
1356
|
+
...finishBehavior,
|
|
599
1357
|
"",
|
|
600
1358
|
"Safety",
|
|
601
1359
|
`- ${safetyLabel}`,
|
|
@@ -611,20 +1369,77 @@ export function shouldStopForCompletionPromise(text: string, expected: string):
|
|
|
611
1369
|
return extractCompletionPromise(text) === expected.trim();
|
|
612
1370
|
}
|
|
613
1371
|
|
|
614
|
-
|
|
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 {
|
|
615
1392
|
const map = new Map(outputs.map((o) => [o.name, o.output]));
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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);
|
|
620
1405
|
}
|
|
621
1406
|
|
|
622
|
-
export function renderRalphBody(
|
|
623
|
-
|
|
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, "");
|
|
624
1414
|
}
|
|
625
1415
|
|
|
626
|
-
export function renderIterationPrompt(
|
|
627
|
-
|
|
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")}`;
|
|
628
1443
|
}
|
|
629
1444
|
|
|
630
1445
|
export function shouldWarnForBashFailure(output: string): boolean {
|