/** * Multi-lane review fan, round chaining, predicates, and verdict emission * (Prop 304 R2, mmnto-ai/totem#2106). * * This module owns everything that turns `review.lanes` into a Prop 302 verdict * artifact: the config validator, the strict per-lane runner wrapper over * `runOrchestrator`, the #2104 panel + #2103 post-check wiring, the round-chain * lineage bookkeeping, the two derived predicates (`settled` and cache * eligibility), and the verdict emission + report line. `shieldCommand` calls * `runReviewFan` on the standard review path when lanes are configured; the * legacy single-lane path is untouched (invariant 7). * * The whole loop state machine lives here in the CLI (Tenet 16): any agent * driving `totem review` gets identical round-chaining/settle capability — the * `review-loop` skill is a thin driver, never a state owner. * * ── TWO HASH DOMAINS (codex fold 1, load-bearing) ──────────────────────────── * `diffScope.diffHash` (the MASKED review-payload identity — what the lanes * reviewed) and `.reviewed-content-hash` (the extension-scoped tracked-source * hash that authorizes a push) bind DIFFERENT state and are never equal. The * caller captures the content hash once PRE-fan; the fan re-hashes ONCE POST-fan, * derives `reviewedState` from that single compare, and reuses it for BOTH the * verdict field and the stamp decision (codex rev-2 fold 1). Drift ⇒ * `reviewedState='drifted'` ⇒ `settled=false` AND cache-ineligible; the fan * stamps the pre-fan hash directly via `writeReviewedContentHashValue` (bypassing * `stampReviewedContentHashIfTreeUnchanged`, whose recompute would be a second, * divergent compare — the single-lane path still uses that helper). */ import type { GroundingBundle, LessonsConsulted, PersistedPostCheckFinding, PostCheckRule, RunArtifact, TotemConfig, VerdictArtifact, VerdictDiffScope, VerdictLane, VerdictRound } from '@mmnto/totem'; import type { ExemptionShared } from '../exemptions/exemption-schema.js'; import { type ShieldFinding } from './shield-templates.js'; /** * Round index at/above which the advisory max-rounds sensor line fires. A * constant (never a config knob this slice) — advisory only, NEVER a block. */ export declare const MAX_ROUNDS_ADVISORY = 5; /** * Validate a configured `review.lanes` array at review startup — a hard init * error on any violation (Prop 304 R2 config boundary; codex fold 7). Reuses * the CLI's `assertValidModelName` (shell-injection + leading-dash gate) and * `parseModelString`, so a lane accepted here resolves identically at invoke. * * Rules (design item 1): * - every entry must be a known `provider:model` (a `:`-prefixed known * provider, or a bare model resolved against the base provider), * - the `shell` provider is REJECTED (a review lane is an LLM lane, never a * shell command), * - empty / whitespace-only entries are rejected, * - duplicate NORMALIZED (`provider:model`) entries are rejected. * * `baseProvider` is the configured orchestrator provider used to resolve a bare * (prefix-less) lane; when absent, a bare lane is rejected (it has no provider * to resolve against). ABSENT `lanes` returns `[]` (the legacy path runs). * * Returns the NORMALIZED (`provider:model`) lane list — the fan's laneIds and * per-lane model routing use exactly these strings, so normalization has one * home. */ export declare function validateReviewLanes(lanes: readonly string[] | undefined, baseProvider: string | undefined, TotemConfigError: TotemConfigErrorClass): string[]; /** The dynamically-imported core `TotemConfigError` class, threaded to the sync validators (rule 64). */ type TotemConfigErrorClass = typeof import('@mmnto/totem').TotemConfigError; /** * Reject fan-incompatible flags at review startup when the fan is active (finding 12). * `--suppress`, `--learn`, and `--auto-capture` have NO defined fan semantics yet, so a * fan-active run rejects them LOUDLY (naming the unsupported combination) rather than * silently ignoring them. Note: `--raw` is diverted to the legacy zero-LLM path upstream * (so the fan never activates with `--raw`; finding 1), and `--out` IS supported by the * fan (it writes the human-readable fan report; finding 2). */ export declare function assertFanFlagsSupported(options: { suppress?: string[]; learn?: boolean; autoCapture?: boolean; }, TotemConfigError: TotemConfigErrorClass): void; /** * The CLI-side review structured-output rule (DECIDABLE). The shipped generic * `structuredOutputRule` bare-`JSON.parse`s `output.content` and would * MIS-VERDICT valid XML-wrapped / fenced Shield output as malformed — so it is * deliberately NOT wired here. This rule runs the SINGLE shared * `extractStructuredVerdict` cascade (the same parser the CLI path uses): an * extractable verdict passes, unextractable output is a decidable fail. * Caller-scoped to `review` runs. */ export declare const reviewStructuredOutputRule: PostCheckRule; /** * A single lane's raw invocation result, as the runner wrapper surfaces it. The * production invoker (`makeLaneInvoker`) forces a fresh invoke, captures the run * artifact via the `onEmitted` callback, and LOADS it so `runArtifact` is * present iff `runArtifactHash` is. Test invokers construct this directly. */ export interface LaneInvocation { /** The model output. A response-cache hit is impossible (fresh forced). */ content: string | undefined; /** The captured run-artifact content address; `undefined` ⇒ no emission fired. */ runArtifactHash: string | undefined; /** The loaded run artifact; present iff `runArtifactHash` is present. */ runArtifact: RunArtifact | undefined; } /** * A per-lane invoker: runs one lane over the shared pre-assembled `deliveredPrompt` * (the masked bytes every lane sees, identical across lanes — codex rev-2 fold 4) * and returns its raw invocation (or throws). The production invoker sends * `deliveredPrompt` verbatim; a test invoker may echo it into the run artifact's * `maskedPrompt` so the persisted `` segment recomputes the stored * `diffHash` (invariant 12/15). */ export type LaneInvoker = (laneModel: string, deliveredPrompt: string) => Promise; /** One lane's fully-classified outcome plus the by-products the fan needs downstream. */ export interface LaneRunResult { /** The verdict-artifact lane record (status-discriminated union). */ lane: VerdictLane; /** The completed/abstained lane's run artifact (for panel + post-checks). */ runArtifact?: RunArtifact; /** The completed lane's exemption-filtered findings (drives predicates + verdict.findings). */ filteredFindings: ShieldFinding[]; } /** * Classify one lane's INVOCATION result (index-tagged for the laneId). The invoker is * called once and NOT wrapped here: an invoker throw REJECTS this promise and the fan's * `Promise.allSettled` maps the rejection to a `failed` lane via * {@link classifyRejectedLane} — an explicit terminal classification, never a bare * swallow (finding 13). A missing artifact emission is a `failed` lane, unextractable * output is `abstained`, and an extractable verdict is `completed` with a severity tally * from its exemption-filtered findings. * * NO retry lives here beyond `runOrchestrator`'s existing logged quota fallback * (the design's "no runner retry"); `resolvedBackend` records what actually ran. */ export declare function runLane(index: number, laneModel: string, invoker: LaneInvoker, shared: ExemptionShared, deliveredPrompt: string): Promise; /** * Map a REJECTED lane promise to a `failed` lane record (finding 13): an invoker throw * is classified and lands in the verdict as a terminal `failed` lane — a lane is never * lost to a rejection. The laneId uses the CONFIGURED lane (a rejection means no backend * resolved). * * The classification consumes slice-B's STRUCTURED `OrchestratorInvokeError` (its * `kind` and `failureArtifactHash`) rather than inferring the category from error prose * (mmnto-ai/totem#2459). When the error carries a persisted failure-evidence hash, it is * recorded on the lane so the verdict reaches B's bounded evidence ONE HOP away — * mirroring how a completed lane carries `runArtifactHash`. */ export declare function classifyRejectedLane(index: number, laneModel: string, reason: unknown): Promise; /** The additive diff-scope metadata `getDiffForReview` now returns. */ export interface DiffScopeMeta { source: 'explicit-range' | 'staged' | 'uncommitted' | 'branch-vs-base'; base?: string; head?: string; /** * The RAW CLI selector form (finding 10) — the operator's exact `--diff` string. It * distinguishes selectors that resolve to the same refs but describe different * lineages: `--diff main` (base-vs-working-tree, no head) vs `--diff main..HEAD` * (range mode) both resolve base='main' head='HEAD' but must NOT share a lineage. * Threaded into `LineageKeyInput.selectorForm` so the two forms produce distinct keys. */ selectorForm?: string; } /** * Build the source-discriminated `VerdictDiffScope` from the resolved scope * metadata + the masked-payload `diffHash`. For `explicit-range` the schema * requires both endpoints; a bare `--diff ` (working-tree comparison) has * no explicit head, so `HEAD` is recorded as the implicit head. */ export declare function buildDiffScope(meta: DiffScopeMeta, diffHash: string): VerdictDiffScope; /** A git command runner seam (injectable for tests). Returns trimmed stdout. */ export type GitExec = (args: readonly string[]) => string; /** The resolved lineage components (item 5). */ export interface LineageResolution { branch: string; mergeBase: string; lineageKey: string; } /** * Resolve the composite lineage key over the RESOLVED scope selector (item 5; * codex rev-2 fold 2). `repoIdentity` is the stable worktree identity (absolute * `git rev-parse --show-toplevel`); `branch` is the current branch * (`git symbolic-ref --short HEAD`), a detached HEAD becoming the literal * `DETACHED:`. The per-source range selectors are contributed to the key so * they describe the *lineage*, never the diff bytes: * - `explicit-range` — the normalized `base` + `head` endpoints (two different * ranges on one branch never cross-link — gate 2). * - `branch-vs-base` — the resolved `base` + `mergeBase` sha (a moved merge-base * forks the chain). * - `staged` / `uncommitted` — no range fields; worktree identity + branch + * source carry the lineage (the index/worktree has no second endpoint). */ export declare function resolveLineage(meta: DiffScopeMeta, gitExec: GitExec): Promise; /** The default `git` runner (production). */ export declare function defaultGitExec(cwd: string): Promise; export interface RoundResolution { round: VerdictRound; /** Warnings to surface (chain restart / lineage mismatch) — never blocks. */ warnings: string[]; } /** * Resolve the round record (item 5). Implicit path: the latest verdict sharing * the computed lineage key links as prior (round = prior + 1); a corrupt/missing * prior restarts the chain at round 0 with a warning. Explicit `--continues * `: load that verdict and link to it (its round + 1); a lineage mismatch * WARNS (honoring the explicit intent) and records the CURRENT lineage key. */ export declare function resolveRound(totemDirAbs: string, lineageKey: string, continuesHash: string | undefined): Promise; /** The panel + post-check by-products the verdict assembly consumes. */ export interface PanelAndChecks { /** All post-check rows across completed lanes, flattened. */ postChecks: PersistedPostCheckFinding[]; /** The panel content address — present iff ≥2 completed lanes assembled a panel. */ panelArtifactHash?: string; /** The top-level panel diversity summary — present iff a panel was assembled. */ diversity?: VerdictArtifact['diversity']; } /** * Run the #2103 post-check engine over every lane that emitted a run artifact * (completed AND abstained — finding 8) and, with ≥2 COMPLETED lanes, assemble + write * the #2104 panel from the completed lanes' run artifacts ONLY (failed/abstained lanes * never reach `assemblePanelArtifact`). * * Panel inputs stay completed-only, but post-checks ALSO cover abstained lanes: an * abstained lane's unextractable output is exactly what the review-specific decidable * structured-output rule must persist a 'fail' row for, so its failure lands honestly * in `verdict.postChecks` instead of vanishing. */ export declare function runPanelAndPostChecks(laneResults: readonly LaneRunResult[], totemDirAbs: string, configRoot: string, createdAt: string): Promise; /** All inputs needed to assemble the full verdict artifact in memory. */ export interface VerdictAssemblyInputs { diffScope: VerdictDiffScope; laneResults: readonly LaneRunResult[]; panelAndChecks: PanelAndChecks; round: VerdictRound; /** Post-fan tree compare (codex rev-2 fold 1) — recorded AND fed into `settled`. */ reviewedState: 'matched' | 'drifted'; createdAt: string; /** * The round's lesson-recall record (mmnto-ai/totem#2363) — derived from the * shared grounding bundle via core's `deriveLessonsConsulted`, never * hand-built. Optional in the INPUTS (additive 1.x — structural callers * predate it); the production fan always supplies it, so every fan verdict * carries recall state (`hit`/`empty`). Absent ⇒ the field is omitted from * the artifact (honest-absent), never fabricated as `empty`. */ lessonsConsulted?: LessonsConsulted; } /** * Assemble the full verdict artifact in memory (item 7). Counts are DERIVED * from `lanes` (never mirrored on trust — the schema re-validates them), the * findings union is the completed lanes' exemption-filtered findings, and * `settled` is the derived predicate over this artifact's own content (including * the `reviewedState` drift clause — codex rev-2 fold 1). */ export declare function assembleVerdict(inputs: VerdictAssemblyInputs, deriveSettled: (typeof import('@mmnto/totem'))['deriveSettled'], VERDICT_ARTIFACT_SCHEMA_VERSION: (typeof import('@mmnto/totem'))['VERDICT_ARTIFACT_SCHEMA_VERSION']): VerdictArtifact; /** Everything `shieldCommand` hands the fan on the standard review path. */ export interface ReviewFanContext { /** Normalized fan lane models (each a `provider:model` string). */ laneModels: string[]; /** The assembled review prompt — identical for every lane (identical-kit discipline). */ prompt: string; /** The code-only filtered diff the lanes review (masked here for `diffHash`). */ filteredDiff: string; /** Resolved diff-scope metadata from `getDiffForReview`. */ diffMeta: DiffScopeMeta; config: TotemConfig; cwd: string; configRoot: string; /** Absolute `.totem` dir (`configRoot`/`config.totemDir`). */ totemDirAbs: string; /** * The shield options the fan reads. The fan forces `fresh` per lane and forces `raw` * OFF (a fan-configured `--raw` is diverted to the legacy zero-LLM path upstream, so * `raw` never reaches here true). `out` writes the human-readable fan report; * `failOn` (`critical`|`warn`) opts into a non-zero exit; `override` converts a * `--fail-on` failure to pass AND authorizes the trap-ledgered stamp (finding 3). */ options: { raw?: boolean; out?: string; model?: string; fresh?: boolean; override?: string; failOn?: 'critical' | 'warn'; }; /** Grounding identity for the run-artifact request (same as the single-lane path). */ groundingHash: string; provenanceSummary: string; groundingBundle: GroundingBundle; totalResults: number; codeBlind: boolean; /** Shared exemptions (read once by the caller; passed in side-effect-free). */ shared: ExemptionShared; /** The pre-fan content hash (codex fold 1) — the stamp binds exactly this tree. */ preFanContentHash: string | null; /** Explicit `--continues ` round override. */ continues?: string; /** Injected per-lane invoker (production builds one over `runOrchestrator`). */ invoker?: LaneInvoker; /** Injected git runner (production uses `safeExec('git', ...)`). */ gitExec?: GitExec; /** Injected clock (production uses `new Date().toISOString`). */ now?: () => string; /** * Injected POST-fan content-hash computer (codex rev-2 fold 1) — production * re-hashes the tracked-source tree via `computeReviewedContentHash`. Called * ONCE after the fan; its result vs `preFanContentHash` derives `reviewedState` * (and, transitively, the stamp decision). Tests inject a value that differs * from `preFanContentHash` to simulate a mid-fan tree mutation. */ contentHash?: () => Promise; } /** * The standard-path fan entry. Runs every configured lane IN PARALLEL (finding 13), * canonicalizes the results into configured-lane order, runs the panel + post-checks, * resolves the round chain, takes the single post-fan tree compare in a short critical * section (finding 6), assembles + saves the verdict, renders the findings + covariate * line, and enforces the exit contract (finding 3 / Gate G5): * * - DEFAULT: sensor exit 0 — the verdict, the covariate line, and the findings render * are always emitted; a findings-bearing or degraded-coverage round does NOT throw. * - `--fail-on `: throw `SHIELD_FAILED` when the round has findings at/above * that severity OR is not cache-eligible. * - `--override `: converts a `--fail-on` failure to pass AND authorizes the * trap-ledgered cache stamp on a non-cache-eligible round (matched trees only — * drift is never stampable, even overridden). * * Cache-eligible ⇒ stamp the pre-fan hash. ALL lanes terminal-failed (Gate G3) ⇒ the * honest verdict is WRITTEN FIRST, then the run hard-errors. Zero configured lanes ⇒ a * pre-attempt hard error with no verdict. */ export declare function runReviewFan(ctx: ReviewFanContext): Promise; /** `printCovariateLine` inputs — the resolved diff-scope metadata plus store location. */ export interface CovariateQuery { /** * Resolved diff-scope metadata from `getDiffForReview`, or `null` when no diff was * detected (no scope ⇒ no lineage ⇒ loud sensor message, exit 0). */ diffMeta: DiffScopeMeta | null; /** Absolute `.totem` dir. */ totemDirAbs: string; cwd: string; /** Injected git runner (tests); production uses `safeExec('git', ...)`. */ gitExec?: GitExec; } /** * `totem review --covariate` (rev-5 item 4): the EXECUTABLE covariate transport. * Read-only and zero-LLM — resolves the CURRENT lineage through exactly the same * {@link resolveLineage} path `runReviewFan` uses (never a re-implementation), loads * the latest verdict for that lineage, and prints the core-owned * {@link renderCovariateLine} to STDOUT (the skills pipe it into the consolidated * round-disposition comment). No verdict for the lineage ⇒ a LOUD sensor message and * a clean return (exit 0) — the caller learns there is no line to carry, nothing gates. */ export declare function printCovariateLine(query: CovariateQuery): Promise; export {}; //# sourceMappingURL=review-fan.d.ts.map