@hicaru/pi-rlm 0.1.7 → 0.1.9
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 +41 -4
- package/package.json +2 -1
- package/src/bridge/library.ts +155 -0
- package/src/bridge/llm-query.ts +1 -0
- package/src/bridge/rlm-query.ts +56 -12
- package/src/config/defaults.ts +2 -0
- package/src/config/settings.ts +4 -0
- package/src/context/library-context.ts +266 -0
- package/src/context/repomix-context.ts +2 -48
- package/src/core/answer.ts +1 -10
- package/src/core/artifacts.ts +88 -0
- package/src/core/critique.ts +92 -0
- package/src/core/engine.ts +446 -53
- package/src/core/gates.ts +301 -0
- package/src/core/iteration.ts +7 -2
- package/src/core/pipeline.ts +196 -28
- package/src/core/types.ts +5 -3
- package/src/index.ts +3 -6
- package/src/mode/native-guards.ts +2 -2
- package/src/prompts/phases.ts +104 -0
- package/src/prompts/system.ts +59 -16
- package/src/prompts/user.ts +12 -4
- package/src/sandbox/protocol.ts +29 -11
- package/src/sandbox/sandbox.ts +77 -2
- package/src/sandbox/worker.py +215 -46
- package/src/state/index.ts +2 -1
- package/src/state/paths.ts +4 -2
- package/src/state/reads.ts +31 -2
- package/src/state/resume.ts +31 -6
- package/src/state/rows.ts +8 -2
- package/src/state/writes.ts +5 -3
- package/src/text/tokens.ts +7 -1
- package/src/tool/repl-details.ts +2 -3
- package/src/tool/repl-tool.ts +52 -57
- package/src/tool/rlm-aggregator.ts +7 -7
- package/src/tool/rlm-details.ts +6 -3
- package/src/tool/rlm-events.ts +14 -11
- package/src/tool/rlm-tool.ts +2 -8
- package/src/tool/subcall-store.ts +2 -0
- package/src/ui/config-panel.ts +8 -1
- package/src/registry/edit-registry.ts +0 -22
- package/src/text/edits.ts +0 -16
- package/src/tool/apply-edits-tool.ts +0 -288
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Uses repomix internally (worker-thread pool, built-in gitignore support)
|
|
7
7
|
* and caches results in a module-level Map with TTL to avoid re-packing on
|
|
8
|
-
* every run within the same process.
|
|
9
|
-
* cached bundle in-memory after file edits are applied to disk — no re-packing.
|
|
8
|
+
* every run within the same process.
|
|
10
9
|
*/
|
|
11
10
|
|
|
12
11
|
import { pack } from "repomix";
|
|
@@ -172,42 +171,6 @@ export async function packRepository(
|
|
|
172
171
|
}
|
|
173
172
|
}
|
|
174
173
|
|
|
175
|
-
export function patchContextAfterEdits(
|
|
176
|
-
cached: ContextBundle,
|
|
177
|
-
edits: readonly { readonly path: string; readonly newContent: string }[],
|
|
178
|
-
): ContextBundle {
|
|
179
|
-
const editMap = new Map<string, string>();
|
|
180
|
-
for (const edit of edits) {
|
|
181
|
-
editMap.set(edit.path, edit.newContent);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const files = new Array<ContextFile>(cached.files.length);
|
|
185
|
-
let totalTokens = 0;
|
|
186
|
-
let totalChars = 0;
|
|
187
|
-
|
|
188
|
-
for (let i = 0; i < cached.files.length; i++) {
|
|
189
|
-
const file = cached.files[i];
|
|
190
|
-
const newContent = editMap.get(file.path);
|
|
191
|
-
if (newContent !== undefined) {
|
|
192
|
-
const tokens = Math.ceil(newContent.length / ESTIMATED_CHARS_PER_TOKEN);
|
|
193
|
-
files[i] = { path: file.path, content: newContent, tokens };
|
|
194
|
-
totalTokens += tokens;
|
|
195
|
-
totalChars += newContent.length;
|
|
196
|
-
} else {
|
|
197
|
-
files[i] = file;
|
|
198
|
-
totalTokens += file.tokens;
|
|
199
|
-
totalChars += file.content.length;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
files,
|
|
205
|
-
totalFiles: files.length,
|
|
206
|
-
totalTokens,
|
|
207
|
-
totalChars,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
174
|
export function serializeForSandbox(
|
|
212
175
|
bundle: ContextBundle,
|
|
213
176
|
): readonly ContextFile[] {
|
|
@@ -243,13 +206,4 @@ export function formatForLLM(bundle: ContextBundle): string {
|
|
|
243
206
|
].join("\n");
|
|
244
207
|
}
|
|
245
208
|
|
|
246
|
-
|
|
247
|
-
cwd: string,
|
|
248
|
-
edits: readonly { readonly path: string; readonly newContent: string }[],
|
|
249
|
-
): void {
|
|
250
|
-
const key = cacheKey(cwd);
|
|
251
|
-
const entry = cache.get(key);
|
|
252
|
-
if (!entry) return;
|
|
253
|
-
const patched = patchContextAfterEdits(entry.bundle, edits);
|
|
254
|
-
cache.set(key, { bundle: patched, ts: entry.ts });
|
|
255
|
-
}
|
|
209
|
+
|
package/src/core/answer.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Helpers for detecting and formatting the RLM final answer from a turn's REPL results. */
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { ReplResult } from "../sandbox/protocol.ts";
|
|
4
4
|
import { truncateOutput } from "../text/parsing.ts";
|
|
5
5
|
|
|
6
6
|
/** First non-null final answer across a turn's executed blocks, or null. */
|
|
@@ -18,15 +18,6 @@ export function latestAnswerContentOf(results: readonly ReplResult[]): string |
|
|
|
18
18
|
return null;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/** Last cumulative legacy anchor proposed-edit set reported by a turn. */
|
|
22
|
-
export function collectEdits(results: readonly ReplResult[]): ProposedEdit[] {
|
|
23
|
-
for (let i = results.length - 1; i >= 0; i--) {
|
|
24
|
-
const edits = results[i]?.edits;
|
|
25
|
-
if (edits && edits.length > 0) return [...edits];
|
|
26
|
-
}
|
|
27
|
-
return [];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
21
|
/** True if any block in the turn raised an exception. Plain stderr does not count. */
|
|
31
22
|
export function turnHadError(results: readonly ReplResult[]): boolean {
|
|
32
23
|
return results.some((r) => r.raised);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifact plumbing for the gated RLM pipeline: goal capture, baseline dirty-tree
|
|
3
|
+
* snapshot, and timestamped stage artifact writes under `.rlm/artifacts/`.
|
|
4
|
+
*/
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import type { Result } from "../util/errors.ts";
|
|
9
|
+
|
|
10
|
+
export const ARTIFACTS_DIR = ".rlm/artifacts";
|
|
11
|
+
|
|
12
|
+
const stamp = (): string => new Date().toISOString().replace(/[:.]/g, "-");
|
|
13
|
+
|
|
14
|
+
export interface GoalCapture {
|
|
15
|
+
readonly goalPath: string; // repo-relative
|
|
16
|
+
readonly baselinePath: string; // repo-relative
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type SaveOutcome =
|
|
20
|
+
| { readonly ok: true; readonly path: string }
|
|
21
|
+
| { readonly ok: false; readonly error: string };
|
|
22
|
+
|
|
23
|
+
export type GoalCaptureResult =
|
|
24
|
+
| { readonly ok: true; readonly value: GoalCapture }
|
|
25
|
+
| { readonly ok: false; readonly error: string };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Capture the user's brief VERBATIM: no frontmatter, no headers — the raw file
|
|
29
|
+
* is the only artifact that preserves explicit user constraints unrefracted. The
|
|
30
|
+
* baseline snapshot records paths ALREADY dirty before the run, so validate
|
|
31
|
+
* judges only the run's own delta. Best-effort: git failure ⇒ empty baseline.
|
|
32
|
+
* Failures never throw (unwritable cwd etc.) — returns error for the engine to surface.
|
|
33
|
+
*/
|
|
34
|
+
export function captureGoal(cwd: string, brief: string): GoalCaptureResult {
|
|
35
|
+
try {
|
|
36
|
+
const ts = stamp();
|
|
37
|
+
const dir = join(ARTIFACTS_DIR, "goal");
|
|
38
|
+
mkdirSync(join(cwd, dir), { recursive: true });
|
|
39
|
+
const goalPath = join(dir, `goal-${ts}.md`);
|
|
40
|
+
writeFileSync(join(cwd, goalPath), brief, "utf-8");
|
|
41
|
+
let paths: readonly string[] = [];
|
|
42
|
+
try {
|
|
43
|
+
paths = execFileSync("git", ["status", "--short"], {
|
|
44
|
+
cwd,
|
|
45
|
+
encoding: "utf-8",
|
|
46
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
47
|
+
})
|
|
48
|
+
.split("\n")
|
|
49
|
+
.filter((l) => l.trim() !== "")
|
|
50
|
+
.map((l) => {
|
|
51
|
+
const rest = l.slice(3).trim();
|
|
52
|
+
const arrow = rest.indexOf(" -> ");
|
|
53
|
+
return arrow >= 0 ? rest.slice(arrow + 4).trim() : rest;
|
|
54
|
+
});
|
|
55
|
+
} catch {
|
|
56
|
+
paths = [];
|
|
57
|
+
}
|
|
58
|
+
const baselinePath = join(dir, `baseline-${ts}.json`);
|
|
59
|
+
writeFileSync(join(cwd, baselinePath), JSON.stringify({ paths }, null, 2), "utf-8");
|
|
60
|
+
return { ok: true, value: { goalPath, baselinePath } };
|
|
61
|
+
} catch (err) {
|
|
62
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
63
|
+
return { ok: false, error: message };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Write a stage artifact under its dir; timestamped so runs never collide. */
|
|
68
|
+
export function saveArtifact(cwd: string, dir: string, slug: string, content: string): SaveOutcome {
|
|
69
|
+
try {
|
|
70
|
+
const rel = join(ARTIFACTS_DIR, dir, `${stamp()}_${slug}.md`);
|
|
71
|
+
mkdirSync(join(cwd, ARTIFACTS_DIR, dir), { recursive: true });
|
|
72
|
+
writeFileSync(join(cwd, rel), content, "utf-8");
|
|
73
|
+
return { ok: true, path: rel };
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
76
|
+
return { ok: false, error: message };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Read a previously saved artifact (repo-relative path). Failures never throw. */
|
|
81
|
+
export function readArtifact(cwd: string, relPath: string): Result<string, string> {
|
|
82
|
+
try {
|
|
83
|
+
return { ok: true, value: readFileSync(join(cwd, relPath), "utf-8") };
|
|
84
|
+
} catch (err) {
|
|
85
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
86
|
+
return { ok: false, error: `could not read artifact ${relPath}: ${message}` };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preflight critique for stage artifacts — port of codex-bais `model_critique`.
|
|
3
|
+
*
|
|
4
|
+
* Blocking truth comes from `stage.gate` (the same function `advance_phase` runs), so the
|
|
5
|
+
* two can never drift. `warnings` are advisory model-risk diagnostics that never block.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
countBulletsUnderHeading,
|
|
9
|
+
phasesMissingSuccessCriteria,
|
|
10
|
+
sectionHasNonEmptyBody,
|
|
11
|
+
} from "./gates.ts";
|
|
12
|
+
import type { StageDef, StageGateData } from "./pipeline.ts";
|
|
13
|
+
|
|
14
|
+
export interface Critique {
|
|
15
|
+
/** Blocking — `advance_phase` will reject while non-empty. */
|
|
16
|
+
readonly issues: readonly string[];
|
|
17
|
+
/** Advisory — surfaced to the model, never blocking. */
|
|
18
|
+
readonly warnings: readonly string[];
|
|
19
|
+
/**
|
|
20
|
+
* Convenience alias for `issues.length === 0` — do not set independently;
|
|
21
|
+
* always derive from `issues`.
|
|
22
|
+
*/
|
|
23
|
+
readonly canAdvance: boolean;
|
|
24
|
+
/** Gate payload when `canAdvance` — reused by advance_phase (no second gate run). */
|
|
25
|
+
readonly gateData?: StageGateData;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const NO_STRINGS: readonly string[] = Object.freeze([]);
|
|
29
|
+
|
|
30
|
+
/** Advisory checks per artifact kind. Pure; returns a frozen array. */
|
|
31
|
+
function advisoriesFor(stage: StageDef, content: string): readonly string[] {
|
|
32
|
+
switch (stage.artifactKind) {
|
|
33
|
+
case "plan": {
|
|
34
|
+
const missing = phasesMissingSuccessCriteria(content);
|
|
35
|
+
if (missing.length > 0) {
|
|
36
|
+
return Object.freeze([
|
|
37
|
+
`${missing.join(", ")} ${missing.length === 1 ? "has" : "have"} no `
|
|
38
|
+
+ "'### Success Criteria' — validate cannot check them",
|
|
39
|
+
]);
|
|
40
|
+
}
|
|
41
|
+
return NO_STRINGS;
|
|
42
|
+
}
|
|
43
|
+
case "clarification": {
|
|
44
|
+
const open = countBulletsUnderHeading(content, "Open Questions");
|
|
45
|
+
if (open > 0) {
|
|
46
|
+
return Object.freeze([
|
|
47
|
+
`${open} Open Question(s) carried into blueprint — re-ask any that block the design`,
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
return NO_STRINGS;
|
|
51
|
+
}
|
|
52
|
+
case "research": {
|
|
53
|
+
if (!sectionHasNonEmptyBody(content, "Findings")) {
|
|
54
|
+
return Object.freeze(["research artifact has no '## Findings' section"]);
|
|
55
|
+
}
|
|
56
|
+
return NO_STRINGS;
|
|
57
|
+
}
|
|
58
|
+
default:
|
|
59
|
+
return NO_STRINGS;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function critiqueArtifact(
|
|
64
|
+
stage: StageDef,
|
|
65
|
+
content: string,
|
|
66
|
+
path: string,
|
|
67
|
+
cwd: string,
|
|
68
|
+
): Critique {
|
|
69
|
+
const gate = stage.gate(content, path, cwd);
|
|
70
|
+
const issues = gate.ok ? NO_STRINGS : Object.freeze([gate.error]);
|
|
71
|
+
return Object.freeze({
|
|
72
|
+
issues,
|
|
73
|
+
warnings: advisoriesFor(stage, content),
|
|
74
|
+
canAdvance: issues.length === 0,
|
|
75
|
+
gateData: gate.ok ? gate.value : undefined,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Human-readable renderer for the `save_artifact` return value. */
|
|
80
|
+
export function formatCritique(critique: Critique): string {
|
|
81
|
+
if (critique.issues.length === 0 && critique.warnings.length === 0) {
|
|
82
|
+
return "gate: clean — call advance_phase when ready.";
|
|
83
|
+
}
|
|
84
|
+
const lines = new Array<string>(critique.issues.length + critique.warnings.length);
|
|
85
|
+
let n = 0;
|
|
86
|
+
for (let i = 0; i < critique.issues.length; i++) lines[n++] = ` BLOCKER: ${critique.issues[i]}`;
|
|
87
|
+
for (let i = 0; i < critique.warnings.length; i++) lines[n++] = ` warning: ${critique.warnings[i]}`;
|
|
88
|
+
const head = critique.canAdvance
|
|
89
|
+
? "gate: passes, with advisories —"
|
|
90
|
+
: "gate: WOULD REJECT — fix before advance_phase:";
|
|
91
|
+
return `${head}\n${lines.join("\n")}`;
|
|
92
|
+
}
|