@hicaru/pi-rlm 0.2.1 → 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.
- package/README.md +12 -35
- package/README.ru.md +18 -23
- package/README.zh-CN.md +17 -28
- package/package.json +1 -1
- package/src/bridge/library.ts +61 -26
- 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 +6 -17
- package/src/config/settings.ts +8 -32
- package/src/context/library-context.ts +90 -17
- package/src/core/engine.ts +55 -335
- 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 +3 -36
- package/src/index.ts +23 -12
- package/src/mode/llm-model.ts +54 -0
- package/src/mode/rlm-mode.ts +26 -57
- package/src/prompts/glossary.ts +287 -0
- package/src/prompts/native.ts +127 -0
- package/src/prompts/system.ts +14 -407
- package/src/sandbox/context-file.ts +154 -0
- package/src/sandbox/interrupts.ts +145 -0
- package/src/sandbox/protocol.ts +8 -69
- package/src/sandbox/py/guards.py +150 -0
- package/src/sandbox/py/retrieval.py +265 -0
- package/src/sandbox/py/tasks.py +116 -0
- package/src/sandbox/{worker.py → py/worker.py} +76 -696
- package/src/sandbox/sandbox-manager.ts +13 -0
- package/src/sandbox/sandbox.ts +99 -193
- package/src/text/tokens.ts +29 -3
- 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 +37 -159
- 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 +1 -12
- package/src/ui/config-panel.ts +4 -16
- 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/pi-interactive.ts +0 -41
- 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/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
package/src/util/concurrency.ts
CHANGED
|
@@ -80,10 +80,14 @@ export interface SubcallGates {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
|
-
* Worst case is `maxDepth ×
|
|
84
|
-
*
|
|
85
|
-
*
|
|
83
|
+
* Worst case is `(maxDepth - 1) × childLimit` concurrent child engines — the cap short-circuits
|
|
84
|
+
* at `childDepth >= maxDepth`, so engines exist at depths 1..maxDepth-1 — plus `leafLimit` leaf
|
|
85
|
+
* completions.
|
|
86
|
+
*
|
|
87
|
+
* Children get their own, smaller bound because they are far heavier than leaves: each owns a
|
|
88
|
+
* Python subprocess AND its own copy of the context it inherited from its parent, where a leaf
|
|
89
|
+
* is one HTTP request. See DEFAULT_CONFIG.maxConcurrentChildren.
|
|
86
90
|
*/
|
|
87
|
-
export function createSubcallGates(
|
|
88
|
-
return Object.freeze({ leaf: new Semaphore(
|
|
91
|
+
export function createSubcallGates(leafLimit: number, childLimit: number = leafLimit): SubcallGates {
|
|
92
|
+
return Object.freeze({ leaf: new Semaphore(leafLimit), rlm: new DepthGates(childLimit) });
|
|
89
93
|
}
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { formatError } from "../util/errors.ts";
|
|
2
|
-
|
|
3
|
-
type TaskStatus = "pending" | "in_progress" | "completed" | "deleted";
|
|
4
|
-
|
|
5
|
-
interface Task {
|
|
6
|
-
readonly id: number;
|
|
7
|
-
readonly subject: string;
|
|
8
|
-
readonly description?: string;
|
|
9
|
-
readonly status: TaskStatus;
|
|
10
|
-
readonly activeForm?: string;
|
|
11
|
-
readonly blockedBy?: readonly number[];
|
|
12
|
-
readonly owner?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface TodoParams {
|
|
16
|
-
readonly id?: number;
|
|
17
|
-
readonly subject?: string;
|
|
18
|
-
readonly description?: string;
|
|
19
|
-
readonly status?: TaskStatus;
|
|
20
|
-
readonly activeForm?: string;
|
|
21
|
-
readonly blockedBy?: readonly number[];
|
|
22
|
-
readonly addBlockedBy?: readonly number[];
|
|
23
|
-
readonly removeBlockedBy?: readonly number[];
|
|
24
|
-
readonly owner?: string;
|
|
25
|
-
readonly filterStatus?: string;
|
|
26
|
-
readonly includeDeleted?: boolean;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const TODO_STATUSES = Object.freeze(new Set<unknown>(["pending", "in_progress", "completed", "deleted"]));
|
|
30
|
-
|
|
31
|
-
function isTaskStatus(value: unknown): value is TaskStatus {
|
|
32
|
-
return TODO_STATUSES.has(value);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function numericArray(value: unknown): readonly number[] | undefined {
|
|
36
|
-
return Array.isArray(value) ? Object.freeze(value.filter((n): n is number => typeof n === "number")) : undefined;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function toTodoParams(raw: Record<string, unknown>): TodoParams {
|
|
40
|
-
const blockedBy = numericArray(raw.blockedBy);
|
|
41
|
-
const addBlockedBy = numericArray(raw.addBlockedBy);
|
|
42
|
-
const removeBlockedBy = numericArray(raw.removeBlockedBy);
|
|
43
|
-
return Object.freeze({
|
|
44
|
-
...(typeof raw.id === "number" ? { id: raw.id } : {}),
|
|
45
|
-
...(typeof raw.subject === "string" ? { subject: raw.subject } : {}),
|
|
46
|
-
...(typeof raw.description === "string" ? { description: raw.description } : {}),
|
|
47
|
-
...(isTaskStatus(raw.status) ? { status: raw.status } : {}),
|
|
48
|
-
...(typeof raw.activeForm === "string" ? { activeForm: raw.activeForm } : {}),
|
|
49
|
-
...(blockedBy ? { blockedBy } : {}),
|
|
50
|
-
...(addBlockedBy ? { addBlockedBy } : {}),
|
|
51
|
-
...(removeBlockedBy ? { removeBlockedBy } : {}),
|
|
52
|
-
...(typeof raw.owner === "string" ? { owner: raw.owner } : {}),
|
|
53
|
-
...(typeof raw.filterStatus === "string" ? { filterStatus: raw.filterStatus } : {}),
|
|
54
|
-
...(raw.includeDeleted === true ? { includeDeleted: true } : {}),
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function patchedBlockedBy(task: Task, params: TodoParams): readonly number[] | undefined {
|
|
59
|
-
if (params.blockedBy) return params.blockedBy;
|
|
60
|
-
|
|
61
|
-
let next = task.blockedBy ?? Object.freeze([] as readonly number[]);
|
|
62
|
-
if (params.addBlockedBy) next = Object.freeze([...next, ...params.addBlockedBy]);
|
|
63
|
-
|
|
64
|
-
if (params.removeBlockedBy) {
|
|
65
|
-
const removeSet = new Set(params.removeBlockedBy);
|
|
66
|
-
next = Object.freeze(next.filter((n) => !removeSet.has(n)));
|
|
67
|
-
}
|
|
68
|
-
return next.length ? next : undefined;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function taskLines(task: Task): readonly string[] {
|
|
72
|
-
const lines: string[] = [`#${task.id} [${task.status}] ${task.subject}`];
|
|
73
|
-
if (task.description) lines.push(` description: ${task.description}`);
|
|
74
|
-
if (task.activeForm) lines.push(` activeForm: ${task.activeForm}`);
|
|
75
|
-
if (task.blockedBy?.length) lines.push(` blockedBy: ${task.blockedBy.map((n) => `#${n}`).join(", ")}`);
|
|
76
|
-
if (task.owner) lines.push(` owner: ${task.owner}`);
|
|
77
|
-
return lines;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function withPatch(task: Task, params: TodoParams): Task {
|
|
81
|
-
const blockedBy = patchedBlockedBy(task, params);
|
|
82
|
-
return Object.freeze({
|
|
83
|
-
...task,
|
|
84
|
-
...(params.subject !== undefined ? { subject: params.subject } : {}),
|
|
85
|
-
...(params.description !== undefined ? { description: params.description } : {}),
|
|
86
|
-
...(params.status !== undefined ? { status: params.status } : {}),
|
|
87
|
-
...(params.activeForm !== undefined ? { activeForm: params.activeForm } : {}),
|
|
88
|
-
...(blockedBy ? { blockedBy } : {}),
|
|
89
|
-
...(params.owner !== undefined ? { owner: params.owner } : {}),
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** The actions `worker.py::_todo` documents. Anything else is a caller error, not a missing task. */
|
|
94
|
-
const TODO_ACTIONS: ReadonlySet<string> = Object.freeze(new Set([
|
|
95
|
-
"create", "update", "list", "get", "delete", "clear",
|
|
96
|
-
]));
|
|
97
|
-
|
|
98
|
-
export function createTodoFallback(): (action: string, params: Record<string, unknown>) => Promise<string> {
|
|
99
|
-
let nextId = 1;
|
|
100
|
-
let tasks: readonly Task[] = Object.freeze([]);
|
|
101
|
-
const fmt = (task: Task): string => taskLines(task)[0] ?? `#${task.id}`;
|
|
102
|
-
|
|
103
|
-
const apply = (action: string, rawParams: Record<string, unknown>): string => {
|
|
104
|
-
// Validated BEFORE the id lookup below: an unknown action used to fall through to it and
|
|
105
|
-
// report "task #? not found", which reads as a missing task rather than a bad action.
|
|
106
|
-
if (!TODO_ACTIONS.has(action)) {
|
|
107
|
-
return formatError(`unknown todo action '${action}' — expected ${[...TODO_ACTIONS].join(", ")}`);
|
|
108
|
-
}
|
|
109
|
-
const params = toTodoParams(rawParams);
|
|
110
|
-
if (action === "clear") {
|
|
111
|
-
const count = tasks.length;
|
|
112
|
-
tasks = Object.freeze([]);
|
|
113
|
-
nextId = 1;
|
|
114
|
-
return `Cleared ${count} task(s).`;
|
|
115
|
-
}
|
|
116
|
-
if (action === "create") {
|
|
117
|
-
const subject = typeof params.subject === "string" && params.subject.trim() ? params.subject.trim() : undefined;
|
|
118
|
-
if (!subject) return formatError("create requires subject");
|
|
119
|
-
const task = withPatch(Object.freeze({ id: nextId, subject, status: "pending" }), params);
|
|
120
|
-
nextId += 1;
|
|
121
|
-
tasks = Object.freeze([...tasks, task]);
|
|
122
|
-
return `Created ${fmt(task)}`;
|
|
123
|
-
}
|
|
124
|
-
if (action === "list") {
|
|
125
|
-
const filter = params.filterStatus ?? params.status;
|
|
126
|
-
const includeDeleted = params.includeDeleted === true;
|
|
127
|
-
const rows = tasks.filter((task) => (includeDeleted || task.status !== "deleted") && (!filter || task.status === filter)).map(fmt);
|
|
128
|
-
return rows.length ? rows.join("\n") : "No tasks.";
|
|
129
|
-
}
|
|
130
|
-
const id = params.id;
|
|
131
|
-
const task = id !== undefined ? tasks.find((item) => item.id === id) : undefined;
|
|
132
|
-
if (!task) return formatError(`task #${id ?? "?"} not found`);
|
|
133
|
-
if (action === "get") return taskLines(task).join("\n");
|
|
134
|
-
if (action === "delete") {
|
|
135
|
-
const deleted = Object.freeze({ ...task, status: "deleted" as const });
|
|
136
|
-
tasks = Object.freeze(tasks.map((item) => item.id === task.id ? deleted : item));
|
|
137
|
-
return `Deleted ${fmt(deleted)}`;
|
|
138
|
-
}
|
|
139
|
-
if (action === "update") {
|
|
140
|
-
const updated = withPatch(task, params);
|
|
141
|
-
tasks = Object.freeze(tasks.map((item) => item.id === task.id ? updated : item));
|
|
142
|
-
return `Updated ${fmt(updated)}`;
|
|
143
|
-
}
|
|
144
|
-
// Unreachable: every TODO_ACTIONS member is handled above. Kept as the exhaustiveness arm.
|
|
145
|
-
return formatError(`unhandled todo action '${action}'`);
|
|
146
|
-
};
|
|
147
|
-
return async (action, params) => apply(action, params);
|
|
148
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import type { AskAnswer, AskQuestion } from "../sandbox/protocol.ts";
|
|
2
|
-
import type { SubLlmHandlers } from "../sandbox/sandbox.ts";
|
|
3
|
-
import type { RlmEmitter } from "../tool/rlm-events.ts";
|
|
4
|
-
import { formatError } from "../util/errors.ts";
|
|
5
|
-
|
|
6
|
-
export interface InteractiveBridgeOpts {
|
|
7
|
-
readonly onAskUserQuestion?: (questions: readonly AskQuestion[]) => Promise<AskAnswer[]>;
|
|
8
|
-
readonly onTodo?: (action: string, params: Record<string, unknown>) => Promise<string>;
|
|
9
|
-
readonly onTodoRow?: (action: string, params: Record<string, unknown>, result: string) => void | Promise<void>;
|
|
10
|
-
readonly emitter?: RlmEmitter;
|
|
11
|
-
readonly depth: number;
|
|
12
|
-
readonly parentId?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function buildInteractiveHandlers(opts: InteractiveBridgeOpts): {
|
|
16
|
-
askUserQuestion: SubLlmHandlers["askUserQuestion"];
|
|
17
|
-
todo: SubLlmHandlers["todo"];
|
|
18
|
-
} {
|
|
19
|
-
return {
|
|
20
|
-
async askUserQuestion(questions, depth) {
|
|
21
|
-
if (depth > 0) return questions.map((q) => ({
|
|
22
|
-
question: q.question,
|
|
23
|
-
selected: [],
|
|
24
|
-
custom: formatError("ask_user_question not available inside rlm_query sub-calls"),
|
|
25
|
-
}));
|
|
26
|
-
|
|
27
|
-
const id = opts.emitter?.emitSubcallCreated({
|
|
28
|
-
kind: "tool", parentId: opts.parentId,
|
|
29
|
-
label: "ask_user_question",
|
|
30
|
-
args: `${questions.length} question(s)`,
|
|
31
|
-
depth,
|
|
32
|
-
});
|
|
33
|
-
try {
|
|
34
|
-
const cb = opts.onAskUserQuestion;
|
|
35
|
-
if (!cb) throw new Error("ask_user_question not configured (no onAskUserQuestion callback)");
|
|
36
|
-
const answers = await cb(questions);
|
|
37
|
-
if (id) opts.emitter?.emitSubcallUpdated({ id, status: "done" });
|
|
38
|
-
return answers;
|
|
39
|
-
} catch (err) {
|
|
40
|
-
if (id) opts.emitter?.emitSubcallUpdated({ id, status: "error", detail: String(err) });
|
|
41
|
-
throw err;
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
|
|
45
|
-
async todo(action, params, depth) {
|
|
46
|
-
const id = opts.emitter?.emitSubcallCreated({
|
|
47
|
-
kind: "tool", parentId: opts.parentId,
|
|
48
|
-
label: `todo:${action}`,
|
|
49
|
-
args: params.subject ? String(params.subject) : String(params.id ?? ""),
|
|
50
|
-
depth,
|
|
51
|
-
});
|
|
52
|
-
try {
|
|
53
|
-
const cb = opts.onTodo;
|
|
54
|
-
if (!cb) throw new Error("todo not configured (no onTodo callback)");
|
|
55
|
-
const result = await cb(action, params);
|
|
56
|
-
await opts.onTodoRow?.(action, params, result);
|
|
57
|
-
if (id) opts.emitter?.emitSubcallUpdated({ id, status: "done", resultPreview: result.slice(0, 80) });
|
|
58
|
-
return result;
|
|
59
|
-
} catch (err) {
|
|
60
|
-
if (id) opts.emitter?.emitSubcallUpdated({ id, status: "error", detail: String(err) });
|
|
61
|
-
throw err;
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { InteractiveDeps } from "../core/types.ts";
|
|
3
|
-
import type { AskAnswer, AskQuestion } from "../sandbox/protocol.ts";
|
|
4
|
-
import { formatError } from "../util/errors.ts";
|
|
5
|
-
import { createTodoFallback } from "./fallback-todo.ts";
|
|
6
|
-
|
|
7
|
-
async function askViaUi(ctx: ExtensionContext, questions: readonly AskQuestion[]): Promise<AskAnswer[]> {
|
|
8
|
-
if (!ctx.hasUI) throw new Error("ask_user_question requires UI");
|
|
9
|
-
const answers = new Array<AskAnswer>(questions.length);
|
|
10
|
-
for (let i = 0; i < questions.length; i++) {
|
|
11
|
-
const q = questions[i];
|
|
12
|
-
if (!q) {
|
|
13
|
-
answers[i] = { question: "", selected: [], custom: formatError("malformed question") };
|
|
14
|
-
continue;
|
|
15
|
-
}
|
|
16
|
-
if (q.multiSelect) {
|
|
17
|
-
const selected: string[] = [];
|
|
18
|
-
while (true) {
|
|
19
|
-
const pick = await ctx.ui.select(`${q.header}: ${q.question}`, [...q.options.map((o) => o.label), "Done"]);
|
|
20
|
-
if (!pick || pick === "Done") break;
|
|
21
|
-
if (!selected.includes(pick)) selected.push(pick);
|
|
22
|
-
}
|
|
23
|
-
answers[i] = { question: q.question, selected };
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
const pick = await ctx.ui.select(`${q.header}: ${q.question}`, [...q.options.map((o) => o.label), "Type something."]);
|
|
27
|
-
if (!pick) answers[i] = { question: q.question, selected: [], custom: formatError("user cancelled") };
|
|
28
|
-
else if (pick === "Type something.") answers[i] = { question: q.question, selected: [], custom: await ctx.ui.input(q.question) ?? "" };
|
|
29
|
-
else answers[i] = { question: q.question, selected: [pick] };
|
|
30
|
-
}
|
|
31
|
-
return answers;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function createPiInteractiveDeps(ctx: ExtensionContext): InteractiveDeps {
|
|
35
|
-
const fallbackTodo = createTodoFallback();
|
|
36
|
-
return Object.freeze({
|
|
37
|
-
onAskUserQuestion: (questions: readonly AskQuestion[]): Promise<AskAnswer[]> => askViaUi(ctx, questions),
|
|
38
|
-
onTodo: (action: string, params: Record<string, unknown>): Promise<string> =>
|
|
39
|
-
Promise.resolve(fallbackTodo(action, params)),
|
|
40
|
-
});
|
|
41
|
-
}
|
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
|
-
}
|