@hicaru/pi-rlm 0.2.1 → 0.3.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/README.md +28 -47
- package/README.ru.md +18 -23
- package/README.zh-CN.md +17 -28
- package/package.json +22 -19
- package/src/bridge/add-context.ts +322 -0
- package/src/bridge/subcall-handlers.ts +63 -17
- package/src/commands/rlm-config.ts +47 -18
- package/src/commands/rlm.ts +3 -152
- package/src/config/defaults.ts +8 -18
- package/src/config/settings.ts +13 -34
- package/src/context/anydoc.ts +67 -0
- package/src/context/listing.ts +70 -0
- package/src/context/md-cache.ts +112 -0
- package/src/context/merge.ts +97 -0
- package/src/context/namespace.ts +180 -0
- package/src/context/resolve.ts +122 -0
- package/src/context/source-dir.ts +166 -0
- package/src/context/source-doc.ts +71 -0
- package/src/context/source-git.ts +51 -0
- package/src/context/source-text.ts +45 -0
- package/src/context/types.ts +88 -0
- package/src/context/walk.ts +250 -0
- package/src/core/engine.ts +61 -345
- package/src/core/history.ts +1 -1
- package/src/core/limits.ts +5 -12
- package/src/core/resource-limits.ts +0 -2
- package/src/core/types.ts +10 -38
- package/src/index.ts +92 -54
- package/src/mode/llm-model.ts +54 -0
- package/src/mode/rlm-mode.ts +28 -58
- package/src/prompts/glossary.ts +290 -0
- package/src/prompts/native.ts +127 -0
- package/src/prompts/system.ts +15 -408
- package/src/sandbox/context-file.ts +154 -0
- package/src/sandbox/interrupts.ts +160 -0
- package/src/sandbox/protocol.ts +20 -75
- package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/retrieval.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/tasks.cpython-314.pyc +0 -0
- package/src/sandbox/py/guards.py +150 -0
- package/src/sandbox/py/retrieval.py +265 -0
- package/src/sandbox/py/tasks.py +129 -0
- package/src/sandbox/py/worker.py +856 -0
- package/src/sandbox/sandbox-manager.ts +24 -9
- package/src/sandbox/sandbox.ts +99 -193
- package/src/text/tokens.ts +31 -5
- package/src/tool/repl-details.ts +2 -2
- package/src/tool/repl-render.ts +58 -0
- package/src/tool/repl-result.ts +70 -0
- package/src/tool/repl-tool.ts +60 -170
- package/src/tool/rlm-aggregator.ts +2 -10
- package/src/tool/rlm-details.ts +0 -2
- package/src/tool/rlm-events.ts +0 -14
- package/src/tool/rlm-tool.ts +2 -13
- package/src/ui/config-panel.ts +12 -20
- package/src/ui/intro.ts +1 -2
- package/src/ui/model-picker.ts +34 -10
- package/src/ui/status.ts +3 -7
- package/src/util/concurrency.ts +9 -5
- package/src/bridge/fallback-todo.ts +0 -148
- package/src/bridge/interactive.ts +0 -65
- package/src/bridge/library.ts +0 -155
- package/src/bridge/pi-interactive.ts +0 -41
- package/src/context/library-context.ts +0 -266
- package/src/context/repomix-context.ts +0 -204
- package/src/core/artifacts.ts +0 -89
- package/src/core/critique.ts +0 -92
- package/src/core/gates.ts +0 -301
- package/src/core/pipeline-handlers.ts +0 -319
- package/src/core/pipeline.ts +0 -268
- package/src/prompts/phases.ts +0 -104
- package/src/sandbox/worker.py +0 -1456
- package/src/state/index.ts +0 -24
- package/src/state/internal.ts +0 -46
- package/src/state/paths.ts +0 -44
- package/src/state/reads.ts +0 -133
- package/src/state/resume.ts +0 -173
- package/src/state/rows.ts +0 -123
- package/src/state/writes.ts +0 -58
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolve load_library(source) into a sandbox-ready payload.
|
|
3
|
-
*
|
|
4
|
-
* Sources: local directory (repomix-packed), single file (utf-8), or remote git URL
|
|
5
|
-
* (shallow clone then pack). Host-side only — never runs in the sandbox.
|
|
6
|
-
*
|
|
7
|
-
* Every successful payload is a namespaced list of ContextFile under
|
|
8
|
-
* `lib/<source_id>/…` so the worker can append into the single `context` variable.
|
|
9
|
-
* Source ids include a short content fingerprint so two libraries that share a
|
|
10
|
-
* basename never collide.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { createHash } from "node:crypto";
|
|
14
|
-
import { execFile } from "node:child_process";
|
|
15
|
-
import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
|
|
16
|
-
import { basename, isAbsolute, join, resolve } from "node:path";
|
|
17
|
-
import { tmpdir } from "node:os";
|
|
18
|
-
import { promisify } from "node:util";
|
|
19
|
-
import {
|
|
20
|
-
packRepository,
|
|
21
|
-
serializeForSandbox,
|
|
22
|
-
type ContextBundle,
|
|
23
|
-
type ContextFile,
|
|
24
|
-
} from "./repomix-context.ts";
|
|
25
|
-
import { estimateTokens } from "../text/tokens.ts";
|
|
26
|
-
import type { Result } from "../util/errors.ts";
|
|
27
|
-
import { errorMessage } from "../util/errors.ts";
|
|
28
|
-
|
|
29
|
-
const execFileP = promisify(execFile);
|
|
30
|
-
|
|
31
|
-
/** Single-file sources above this must use open() + llm_query_chunked in the REPL. */
|
|
32
|
-
export const MAX_LIBRARY_FILE_BYTES = 8 * 1024 * 1024;
|
|
33
|
-
|
|
34
|
-
/** Legacy catch-all prefix for pre-namespace string sidecars — never an identity key. */
|
|
35
|
-
const LEGACY_UNKNOWN_PREFIX = "lib/unknown/";
|
|
36
|
-
|
|
37
|
-
export interface LibrarySource {
|
|
38
|
-
/** Always a namespaced file list (dirs, single files, and git clones). */
|
|
39
|
-
readonly payload: readonly ContextFile[];
|
|
40
|
-
readonly files: number;
|
|
41
|
-
/** Sum of raw content lengths — what the model should size batches against. */
|
|
42
|
-
readonly chars: number;
|
|
43
|
-
readonly sourceId: string;
|
|
44
|
-
readonly pathPrefix: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface LibraryNamespace {
|
|
48
|
-
readonly sourceId: string;
|
|
49
|
-
readonly pathPrefix: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** https://host/… or git@host:… — option-injection safe (never starts with "-"). */
|
|
53
|
-
const GIT_URL = /^(https:\/\/|git@)[\w.-]+[:/]\S+$/;
|
|
54
|
-
|
|
55
|
-
/** Short, stable discriminator so two sources never share a namespace. */
|
|
56
|
-
function sourceFingerprint(canonical: string): string {
|
|
57
|
-
return createHash("sha256").update(canonical).digest("hex").slice(0, 8);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Sanitize a path/url basename into a stable, filesystem-safe source id.
|
|
62
|
-
* `resolvedPath` (absolute path) or the git URL is fingerprinted into the id so
|
|
63
|
-
* distinct sources sharing a basename get distinct namespaces
|
|
64
|
-
* (`lib/utils-3f9a1c02/` vs `lib/utils-a1b2c3d4/`).
|
|
65
|
-
*/
|
|
66
|
-
export function librarySourceId(source: string, resolvedPath?: string): string {
|
|
67
|
-
const trimmed = source.trim();
|
|
68
|
-
const isGit = GIT_URL.test(trimmed);
|
|
69
|
-
let raw: string;
|
|
70
|
-
if (isGit) {
|
|
71
|
-
const m = trimmed.match(/(?:\/|:)([\w.-]+?)(?:\.git)?\/?\s*$/);
|
|
72
|
-
raw = m?.[1] ?? "repo";
|
|
73
|
-
} else {
|
|
74
|
-
raw = basename(resolvedPath ?? trimmed);
|
|
75
|
-
}
|
|
76
|
-
const cleaned = raw
|
|
77
|
-
.replace(/\.git$/i, "")
|
|
78
|
-
.replace(/[^\w.-]+/g, "-")
|
|
79
|
-
.replace(/^-+|-+$/g, "")
|
|
80
|
-
.slice(0, 80);
|
|
81
|
-
const canonical = isGit ? trimmed : (resolvedPath ?? trimmed);
|
|
82
|
-
return `${cleaned.length > 0 ? cleaned : "lib"}-${sourceFingerprint(canonical)}`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function pathPrefixFor(sourceId: string): string {
|
|
86
|
-
return `lib/${sourceId}/`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Derive the namespace for a source string without packing (host-side idempotency).
|
|
91
|
-
* Returns both sourceId and pathPrefix so callers never un-parse the prefix.
|
|
92
|
-
*/
|
|
93
|
-
export function libraryNamespace(source: string, cwd: string): LibraryNamespace {
|
|
94
|
-
const trimmed = source.trim();
|
|
95
|
-
const sourceId = GIT_URL.test(trimmed)
|
|
96
|
-
? librarySourceId(trimmed)
|
|
97
|
-
: librarySourceId(trimmed, isAbsolute(trimmed) ? trimmed : resolve(cwd, trimmed));
|
|
98
|
-
return Object.freeze({ sourceId, pathPrefix: pathPrefixFor(sourceId) });
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Derive the path prefix for a source string without packing. */
|
|
102
|
-
export function libraryPathPrefix(source: string, cwd: string): string {
|
|
103
|
-
return libraryNamespace(source, cwd).pathPrefix;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Namespaced files plus summed content chars (one pass). */
|
|
107
|
-
export function namespaceLibraryFilesWithChars(
|
|
108
|
-
payload: unknown,
|
|
109
|
-
sourceId: string,
|
|
110
|
-
): { readonly files: readonly ContextFile[]; readonly chars: number } {
|
|
111
|
-
const prefix = pathPrefixFor(sourceId);
|
|
112
|
-
if (typeof payload === "string") {
|
|
113
|
-
return Object.freeze({
|
|
114
|
-
files: Object.freeze([
|
|
115
|
-
Object.freeze({
|
|
116
|
-
path: `${prefix}content`,
|
|
117
|
-
content: payload,
|
|
118
|
-
tokens: Math.max(1, estimateTokens(payload.length)),
|
|
119
|
-
}),
|
|
120
|
-
]),
|
|
121
|
-
chars: payload.length,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
if (!Array.isArray(payload)) return Object.freeze({ files: Object.freeze([]), chars: 0 });
|
|
125
|
-
const out = new Array<ContextFile>(payload.length);
|
|
126
|
-
let n = 0;
|
|
127
|
-
let chars = 0;
|
|
128
|
-
for (const item of payload) {
|
|
129
|
-
if (item === null || typeof item !== "object") continue;
|
|
130
|
-
const rec = item as Record<string, unknown>;
|
|
131
|
-
const content = typeof rec.content === "string" ? rec.content : String(rec.content ?? "");
|
|
132
|
-
let path = typeof rec.path === "string" ? rec.path : "unknown";
|
|
133
|
-
path = path.replace(/^\/+/, "");
|
|
134
|
-
if (!path.startsWith(prefix)) path = `${prefix}${path}`;
|
|
135
|
-
const tokens = typeof rec.tokens === "number" && Number.isFinite(rec.tokens)
|
|
136
|
-
? Math.max(0, Math.floor(rec.tokens))
|
|
137
|
-
: Math.max(1, estimateTokens(content.length));
|
|
138
|
-
out[n++] = Object.freeze({ path, content, tokens });
|
|
139
|
-
chars += content.length;
|
|
140
|
-
}
|
|
141
|
-
out.length = n;
|
|
142
|
-
return Object.freeze({ files: Object.freeze(out), chars });
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** Namespace file entries under `lib/<sourceId>/…` (single shared implementation). */
|
|
146
|
-
export function namespaceLibraryFiles(
|
|
147
|
-
payload: unknown,
|
|
148
|
-
sourceId: string,
|
|
149
|
-
): readonly ContextFile[] {
|
|
150
|
-
return namespaceLibraryFilesWithChars(payload, sourceId).files;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** First `lib/<id>/` prefix found in the payload, or undefined. Skips `lib/unknown/`. */
|
|
154
|
-
export function payloadPrefix(payload: readonly unknown[]): string | undefined {
|
|
155
|
-
for (let i = 0; i < payload.length; i++) {
|
|
156
|
-
const item = payload[i];
|
|
157
|
-
if (item === null || typeof item !== "object") continue;
|
|
158
|
-
const path = (item as { path?: unknown }).path;
|
|
159
|
-
if (typeof path !== "string") continue;
|
|
160
|
-
const m = /^(lib\/[^/]+\/)/.exec(path);
|
|
161
|
-
// `lib/unknown/` is the legacy catch-all: never treat it as an identity.
|
|
162
|
-
if (m?.[1] !== undefined && m[1] !== LEGACY_UNKNOWN_PREFIX) return m[1];
|
|
163
|
-
}
|
|
164
|
-
return undefined;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Append a library payload into an existing list context (host-side resume merge).
|
|
169
|
-
* Skips a payload whose path prefix is already present (resume-safe dedup).
|
|
170
|
-
*/
|
|
171
|
-
export function mergeLibraryIntoContext(base: unknown, libraryPayload: unknown): unknown {
|
|
172
|
-
if (!Array.isArray(base)) return base;
|
|
173
|
-
if (Array.isArray(libraryPayload)) {
|
|
174
|
-
if (libraryPayload.length === 0) return base;
|
|
175
|
-
const prefix = payloadPrefix(libraryPayload);
|
|
176
|
-
if (prefix !== undefined) {
|
|
177
|
-
for (let i = 0; i < base.length; i++) {
|
|
178
|
-
const item = base[i];
|
|
179
|
-
if (item !== null && typeof item === "object"
|
|
180
|
-
&& typeof (item as { path?: unknown }).path === "string"
|
|
181
|
-
&& (item as { path: string }).path.startsWith(prefix)) {
|
|
182
|
-
return base; // already present
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
const merged = new Array<unknown>(base.length + libraryPayload.length);
|
|
187
|
-
for (let i = 0; i < base.length; i++) merged[i] = base[i];
|
|
188
|
-
for (let i = 0; i < libraryPayload.length; i++) merged[base.length + i] = libraryPayload[i];
|
|
189
|
-
return merged;
|
|
190
|
-
}
|
|
191
|
-
if (typeof libraryPayload === "string") {
|
|
192
|
-
// Legacy string sidecars: wrap once under an unknown prefix.
|
|
193
|
-
return mergeLibraryIntoContext(base, namespaceLibraryFiles(libraryPayload, "unknown"));
|
|
194
|
-
}
|
|
195
|
-
return base;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function toLibrarySource(payload: unknown, sourceId: string): LibrarySource {
|
|
199
|
-
const { files, chars } = namespaceLibraryFilesWithChars(payload, sourceId);
|
|
200
|
-
return {
|
|
201
|
-
payload: files,
|
|
202
|
-
files: files.length,
|
|
203
|
-
chars,
|
|
204
|
-
sourceId,
|
|
205
|
-
pathPrefix: pathPrefixFor(sourceId),
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export async function resolveLibrarySource(
|
|
210
|
-
source: string,
|
|
211
|
-
cwd: string,
|
|
212
|
-
signal?: AbortSignal,
|
|
213
|
-
): Promise<Result<LibrarySource, string>> {
|
|
214
|
-
const trimmed = source.trim();
|
|
215
|
-
if (trimmed === "") return { ok: false, error: "load_library: empty source" };
|
|
216
|
-
if (GIT_URL.test(trimmed)) return await cloneAndPack(trimmed, signal);
|
|
217
|
-
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) {
|
|
218
|
-
return { ok: false, error: `unsupported URL scheme (only https:// and git@ are allowed): ${trimmed}` };
|
|
219
|
-
}
|
|
220
|
-
const path = isAbsolute(trimmed) ? trimmed : resolve(cwd, trimmed);
|
|
221
|
-
let s: Awaited<ReturnType<typeof stat>>;
|
|
222
|
-
try {
|
|
223
|
-
s = await stat(path);
|
|
224
|
-
} catch {
|
|
225
|
-
return { ok: false, error: `load_library: path not found: ${path}` };
|
|
226
|
-
}
|
|
227
|
-
const sourceId = librarySourceId(trimmed, path);
|
|
228
|
-
if (s.isDirectory()) return await packDir(path, sourceId, signal);
|
|
229
|
-
if (s.size > MAX_LIBRARY_FILE_BYTES) {
|
|
230
|
-
return {
|
|
231
|
-
ok: false,
|
|
232
|
-
error: `load_library: ${path} is ${s.size.toLocaleString()} bytes `
|
|
233
|
-
+ `(limit ${MAX_LIBRARY_FILE_BYTES.toLocaleString()}) — `
|
|
234
|
-
+ "open() it in the REPL and delegate with llm_query_chunked instead",
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
const text = await readFile(path, "utf-8");
|
|
238
|
-
return { ok: true, value: toLibrarySource(text, sourceId) };
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function packDir(
|
|
242
|
-
dir: string,
|
|
243
|
-
sourceId: string,
|
|
244
|
-
signal?: AbortSignal,
|
|
245
|
-
): Promise<Result<LibrarySource, string>> {
|
|
246
|
-
const packed = await packRepository(dir, signal);
|
|
247
|
-
if (!packed.ok) return { ok: false, error: `pack failed for ${dir} — ${packed.error}` };
|
|
248
|
-
return { ok: true, value: bundleToSource(packed.value, sourceId) };
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function bundleToSource(bundle: ContextBundle, sourceId: string): LibrarySource {
|
|
252
|
-
return toLibrarySource(serializeForSandbox(bundle), sourceId);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
async function cloneAndPack(url: string, signal?: AbortSignal): Promise<Result<LibrarySource, string>> {
|
|
256
|
-
const dir = await mkdtemp(join(tmpdir(), "rlm-lib-"));
|
|
257
|
-
const sourceId = librarySourceId(url);
|
|
258
|
-
try {
|
|
259
|
-
await execFileP("git", ["clone", "--depth", "1", "--", url, dir], { signal, timeout: 120_000 });
|
|
260
|
-
return await packDir(dir, sourceId, signal);
|
|
261
|
-
} catch (err: unknown) {
|
|
262
|
-
return { ok: false, error: `git clone failed for ${url} — ${errorMessage(err)}` };
|
|
263
|
-
} finally {
|
|
264
|
-
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
265
|
-
}
|
|
266
|
-
}
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* repomix-context — pre-packs the entire codebase into a structured JSON array
|
|
3
|
-
* for the RLM sandbox. Replaces the legacy filesystem-tool context
|
|
4
|
-
* (buildProjectManifest / listProjectFiles / gitLsFiles).
|
|
5
|
-
*
|
|
6
|
-
* Uses repomix internally (worker-thread pool, built-in gitignore support)
|
|
7
|
-
* and caches results in a module-level Map with TTL to avoid re-packing on
|
|
8
|
-
* every run within the same process.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { pack } from "repomix";
|
|
12
|
-
import type { PackResult as RepomixPackResult } from "repomix";
|
|
13
|
-
/** repomix's own config parameter type — `satisfies` keeps the literal checked against it. */
|
|
14
|
-
type PackConfig = NonNullable<Parameters<typeof pack>[1]>;
|
|
15
|
-
import { resolve } from "node:path";
|
|
16
|
-
import { tmpdir } from "node:os";
|
|
17
|
-
import { errorMessage } from "../util/errors.ts";
|
|
18
|
-
import { estimateTokens } from "../text/tokens.ts";
|
|
19
|
-
|
|
20
|
-
// ── Public types ──
|
|
21
|
-
|
|
22
|
-
export interface ContextFile {
|
|
23
|
-
readonly path: string;
|
|
24
|
-
readonly content: string;
|
|
25
|
-
readonly tokens: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface ContextBundle {
|
|
29
|
-
readonly files: readonly ContextFile[];
|
|
30
|
-
readonly totalFiles: number;
|
|
31
|
-
readonly totalTokens: number;
|
|
32
|
-
readonly totalChars: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface PackSuccess {
|
|
36
|
-
readonly ok: true;
|
|
37
|
-
readonly value: ContextBundle;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface PackFailure {
|
|
41
|
-
readonly ok: false;
|
|
42
|
-
readonly error: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export type PackResult = PackSuccess | PackFailure;
|
|
46
|
-
|
|
47
|
-
// ── Module-level cache ──
|
|
48
|
-
|
|
49
|
-
interface CacheEntry {
|
|
50
|
-
readonly bundle: ContextBundle;
|
|
51
|
-
readonly ts: number;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const cache = new Map<string, CacheEntry>();
|
|
55
|
-
const DEFAULT_CACHE_TTL_MS = 30_000;
|
|
56
|
-
|
|
57
|
-
function cacheKey(cwd: string): string {
|
|
58
|
-
return resolve(cwd);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function cacheGet(key: string, ttlMs: number): ContextBundle | undefined {
|
|
62
|
-
const entry = cache.get(key);
|
|
63
|
-
if (!entry) return undefined;
|
|
64
|
-
if (Date.now() - entry.ts > ttlMs) {
|
|
65
|
-
cache.delete(key);
|
|
66
|
-
return undefined;
|
|
67
|
-
}
|
|
68
|
-
return entry.bundle;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function cacheSet(key: string, bundle: ContextBundle): void {
|
|
72
|
-
cache.set(key, { bundle, ts: Date.now() });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ── Core functions ──
|
|
76
|
-
|
|
77
|
-
export async function packRepository(
|
|
78
|
-
cwd: string,
|
|
79
|
-
signal?: AbortSignal,
|
|
80
|
-
ttlMs: number = DEFAULT_CACHE_TTL_MS,
|
|
81
|
-
): Promise<PackResult> {
|
|
82
|
-
if (signal?.aborted) return { ok: false, error: "aborted" };
|
|
83
|
-
|
|
84
|
-
const key = cacheKey(cwd);
|
|
85
|
-
const cached = cacheGet(key, ttlMs);
|
|
86
|
-
if (cached) return { ok: true, value: cached };
|
|
87
|
-
|
|
88
|
-
try {
|
|
89
|
-
const result: RepomixPackResult = await Promise.race([
|
|
90
|
-
pack([cwd], {
|
|
91
|
-
input: { maxFileSize: 1048576 },
|
|
92
|
-
cwd,
|
|
93
|
-
output: {
|
|
94
|
-
filePath: `${tmpdir()}/repomix-out-${Date.now()}.txt`,
|
|
95
|
-
style: "plain",
|
|
96
|
-
filePathStyle: "cwd-relative",
|
|
97
|
-
parsableStyle: false,
|
|
98
|
-
headerText: undefined,
|
|
99
|
-
instructionFilePath: undefined,
|
|
100
|
-
fileSummary: false,
|
|
101
|
-
directoryStructure: false,
|
|
102
|
-
files: true,
|
|
103
|
-
removeComments: false,
|
|
104
|
-
removeEmptyLines: false,
|
|
105
|
-
compress: false,
|
|
106
|
-
topFilesLength: 5,
|
|
107
|
-
showLineNumbers: false,
|
|
108
|
-
truncateBase64: false,
|
|
109
|
-
copyToClipboard: false,
|
|
110
|
-
includeEmptyDirectories: undefined,
|
|
111
|
-
includeFullDirectoryStructure: false,
|
|
112
|
-
splitOutput: undefined,
|
|
113
|
-
tokenCountTree: false,
|
|
114
|
-
tokenBudget: undefined,
|
|
115
|
-
git: {
|
|
116
|
-
sortByChanges: true,
|
|
117
|
-
sortByChangesMaxCommits: 100,
|
|
118
|
-
includeDiffs: false,
|
|
119
|
-
includeLogs: false,
|
|
120
|
-
includeLogsCount: 50,
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
include: [],
|
|
124
|
-
ignore: {
|
|
125
|
-
useGitignore: true,
|
|
126
|
-
useDotIgnore: true,
|
|
127
|
-
useDefaultPatterns: true,
|
|
128
|
-
customPatterns: [],
|
|
129
|
-
},
|
|
130
|
-
security: { enableSecurityCheck: false },
|
|
131
|
-
tokenCount: { encoding: "o200k_base" as const },
|
|
132
|
-
} satisfies PackConfig),
|
|
133
|
-
new Promise<never>((_, reject) => {
|
|
134
|
-
signal?.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
|
|
135
|
-
}),
|
|
136
|
-
]);
|
|
137
|
-
|
|
138
|
-
const processedFiles = result.processedFiles;
|
|
139
|
-
const files = new Array<ContextFile>(processedFiles.length);
|
|
140
|
-
const tokenCounts = result.fileTokenCounts;
|
|
141
|
-
let totalTokens = 0;
|
|
142
|
-
let totalChars = 0;
|
|
143
|
-
|
|
144
|
-
for (let i = 0; i < processedFiles.length; i++) {
|
|
145
|
-
const file = processedFiles[i];
|
|
146
|
-
const tokens = tokenCounts[file.path] ?? estimateTokens(file.content.length);
|
|
147
|
-
files[i] = { path: file.path, content: file.content, tokens };
|
|
148
|
-
totalTokens += tokens;
|
|
149
|
-
totalChars += file.content.length;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const bundle: ContextBundle = {
|
|
153
|
-
files,
|
|
154
|
-
totalFiles: files.length,
|
|
155
|
-
totalTokens,
|
|
156
|
-
totalChars,
|
|
157
|
-
};
|
|
158
|
-
cacheSet(key, bundle);
|
|
159
|
-
return { ok: true, value: bundle };
|
|
160
|
-
} catch (err: unknown) {
|
|
161
|
-
if (signal?.aborted) return { ok: false, error: "aborted" };
|
|
162
|
-
return {
|
|
163
|
-
ok: false,
|
|
164
|
-
error: errorMessage(err),
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export function serializeForSandbox(
|
|
170
|
-
bundle: ContextBundle,
|
|
171
|
-
): readonly ContextFile[] {
|
|
172
|
-
return bundle.files;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Maximum files shown in the compact LLM listing before truncation. */
|
|
176
|
-
const MAX_LLM_LISTING_FILES = 200;
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Produces a compact human-readable text block for the parent LLM's context window.
|
|
180
|
-
* Shows file paths and token estimates — NOT full file contents (those are too large
|
|
181
|
-
* for the context window). The LLM inspects files via repl() over the pre-loaded context variable.
|
|
182
|
-
*/
|
|
183
|
-
export function formatForLLM(bundle: ContextBundle): string {
|
|
184
|
-
const files = bundle.files.slice(0, MAX_LLM_LISTING_FILES);
|
|
185
|
-
const truncated = bundle.totalFiles > MAX_LLM_LISTING_FILES
|
|
186
|
-
? `... and ${bundle.totalFiles - MAX_LLM_LISTING_FILES} more files (truncated)`
|
|
187
|
-
: "";
|
|
188
|
-
|
|
189
|
-
const listing = files.map((f) =>
|
|
190
|
-
`${f.path} (${f.tokens.toLocaleString()} tok, ${f.content.length.toLocaleString()} chars)`,
|
|
191
|
-
).join("\n");
|
|
192
|
-
|
|
193
|
-
return [
|
|
194
|
-
`Repository context: ${bundle.totalFiles.toLocaleString()} files, ${bundle.totalTokens.toLocaleString()} estimated tokens, ${bundle.totalChars.toLocaleString()} total characters.`,
|
|
195
|
-
"",
|
|
196
|
-
listing,
|
|
197
|
-
truncated,
|
|
198
|
-
"",
|
|
199
|
-
"All file contents are pre-loaded in the REPL `context` variable — file-reading tools are disabled.",
|
|
200
|
-
"Use repl({code}) and delegate semantic reading to llm_query / llm_query_batched / llm_query_chunked.",
|
|
201
|
-
].join("\n");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
package/src/core/artifacts.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
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
|
-
import { errorMessage } from "../util/errors.ts";
|
|
10
|
-
|
|
11
|
-
export const ARTIFACTS_DIR = ".rlm/artifacts";
|
|
12
|
-
|
|
13
|
-
const stamp = (): string => new Date().toISOString().replace(/[:.]/g, "-");
|
|
14
|
-
|
|
15
|
-
export interface GoalCapture {
|
|
16
|
-
readonly goalPath: string; // repo-relative
|
|
17
|
-
readonly baselinePath: string; // repo-relative
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export type SaveOutcome =
|
|
21
|
-
| { readonly ok: true; readonly path: string }
|
|
22
|
-
| { readonly ok: false; readonly error: string };
|
|
23
|
-
|
|
24
|
-
export type GoalCaptureResult =
|
|
25
|
-
| { readonly ok: true; readonly value: GoalCapture }
|
|
26
|
-
| { readonly ok: false; readonly error: string };
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Capture the user's brief VERBATIM: no frontmatter, no headers — the raw file
|
|
30
|
-
* is the only artifact that preserves explicit user constraints unrefracted. The
|
|
31
|
-
* baseline snapshot records paths ALREADY dirty before the run, so validate
|
|
32
|
-
* judges only the run's own delta. Best-effort: git failure ⇒ empty baseline.
|
|
33
|
-
* Failures never throw (unwritable cwd etc.) — returns error for the engine to surface.
|
|
34
|
-
*/
|
|
35
|
-
export function captureGoal(cwd: string, brief: string): GoalCaptureResult {
|
|
36
|
-
try {
|
|
37
|
-
const ts = stamp();
|
|
38
|
-
const dir = join(ARTIFACTS_DIR, "goal");
|
|
39
|
-
mkdirSync(join(cwd, dir), { recursive: true });
|
|
40
|
-
const goalPath = join(dir, `goal-${ts}.md`);
|
|
41
|
-
writeFileSync(join(cwd, goalPath), brief, "utf-8");
|
|
42
|
-
let paths: readonly string[] = [];
|
|
43
|
-
try {
|
|
44
|
-
paths = execFileSync("git", ["status", "--short"], {
|
|
45
|
-
cwd,
|
|
46
|
-
encoding: "utf-8",
|
|
47
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
48
|
-
})
|
|
49
|
-
.split("\n")
|
|
50
|
-
.filter((l) => l.trim() !== "")
|
|
51
|
-
.map((l) => {
|
|
52
|
-
const rest = l.slice(3).trim();
|
|
53
|
-
const arrow = rest.indexOf(" -> ");
|
|
54
|
-
return arrow >= 0 ? rest.slice(arrow + 4).trim() : rest;
|
|
55
|
-
});
|
|
56
|
-
} catch {
|
|
57
|
-
paths = [];
|
|
58
|
-
}
|
|
59
|
-
const baselinePath = join(dir, `baseline-${ts}.json`);
|
|
60
|
-
writeFileSync(join(cwd, baselinePath), JSON.stringify({ paths }, null, 2), "utf-8");
|
|
61
|
-
return { ok: true, value: { goalPath, baselinePath } };
|
|
62
|
-
} catch (err) {
|
|
63
|
-
const message = errorMessage(err);
|
|
64
|
-
return { ok: false, error: message };
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Write a stage artifact under its dir; timestamped so runs never collide. */
|
|
69
|
-
export function saveArtifact(cwd: string, dir: string, slug: string, content: string): SaveOutcome {
|
|
70
|
-
try {
|
|
71
|
-
const rel = join(ARTIFACTS_DIR, dir, `${stamp()}_${slug}.md`);
|
|
72
|
-
mkdirSync(join(cwd, ARTIFACTS_DIR, dir), { recursive: true });
|
|
73
|
-
writeFileSync(join(cwd, rel), content, "utf-8");
|
|
74
|
-
return { ok: true, path: rel };
|
|
75
|
-
} catch (err) {
|
|
76
|
-
const message = errorMessage(err);
|
|
77
|
-
return { ok: false, error: message };
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Read a previously saved artifact (repo-relative path). Failures never throw. */
|
|
82
|
-
export function readArtifact(cwd: string, relPath: string): Result<string, string> {
|
|
83
|
-
try {
|
|
84
|
-
return { ok: true, value: readFileSync(join(cwd, relPath), "utf-8") };
|
|
85
|
-
} catch (err) {
|
|
86
|
-
const message = errorMessage(err);
|
|
87
|
-
return { ok: false, error: `could not read artifact ${relPath}: ${message}` };
|
|
88
|
-
}
|
|
89
|
-
}
|
package/src/core/critique.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
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
|
-
}
|