@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,319 +0,0 @@
1
- /**
2
- * The phase pipeline's stateful half: the `save_artifact` / `advance_phase` sandbox handlers
3
- * and the validate-phase finalize routing.
4
- *
5
- * Pulled out of the engine's run closure so the pipeline's mutable state (current phase, the
6
- * per-phase latest save, serviced ask rounds, accumulated warnings, a pending history reset)
7
- * lives in one owner instead of six `let`s threaded through a 640-line function.
8
- *
9
- * Two invariants this file exists to protect:
10
- * - Gates measure the CURRENT phase's latest save (`lastSaved`) only — never
11
- * `phase.artifacts`, whose paths are the completed channel and go stale across a
12
- * corrective loop-back.
13
- * - `lastSaved` and `askRounds` are session-only. They are never rehydrated from the trail,
14
- * so a resumed run must genuinely re-save and re-interview rather than re-gate stale work.
15
- */
16
-
17
- import type { ChatMsg } from "../bridge/model.ts";
18
- import type { RlmEmitter } from "../tool/rlm-events.ts";
19
- import type { PhaseRecon } from "../state/resume.ts";
20
- import { formatError } from "../util/errors.ts";
21
- import { readArtifact, saveArtifact, type GoalCapture } from "./artifacts.ts";
22
- import { critiqueArtifact, formatCritique } from "./critique.ts";
23
- import {
24
- advancePhase as validatePhaseTransition,
25
- initialPhaseState,
26
- isPhase,
27
- PHASES,
28
- reconcilePhase,
29
- routeAfterValidate,
30
- stageForArtifactKind,
31
- STAGES,
32
- type ArtifactRef,
33
- type Phase,
34
- type PhaseState,
35
- type SavedArtifact,
36
- type StageGateData,
37
- } from "./pipeline.ts";
38
-
39
- /** Builds the fresh-session history for a phase. Supplied by the engine (its own policy). */
40
- export type ResetHistoryForPhase = (
41
- state: PhaseState,
42
- options: { readonly goal?: GoalCapture; readonly validation?: import("./gates.ts").ValidationGateData; readonly notice?: string },
43
- ) => ChatMsg[];
44
-
45
- /** Appends a `phase` row to the run trail. Supplied by the engine (owns persistence state). */
46
- export type PersistPhaseRow = (
47
- state: PhaseState,
48
- artifactPath: string | undefined,
49
- artifactPhase: Phase | undefined,
50
- gateData: StageGateData | undefined,
51
- supersededPath?: string,
52
- ) => Promise<void>;
53
-
54
- export interface PipelineDeps {
55
- /** Repo root that artifact paths are resolved against. */
56
- readonly runCwd: string;
57
- readonly maxBackwardJumps: number;
58
- readonly emitter: RlmEmitter;
59
- /** Turns completed so far — phase rows and `advancedAt` are stamped with it. */
60
- readonly completedTurns: () => number;
61
- readonly resetHistoryForPhase: ResetHistoryForPhase;
62
- readonly persistPhaseRow: PersistPhaseRow;
63
- }
64
-
65
- /** What the engine should do with a finalize submitted while in the `validate` phase. */
66
- export type ValidateOutcome =
67
- /** Not finalizable yet — feed `error` back as the next turn's REPL output. */
68
- | { readonly kind: "reject"; readonly error: string }
69
- /** Blockers found — re-enter `blueprint` with this fresh history. */
70
- | { readonly kind: "loop-back"; readonly history: ChatMsg[] }
71
- /** Backward-jump cap reached — terminate with this report. */
72
- | { readonly kind: "halt"; readonly report: string }
73
- /** Validation passed — take the model's final answer. */
74
- | { readonly kind: "accept" };
75
-
76
- /** The sandbox handlers the pipeline contributes. Shape-compatible with `SubLlmHandlers`. */
77
- export interface PipelineHandlers {
78
- saveArtifact(kind: string, content: string): Promise<string>;
79
- advancePhase(phase: string, summary: string | undefined): Promise<string>;
80
- }
81
-
82
- export class PipelineController {
83
- /** Current phase state; `undefined` until seeded. Read by the engine for gate prompts/rows. */
84
- phase: PhaseState | undefined;
85
-
86
- /**
87
- * Latest save per phase: path plus an optional gate memo, as ONE record so the two cannot
88
- * desync. Cleared on phase exit and on loop-back.
89
- */
90
- private lastSaved: Partial<Record<Phase, SavedArtifact>> = {};
91
-
92
- /** Serviced ask_user_question rounds in the current phase (session-only). */
93
- private askRounds = 0;
94
-
95
- /** Advisory critique warnings accumulated across saves (surfaced in the TUI). */
96
- private warnings: readonly string[] = [];
97
-
98
- /** History replacement scheduled by advance_phase; the engine drains it at a turn boundary. */
99
- private pendingReset: ChatMsg[] | undefined;
100
-
101
- private goal: GoalCapture | undefined;
102
-
103
- constructor(private readonly deps: PipelineDeps) {}
104
-
105
- /** Called after each successfully serviced root-depth ask_user_question round. */
106
- noteAskRound(): void {
107
- this.askRounds++;
108
- }
109
-
110
- /** Take and clear the scheduled fresh-session history, if advance_phase left one. */
111
- takePendingReset(): ChatMsg[] | undefined {
112
- const reset = this.pendingReset;
113
- this.pendingReset = undefined;
114
- return reset;
115
- }
116
-
117
- /** Seed a fresh run: capture goal, enter `startPhase`, and build the opening history. */
118
- seedFresh(startPhase: Phase, goal: GoalCapture | undefined, notice: string | undefined): ChatMsg[] {
119
- this.goal = goal;
120
- this.phase = initialPhaseState(0, startPhase);
121
- return this.deps.resetHistoryForPhase(this.phase, { goal, notice });
122
- }
123
-
124
- /**
125
- * Rehydrate phase state from a trail. `lastSaved`/`askRounds` stay empty by design: a
126
- * resume must re-save and re-interview rather than re-gate work from a previous process.
127
- */
128
- seedFromResume(recon: PhaseRecon): void {
129
- const artifacts: Partial<Record<Phase, ArtifactRef>> = {};
130
- for (const [key, value] of Object.entries(recon.artifacts ?? {})) {
131
- if (value === undefined || !isPhase(key)) continue;
132
- artifacts[key] = Object.freeze({
133
- path: value.path,
134
- status: value.superseded ? ("superseded" as const) : ("active" as const),
135
- });
136
- }
137
- this.phase = {
138
- current: reconcilePhase(recon.current),
139
- advancedAt: recon.advancedAt,
140
- summary: recon.summary,
141
- artifacts,
142
- backwardJumps: recon.backwardJumps ?? 0,
143
- };
144
- this.lastSaved = {};
145
- this.askRounds = 0;
146
- }
147
-
148
- handlers(): PipelineHandlers {
149
- return {
150
- saveArtifact: (kind, content) => this.handleSaveArtifact(kind, content),
151
- advancePhase: (phase, summary) => this.handleAdvancePhase(phase, summary),
152
- };
153
- }
154
-
155
- // ── save_artifact ──
156
-
157
- private async handleSaveArtifact(kind: string, content: string): Promise<string> {
158
- const stage = stageForArtifactKind(kind);
159
- if (stage === undefined) {
160
- return formatError(`unknown artifact kind '${kind}' (valid: clarification, research, plan, validation)`);
161
- }
162
- const current = this.currentPhase();
163
- if (stage.phase !== current) {
164
- return formatError(`artifact kind '${kind}' belongs to phase '${stage.phase}', but the pipeline is in '${current}'`);
165
- }
166
- const saved = saveArtifact(this.deps.runCwd, stage.artifactDir, kind, content);
167
- if (!saved.ok) return formatError(saved.error);
168
-
169
- // Preflight: run the SAME gate advance_phase will run, now instead of a turn later, and
170
- // memoize the verdict so the transition does not re-read and re-gate the file.
171
- const critique = critiqueArtifact(stage, content, saved.path, this.deps.runCwd);
172
- this.lastSaved = {
173
- ...this.lastSaved,
174
- [stage.phase]: Object.freeze({ path: saved.path, gateData: critique.gateData }),
175
- };
176
- if (critique.warnings.length > 0) {
177
- this.warnings = Object.freeze([...this.warnings, ...critique.warnings]);
178
- this.deps.emitter.emitWarnings(this.warnings);
179
- }
180
- return `ok — saved ${saved.path}.\n${formatCritique(critique)}`;
181
- }
182
-
183
- // ── advance_phase ──
184
-
185
- private async handleAdvancePhase(phase: string, summary: string | undefined): Promise<string> {
186
- const current = this.currentPhase();
187
- const outcome = validatePhaseTransition(current, phase);
188
- if (!outcome.ok) return formatError(outcome.error);
189
-
190
- // Clarify interview gate: the engine counts serviced rounds itself, so the model cannot
191
- // advance by merely claiming to have interviewed the user.
192
- if (current === "clarify" && this.askRounds === 0) {
193
- return formatError("clarify requires at least one ask_user_question round — interview the user before advancing");
194
- }
195
-
196
- const gated = this.gateCurrentPhase(current);
197
- if (!gated.ok) return formatError(gated.error);
198
- const { artifactPath, gateData } = gated;
199
-
200
- const nextArtifacts: Partial<Record<Phase, ArtifactRef>> = { ...(this.phase?.artifacts ?? {}) };
201
- if (artifactPath !== undefined) {
202
- nextArtifacts[current] = Object.freeze({ path: artifactPath, status: "active" });
203
- }
204
- this.phase = {
205
- current: outcome.phase,
206
- advancedAt: this.deps.completedTurns(),
207
- summary,
208
- artifacts: nextArtifacts,
209
- backwardJumps: this.phase?.backwardJumps ?? 0,
210
- };
211
- await this.deps.persistPhaseRow(this.phase, artifactPath, artifactPath !== undefined ? current : undefined, gateData);
212
- this.clearLastSaved(current);
213
- this.askRounds = 0;
214
- this.pendingReset = this.deps.resetHistoryForPhase(this.phase, { goal: this.goal });
215
- return `ok — phase advanced to '${outcome.phase}' (was '${current}'${summary ? `, summary: ${summary.slice(0, 80)}` : ""})`;
216
- }
217
-
218
- /**
219
- * Measure the current phase's latest save. Deliberately reads `lastSaved` only — falling
220
- * back to `phase.artifacts` would let a stale path from before a loop-back pass the gate.
221
- */
222
- private gateCurrentPhase(
223
- current: Phase,
224
- ): { ok: true; artifactPath: string | undefined; gateData: StageGateData | undefined } | { ok: false; error: string } {
225
- const stage = STAGES[current];
226
- if (stage.artifactDir === "") return { ok: true, artifactPath: undefined, gateData: undefined };
227
-
228
- const savedEntry = this.lastSaved[current];
229
- const artifactPath = savedEntry?.path;
230
- if (artifactPath === undefined) {
231
- return {
232
- ok: false,
233
- error: `phase '${current}' has no saved artifact — call save_artifact("${stage.artifactKind}", content) first`,
234
- };
235
- }
236
- if (savedEntry?.gateData !== undefined) {
237
- return { ok: true, artifactPath, gateData: savedEntry.gateData };
238
- }
239
- // No memo (e.g. the file was written outside save_artifact) — read and gate it now.
240
- const content = readArtifact(this.deps.runCwd, artifactPath);
241
- if (!content.ok) return { ok: false, error: content.error };
242
- const gate = stage.gate(content.value, artifactPath, this.deps.runCwd);
243
- if (!gate.ok) return { ok: false, error: gate.error };
244
- this.lastSaved = {
245
- ...this.lastSaved,
246
- [current]: Object.freeze({ path: artifactPath, gateData: gate.value }),
247
- };
248
- return { ok: true, artifactPath, gateData: gate.value };
249
- }
250
-
251
- // ── validate-phase finalize ──
252
-
253
- /**
254
- * Decide what a finalize submitted during `validate` means. As with advance_phase, this
255
- * measures THIS turn's validation save only, never `phase.artifacts`.
256
- */
257
- async finalizeInValidate(final: string): Promise<ValidateOutcome> {
258
- const phase = this.phase;
259
- if (phase === undefined) return { kind: "accept" };
260
-
261
- const vPath = this.lastSaved.validate?.path;
262
- if (vPath === undefined) {
263
- return {
264
- kind: "reject",
265
- error: formatError(
266
- 'finalize rejected — save the validation artifact first via save_artifact("validation", content) with status: ready, blockers_count, and verdict',
267
- ),
268
- };
269
- }
270
- const content = readArtifact(this.deps.runCwd, vPath);
271
- if (!content.ok) return { kind: "reject", error: formatError(content.error) };
272
-
273
- const gate = STAGES.validate.gate(content.value, vPath, this.deps.runCwd);
274
- if (!gate.ok) return { kind: "reject", error: formatError(gate.error) };
275
- if (gate.value.kind !== "validation") {
276
- return { kind: "reject", error: formatError("internal: validate gate did not return validation data") };
277
- }
278
-
279
- const { validation } = gate.value;
280
- const route = routeAfterValidate(validation, phase.backwardJumps, this.deps.maxBackwardJumps);
281
- if (route.kind === "halt") return { kind: "halt", report: `${route.reason}\n\n${final}` };
282
- if (route.kind !== "loop-back") return { kind: "accept" };
283
-
284
- // Loop back to blueprint. Prior artifacts are kept — the append-only journal marks the
285
- // blueprint superseded by this validation rather than dropping it.
286
- const prior = phase.artifacts.blueprint;
287
- const nextArtifacts: Partial<Record<Phase, ArtifactRef>> = {
288
- ...phase.artifacts,
289
- validate: Object.freeze({ path: vPath, status: "active" }),
290
- };
291
- if (prior !== undefined) {
292
- nextArtifacts.blueprint = Object.freeze({ path: prior.path, status: "superseded", supersededBy: vPath });
293
- }
294
- this.phase = {
295
- current: "blueprint",
296
- advancedAt: this.deps.completedTurns(),
297
- summary: `loop-back: ${validation.blockersCount} blocker(s)`,
298
- artifacts: nextArtifacts,
299
- backwardJumps: phase.backwardJumps + 1,
300
- };
301
- await this.deps.persistPhaseRow(this.phase, vPath, "validate", gate.value, prior?.path);
302
- // Clear BOTH so the re-entered blueprint must produce a genuinely fresh plan and a fresh
303
- // validation of it, rather than re-gating what was just rejected.
304
- this.clearLastSaved("blueprint", "validate");
305
- this.askRounds = 0;
306
- this.pendingReset = undefined;
307
- return { kind: "loop-back", history: this.deps.resetHistoryForPhase(this.phase, { goal: this.goal, validation }) };
308
- }
309
-
310
- private currentPhase(): Phase {
311
- return this.phase?.current ?? PHASES[0];
312
- }
313
-
314
- private clearLastSaved(...phases: readonly Phase[]): void {
315
- const next: Partial<Record<Phase, SavedArtifact>> = { ...this.lastSaved };
316
- for (const phase of phases) delete next[phase];
317
- this.lastSaved = next;
318
- }
319
- }
@@ -1,268 +0,0 @@
1
- /**
2
- * RLM pipeline stage graph — data-driven transitions with deterministic gates.
3
- *
4
- * Stages: clarify → research → blueprint → validate
5
- * (clarify skipped when askUserQuestion is off).
6
- * The root RLM writes artifacts via save_artifact(); advance_phase() is gated by
7
- * TypeScript floors (never LLM judgment). Validate routes on measured
8
- * blockers_count with a bounded corrective loop back to blueprint.
9
- * The pipeline is read-only by design: it produces a validated plan and does not
10
- * write source. Accidental sandbox writes are blocked (open/pathlib/os.open);
11
- * this is steering, not a hard security boundary.
12
- */
13
- import {
14
- checkStatusReady,
15
- clarificationRecord,
16
- type ClarificationGateData,
17
- type GateResult,
18
- planPhaseRecords,
19
- type PlanGateData,
20
- type ValidationGateData,
21
- validationRecord,
22
- verifyCitations,
23
- } from "./gates.ts";
24
-
25
- export type Phase = "clarify" | "research" | "blueprint" | "validate";
26
-
27
- export const PHASES = Object.freeze([
28
- "clarify",
29
- "research",
30
- "blueprint",
31
- "validate",
32
- ] as const satisfies readonly Phase[]);
33
-
34
- const PHASE_SET: ReadonlySet<string> = Object.freeze(new Set<string>(PHASES));
35
-
36
- export function isPhase(value: unknown): value is Phase {
37
- return typeof value === "string" && PHASE_SET.has(value);
38
- }
39
-
40
- /**
41
- * Map a persisted phase name onto the current graph. Trails written before the
42
- * implement phase was retired resume at `blueprint` — the last phase whose
43
- * artifact is still meaningful.
44
- */
45
- export function reconcilePhase(persisted: unknown): Phase {
46
- if (isPhase(persisted)) return persisted;
47
- return "blueprint";
48
- }
49
-
50
- /** Kind string the model passes to save_artifact(kind, content). */
51
- export type ArtifactKind = "clarification" | "research" | "plan" | "validation";
52
-
53
- /** Structured data a stage gate extracts from its artifact (what edges route on). */
54
- export type StageGateData =
55
- | { readonly kind: "clarification"; readonly clarification: ClarificationGateData }
56
- | { readonly kind: "research" }
57
- | { readonly kind: "plan"; readonly plan: PlanGateData }
58
- | { readonly kind: "validation"; readonly validation: ValidationGateData };
59
-
60
- export interface StageDef {
61
- readonly phase: Phase;
62
- /** Subdir under .rlm/artifacts/ ("" = side-effect stage, no artifact). */
63
- readonly artifactDir: string;
64
- /** save_artifact kind, or "" when the stage produces no artifact. */
65
- readonly artifactKind: ArtifactKind | "";
66
- /** Deterministic floor run on the artifact BEFORE leaving this stage. */
67
- readonly gate: (content: string, path: string, cwd: string) => GateResult<StageGateData>;
68
- }
69
-
70
- const clarifyGate: StageDef["gate"] = (content, path) => {
71
- const status = checkStatusReady(content, path);
72
- if (!status.ok) return status;
73
- const rec = clarificationRecord(content, path);
74
- if (!rec.ok) return rec;
75
- return { ok: true, value: { kind: "clarification", clarification: rec.value } };
76
- };
77
-
78
- /** Compose floors: status: ready → citations → stage-specific contract. */
79
- const researchGate: StageDef["gate"] = (content, path, cwd) => {
80
- const status = checkStatusReady(content, path);
81
- if (!status.ok) return status;
82
- const cites = verifyCitations(content, cwd);
83
- if (!cites.ok) return cites;
84
- return { ok: true, value: { kind: "research" } };
85
- };
86
-
87
- const blueprintGate: StageDef["gate"] = (content, path, cwd) => {
88
- const status = checkStatusReady(content, path);
89
- if (!status.ok) return status;
90
- const cites = verifyCitations(content, cwd);
91
- if (!cites.ok) return cites;
92
- const plan = planPhaseRecords(content, path);
93
- if (!plan.ok) return plan;
94
- return { ok: true, value: { kind: "plan", plan: plan.value } };
95
- };
96
-
97
- const validateGate: StageDef["gate"] = (content, path) => {
98
- const status = checkStatusReady(content, path);
99
- if (!status.ok) return status;
100
- const rec = validationRecord(content, path);
101
- if (!rec.ok) return rec;
102
- return { ok: true, value: { kind: "validation", validation: rec.value } };
103
- };
104
-
105
- /** Single source of truth for gates, artifact dirs/kinds, and routing. */
106
- export const STAGES: Readonly<Record<Phase, StageDef>> = Object.freeze({
107
- clarify: { phase: "clarify", artifactDir: "clarifications", artifactKind: "clarification", gate: clarifyGate },
108
- research: { phase: "research", artifactDir: "research", artifactKind: "research", gate: researchGate },
109
- blueprint: { phase: "blueprint", artifactDir: "plans", artifactKind: "plan", gate: blueprintGate },
110
- validate: { phase: "validate", artifactDir: "validations", artifactKind: "validation", gate: validateGate },
111
- });
112
-
113
- /** Lookup stage by save_artifact kind — single map derived from STAGES (DRY). */
114
- const STAGE_BY_KIND: Readonly<Partial<Record<ArtifactKind, StageDef>>> = Object.freeze(
115
- (Object.values(STAGES) as readonly StageDef[]).reduce<Partial<Record<ArtifactKind, StageDef>>>((acc, stage) => {
116
- if (stage.artifactKind !== "") acc[stage.artifactKind] = stage;
117
- return acc;
118
- }, {}),
119
- );
120
-
121
- export function stageForArtifactKind(kind: string): StageDef | undefined {
122
- if (kind === "clarification" || kind === "research" || kind === "plan" || kind === "validation") {
123
- return STAGE_BY_KIND[kind];
124
- }
125
- return undefined;
126
- }
127
-
128
- /** An artifact path plus its lifecycle status — append-only, never deleted. */
129
- export interface ArtifactRef {
130
- readonly path: string;
131
- readonly status: "active" | "superseded";
132
- /** Path of the artifact that superseded this one (the rejecting validation). */
133
- readonly supersededBy?: string;
134
- }
135
-
136
- /** The latest artifact saved for a stage, plus its gate result when it passed. */
137
- export interface SavedArtifact {
138
- readonly path: string;
139
- /** Gate payload when the artifact passed; undefined ⇒ advance_phase must re-gate. */
140
- readonly gateData?: StageGateData;
141
- }
142
-
143
- export interface PhaseState {
144
- readonly current: Phase;
145
- readonly advancedAt: number;
146
- readonly summary?: string;
147
- /** Artifact each completed stage produced — the named channels. */
148
- readonly artifacts: Readonly<Partial<Record<Phase, ArtifactRef>>>;
149
- /** Corrective validate→blueprint re-entries taken so far. */
150
- readonly backwardJumps: number;
151
- }
152
-
153
- /**
154
- * @param start — first phase of the run (`clarify` when interviews are on, else `research`).
155
- */
156
- export function initialPhaseState(advancedAt = 0, start: Phase = "clarify"): PhaseState {
157
- return { current: start, advancedAt, artifacts: Object.freeze({}), backwardJumps: 0 };
158
- }
159
-
160
- export type RouteDecision =
161
- | { readonly kind: "done" }
162
- | { readonly kind: "loop-back"; readonly next: "blueprint" }
163
- | { readonly kind: "halt"; readonly reason: string };
164
-
165
- /**
166
- * Route out of `validate` on MEASURED gate data: blockers_count === 0 → done;
167
- * blockers_count > 0 → loop back to blueprint, bounded by maxBackwardJumps.
168
- */
169
- export function routeAfterValidate(
170
- data: ValidationGateData,
171
- backwardJumps: number,
172
- maxBackwardJumps: number,
173
- ): RouteDecision {
174
- if (data.blockersCount === 0) return { kind: "done" };
175
- if (backwardJumps >= maxBackwardJumps) {
176
- return {
177
- kind: "halt",
178
- reason: `validation reports ${data.blockersCount} blocker(s) after ${backwardJumps} corrective pass(es) — backward-jump limit (${maxBackwardJumps}) reached; surfacing the validation report as the final answer`,
179
- };
180
- }
181
- return { kind: "loop-back", next: "blueprint" };
182
- }
183
-
184
- /** Forward transitions only; corrective loop-back is ENGINE-initiated via routeAfterValidate. */
185
- export function nextForward(current: Phase): Phase | undefined {
186
- const idx = PHASES.indexOf(current);
187
- return idx >= 0 && idx < PHASES.length - 1 ? PHASES[idx + 1] : undefined;
188
- }
189
-
190
- export interface AdvancePhaseResult {
191
- readonly ok: true;
192
- readonly phase: Phase;
193
- }
194
-
195
- export interface AdvancePhaseFailure {
196
- readonly ok: false;
197
- readonly error: string;
198
- readonly phase: Phase;
199
- }
200
-
201
- export type AdvancePhaseOutcome = AdvancePhaseResult | AdvancePhaseFailure;
202
-
203
- /**
204
- * Pure order check: only the immediate next phase is allowed.
205
- * Artifact gates are applied by the engine after this returns ok.
206
- */
207
- export function advancePhase(
208
- current: Phase | undefined,
209
- target: string,
210
- ): AdvancePhaseOutcome {
211
- if (!isPhase(target)) {
212
- return {
213
- ok: false,
214
- error: `unknown phase '${target}'; valid phases: ${PHASES.join(", ")}`,
215
- phase: current ?? PHASES[0],
216
- };
217
- }
218
- const from = current ?? PHASES[0];
219
- const expected = nextForward(from);
220
- if (expected === undefined) {
221
- return {
222
- ok: false,
223
- error: `'${from}' is the terminal phase; finalize via answer["ready"] = True after saving the validation artifact`,
224
- phase: from,
225
- };
226
- }
227
- if (target !== expected) {
228
- return {
229
- ok: false,
230
- error: `cannot advance from '${from}' to '${target}' — the next phase is '${expected}'`,
231
- phase: from,
232
- };
233
- }
234
- return { ok: true, phase: target };
235
- }
236
-
237
- /** Return the current phase (defaults to first phase if undefined). */
238
- export function currentPhase(state: PhaseState | undefined): Phase {
239
- return state?.current ?? PHASES[0];
240
- }
241
-
242
- /** Return the number of turns spent in the current phase. */
243
- export function turnsInPhase(state: PhaseState | undefined, completedTurns: number): number {
244
- return state ? completedTurns - state.advancedAt : completedTurns;
245
- }
246
-
247
- /** PHASE_GATE_TURNS: if the model stays in one phase for this many turns, the engine re-prompts. */
248
- export const PHASE_GATE_TURNS = 4;
249
-
250
- /** Produce a re-prompt message when the model stalls in a phase for too long. */
251
- export function phaseGatePrompt(
252
- state: PhaseState | undefined,
253
- completedTurns: number,
254
- ): string | undefined {
255
- const turns = turnsInPhase(state, completedTurns);
256
- const phase = currentPhase(state);
257
- if (turns >= PHASE_GATE_TURNS && turns % PHASE_GATE_TURNS === 0) {
258
- const next = nextForward(phase);
259
- const hint = next
260
- ? ` Consider calling advance_phase("${next}") if your ${phase} work is complete (after save_artifact when required).`
261
- : "";
262
- return [
263
- `You have spent ${turns} turns in the '${phase}' phase.`,
264
- `If the ${phase} phase is complete, advance to the next phase.${hint}`,
265
- ].join(" ");
266
- }
267
- return undefined;
268
- }