@lnilluv/pi-ralph-loop 0.2.1 → 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.
Files changed (48) hide show
  1. package/.github/workflows/ci.yml +5 -2
  2. package/.github/workflows/release.yml +15 -43
  3. package/README.md +51 -113
  4. package/package.json +13 -5
  5. package/scripts/version-helper.ts +210 -0
  6. package/src/index.ts +1360 -275
  7. package/src/ralph-draft-context.ts +618 -0
  8. package/src/ralph-draft-llm.ts +297 -0
  9. package/src/ralph-draft.ts +33 -0
  10. package/src/ralph.ts +1457 -0
  11. package/src/runner-rpc.ts +434 -0
  12. package/src/runner-state.ts +822 -0
  13. package/src/runner.ts +957 -0
  14. package/src/secret-paths.ts +66 -0
  15. package/src/shims.d.ts +0 -3
  16. package/tests/fixtures/parity/migrate/OPEN_QUESTIONS.md +3 -0
  17. package/tests/fixtures/parity/migrate/RALPH.md +27 -0
  18. package/tests/fixtures/parity/migrate/golden/MIGRATED.md +15 -0
  19. package/tests/fixtures/parity/migrate/legacy/source.md +6 -0
  20. package/tests/fixtures/parity/migrate/legacy/source.yaml +3 -0
  21. package/tests/fixtures/parity/migrate/scripts/show-legacy.sh +10 -0
  22. package/tests/fixtures/parity/migrate/scripts/verify.sh +15 -0
  23. package/tests/fixtures/parity/research/OPEN_QUESTIONS.md +3 -0
  24. package/tests/fixtures/parity/research/RALPH.md +45 -0
  25. package/tests/fixtures/parity/research/claim-evidence-checklist.md +15 -0
  26. package/tests/fixtures/parity/research/expected-outputs.md +22 -0
  27. package/tests/fixtures/parity/research/scripts/show-snapshots.sh +13 -0
  28. package/tests/fixtures/parity/research/scripts/verify.sh +55 -0
  29. package/tests/fixtures/parity/research/snapshots/app-factory-ai-cli.md +11 -0
  30. package/tests/fixtures/parity/research/snapshots/docs-factory-ai-cli-features-missions.md +11 -0
  31. package/tests/fixtures/parity/research/snapshots/factory-ai-news-missions.md +11 -0
  32. package/tests/fixtures/parity/research/source-manifest.md +20 -0
  33. package/tests/index.test.ts +3529 -0
  34. package/tests/parity/README.md +9 -0
  35. package/tests/parity/harness.py +526 -0
  36. package/tests/parity-harness.test.ts +42 -0
  37. package/tests/parity-research-fixture.test.ts +34 -0
  38. package/tests/ralph-draft-context.test.ts +672 -0
  39. package/tests/ralph-draft-llm.test.ts +434 -0
  40. package/tests/ralph-draft.test.ts +168 -0
  41. package/tests/ralph.test.ts +1840 -0
  42. package/tests/runner-event-contract.test.ts +235 -0
  43. package/tests/runner-rpc.test.ts +358 -0
  44. package/tests/runner-state.test.ts +553 -0
  45. package/tests/runner.test.ts +1347 -0
  46. package/tests/secret-paths.test.ts +55 -0
  47. package/tests/version-helper.test.ts +75 -0
  48. package/tsconfig.json +3 -2
