import type { Runner } from "../core/runner/runtask.js"; import type { TaskSpec } from "../core/types.js"; import type { ExecutionEnv } from "../internal/harness.js"; import type { VerificationResult } from "./verify.js"; import type { Checkpoint } from "../core/checkpoint-store.js"; import { type OracleIsolationVerdict } from "../core/oracle-isolation.js"; /** * SAFE-tier oracle self-repair loop (design/76 D1 命门, design/78 Slice-1). Given an implementation spec * and an **injected, trusted oracle**, this runs the worker, grades it with the oracle, and — on failure — * loops a fix turn (and a bounded clean-restart) until the oracle passes or the loop gives up. The terminal * is then projected through the {@link terminalForTier} table, which **caps every PASS at `candidate_only`**: * this layer NEVER auto-accepts. Auto-accept (`fixed`) requires the OFF Gate-1 out-of-process * `oracleIsolation` boundary that this slice deliberately does not build — SAFE-tier escalates to a human * (`needs_human_oracle`) or surfaces a candidate, but never clears its own work. * * Like {@link verifyCompleted}, this is a **thin composition** over `runner.runTask` (verify.ts:18-21 posture) * — it adds no Runner-core surface, touches no vendored code, and is called by a leader/profile (the sibling * of {@link runWithVerification}). The oracle internals (which gate/judge/property-harness to compose) live * in the PROFILE-injected {@link RepairOracle} closure; core only fixes the {@link OracleResult} shape and the * read-only / identity contract (§1 裁决①). N-parallel candidate fan-out + a robustness-first selection ranker * are PROFILE concerns (§3 裁决②) — core stays single-candidate. */ /** * The provenance/strength tier of the oracle that produced a verdict. The control plane assigns this — it is * NEVER derived from agent-visible state (not a {@link TaskSpec} field), mirroring design/44 §7 Q4. Drives the * {@link terminalForTier} projection. * - `trusted_hidden` — a hidden, spec-derived held-out oracle (strongest; still candidate_only here). * - `trusted_visible` — a visible trusted oracle (must be paired with an anchor + no property-harness regression + L3). * - `property_harness_weak` — derived property invariants only (a weak signal). * - `l3_judge_advisory` — an L3 read-only judge that advises but does not clear. * - `none` — no oracle available → only a human can adjudicate. */ export type OracleTier = "trusted_hidden" | "trusted_visible" | "property_harness_weak" | "l3_judge_advisory" | "none"; /** * The terminal state the repair loop resolves to. SAFE-tier (this slice) can reach every value EXCEPT `fixed`: * `fixed` is reserved for auto-accept, which needs the OFF Gate-1 out-of-process oracleIsolation boundary and * is therefore unreachable here (terminal-by-tier §2 caps PASS at `candidate_only`). * - `fixed` — auto-accepted (NOT reachable SAFE-tier; reserved for a future Gate-1 slice). * - `candidate_only` — the oracle passed; surface the candidate for human acceptance, do not auto-accept. * - `needs_human_oracle` — no usable oracle (`tier: "none"`) → only a human can adjudicate. * - `gave_up` — attempts exhausted without a passing oracle (a first-class abstain, never "satisfy the test"). * - `conflict` — the spec and the oracle disagree (escalate; never rewrite the work to satisfy the test). * Slice-1 core does NOT synthesize this (the test-tampering diff-monitor is §6-deferred to the profile — * core honestly `gave_up`s rather than under-detect a conflict); it is in the vocabulary for the profile. * - `oracle.unprotected` — fail-closed: the grader env was not isolated from the worker env (identity check). * * `needs_human_oracle` is disjoint from `needs_review` (a future dry-run gate) and `irreversible_ask` (shipped). */ export type RepairTerminal = "fixed" | "candidate_only" | "needs_human_oracle" | "gave_up" | "conflict" | "oracle.unprotected"; /** * One oracle verdict. Produced by the injected {@link RepairOracle} closure. `flaky` records that the oracle * was non-deterministic across `retries` (a flaky verdict is NEVER projected to `fixed` — §5 / §6 flakyK). * `trace` is the raw failure output fed back into the next fix turn; it is treated as untrusted worker-adjacent * data (delimited into context, sanitized into the objective) but its INNER text is NEVER rewritten (§5.5 #8a) * — rewriting it would destroy the real-trace feedback lever. */ export interface OracleResult { tier: OracleTier; passed: boolean; /** Raw failure trace (on a failed verdict). Untrusted; fed back verbatim into the fix turn (never inner-rewritten). */ trace?: string; /** The oracle's verdict was non-deterministic across `retries` re-isolations. A flaky verdict never → `fixed`. */ flaky: boolean; /** How many re-isolation retries the oracle ran to settle the verdict. */ retries: number; } /** * The PROFILE-injected oracle (§1 裁决①). The profile composes the actual grading inside this closure — * `runExecGate(graderEnv, steps)` (exec-gate.ts:185, env is the FIRST param = L2 provenance bound to the * grader) → `verifyCompleted` (verify.ts:212, the read-only L3 judge) → `checkInvariants` * (property-harness.ts:217, explore-only) — and maps the composite to an {@link OracleResult}. Core never * composes these internals (so the oracle type can't grow a long discriminated union); it only fixes the * `OracleResult` shape and the read-only/identity contract. `graderEnv` is the isolated grader env; `evidence` * is the diff/results to judge (recompute it from the post-resume working tree on a resume — verify.ts:328 BUG5). */ export type RepairOracle = (graderEnv: ExecutionEnv, evidence: string | undefined) => Promise; /** * The durable, JSON-safe repair state carried across an orthogonal durable suspend (§4). Serialized ONLY when * an unrelated resource/HITL suspend interleaves the loop (happy-path is in-memory) and re-seeded on resume so * `attemptCount` advances MONOTONICALLY (never reset). Contains NO functions/Dates (epoch ms if a timestamp is * ever needed) so `structuredClone`/`JSON.stringify` round-trip it on the checkpoint. `baselinePassTests` is * DELIBERATELY ABSENT (§4 MAJOR-C): the anchor is grader-computed out-of-process so a worker can't shrink it. */ export interface RepairBundle { /** The latest failure trace (untrusted; fed back into the fix turn). */ failureTrace: string; /** Reflexion-style diagnoses accumulated across attempts (≤3, Reflexion cap — §1). */ diagnostics: string[]; /** Hypotheses tried and rejected (so a re-seed doesn't re-explore them). */ rejectedHypotheses: string[]; /** The in-loop attempt counter — re-seeded MONOTONICALLY on resume, never reset/max/downscaled (§4 / §5). */ attemptCount: number; /** The tier of the last oracle verdict (for the resumed loop's projection). */ oracleTier: OracleTier; } export interface RepairLoopConfig { /** The PROFILE-injected, trusted oracle (§1 裁决①). Core calls it; it never composes the oracle internals. */ oracle: RepairOracle; /** * The ISOLATED grader env the oracle grades in. MUST be a distinct object from {@link workerEnv}: if they * are the SAME reference the loop fails closed to `oracle.unprotected` BEFORE running anything (§5.1 — * necessary-not-sufficient; the real out-of-process boundary is the OFF Gate-1). */ graderEnv: ExecutionEnv; /** * The worker's execution env (the env `runner.runTask` runs the impl in), passed by the trusted caller so * the §5.1 identity check can run: `graderEnv === workerEnv` → fail-closed `oracle.unprotected`. `TaskSpec` * deliberately has NO `executionEnv` (the worker env is wired on the Runner deps / `executionEnvFactory`, not * reachable from `implSpec`), so the caller must supply the reference here for the check to be meaningful. * Omit it only when the worker env genuinely can't collide with the grader (e.g. distinct factories) — the * check is then skipped (no reference to compare) and isolation is the caller's deployment contract. */ workerEnv?: ExecutionEnv; /** * design/77 §1 Gate-1 (oracleIsolation): the paths the oracle/spec lives behind that the worker must NOT be * able to corrupt (e.g. a hidden held-out test dir). When supplied **together with** {@link workerEnv}, the * loop runs the FULL structural {@link assertOracleIsolation} (identity + structural-class + bash write-probe) * instead of the bare reference-identity check, and surfaces {@link RepairResult.isolationClass}. Omit it (the * default) to keep the existing necessary-not-sufficient reference-identity check (the merged Slice-1 posture). * * The real isolated grader env is SERVICE-provided ({@link import("../core/types.js").RunnerDeps.graderEnvFactory}); * `graderEnv` here is its `.env`, and the control-plane `provenance` brand is reattached internally for the * assertion. The assertion only matters for the (mandate-OFF) auto-accept path: `isolationClass` caps a * non-`out_of_process` grader at `candidate_only`. SAFE-tier never auto-accepts regardless. */ immutableOraclePaths?: string[]; /** * In-loop attempt ceiling (the loop runs at most this many GENERATE/oracle attempts). Profile validates this * to 2-3; core only enforces it as a ceiling, it does not fence the value. This counter is DISTINCT from * `suspendCount`(maxSuspends) and `sliceCount`(maxSlices) — it never borrows those budgets (§5 / §7). */ maxAttempts: number; /** * On a resume after an orthogonal durable suspend, the restored {@link RepairBundle} (from * `PrepareResume.seed.repairBundle`). `attemptCount` is re-seeded from it MONOTONICALLY (§4) — never reset. */ resumeBundle?: RepairBundle; /** Stop the loop once cumulative cost (generate + fix + oracle nested) reaches this (verify.ts:278 backstop). */ costCeilingMicroUsd?: number; /** Overall wall-clock ceiling for the whole loop (verify.ts:277 backstop). */ totalTimeoutMs?: number; /** Per-attempt callback (observability). */ onAttempt?: (info: { attempt: number; terminal?: RepairTerminal; oracle: OracleResult; }) => void; /** * Operational-warning sink (observability). MINOR-2: the §5.1 isolation identity check is SKIPPED when * `workerEnv` is undefined (no reference to compare). An ACCIDENTAL omission — `graderEnv` supplied but * `workerEnv` forgotten — would then disable the check silently. When wired, that case is surfaced here * once at loop start so the gap is observable; the posture stays necessary-not-sufficient (no hard-fail). */ onWarn?: (warning: Error) => void; } export interface RepairResult extends VerificationResult { /** The projected terminal (§2). SAFE-tier never returns `fixed`. */ terminal: RepairTerminal; /** The final repair state (in-memory unless an orthogonal suspend serialized it — §4). */ bundle: RepairBundle; /** * design/77 §1 Gate-1: the structural isolation class of the grader env, when the full * {@link assertOracleIsolation} ran (i.e. {@link RepairLoopConfig.immutableOraclePaths} + `workerEnv` were * supplied). `"out_of_process"` is the ONLY class the (mandate-OFF) auto-accept path may consider; * `"in_process_probe_only"` CAPS the run at `candidate_only`. Undefined when only the bare reference-identity * check ran. SAFE-tier never auto-accepts regardless of this value. */ isolationClass?: OracleIsolationVerdict["isolationClass"]; } /** * design/77 §1 Gate-1 invariant — auto-accept (`fixed`) is UNREACHABLE here. The terminal-by-tier projection * (§2) already caps every PASS at `candidate_only`, so `isolationClass` only ever matters for the (OFF) * auto-accept path. This guard makes the invariant load-bearing rather than caller discipline: a non-isolated * (`in_process_probe_only`) or unprotected verdict can NEVER carry a `fixed` terminal — at most * `candidate_only` / `oracle.unprotected`. It is a pure assertion over the projected terminal; it never * upgrades anything (auto-accept stays OFF — only a future Gate-1 slice running on an `out_of_process` grader * may even consider it). */ export declare function isolationPermitsAutoAccept(verdict: OracleIsolationVerdict): boolean; /** * CONSUMER seam (design/78 Slice-1, MAJOR-3 wiring — the re-entry side of the round-trip). Read the durable * {@link RepairBundle} a resumed checkpoint carries, so a leader/profile resuming a repair-interleaved * suspend can re-seed a FRESH {@link runRepairLoop} call with it. * * ## The contract (core vs profile) * - **CORE owns the persistence + restore.** When an orthogonal durable suspend (resource/HITL) interleaves a * `runRepairLoop` attempt, the Runner serializes the live bundle onto the minted checkpoint * (`prepareTask.serializeCheckpointState` sources `internals.repairBundle`), and on resume re-seeds it back * into `prepareTask` from `cp.state.repairBundle`. So `cp.state.repairBundle` is round-trip-true after a * `runner.resume(token, …)`: it survives the suspend with `attemptCount` intact (never reset — §4 / MAJOR-A). * - **PROFILE owns the re-entry loop.** The leader that resumes the worker (`runner.resume`) is a PROFILE * concern — core does NOT build the "resume → grade → re-seed `runRepairLoop`" loop. The profile reads the * restored bundle (via this accessor) off the SAME checkpoint it resumed, and on its NEXT repair attempt * passes it as {@link RepairLoopConfig.resumeBundle}. `runRepairLoop` then re-seeds `attemptCount` * MONOTONICALLY from it (`config.resumeBundle` → the loop's `bundle`), so the repair ratchet is preserved * across the durable suspend rather than starting the attempt budget over. * * Returns `undefined` when the checkpoint carries no repair state (a non-repair task, or a repair attempt that * suspended on its very first GENERATE before any oracle verdict — the re-seed treats `undefined` as a clean * start). The returned object is the checkpoint's own (already an independent `structuredClone` from the * store); pass it straight into `resumeBundle` — `runRepairLoop` copies its arrays defensively on re-seed. */ export declare function repairBundleFromCheckpoint(cp: Checkpoint): RepairBundle | undefined; /** * The terminal-by-tier projection (§2) — a PURE function, the core invariant. **SAFE-tier caps every PASS at * `candidate_only`**; `fixed` (auto-accept) is unreachable here because it needs the OFF Gate-1 out-of-process * boundary. Enforced HERE (in the projection table), not as caller discipline, so no consumer can promote a * PASS to `fixed`. * * - any tier, `passed: true` → `candidate_only` (trusted_hidden/trusted_visible/property_harness_weak alike). * - `l3_judge_advisory` → advisory only; even a "pass" can't clear → `candidate_only` (it never self-clears). * - `none` → `needs_human_oracle` (no usable oracle → only a human adjudicates). * - not-passed with a usable tier → not a terminal yet (the loop continues / gives up); represented as * `undefined` so the caller's state machine drives the `gave_up`/`conflict` path explicitly. */ export declare function terminalForTier(oracle: OracleResult): RepairTerminal | undefined; /** * Run the SAFE-tier oracle self-repair loop over `implSpec` (design/78 §1 state machine — a DETERMINISTIC * switch, no LLM in the control flow): * * `ASSERT_ISOLATION` (identity check → fail-closed `oracle.unprotected`) → `GENERATE` (`runner.runTask`) → * `ORACLE` (the injected closure) → { passed → terminal-by-tier projection } | { failed & attempt; //# sourceMappingURL=repair-loop.d.ts.map