@hicaru/pi-rlm 0.2.0 → 0.2.2

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 (68) hide show
  1. package/README.md +12 -35
  2. package/README.ru.md +18 -23
  3. package/README.zh-CN.md +17 -28
  4. package/package.json +1 -1
  5. package/src/bridge/library.ts +61 -26
  6. package/src/bridge/subcall-handlers.ts +382 -0
  7. package/src/commands/rlm-config.ts +47 -18
  8. package/src/commands/rlm.ts +3 -152
  9. package/src/config/defaults.ts +7 -15
  10. package/src/config/settings.ts +8 -32
  11. package/src/context/library-context.ts +90 -17
  12. package/src/core/engine.ts +115 -360
  13. package/src/core/history.ts +1 -1
  14. package/src/core/limits.ts +5 -12
  15. package/src/core/resource-limits.ts +0 -2
  16. package/src/core/types.ts +3 -36
  17. package/src/index.ts +49 -10
  18. package/src/mode/llm-model.ts +54 -0
  19. package/src/mode/rlm-mode.ts +26 -57
  20. package/src/prompts/glossary.ts +287 -0
  21. package/src/prompts/native.ts +127 -0
  22. package/src/prompts/system.ts +14 -386
  23. package/src/sandbox/context-file.ts +154 -0
  24. package/src/sandbox/interrupts.ts +145 -0
  25. package/src/sandbox/protocol.ts +14 -69
  26. package/src/sandbox/py/guards.py +150 -0
  27. package/src/sandbox/py/retrieval.py +265 -0
  28. package/src/sandbox/py/tasks.py +116 -0
  29. package/src/sandbox/py/worker.py +836 -0
  30. package/src/sandbox/sandbox-manager.ts +33 -6
  31. package/src/sandbox/sandbox.ts +153 -182
  32. package/src/text/tokens.ts +29 -3
  33. package/src/tool/background-tasks.ts +95 -0
  34. package/src/tool/repl-details.ts +4 -2
  35. package/src/tool/repl-render.ts +58 -0
  36. package/src/tool/repl-result.ts +70 -0
  37. package/src/tool/repl-tool.ts +178 -216
  38. package/src/tool/rlm-aggregator.ts +2 -10
  39. package/src/tool/rlm-details.ts +0 -2
  40. package/src/tool/rlm-events.ts +10 -16
  41. package/src/tool/rlm-tool.ts +1 -12
  42. package/src/tool/subcall-render.ts +15 -3
  43. package/src/tool/subcall-store.ts +57 -1
  44. package/src/ui/config-panel.ts +4 -16
  45. package/src/ui/intro.ts +1 -2
  46. package/src/ui/model-picker.ts +34 -10
  47. package/src/ui/status.ts +3 -7
  48. package/src/util/concurrency.ts +91 -13
  49. package/src/util/trace.ts +42 -0
  50. package/src/bridge/fallback-todo.ts +0 -137
  51. package/src/bridge/interactive.ts +0 -65
  52. package/src/bridge/llm-query.ts +0 -156
  53. package/src/bridge/pi-interactive.ts +0 -41
  54. package/src/bridge/rlm-query.ts +0 -108
  55. package/src/core/artifacts.ts +0 -89
  56. package/src/core/critique.ts +0 -92
  57. package/src/core/gates.ts +0 -301
  58. package/src/core/pipeline-handlers.ts +0 -319
  59. package/src/core/pipeline.ts +0 -268
  60. package/src/prompts/phases.ts +0 -104
  61. package/src/sandbox/worker.py +0 -1078
  62. package/src/state/index.ts +0 -24
  63. package/src/state/internal.ts +0 -46
  64. package/src/state/paths.ts +0 -44
  65. package/src/state/reads.ts +0 -133
  66. package/src/state/resume.ts +0 -173
  67. package/src/state/rows.ts +0 -123
  68. package/src/state/writes.ts +0 -58