@@ -0,0 +1,297 @@
1
+ import { complete, type AssistantMessage, type Context, type Model } from "@mariozechner/pi-ai";
2
+ import { basename } from "node:path";
3
+ import { filterSecretBearingTopLevelNames } from "./secret-paths.ts";
4
+ import {
5
+ acceptStrengthenedDraft,
6
+ hasFakeRuntimeEnforcementClaim,
7
+ normalizeStrengthenedDraft,
8
+ parseRalphMarkdown,
9
+ validateFrontmatter,
10
+ type DraftPlan,
11
+ type DraftRequest,
12
+ type DraftStrengtheningScope,
13
+ type ParsedRalph,
14
+ } from "./ralph.ts";
15
+
16
+ export const DRAFT_LLM_TIMEOUT_MS = 20_000;
17
+
18
+ export type StrengthenDraftRuntime = {
19
+ model: Model<string> | undefined;
20
+ modelRegistry: {
21
+ getApiKeyAndHeaders(model: Model<string>): Promise<AuthResult | AuthFailure>;
22
+ };
23
+ };
24
+
25
+ export type StrengthenDraftOptions = {
26
+ scope?: DraftStrengtheningScope;
27
+ timeoutMs?: number;
28
+ completeImpl?: typeof complete;
29
+ };
30
+
31
+ export type StrengthenDraftResult =
32
+ | {
33
+ kind: "llm-strengthened";
34
+ draft: DraftPlan;
35
+ }
36
+ | {
37
+ kind: "fallback";
38
+ };
39
+
40
+ type AuthResult = {
41
+ ok: true;
42
+ apiKey?: string;
43
+ headers?: Record<string, string>;
44
+ };
45
+
46
+ type AuthFailure = {
47
+ ok: false;
48
+ error?: string;
49
+ };
50
+
51
+ type CompleteOutcome =
52
+ | {
53
+ kind: "message";
54
+ message: AssistantMessage;
55
+ }
56
+ | {
57
+ kind: "timeout";
58
+ }
59
+ | {
60
+ kind: "error";
61
+ error: unknown;
62
+ };
63
+
64
+ function normalizeText(raw: string): string {
65
+ return raw.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
66
+ }
67
+
68
+ function hasCompleteRalphFrontmatter(raw: string): boolean {
69
+ return /^(?:\s*<!--[\s\S]*?-->\s*)*---\n[\s\S]*?\n---\n?[\s\S]*$/.test(normalizeText(raw).trimStart());
70
+ }
71
+
72
+ function joinTextBlocks(message: AssistantMessage): string {
73
+ return message.content
74
+ .filter((block): block is Extract<AssistantMessage["content"][number], { type: "text" }> => block.type === "text")
75
+ .map((block) => block.text)
76
+ .join("");
77
+ }
78
+
79
+ function areFrontmattersEquivalent(
80
+ baseline: ParsedRalph["frontmatter"],
81
+ strengthened: ParsedRalph["frontmatter"],
82
+ ): boolean {
83
+ return JSON.stringify(baseline) === JSON.stringify(strengthened);
84
+ }
85
+
86
+
87
+ function isWeakStrengthenedDraftForScope(
88
+ baseline: ParsedRalph,
89
+ strengthened: ParsedRalph,
90
+ scope: DraftStrengtheningScope,
91
+ analysisText: string,
92
+ ): boolean {
93
+ const bodyUnchanged = baseline.body.trim() === strengthened.body.trim();
94
+ const frontmatterUnchanged = areFrontmattersEquivalent(baseline.frontmatter, strengthened.frontmatter);
95
+
96
+ if (scope === "body-only") {
97
+ return bodyUnchanged || hasFakeRuntimeEnforcementClaim(analysisText) || hasFakeRuntimeEnforcementClaim(strengthened.body);
98
+ }
99
+
100
+ return (bodyUnchanged && frontmatterUnchanged) || hasFakeRuntimeEnforcementClaim(analysisText) || hasFakeRuntimeEnforcementClaim(strengthened.body);
101
+ }
102
+
103
+ function summarizeRepoSignals(request: DraftRequest): string[] {
104
+ if (request.repoContext.summaryLines.length > 0) return request.repoContext.summaryLines.map((line) => String(line));
105
+
106
+ const topLevelDirs = filterSecretBearingTopLevelNames(request.repoSignals.topLevelDirs);
107
+ const topLevelFiles = filterSecretBearingTopLevelNames(request.repoSignals.topLevelFiles);
108
+
109
+ return [
110
+ `package manager: ${request.repoSignals.packageManager ?? "unknown"}`,
111
+ `test command: ${request.repoSignals.testCommand ?? "none"}`,
112
+ `lint command: ${request.repoSignals.lintCommand ?? "none"}`,
113
+ `git repository: ${request.repoSignals.hasGit ? "present" : "absent"}`,
114
+ `top-level dirs: ${topLevelDirs.length > 0 ? topLevelDirs.join(", ") : "none"}`,
115
+ `top-level files: ${topLevelFiles.length > 0 ? topLevelFiles.join(", ") : "none"}`,
116
+ ];
117
+ }
118
+
119
+ function truncateExcerpt(content: string, maxChars = 800): string {
120
+ const normalized = normalizeText(content).trim();
121
+ if (normalized.length <= maxChars) return normalized || "(no excerpt available)";
122
+ return `${normalized.slice(0, maxChars)}\n… [truncated]`;
123
+ }
124
+
125
+ function renderSelectedFilesSection(request: DraftRequest): string {
126
+ if (request.repoContext.selectedFiles.length === 0) {
127
+ return "- none";
128
+ }
129
+
130
+ return request.repoContext.selectedFiles
131
+ .map((file) => {
132
+ const excerpt = truncateExcerpt(file.content);
133
+ return [
134
+ `### ${file.path}`,
135
+ `Reason: ${file.reason}`,
136
+ "Excerpt:",
137
+ "```text",
138
+ excerpt,
139
+ "```",
140
+ ].join("\n");
141
+ })
142
+ .join("\n\n");
143
+ }
144
+
145
+ function buildCompatibilityContractSection(scope: DraftStrengtheningScope): string {
146
+ if (scope !== "body-and-commands") {
147
+ return [
148
+ "Compatibility contract:",
149
+ "- body-only scope",
150
+ "- keep deterministic frontmatter unchanged",
151
+ "- edit the body only",
152
+ ].join("\n");
153
+ }
154
+
155
+ return [
156
+ "Compatibility contract:",
157
+ "- body-and-commands scope",
158
+ "- commands may only be reordered, dropped, or have timeouts reduced/kept within limits",
159
+ "- command names and run strings must match the deterministic baseline exactly",
160
+ "- max_iterations may stay the same or decrease from the deterministic baseline, never increase",
161
+ "- top-level timeout may stay the same or decrease from the deterministic baseline, never increase",
162
+ "- per-command timeout may stay the same or decrease from that command's baseline timeout, and must still be <= timeout",
163
+ "- completion_promise must remain unchanged, including remaining absent when absent from the baseline",
164
+ "- every {{ commands.<name> }} must refer to an accepted command",
165
+ "- baseline guardrails remain fixed in this phase",
166
+ "- unsupported frontmatter changes are rejected and fall back automatically",
167
+ ].join("\n");
168
+ }
169
+
170
+ function buildStrengtheningPromptText(request: DraftRequest, scope: DraftStrengtheningScope): string {
171
+ const repoSignals = summarizeRepoSignals(request).map((line) => `- ${line}`).join("\n");
172
+ const selectedFiles = renderSelectedFilesSection(request);
173
+
174
+ return [
175
+ `Task: ${request.task}`,
176
+ `Inferred mode: ${request.mode}`,
177
+ `Target file: ${basename(request.target.ralphPath)}`,
178
+ `Strengthening scope: ${scope}`,
179
+ buildCompatibilityContractSection(scope),
180
+ "",
181
+ "Repo signals summary:",
182
+ repoSignals,
183
+ "",
184
+ "Selected file excerpts with reasons:",
185
+ selectedFiles,
186
+ "",
187
+ "Deterministic baseline draft:",
188
+ "~~~md",
189
+ request.baselineDraft,
190
+ "~~~",
191
+ ].join("\n");
192
+ }
193
+
194
+ export function buildStrengtheningPrompt(request: DraftRequest, scope: DraftStrengtheningScope): Context {
195
+ return {
196
+ systemPrompt:
197
+ "You strengthen existing RALPH.md drafts. Follow the scope contract in the user message exactly. Return only a complete RALPH.md. Do not explain, do not wrap the output in fences, and do not omit required frontmatter.",
198
+ messages: [
199
+ {
200
+ role: "user",
201
+ content: buildStrengtheningPromptText(request, scope),
202
+ timestamp: 0,
203
+ },
204
+ ],
205
+ };
206
+ }
207
+
208
+ async function runCompleteWithTimeout(
209
+ model: NonNullable<StrengthenDraftRuntime["model"]>,
210
+ prompt: Context,
211
+ options: Required<Pick<StrengthenDraftOptions, "timeoutMs" | "completeImpl">>,
212
+ apiKey: string,
213
+ headers?: Record<string, string>,
214
+ ): Promise<CompleteOutcome> {
215
+ const abortController = new AbortController();
216
+ let timer: ReturnType<typeof setTimeout> | undefined;
217
+
218
+ const completion = Promise.resolve()
219
+ .then(() =>
220
+ options.completeImpl(model, prompt, {
221
+ apiKey,
222
+ headers,
223
+ signal: abortController.signal,
224
+ temperature: 0,
225
+ }),
226
+ )
227
+ .then((message): CompleteOutcome => ({ kind: "message", message }))
228
+ .catch((error): CompleteOutcome => ({ kind: "error", error }));
229
+
230
+ const timeout = new Promise<CompleteOutcome>((resolve) => {
231
+ timer = setTimeout(() => {
232
+ abortController.abort();
233
+ resolve({ kind: "timeout" });
234
+ }, options.timeoutMs);
235
+ });
236
+
237
+ try {
238
+ return await Promise.race([completion, timeout]);
239
+ } finally {
240
+ if (timer) clearTimeout(timer);
241
+ }
242
+ }
243
+
244
+
245
+ export async function strengthenDraftWithLlm(
246
+ request: DraftRequest,
247
+ runtime: StrengthenDraftRuntime,
248
+ options: StrengthenDraftOptions = {},
249
+ ): Promise<StrengthenDraftResult> {
250
+ try {
251
+ const model = runtime.model;
252
+ if (!model) return { kind: "fallback" };
253
+
254
+ const authResult = await runtime.modelRegistry.getApiKeyAndHeaders(model);
255
+ if (!authResult.ok || !authResult.apiKey) return { kind: "fallback" };
256
+
257
+ const scope = options.scope ?? "body-only";
258
+ const prompt = buildStrengtheningPrompt(request, scope);
259
+ const completion = await runCompleteWithTimeout(
260
+ model,
261
+ prompt,
262
+ {
263
+ timeoutMs: options.timeoutMs ?? DRAFT_LLM_TIMEOUT_MS,
264
+ completeImpl: options.completeImpl ?? complete,
265
+ },
266
+ authResult.apiKey,
267
+ authResult.headers,
268
+ );
269
+
270
+ if (completion.kind !== "message") return { kind: "fallback" };
271
+
272
+ const rawText = joinTextBlocks(completion.message).trim();
273
+ if (!rawText) return { kind: "fallback" };
274
+ if (!hasCompleteRalphFrontmatter(rawText)) return { kind: "fallback" };
275
+
276
+ const baseline = parseRalphMarkdown(request.baselineDraft);
277
+ const strengthened = parseRalphMarkdown(rawText);
278
+ const validationError = validateFrontmatter(strengthened.frontmatter);
279
+ if (validationError) return { kind: "fallback" };
280
+
281
+ if (strengthened.body.trim().length === 0) return { kind: "fallback" };
282
+ if (isWeakStrengthenedDraftForScope(baseline, strengthened, scope, rawText)) return { kind: "fallback" };
283
+
284
+ if (scope === "body-and-commands") {
285
+ const accepted = acceptStrengthenedDraft(request, rawText);
286
+ if (!accepted) return { kind: "fallback" };
287
+ return { kind: "llm-strengthened", draft: accepted };
288
+ }
289
+
290
+ return {
291
+ kind: "llm-strengthened",
292
+ draft: normalizeStrengthenedDraft(request, rawText, scope),
293
+ };
294
+ } catch {
295
+ return { kind: "fallback" };
296
+ }
297
+ }
@@ -0,0 +1,33 @@
1
+ import { assembleRepoContext } from "./ralph-draft-context.ts";
2
+ import {
3
+ buildDraftRequest,
4
+ classifyTaskMode,
5
+ generateDraftFromRequest,
6
+ inspectRepo,
7
+ type DraftPlan,
8
+ type DraftTarget,
9
+ } from "./ralph.ts";
10
+ import { strengthenDraftWithLlm, type StrengthenDraftRuntime } from "./ralph-draft-llm.ts";
11
+
12
+ export type CreateDraftPlanOptions = {
13
+ strengthenDraftWithLlmImpl?: typeof strengthenDraftWithLlm;
14
+ };
15
+ export async function createDraftPlan(
16
+ task: string,
17
+ target: DraftTarget,
18
+ cwd: string,
19
+ runtime?: StrengthenDraftRuntime,
20
+ options: CreateDraftPlanOptions = {},
21
+ ): Promise<DraftPlan> {
22
+ const repoSignals = inspectRepo(cwd);
23
+ const mode = classifyTaskMode(task);
24
+ const repoContext = assembleRepoContext(cwd, task, mode, repoSignals);
25
+ const request = buildDraftRequest(task, target, repoSignals, repoContext);
26
+ if (runtime?.model) {
27
+ const strengthen = options.strengthenDraftWithLlmImpl ?? strengthenDraftWithLlm;
28
+ const strengthened = await strengthen(request, runtime, { scope: "body-and-commands" });
29
+ if (strengthened.kind === "llm-strengthened") return strengthened.draft;
30
+ }
31
+
32
+ return generateDraftFromRequest(request, "fallback");
33
+ }