/** * Validator Max-N Runtime — Lead↔Worker round-trip orchestration. * * Drives the canonical Lead → Worker → Validator loop defined by the * `cleo-validator` skill ({@link * ../../../../../.cleo/skills/cleo-validator/SKILL.md}). For one Worker * task, this runtime: * * 1. Spawns the Validator via the injected `spawnValidator` callback * (T10511's `spawn.validator` SDK tool). * 2. Waits for the verdict (attest or reject) OR for an infra-fault to be * emitted by `spawnValidator` / `awaitVerdict`. * 3. On `attest`: returns success. * 4. On `reject` (semantic fault: the Worker's code is wrong): increments * the shared retry counter, re-spawns the Worker with rejection * findings, then re-spawns the Validator. * 5. On infra-fault (timeout, conduit-drop, validator-OOM): increments * the SHARED retry counter, applies the canonical Max-N row (retry * count, backoff strategy, transient/permanent classification) and * either re-spawns a FRESH Validator or escalates to Lead. * * ## Design contract * * The runtime is decoupled from T10511's concrete SDK-tool implementations * via the {@link ValidatorRuntimeDeps} injection contract — callers pass * functions that produce verdicts / fault classifications. Unit tests stub * those callbacks; production wiring (a follow-up task) supplies the real * `spawn.validator`, `validator.attest`, `validator.reject`, and * `validator.evidence-run` adapters. * * ## Shared retry-counter accounting (VAL-007 + Max-N table) * * Both semantic faults (REJECT) AND infra faults (timeout / conduit-drop / * validator-OOM) increment the SAME `validatorRetryAttempts` counter, * bounded by `validatorRetryMax` (default N=3). This prevents an * adversarial alternation where a Worker flips between fault families to * bypass the cap (e.g. REJECT → timeout → REJECT → timeout → ... forever). * * ## Audit trail * * Every retry attempt — semantic OR infra — appends ONE line to * `/.cleo/audit/validator-retries.jsonl`. Each line is * standalone JSON ({@link ValidatorRetryAuditEntry}) and includes * `timestamp`, `taskId`, `attemptNumber`, `faultKind`, `classification`, * and `retryDecision`. The append-only convention matches * `force-bypass.jsonl` and `contract-violations.jsonl`. * * @module lifecycle/validator/runtime * @task T10512 * @epic T10383 * @saga T10377 (SG-IVTR-AC-BINDING) */ import type { ValidatorRejection, ValidatorVerdict } from '@cleocode/contracts'; /** * Default shared retry cap (semantic + infra). Configurable via the * `validatorRetryMax` option on {@link runValidatorMaxN}. Mirrors * `delegation.validatorRetryMax` from the SKILL.md. * * @task T10512 */ export declare const DEFAULT_VALIDATOR_RETRY_MAX = 3; /** * Default subagent timeout in milliseconds before a Validator spawn is * classified as an infra-fault `timeout`. Mirrors `subagentTimeoutSeconds` * (300 s) from the SKILL.md tier-1 default. * * @task T10512 */ export declare const DEFAULT_SUBAGENT_TIMEOUT_MS = 300000; /** * Canonical Max-N row catalogue keyed by fault kind. Every fault produced * by `spawnValidator` / `awaitVerdict` resolves to ONE of these rows. * * `retryCount` is the per-row cap (not the shared cap); the shared cap * (`validatorRetryMax`) overrides per-row caps when reached first. * * `backoff` is the strategy applied BEFORE the next retry attempt: * - `exponential(b, m)` — sleeps `b` then `m` (clamped by maxDelayMs) * - `immediate` — zero delay * - `immediate-downgrade` — zero delay PLUS model-tier downgrade * (e.g. Sonnet → Haiku) requested via the spawn callback * * `classification`: * - `transient` — retryable; continues consuming the shared counter * - `permanent` — non-retryable; short-circuits to HITL on FIRST occurrence * * @task T10512 */ export declare const MAX_N_ROWS: { readonly 'validator-rejected-no-acs': { readonly family: "semantic"; readonly retryCount: 0; readonly backoff: { readonly kind: "immediate"; }; readonly classification: "permanent"; readonly escalationAtom: "E_VALIDATOR_NO_ACS"; }; readonly 'validator-partial': { readonly family: "semantic"; readonly retryCount: 1; readonly backoff: { readonly kind: "immediate"; }; readonly classification: "transient"; readonly escalationAtom: "E_VALIDATOR_PARTIAL"; }; readonly 'validator-unreachable': { readonly family: "semantic"; readonly retryCount: 2; readonly backoff: { readonly kind: "exponential"; readonly firstMs: 10000; readonly secondMs: 30000; }; readonly classification: "transient"; readonly escalationAtom: "E_VALIDATOR_UNREACHABLE"; }; readonly 'tool-not-resolved': { readonly family: "semantic"; readonly retryCount: 0; readonly backoff: { readonly kind: "immediate"; }; readonly classification: "permanent"; readonly escalationAtom: "E_TOOL_NOT_RESOLVED"; }; readonly timeout: { readonly family: "infra"; readonly retryCount: 2; readonly backoff: { readonly kind: "exponential"; readonly firstMs: 5000; readonly secondMs: 30000; }; readonly classification: "transient"; readonly escalationAtom: "E_VALIDATOR_TIMEOUT"; }; readonly 'conduit-drop': { readonly family: "infra"; readonly retryCount: 3; readonly backoff: { readonly kind: "immediate"; }; readonly classification: "transient"; readonly escalationAtom: "E_VALIDATOR_VERDICT_DROPPED"; }; readonly 'validator-OOM': { readonly family: "infra"; readonly retryCount: 1; readonly backoff: { readonly kind: "immediate-downgrade"; }; readonly classification: "transient-then-permanent"; readonly escalationAtom: "E_VALIDATOR_OOM"; }; }; /** * Canonical fault kinds — the keys of {@link MAX_N_ROWS}. * * @task T10512 */ export type ValidatorFaultKind = keyof typeof MAX_N_ROWS; /** * Canonical fault families. Semantic faults are decisions by the Validator * (or its tools); infra faults are process/transport failures. * * @task T10512 */ export type ValidatorFaultFamily = 'semantic' | 'infra'; /** * Backoff strategy applied between retries. * * - `immediate` — zero delay * - `exponential` — `firstMs` before the first retry, `secondMs` before any * subsequent retries within the same row * - `immediate-downgrade` — zero delay AND the next spawn should request a * smaller model tier (e.g. Sonnet → Haiku) to fit a tighter context * * @task T10512 */ export type BackoffStrategy = { kind: 'immediate'; } | { kind: 'exponential'; firstMs: number; secondMs: number; } | { kind: 'immediate-downgrade'; }; /** * Uniform fault envelope returned by {@link ValidatorRuntimeDeps.spawnValidator} * or {@link ValidatorRuntimeDeps.awaitVerdict} when something goes wrong. * * The runtime resolves `kind` against {@link MAX_N_ROWS} to pick the retry * policy. * * @task T10512 */ export interface ValidatorFault { kind: ValidatorFaultKind; /** Free-form diagnostic from the failing callback. */ message: string; /** * Optional structured detail — captured into the audit row's `detail` * field for post-mortem analysis. */ detail?: Record; } /** * Spawn-request envelope passed to {@link ValidatorRuntimeDeps.spawnValidator}. * * @task T10512 */ export interface ValidatorSpawnRequest { /** Worker task being validated. */ workerTaskId: string; /** Attempt number (1-based; resets to 1 on Worker re-spawn). */ attemptNumber: number; /** * When true (set after a `validator-OOM` infra fault) the spawner SHOULD * downgrade the model tier (e.g. Sonnet → Haiku) for this attempt to * shrink the context window. The spawner is responsible for the actual * model selection; this flag is advisory. */ downgradeModelTier?: boolean; /** * On Worker re-spawn after a REJECT, this carries the rejection envelope * the Worker should use to fix the failing ACs. Absent on Validator-only * spawns. */ workerRespawnContext?: { rejection: ValidatorRejection; }; } /** * Result envelope returned by {@link ValidatorRuntimeDeps.spawnValidator} or * {@link ValidatorRuntimeDeps.awaitVerdict}: either a verdict (success) or a * fault classification. * * @task T10512 */ export type ValidatorRoundResult = { ok: true; verdict: ValidatorVerdict; } | { ok: false; fault: ValidatorFault; }; /** * Worker re-spawn callback — invoked when the Validator returns a * REJECT verdict (semantic fault: the Worker's code is wrong, not an infra * failure). The callback is responsible for re-dispatching the Worker with * the rejection findings as feedback. * * Returns the new worker submission state OR a fault (e.g. the Worker * itself crashed during re-spawn). * * @task T10512 */ export type WorkerRespawnFn = (workerTaskId: string, rejection: ValidatorRejection, attemptNumber: number) => Promise<{ ok: true; } | { ok: false; fault: ValidatorFault; }>; /** * Injectable dependency contract for the validator Max-N runtime. Callers * (production wiring AND tests) supply functions that wrap the underlying * T10511 SDK tools. The runtime never imports T10511 directly. * * @task T10512 */ export interface ValidatorRuntimeDeps { /** * Spawn a Validator subagent and return either the verdict OR a fault. * The implementer is responsible for the timeout budget — when the * subagent exceeds `subagentTimeoutSeconds` the implementer MUST return * `{ ok: false, fault: { kind: 'timeout', ... } }`. */ spawnValidator(req: ValidatorSpawnRequest): Promise; /** * Re-spawn the Worker with rejection findings. The implementer is * responsible for re-dispatching the Worker and feeding the rejection * envelope into the Worker's spawn prompt. */ respawnWorker: WorkerRespawnFn; /** * Sleep callback (injectable for deterministic tests). Receives the * backoff delay in ms; in tests this can be a no-op. */ sleep?: (ms: number) => Promise; /** * Clock callback (injectable for deterministic tests). Returns ISO * timestamps used in audit entries. */ now?: () => string; } /** * Options passed to {@link runValidatorMaxN}. All fields are optional with * canonical defaults. * * @task T10512 */ export interface RunValidatorMaxNOptions { /** * Shared retry cap (semantic + infra combined). Defaults to * {@link DEFAULT_VALIDATOR_RETRY_MAX} (3). */ validatorRetryMax?: number; /** * Project root used to write the audit JSONL. Defaults to the CLEO * project root resolved via `getProjectRoot()`. Tests should pass an * explicit temp dir. */ projectRoot?: string; /** * When true, the audit appender is suppressed entirely (used by tests * that want to assert behaviour WITHOUT touching the filesystem). */ suppressAudit?: boolean; } /** * Terminal outcome of {@link runValidatorMaxN}. The runtime always reaches * ONE of three terminal states: * * - `attest` — happy path; Validator returned an attestation * - `escalate-hitl` — retry budget exhausted OR permanent fault; Lead must * take over (file a HITL approval gate) * - `escalate-permanent` — first-occurrence permanent fault (e.g. `no-acs`) * * The `attempts` array preserves the full audit trail in memory for the * caller (in addition to the on-disk JSONL). * * @task T10512 */ export type ValidatorRuntimeResult = { outcome: 'attest'; taskId: string; verdict: ValidatorVerdict & { verdict: 'attest'; }; attempts: ValidatorRetryAuditEntry[]; } | { outcome: 'escalate-hitl'; taskId: string; reason: string; attempts: ValidatorRetryAuditEntry[]; } | { outcome: 'escalate-permanent'; taskId: string; fault: ValidatorFault; reason: string; attempts: ValidatorRetryAuditEntry[]; }; /** * Canonical path (relative to project root) for the validator-retry audit * log. Append-only JSONL — one row per retry attempt. * * @task T10512 */ export declare const VALIDATOR_RETRIES_AUDIT_FILE = ".cleo/audit/validator-retries.jsonl"; /** * One row in `.cleo/audit/validator-retries.jsonl`. Append-only, one line * per retry attempt (semantic OR infra). Matches the * `force-bypass.jsonl` / `contract-violations.jsonl` append-only pattern. * * @task T10512 */ export interface ValidatorRetryAuditEntry { /** ISO-8601 timestamp at which this row was written. */ timestamp: string; /** Worker task ID under validation. */ taskId: string; /** * 1-based attempt number against the SHARED retry counter. A run that * succeeds first try emits exactly one row with `attemptNumber: 1` and * `outcome: 'attest'`. */ attemptNumber: number; /** Family of the fault that triggered this row, or `null` on success. */ faultFamily: ValidatorFaultFamily | null; /** Canonical fault kind, or `null` on success. */ faultKind: ValidatorFaultKind | null; /** Transient / permanent classification, or `null` on success. */ classification: 'transient' | 'permanent' | 'transient-then-permanent' | null; /** Backoff applied BEFORE the next attempt, or `null` if no next attempt. */ backoffMs: number | null; /** * Retry decision the runtime took at this row: * - `retry-validator` — re-spawn a fresh Validator * - `retry-worker` — Validator rejected; re-spawn Worker with findings * - `escalate-hitl` — counter exhausted; Lead takes over * - `escalate-permanent` — permanent fault; short-circuit to HITL * - `attest` — happy path; Validator returned attestation */ retryDecision: 'retry-validator' | 'retry-worker' | 'escalate-hitl' | 'escalate-permanent' | 'attest'; /** Free-form diagnostic carried from the underlying fault. */ message: string; /** Structured detail captured for post-mortem analysis. */ detail?: Record; } /** * Run the Lead↔Worker↔Validator Max-N retry loop for one Worker task. * * @remarks * Drives the canonical state machine documented in the `cleo-validator` * SKILL.md "Execution Flow" + "Max-N infra-fault row catalogue" sections. * * The shared counter (`validatorRetryAttempts`) advances on EVERY fault * regardless of family — this is the documented defence against * adversarial fault-kind alternation (a Worker / Validator pairing that * flips between REJECT and `timeout` would otherwise bypass the cap). * * The runtime is purely orchestration — it never calls `cleo verify`, * never mutates the Worker's gate ledger, never modifies the worktree. * All side effects flow through the injected {@link ValidatorRuntimeDeps}. * * @example * ```ts * import { runValidatorMaxN } from '@cleocode/core/lifecycle/validator/runtime'; * * const result = await runValidatorMaxN('T1234', { * spawnValidator: async (req) => { * // Production: call spawn.validator from T10511 SDK tools. * return await sdk.spawn.validator(req); * }, * respawnWorker: async (taskId, rejection, attemptNumber) => { * // Production: dispatch worker with rejection envelope. * return await sdk.spawn.worker({ taskId, rejection, attemptNumber }); * }, * }); * * if (result.outcome === 'attest') { * // Happy path — Lead can mark the task done. * } else if (result.outcome === 'escalate-hitl') { * // Lead opens a HITL approval gate. * } else { * // Permanent fault — Lead routes to AC-backfill or infra team. * } * ``` * * @param workerTaskId - Worker task ID (e.g. `'T1234'`). * @param deps - Injectable runtime dependencies (spawn / respawn / sleep / now). * @param opts - Optional caller overrides (retry cap, audit path, etc.). * @returns The terminal outcome envelope + in-memory audit trail. * * @task T10512 */ export declare function runValidatorMaxN(workerTaskId: string, deps: ValidatorRuntimeDeps, opts?: RunValidatorMaxNOptions): Promise; /** * Resolve the delay in milliseconds for a given backoff strategy and the * 1-based per-row attempt number (1 = first retry, 2 = second retry, ...). * * Internal — exported for unit testing only. * * @task T10512 */ export declare function resolveBackoffMs(strategy: BackoffStrategy, perRowAttempt: number): number; //# sourceMappingURL=runtime.d.ts.map