@@ -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
- }
package/src/core/gates.ts DELETED
@@ -1,301 +0,0 @@
1
- /**
2
- * Deterministic gate floors for the RLM pipeline: the engine measures artifacts
3
- * instead of trusting the model's claim that a phase is complete.
4
- */
5
- import { existsSync, readFileSync, statSync } from "node:fs";
6
- import { isAbsolute, join } from "node:path";
7
- import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
8
- import type { Result } from "../util/errors.ts";
9
-
10
- /** Gate outcome — same shape as Result; alias keeps call sites domain-clear. */
11
- export type GateResult<T> = Result<T, string>;
12
-
13
- export const MAX_PHASES = 32;
14
-
15
- /** One parsed entry of a plan's `phases:` frontmatter array. */
16
- export interface PhaseRecord {
17
- readonly n: number;
18
- readonly title: string;
19
- readonly index: number;
20
- readonly total: number;
21
- }
22
-
23
- export interface PlanGateData {
24
- readonly phases: readonly PhaseRecord[];
25
- }
26
-
27
- export interface ValidationGateData {
28
- readonly blockersCount: number;
29
- readonly verdict: "pass" | "fail";
30
- }
31
-
32
- export interface ClarificationGateData {
33
- readonly decisionsCount: number;
34
- readonly openQuestionsCount: number;
35
- }
36
-
37
- const PLAN_PHASE_RE = /^## Phase (\d+):/;
38
- const STATUS_READY = "ready";
39
- const BULLET_RE = /^-\s+\S/;
40
-
41
- /**
42
- * Walk content lines, invoking `visit` only for lines outside fenced code blocks.
43
- * Shared by heading and bullet counters (DRY — single fence scan).
44
- */
45
- export function forEachLineOutsideFences(
46
- content: string,
47
- visit: (line: string) => void,
48
- ): void {
49
- let inFence = false;
50
- let fenceLen = 0;
51
- for (const line of content.split("\n")) {
52
- const fence = /^\s*(`{3,}|~{3,})/.exec(line);
53
- if (fence) {
54
- const len = (fence[1] ?? "").length;
55
- if (!inFence) {
56
- inFence = true;
57
- fenceLen = len;
58
- } else if (len >= fenceLen && line.trim().length === len) {
59
- inFence = false;
60
- fenceLen = 0;
61
- }
62
- continue;
63
- }
64
- if (!inFence) visit(line);
65
- }
66
- }
67
-
68
- /**
69
- * Count lines matching `re` OUTSIDE fenced code blocks — a `## Phase N:` inside
70
- * a ``` fence is example text, not a structural heading.
71
- */
72
- export function countHeadingsOutsideFences(content: string, re: RegExp): number {
73
- const lineRe = new RegExp(re.source);
74
- let count = 0;
75
- forEachLineOutsideFences(content, (line) => {
76
- if (lineRe.test(line)) count++;
77
- });
78
- return count;
79
- }
80
-
81
- /** Count newlines without materialising a line array. */
82
- export function lineCountOf(text: string): number {
83
- let count = 1;
84
- for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) count++;
85
- return count;
86
- }
87
-
88
- /** Names of `## Phase N:` sections that contain no `### Success Criteria` heading. */
89
- export function phasesMissingSuccessCriteria(content: string): readonly string[] {
90
- const missing: string[] = [];
91
- let currentPhase: string | undefined;
92
- let sawCriteria = false;
93
- const flush = (): void => {
94
- if (currentPhase !== undefined && !sawCriteria) missing.push(currentPhase);
95
- };
96
- forEachLineOutsideFences(content, (line) => {
97
- const phase = /^## Phase (\d+):/.exec(line);
98
- if (phase) {
99
- flush();
100
- currentPhase = `Phase ${phase[1] ?? "?"}`;
101
- sawCriteria = false;
102
- return;
103
- }
104
- if (currentPhase !== undefined && /^### Success Criteria/.test(line)) sawCriteria = true;
105
- });
106
- flush();
107
- return Object.freeze(missing);
108
- }
109
-
110
- /**
111
- * Fence-aware count of top-level (column-0) `- ` bullets under a `## <heading>` section.
112
- * Nested/indented sub-bullets are ignored. The next `## ` heading ends the section.
113
- * Missing heading ⇒ 0.
114
- */
115
- export function countBulletsUnderHeading(content: string, heading: string): number {
116
- const headingRe = new RegExp(`^##\\s+${escapeRegExp(heading)}\\s*$`);
117
- let inSection = false;
118
- let count = 0;
119
- forEachLineOutsideFences(content, (line) => {
120
- if (/^##\s+/.test(line)) {
121
- inSection = headingRe.test(line);
122
- return;
123
- }
124
- // Column-0 only: do not trimStart — indented sub-bullets must not inflate the count.
125
- if (inSection && BULLET_RE.test(line)) count++;
126
- });
127
- return count;
128
- }
129
-
130
- /**
131
- * True when `## <heading>` exists and has non-whitespace body before the next `## `.
132
- * Only the first matching heading is considered (later duplicates are ignored).
133
- */
134
- export function sectionHasNonEmptyBody(content: string, heading: string): boolean {
135
- const headingRe = new RegExp(`^##\\s+${escapeRegExp(heading)}\\s*$`);
136
- let inSection = false;
137
- let seen = false; // first match wins — do not re-enter on a later duplicate heading
138
- let hasBody = false; // only emptiness matters — never accumulate the body itself
139
- forEachLineOutsideFences(content, (line) => {
140
- if (/^##\s+/.test(line)) {
141
- if (inSection) {
142
- inSection = false;
143
- return;
144
- }
145
- if (!seen && headingRe.test(line)) {
146
- inSection = true;
147
- seen = true;
148
- }
149
- return;
150
- }
151
- if (inSection && line.trim().length > 0) hasBody = true;
152
- });
153
- return hasBody;
154
- }
155
-
156
- function escapeRegExp(s: string): string {
157
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
158
- }
159
-
160
- /** Frontmatter as a plain record (parseFrontmatter returns unknown-shaped data). */
161
- function frontmatterOf(content: string): Record<string, unknown> {
162
- const { frontmatter } = parseFrontmatter(content);
163
- return typeof frontmatter === "object" && frontmatter !== null
164
- ? (frontmatter as Record<string, unknown>)
165
- : {};
166
- }
167
-
168
- /** `status: ready` floor — shared by every produces-stage gate. */
169
- export function checkStatusReady(content: string, path: string): GateResult<undefined> {
170
- const status = frontmatterOf(content).status;
171
- return status === STATUS_READY
172
- ? { ok: true, value: undefined }
173
- : { ok: false, error: `artifact ${path} has status '${String(status)}' — set frontmatter status: ready before advancing` };
174
- }
175
-
176
- /**
177
- * Plan-structure floor:
178
- * `phases:` array ≡ fence-aware `## Phase N:` heading count, `phase_count` ≡
179
- * array length, count within [1, MAX_PHASES]. Stale array ⇒ reject, so the
180
- * fanout never dispatches a wrong unit list.
181
- */
182
- export function planPhaseRecords(content: string, path: string): GateResult<PlanGateData> {
183
- const fm = frontmatterOf(content);
184
- const raw = fm.phases;
185
- const phases = Array.isArray(raw) ? raw : [];
186
- const headingCount = countHeadingsOutsideFences(content, PLAN_PHASE_RE);
187
- if (phases.length !== headingCount) {
188
- return { ok: false, error: `plan ${path}: frontmatter phases (${phases.length}) ≠ '## Phase N:' headings (${headingCount}) — rebuild the phases: array from the body headings` };
189
- }
190
- if (fm.phase_count !== phases.length) {
191
- return { ok: false, error: `plan ${path}: phase_count (${String(fm.phase_count)}) ≠ phases length (${phases.length}) — rebuild phase_count` };
192
- }
193
- if (phases.length === 0) {
194
- return { ok: false, error: `plan ${path}: declares no '## Phase N:' sections — a plan needs at least one phase` };
195
- }
196
- if (phases.length > MAX_PHASES) {
197
- return { ok: false, error: `plan ${path}: ${phases.length} phases exceeds MAX_PHASES (${MAX_PHASES}) — split the plan` };
198
- }
199
- const records = new Array<PhaseRecord>(phases.length);
200
- for (let index = 0; index < phases.length; index++) {
201
- const entry = phases[index];
202
- const e = typeof entry === "object" && entry !== null ? (entry as Record<string, unknown>) : {};
203
- records[index] = {
204
- n: typeof e.n === "number" ? e.n : index + 1,
205
- title: typeof e.title === "string" ? e.title : "",
206
- index,
207
- total: phases.length,
208
- };
209
- }
210
- return { ok: true, value: { phases: records } };
211
- }
212
-
213
- /**
214
- * Citation floor (direct path resolution only): every `path/file.ext:NN[-MM]`
215
- * in the artifact body must name a real file with at least NN lines. Unbacked
216
- * citations are fabricated precision — reject before they mislead implement.
217
- */
218
- const FILE_LINE_CITATION_RE =
219
- /((?:(?<![\w.])\.)?(?<!\w)[\w][\w./-]*\.[a-zA-Z][a-zA-Z0-9]{0,4}):(\d+)(?:-(\d+))?/g;
220
-
221
- export function verifyCitations(body: string, cwd: string): GateResult<undefined> {
222
- const errors: string[] = [];
223
- const seen = new Set<string>();
224
- for (const m of body.matchAll(FILE_LINE_CITATION_RE)) {
225
- const path = m[1];
226
- const startStr = m[2];
227
- const endStr = m[3];
228
- if (path === undefined || startStr === undefined) continue;
229
- const key = `${path}:${startStr}${endStr !== undefined ? `-${endStr}` : ""}`;
230
- if (seen.has(key)) continue;
231
- seen.add(key);
232
- const abs = isAbsolute(path) ? path : join(cwd, path);
233
- if (!existsSync(abs) || !statSync(abs).isFile()) {
234
- errors.push(`unbacked citation ${key} — file does not exist (use a repo-root-relative path or drop the line numbers)`);
235
- continue;
236
- }
237
- let lineCount: number;
238
- try {
239
- lineCount = lineCountOf(readFileSync(abs, "utf-8"));
240
- } catch {
241
- errors.push(`unbacked citation ${key} — file could not be read`);
242
- continue;
243
- }
244
- const high = Math.max(Number(startStr), endStr !== undefined ? Number(endStr) : 0);
245
- if (high > lineCount) {
246
- errors.push(`unbacked citation ${key} — file has ${lineCount} lines; correct the range or drop the line numbers`);
247
- }
248
- }
249
- return errors.length === 0
250
- ? { ok: true, value: undefined }
251
- : { ok: false, error: errors.slice(0, 10).join("\n") };
252
- }
253
-
254
- /**
255
- * Validation-contract floor: the validate artifact must carry the numeric gate
256
- * field (`blockers_count`) so routing is measured, never inferred from prose.
257
- */
258
- export function validationRecord(content: string, path: string): GateResult<ValidationGateData> {
259
- const fm = frontmatterOf(content);
260
- const blockers = fm.blockers_count;
261
- const verdict = fm.verdict;
262
- if (typeof blockers !== "number" || !Number.isInteger(blockers) || blockers < 0) {
263
- return { ok: false, error: `validation ${path}: frontmatter blockers_count must be an integer ≥ 0 (got ${String(blockers)})` };
264
- }
265
- if (verdict !== "pass" && verdict !== "fail") {
266
- return { ok: false, error: `validation ${path}: frontmatter verdict must be 'pass' or 'fail' (got ${String(verdict)})` };
267
- }
268
- if (verdict === "pass" && blockers > 0) {
269
- return { ok: false, error: `validation ${path}: verdict 'pass' contradicts blockers_count ${blockers}` };
270
- }
271
- return { ok: true, value: { blockersCount: blockers, verdict } };
272
- }
273
-
274
- /**
275
- * Clarification-contract floor: interview outcome document.
276
- * `decisions_count` / `open_questions_count` must match fence-aware bullet counts;
277
- * `## Problem & Intent` must be present and non-empty (user's words).
278
- */
279
- export function clarificationRecord(content: string, path: string): GateResult<ClarificationGateData> {
280
- const fm = frontmatterOf(content);
281
- const decisions = fm.decisions_count;
282
- const openQs = fm.open_questions_count;
283
- if (typeof decisions !== "number" || !Number.isInteger(decisions) || decisions < 0) {
284
- return { ok: false, error: `clarification ${path}: frontmatter decisions_count must be an integer ≥ 0 (got ${String(decisions)})` };
285
- }
286
- if (typeof openQs !== "number" || !Number.isInteger(openQs) || openQs < 0) {
287
- return { ok: false, error: `clarification ${path}: frontmatter open_questions_count must be an integer ≥ 0 (got ${String(openQs)})` };
288
- }
289
- if (!sectionHasNonEmptyBody(content, "Problem & Intent")) {
290
- return { ok: false, error: `clarification ${path}: '## Problem & Intent' section is missing or empty — record the user's words verbatim` };
291
- }
292
- const decisionBullets = countBulletsUnderHeading(content, "Decisions");
293
- if (decisions !== decisionBullets) {
294
- return { ok: false, error: `clarification ${path}: decisions_count (${decisions}) ≠ '- ' bullets under '## Decisions' (${decisionBullets}) — rebuild the count from the body` };
295
- }
296
- const openBullets = countBulletsUnderHeading(content, "Open Questions");
297
- if (openQs !== openBullets) {
298
- return { ok: false, error: `clarification ${path}: open_questions_count (${openQs}) ≠ '- ' bullets under '## Open Questions' (${openBullets}) — rebuild the count from the body` };
299
- }
300
- return { ok: true, value: { decisionsCount: decisions, openQuestionsCount: openQs } };
301
- }