@lnilluv/pi-ralph-loop 0.1.1 → 0.1.4-dev.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/ci.yml +5 -2
- package/.github/workflows/release.yml +7 -4
- package/README.md +97 -11
- package/package.json +13 -4
- package/src/index.ts +561 -184
- package/src/ralph-draft-context.ts +618 -0
- package/src/ralph-draft-llm.ts +269 -0
- package/src/ralph-draft.ts +33 -0
- package/src/ralph.ts +800 -0
- package/src/secret-paths.ts +66 -0
- package/src/shims.d.ts +23 -0
- package/tests/index.test.ts +464 -0
- package/tests/ralph-draft-context.test.ts +672 -0
- package/tests/ralph-draft-llm.test.ts +361 -0
- package/tests/ralph-draft.test.ts +168 -0
- package/tests/ralph.test.ts +611 -0
- package/tests/secret-paths.test.ts +55 -0
- package/tsconfig.json +3 -2
|
@@ -0,0 +1,269 @@
|
|
|
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
|
+
normalizeStrengthenedDraft,
|
|
6
|
+
parseRalphMarkdown,
|
|
7
|
+
validateFrontmatter,
|
|
8
|
+
type DraftPlan,
|
|
9
|
+
type DraftRequest,
|
|
10
|
+
type DraftStrengtheningScope,
|
|
11
|
+
type ParsedRalph,
|
|
12
|
+
} from "./ralph.ts";
|
|
13
|
+
|
|
14
|
+
export const DRAFT_LLM_TIMEOUT_MS = 20_000;
|
|
15
|
+
|
|
16
|
+
export type StrengthenDraftRuntime = {
|
|
17
|
+
model: Model<string> | undefined;
|
|
18
|
+
modelRegistry: {
|
|
19
|
+
getApiKeyAndHeaders(model: Model<string>): Promise<AuthResult | AuthFailure>;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type StrengthenDraftOptions = {
|
|
24
|
+
scope?: DraftStrengtheningScope;
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
completeImpl?: typeof complete;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type StrengthenDraftResult =
|
|
30
|
+
| {
|
|
31
|
+
kind: "llm-strengthened";
|
|
32
|
+
draft: DraftPlan;
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
kind: "fallback";
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type AuthResult = {
|
|
39
|
+
ok: true;
|
|
40
|
+
apiKey?: string;
|
|
41
|
+
headers?: Record<string, string>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type AuthFailure = {
|
|
45
|
+
ok: false;
|
|
46
|
+
error?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type CompleteOutcome =
|
|
50
|
+
| {
|
|
51
|
+
kind: "message";
|
|
52
|
+
message: AssistantMessage;
|
|
53
|
+
}
|
|
54
|
+
| {
|
|
55
|
+
kind: "timeout";
|
|
56
|
+
}
|
|
57
|
+
| {
|
|
58
|
+
kind: "error";
|
|
59
|
+
error: unknown;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function normalizeText(raw: string): string {
|
|
63
|
+
return raw.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function hasCompleteRalphFrontmatter(raw: string): boolean {
|
|
67
|
+
return /^(?:\s*<!--[\s\S]*?-->\s*)*---\n[\s\S]*?\n---\n?[\s\S]*$/.test(normalizeText(raw).trimStart());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function joinTextBlocks(message: AssistantMessage): string {
|
|
71
|
+
return message.content
|
|
72
|
+
.filter((block): block is Extract<AssistantMessage["content"][number], { type: "text" }> => block.type === "text")
|
|
73
|
+
.map((block) => block.text)
|
|
74
|
+
.join("");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function areFrontmattersEquivalent(
|
|
78
|
+
baseline: ParsedRalph["frontmatter"],
|
|
79
|
+
strengthened: ParsedRalph["frontmatter"],
|
|
80
|
+
): boolean {
|
|
81
|
+
return JSON.stringify(baseline) === JSON.stringify(strengthened);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function hasFakeRuntimeEnforcementClaim(text: string): boolean {
|
|
85
|
+
return /read[-\s]?only enforced|write protection is enforced/i.test(text);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isWeakStrengthenedDraftForScope(
|
|
89
|
+
baseline: ParsedRalph,
|
|
90
|
+
strengthened: ParsedRalph,
|
|
91
|
+
scope: DraftStrengtheningScope,
|
|
92
|
+
analysisText: string,
|
|
93
|
+
): boolean {
|
|
94
|
+
const bodyUnchanged = baseline.body.trim() === strengthened.body.trim();
|
|
95
|
+
const frontmatterUnchanged = areFrontmattersEquivalent(baseline.frontmatter, strengthened.frontmatter);
|
|
96
|
+
|
|
97
|
+
if (scope === "body-only") {
|
|
98
|
+
return bodyUnchanged || hasFakeRuntimeEnforcementClaim(analysisText) || hasFakeRuntimeEnforcementClaim(strengthened.body);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (bodyUnchanged && frontmatterUnchanged) || hasFakeRuntimeEnforcementClaim(analysisText) || hasFakeRuntimeEnforcementClaim(strengthened.body);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function summarizeRepoSignals(request: DraftRequest): string[] {
|
|
105
|
+
if (request.repoContext.summaryLines.length > 0) return request.repoContext.summaryLines.map((line) => String(line));
|
|
106
|
+
|
|
107
|
+
const topLevelDirs = filterSecretBearingTopLevelNames(request.repoSignals.topLevelDirs);
|
|
108
|
+
const topLevelFiles = filterSecretBearingTopLevelNames(request.repoSignals.topLevelFiles);
|
|
109
|
+
|
|
110
|
+
return [
|
|
111
|
+
`package manager: ${request.repoSignals.packageManager ?? "unknown"}`,
|
|
112
|
+
`test command: ${request.repoSignals.testCommand ?? "none"}`,
|
|
113
|
+
`lint command: ${request.repoSignals.lintCommand ?? "none"}`,
|
|
114
|
+
`git repository: ${request.repoSignals.hasGit ? "present" : "absent"}`,
|
|
115
|
+
`top-level dirs: ${topLevelDirs.length > 0 ? topLevelDirs.join(", ") : "none"}`,
|
|
116
|
+
`top-level files: ${topLevelFiles.length > 0 ? topLevelFiles.join(", ") : "none"}`,
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function truncateExcerpt(content: string, maxChars = 800): string {
|
|
121
|
+
const normalized = normalizeText(content).trim();
|
|
122
|
+
if (normalized.length <= maxChars) return normalized || "(no excerpt available)";
|
|
123
|
+
return `${normalized.slice(0, maxChars)}\n… [truncated]`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function renderSelectedFilesSection(request: DraftRequest): string {
|
|
127
|
+
if (request.repoContext.selectedFiles.length === 0) {
|
|
128
|
+
return "- none";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return request.repoContext.selectedFiles
|
|
132
|
+
.map((file) => {
|
|
133
|
+
const excerpt = truncateExcerpt(file.content);
|
|
134
|
+
return [
|
|
135
|
+
`### ${file.path}`,
|
|
136
|
+
`Reason: ${file.reason}`,
|
|
137
|
+
"Excerpt:",
|
|
138
|
+
"```text",
|
|
139
|
+
excerpt,
|
|
140
|
+
"```",
|
|
141
|
+
].join("\n");
|
|
142
|
+
})
|
|
143
|
+
.join("\n\n");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function buildStrengtheningPromptText(request: DraftRequest, scope: DraftStrengtheningScope): string {
|
|
147
|
+
const repoSignals = summarizeRepoSignals(request).map((line) => `- ${line}`).join("\n");
|
|
148
|
+
const selectedFiles = renderSelectedFilesSection(request);
|
|
149
|
+
|
|
150
|
+
return [
|
|
151
|
+
`Task: ${request.task}`,
|
|
152
|
+
`Inferred mode: ${request.mode}`,
|
|
153
|
+
`Target file: ${basename(request.target.ralphPath)}`,
|
|
154
|
+
`Strengthening scope: ${scope}`,
|
|
155
|
+
"",
|
|
156
|
+
"Repo signals summary:",
|
|
157
|
+
repoSignals,
|
|
158
|
+
"",
|
|
159
|
+
"Selected file excerpts with reasons:",
|
|
160
|
+
selectedFiles,
|
|
161
|
+
"",
|
|
162
|
+
"Deterministic baseline draft:",
|
|
163
|
+
"~~~md",
|
|
164
|
+
request.baselineDraft,
|
|
165
|
+
"~~~",
|
|
166
|
+
].join("\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function buildStrengtheningPrompt(request: DraftRequest, scope: DraftStrengtheningScope): Context {
|
|
170
|
+
return {
|
|
171
|
+
systemPrompt:
|
|
172
|
+
"You strengthen existing RALPH.md drafts. Return only a complete RALPH.md. Do not explain, do not wrap the output in fences, and do not omit required frontmatter.",
|
|
173
|
+
messages: [
|
|
174
|
+
{
|
|
175
|
+
role: "user",
|
|
176
|
+
content: buildStrengtheningPromptText(request, scope),
|
|
177
|
+
timestamp: 0,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function runCompleteWithTimeout(
|
|
184
|
+
model: NonNullable<StrengthenDraftRuntime["model"]>,
|
|
185
|
+
prompt: Context,
|
|
186
|
+
options: Required<Pick<StrengthenDraftOptions, "timeoutMs" | "completeImpl">>,
|
|
187
|
+
apiKey: string,
|
|
188
|
+
headers?: Record<string, string>,
|
|
189
|
+
): Promise<CompleteOutcome> {
|
|
190
|
+
const abortController = new AbortController();
|
|
191
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
192
|
+
|
|
193
|
+
const completion = Promise.resolve()
|
|
194
|
+
.then(() =>
|
|
195
|
+
options.completeImpl(model, prompt, {
|
|
196
|
+
apiKey,
|
|
197
|
+
headers,
|
|
198
|
+
signal: abortController.signal,
|
|
199
|
+
temperature: 0,
|
|
200
|
+
}),
|
|
201
|
+
)
|
|
202
|
+
.then((message): CompleteOutcome => ({ kind: "message", message }))
|
|
203
|
+
.catch((error): CompleteOutcome => ({ kind: "error", error }));
|
|
204
|
+
|
|
205
|
+
const timeout = new Promise<CompleteOutcome>((resolve) => {
|
|
206
|
+
timer = setTimeout(() => {
|
|
207
|
+
abortController.abort();
|
|
208
|
+
resolve({ kind: "timeout" });
|
|
209
|
+
}, options.timeoutMs);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
return await Promise.race([completion, timeout]);
|
|
214
|
+
} finally {
|
|
215
|
+
if (timer) clearTimeout(timer);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function isAuthFailure(result: AuthResult | AuthFailure): result is AuthFailure {
|
|
220
|
+
return result.ok === false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function strengthenDraftWithLlm(
|
|
224
|
+
request: DraftRequest,
|
|
225
|
+
runtime: StrengthenDraftRuntime,
|
|
226
|
+
options: StrengthenDraftOptions = {},
|
|
227
|
+
): Promise<StrengthenDraftResult> {
|
|
228
|
+
try {
|
|
229
|
+
const model = runtime.model;
|
|
230
|
+
if (!model) return { kind: "fallback" };
|
|
231
|
+
|
|
232
|
+
const authResult = await runtime.modelRegistry.getApiKeyAndHeaders(model);
|
|
233
|
+
if (!authResult.ok || !authResult.apiKey) return { kind: "fallback" };
|
|
234
|
+
|
|
235
|
+
const scope = options.scope ?? "body-only";
|
|
236
|
+
const prompt = buildStrengtheningPrompt(request, scope);
|
|
237
|
+
const completion = await runCompleteWithTimeout(
|
|
238
|
+
model,
|
|
239
|
+
prompt,
|
|
240
|
+
{
|
|
241
|
+
timeoutMs: options.timeoutMs ?? DRAFT_LLM_TIMEOUT_MS,
|
|
242
|
+
completeImpl: options.completeImpl ?? complete,
|
|
243
|
+
},
|
|
244
|
+
authResult.apiKey,
|
|
245
|
+
authResult.headers,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
if (completion.kind !== "message") return { kind: "fallback" };
|
|
249
|
+
|
|
250
|
+
const rawText = joinTextBlocks(completion.message).trim();
|
|
251
|
+
if (!rawText) return { kind: "fallback" };
|
|
252
|
+
if (!hasCompleteRalphFrontmatter(rawText)) return { kind: "fallback" };
|
|
253
|
+
|
|
254
|
+
const baseline = parseRalphMarkdown(request.baselineDraft);
|
|
255
|
+
const strengthened = parseRalphMarkdown(rawText);
|
|
256
|
+
const validationError = validateFrontmatter(strengthened.frontmatter);
|
|
257
|
+
if (validationError) return { kind: "fallback" };
|
|
258
|
+
|
|
259
|
+
if (strengthened.body.trim().length === 0) return { kind: "fallback" };
|
|
260
|
+
if (isWeakStrengthenedDraftForScope(baseline, strengthened, scope, rawText)) return { kind: "fallback" };
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
kind: "llm-strengthened",
|
|
264
|
+
draft: normalizeStrengthenedDraft(request, rawText, scope),
|
|
265
|
+
};
|
|
266
|
+
} catch {
|
|
267
|
+
return { kind: "fallback" };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -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-only" });
|
|
29
|
+
if (strengthened.kind === "llm-strengthened") return strengthened.draft;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return generateDraftFromRequest(request, "fallback");
|
|
33
|
+
}
|