/** * run-harness.ts — domain orchestrator for `vat skill test run`. * * Implements the full harness sequence as a pure async function (no process.exit): * lock → assert safe workdir + harness root → stage → resolve staged-subject * eval path → bootstrap-check (scaffold template + exit 3 if absent) * → preflight (return early exitCode 2 on failure) → ack enforcement * → dry-run short-circuit → build per-eval work items → run the vat-owned * executor→grader pipeline (bounded-parallel) → merge grader fragments → * write grading.json/friction.json (vat is SOLE writer) → reconcile verdict * → release lock → return result */ import type { SkillSourceDescriptor } from '@vibe-agent-toolkit/resources'; import { type spawnHeadlessClaude } from '@vibe-agent-toolkit/utils'; import type { ResolveSkillSourceContext, SkillSource } from '../skill-source/types.js'; import type { EvalFragment } from './eval-fragment.js'; import { type EvalEntry, type EvalSuite } from './eval-inputs.js'; import { type SkillTestExitCodeValue } from './exit-codes.js'; import { type FrictionItem } from './friction-schema.js'; import { type GradingVerdict } from './grading-adapter.js'; import { type PreflightInput } from './preflight.js'; import { type StageItem } from './staging.js'; import { type SkippedEvalsSummary } from './tier-plan.js'; import { type ToolEvalReport } from './tool-eval-schema.js'; /** Auth modes supported by the harness. */ export type HarnessAuthMode = 'inherit' | 'subscription' | 'api-key' | 'auto'; /** Auth mechanism requirements. */ export type HarnessAuthMechanism = 'subscription' | 'api-key'; export interface RunHarnessOptions { /** The single skill under test (required). */ subject: string; /** * Absolute path to the project/repo root. Used as the resolution anchor for * path-relative skill sources (`{path:'../x'}`) and as the persistence anchor * for the bootstrap evals.json scaffold. Defaults to the harness root when * omitted (degraded — callers should always pass the real project root). */ repoRoot?: string; /** * Subpath of the subject's eval suite, relative to its source directory. * Defaults to `evals/evals.json` (the `evals` config knob overrides it). */ evalsSubpath?: string; /** * REQUIRED companion skills to stage alongside the subject (`--with`/config * `with:`), keyed by name. Each is staged and made invocable exactly like the * subject; unlike {@link withOptional}, a companion here that fails to resolve * fails the whole run (see {@link DuplicateStagedSkillError} for the one * cross-cutting constraint: every staged name — subject, `with`, and * `withOptional` — must be unique). */ withSources?: Record; /** * OPTIONAL companion skills to stage alongside the subject (`--with-optional`/ * config `optional:`), keyed by name. Staged and made invocable exactly like a * `withSources` entry, EXCEPT a companion here that fails to resolve is * skipped-with-warning (recorded in the staging result's `skippedOptional`) * instead of failing the run. */ withOptional?: Record; /** Override the harness working directory (base for harnessKey derivation). */ workdir?: string; /** Override the harness output directory (explicit full path). */ out?: string; /** Force a full re-stage (ignored in v1; wired for future use). */ refresh?: boolean; /** Keep the harness directory after the run (don't clean up). */ keep?: boolean; /** Auth mode for resolving credentials. */ auth?: HarnessAuthMode; /** Required auth mechanism. */ requireAuth?: HarnessAuthMechanism; /** * Pinned grader/judge model for the per-eval grader spawns. Defaults to * {@link DEFAULT_GRADER_MODEL}. Deliberately distinct from `model` (which is * the model under test in the executor spawns) so the judge stays comparable * across runs regardless of the subject model. */ graderModel?: string; /** Bounded-parallel executor→grader pipeline width. Defaults to {@link DEFAULT_CONCURRENCY}. */ concurrency?: number; /** * Injectable spawn seam (tests only). When set, it replaces the real * {@link spawnHeadlessClaude} in BOTH the executor and grader per-eval spawns, * so a test can drive the full harness with a fake `claude` and no real * install. Production callers leave it undefined (the real spawn is used). */ spawn?: typeof spawnHeadlessClaude; /** Enable A/B baseline run (with/without skill). */ baseline?: boolean; /** Allow unverified skill source (skip manifest check). */ allowUnverifiedSkillSource?: boolean; /** Dry-run: assemble the command but don't spawn. */ dryRun?: boolean; /** User has explicitly acknowledged this command runs skill code. */ acknowledgedRunsSkillCode?: boolean; model?: string; maxTurns?: number; maxBudgetUsd?: number; /** Timeout in seconds. */ timeout?: number; /** Stall watchdog in seconds. */ stall?: number; /** * Pre-resolved source for the subject; set by run.ts after project-aware * resolution + build. A built dist dir for a declared skill, or the resolved * as-is source. Absent → legacy { path: subject }. */ subjectSource?: SkillSource; /** * Authored source dir for the subject, where the eval suite (`evals/`, incl. * `fixtures/`) is maintained. Used to (a) overlay that suite onto a built/dist * subject that doesn't carry it, and (b) write the bootstrap template when no * suite exists yet. For a built declared skill this is the SOURCE skill dir, * not the dist. Absent → derived from subject (legacy). */ subjectScaffoldDir?: string; /** * True when run.ts actually rebuilt the subject (declared skill, no * --no-build/--dry-run). Recorded in provenance. Absent/false → staged as-is. */ rebuilt?: boolean; /** * True when the resolved reference is `buildable` — a real run WOULD build + * stage it before spawning. False/absent for plain `source` subjects. Set by * run.ts after project-aware resolution; used in the dry-run summary. */ wouldBuild?: boolean; /** * Meaningful only when wouldBuild is true and dryRun is true. True = the * dry-run staged the EXISTING on-disk dist without rebuilding (may be stale). * False = no dist existed yet so the preview fell back to the source dir. * Absent when not a dry-run or when the subject is a plain source. */ dryRunStagedExistingDist?: boolean; /** Feature B: explicit env var injections (interpolated at stage time). */ env?: Record; /** Feature A: host env var names to forward to the executor spawn if present. */ passEnv?: readonly string[]; /** * Declared executables the subject skill ships (name + kind + howInvoked), * populated by run.ts from the resolved subject's packaging config WHEN cleanly * reachable (a `buildable` ref carries `packagingConfig`; a plain path source * does not — issue #145 Phase T). Passed to the grader on the WITH arm ONLY as * a recognition aid alongside each eval's `toolExpectations`. Absent → the * grader still matches tools by the commands it sees in the transcript. */ declaredExecutables?: Array<{ name: string; howInvoked: string; kind: string; }>; /** * Opt-OUT of eval gating (for interactive use). By DEFAULT (false/absent) a * failing verdict returns exit EvalFailure (4) — fail-closed, so CI catches a * regression without an extra flag. When true, a failing verdict is downgraded * to Ok (0) and the pass/fail count lives only in the summary/grading.json. * Harness-broke codes (1/2/3) are unaffected either way. */ tolerateEvalFailure?: boolean; } export interface RunHarnessResult { harnessPath: string; exitCode: number; summary: string; } /** * Built-in cost/runtime safety ceilings applied PER executor/grader spawn. These * are the SAME values the harness applies as defaults, but exported as an explicit cap so * the CLI precedence layer (run.ts) can enforce a critical asymmetry: * * - a CLI flag (explicit operator intent, typed at the terminal for THIS run) * may RAISE a knob above the built-in ceiling; * - a value sourced from a committed `test.*` config (which rides along in an * untrusted subject repo you may only be testing) may only LOWER a ceiling, * never raise it — so cloning + testing a hostile skill can't silently * escalate the $5 / 50-turn / 5-minute budget the run bills against. * * `timeoutSeconds` is expressed in seconds to match the `--timeout`/`test.timeout` * unit (the harness multiplies by 1000 internally; see resolveTimeoutMs). */ export declare const SKILL_TEST_BUILTIN_CAPS: { readonly maxTurns: 50; readonly maxBudgetUsd: 5; readonly timeoutSeconds: number; }; /** * Resolve the effective PER-EVAL wall-clock timeout (ms). Each executor and * grader spawn is an independent, bounded-parallel unit (issue #145), so the * budget is a flat per-spawn ceiling — NOT scaled by the suite size the way the * old single serial run's budget was. An explicit `--timeout` (seconds) * wins; otherwise the flat {@link DEFAULT_TIMEOUT_MS} default applies. */ export declare function resolveTimeoutMs(opts: RunHarnessOptions): number; /** * Map an eval verdict to a process exit code. Default behavior (fail-closed): a * failing verdict escalates to EvalFailure (4) — distinct from the harness-broke * codes (1/2/3) so a CI consumer can `case $? in 0);; 4) tolerate;; *) hard fail;; esac`. * When `tolerateEvalFailure` is set (interactive opt-out), a failing verdict is * downgraded to Ok (0) and the count lives only in the summary/grading.json. */ export declare function verdictExitCode(allPassed: boolean, tolerateEvalFailure: boolean): SkillTestExitCodeValue; export declare function resolveStallMs(opts: RunHarnessOptions): number | undefined; export declare function resolveKnobs(opts: RunHarnessOptions): { model?: string; maxTurns: number; maxBudgetUsd: number; stallMs?: number; }; /** * Detect the plugin-root layout for a `{ path }` source. The resolver later COPIES * the source into a temp dir (losing its plugin ancestry), so we must detect here * — while the true on-disk source dir is still known — by resolving the path spec * against repoRoot and walking up for `.claude-plugin/plugin.json`. Non-`{path}` * sources (npm/url/vendored/workspace) have no local source tree to walk, so they * are always staged flat (returns undefined). undefined → flat staging. */ export declare function detectItemPluginLayout(source: SkillSource, repoRoot: string): StageItem['pluginLayout'] | undefined; export declare function makeStageItem(name: string, source: SkillSource, repoRoot: string, role: 'subject' | undefined, optional?: true): StageItem; /** * Build the full set of items `stageHarness` will stage: the subject, every * REQUIRED `--with` companion, and every OPTIONAL `--with-optional` companion. * Both `with` and `optional` STAGE the named companion and make it invocable — * they differ only in required-vs-optional resolution (issue #153; a `--with` * name that named no positional skill used to be silently dropped, never staged, * no manifest trace). Fail-closed on a DUPLICATE staged name (subject / `with` / * `optional` all share one namespace): the first repeat throws * {@link DuplicateStagedSkillError} rather than silently letting a later item * clobber an earlier one under the same staged slot. */ export declare function buildStageItems(opts: RunHarnessOptions, repoRoot: string): StageItem[]; export declare function buildResolveCtx(harnessRoot: string, repoRoot: string): ResolveSkillSourceContext; /** * Return a sensible dummy value for a given CLI flag so that value-validation * doesn't reject the argument before `--help` can short-circuit the session. */ export declare function flagDummyValueFor(flag: string): string; export declare function buildPreflightInput(evalsPath: string, pluginDirs: string[], opts: RunHarnessOptions, knobs: { maxBudgetUsd: number; }): PreflightInput; export declare function renderPreflightSummary(checks: { name: string; passed: boolean; message: string; }[]): string; /** * The single source of truth for "is this run acknowledged?": a dry-run never * executes skill code (so it is implicitly acknowledged), otherwise the caller * must pass --i-understand-this-runs-skill-code. Narrowed to the two fields it * reads so run.ts can reuse the SAME predicate to gate the pre-build ack check * (the harness Step-6 check and the run.ts pre-build check cannot diverge). */ export declare function isAcknowledged(opts: Pick): boolean; /** * Format a friction report for human consumption — one line per entry as * `[] : `. Pure; returns the empty string for no * entries so the caller can skip emitting anything. */ export declare function formatFrictionReport(items: readonly FrictionItem[]): string; /** * Resolve the PERSISTENT location where a bootstrap scaffold should be written * so "fill it in and re-run" actually works for the user (the staged copy is * ephemeral). When the subject is a local `{path}` source we scaffold next to * that real source dir; otherwise we anchor under the repo root by skill name. */ /** The subject skill's display name (trailing segment of the subject arg). */ export declare function subjectSkillName(opts: RunHarnessOptions): string; /** * Where this run READS its eval suite from, anchored at the subject's authored * source dir. * * `evalsRef` is `undefined` for the built-in convention and a config/flag value * otherwise, and the two are resolved differently ON PURPOSE: * * - The convention is VAT's OWN constant, not an adopter-supplied reference, so * it stays plain path math. Routing it through {@link resolveAssetReference} * would make `evals/evals.json` a bare specifier, and an installed package * named `evals` would then shadow every skill's own suite. * - An explicit value IS a config-supplied file reference, so it goes through * {@link resolveAssetReference} — the project's canonical resolver — and * therefore accepts a relative path, an ABSOLUTE path, or an npm bare * specifier honoring the target package's `exports` map. * * The absolute case is why this exists. A suite is the answer key, so it is * inherently repo-local; testing a skill you did not author requires supplying * one from outside its tree. `safePath.join` silently folded an absolute path * into `/`, which does not exist — so the run did not fail, it * bootstrapped a starter template at the bogus location and graded that. */ export declare function resolveScaffoldEvalsPath(opts: RunHarnessOptions, repoRoot: string, evalsRef: string | undefined): string; /** * The vat-only directory a subject's eval suite is relocated to when the resolved * artifact is the only place it exists (npm/url/vendored, or a source tree that * ships its own evals). Deliberately OUTSIDE the harness root — the harness root is * the executor's sandbox, and the suite is the answer key to the task the executor * is performing. Created 0700 and removed in the run's cleanup, exactly like * {@link resolveGraderOutDir}. Pure (derives a path only). */ export declare function resolveEvalSuiteHoldDir(dirToken: string): string; /** * Locate the eval suite the run will read, WITHOUT it ever being reachable by the * executor. Precedence, and why: * * 1. **The authored source** (`subjectScaffoldDir`, else the subject path). This is * the copy a developer edits, so a re-run always reflects the edit — even under * `--no-build`, where the built dist's copy could be stale. * 2. **The vat-only hold dir**, when staging harvested a suite that exists nowhere * else (a fetched artifact). Its layout mirrors an authored evals dir, so * `fixtures/` resolve relative to it unchanged. * 3. Neither → `undefined`, and the caller bootstraps a template (exit 3). * * Returns the suite's absolute path; its `dirname` is the base for each eval's * declared input `files`. */ export declare function resolveEvalSuitePath(input: { opts: RunHarnessOptions; repoRoot: string; /** Explicit `test.evals` / `--evals` value, or `undefined` for the convention. */ evalsRef: string | undefined; /** Defaulted subpath — names the suite FILE inside a held artifact. */ evalsSubpath: string; holdDir: string; subjectEvalSuiteHeld: boolean; }): string | undefined; /** Parse the staged eval suite and materialize each eval's input `files` into * `/workspaces//`. Returns the workspaces root, the parsed * {@link EvalSuite} (so the eval loop has the entries without re-reading), and the * declared eval count (derived from the suite). The dir is wiped first so a reused * harness root cannot leak a prior run's inputs. Throws {@link EvalInputError} * (mapped by the caller to exit 2) on a bad suite or a missing input file. */ export declare function stageWorkspacesForRun(evalsPath: string, harnessRoot: string): { workspacesRoot: string; declaredEvalCount: number; suite: EvalSuite; }; /** Inputs for the dry-run summary string. */ export interface DryRunSummaryInput { /** True when the resolved subject is buildable (a real run would build + stage it). */ wouldBuild: boolean; /** * When wouldBuild is true: true = the dry-run staged the existing dist without * rebuilding (may be stale); false = no dist existed, fell back to source dir. */ dryRunStagedExistingDist?: boolean; /** Absolute path to the written provenance.json (already on disk). */ provenancePath: string; /** Content fingerprint from the staged manifest. */ provenanceFingerprint: string; /** Number of entries in the staged manifest. */ provenanceEntryCount: number; /** The assembled model flag string for the executor (e.g. `--model claude-opus-4-8`). */ modelFlag: string; /** Number of declared evals a real run would execute. */ evalCount: number; /** Bounded-parallel executor→grader pipeline width a real run would use. */ concurrency: number; /** Pinned grader/judge model a real run would grade with. */ graderModel: string; } /** * Build the "stale dist" warning lines for a `--dry-run` preview that staged an * EXISTING built dist WITHOUT rebuilding it (source may have moved on since). * The ONE construction shared by {@link buildDryRunSummary} (the SUBJECT, no * `roleLabel`: "This preview…") and `resolveCompanionSpec` in the CLI's run.ts * (a COMPANION, `roleLabel` set to e.g. "companion 'foo' (declared skill * 'bar')": "This preview of companion 'foo' (declared skill 'bar')…") — the * identical fact (a stale dist previewed) must warn EITHER role, worded so two * stale warnings in one run are distinguishable. Keeping it as one construction is * the point: when the warning existed only inside the subject's summary, a * companion previewed from a stale dist warned nobody while the subject warned * loudly for the same fact. Exported so run.ts reuses this construction rather * than copying the string. */ export declare function buildStaleDistWarningLines(roleLabel?: string): string[]; /** * Build the dry-run summary string. Pure function so it can be unit-tested * without running the full harness. * * The summary covers three scenarios: * 1. Declared (buildable) subject — no dist existed, fell back to source dir. * 2. Declared (buildable) subject — existing dist was staged WITHOUT rebuilding * (potentially stale). * 3. Plain source subject — staged as-is, no build step. * * Always includes the assembled spawn command, the staged-manifest entry count + * fingerprint, and the provenance.json path so a stale tree is visible at a glance. */ export declare function buildDryRunSummary(input: DryRunSummaryInput): string; export interface CleanupHarnessOptions { /** User asked to retain the harness dir (`--keep`) — never remove. */ keep: boolean; /** * True only when the harness itself created the dir under the OS tmp dir (no * `--out`/`--workdir`). A user-supplied location is theirs to keep, so we only * auto-remove the dir we created. */ created: boolean; } /** * Remove the harness directory after a run so staged untrusted skill bytes and * prompts do not accumulate in OS tmp. No-op when the user asked to keep it, when * the dir is a user-supplied location (`--out`/`--workdir`), or when it is already * gone. Idempotent and never throws — it runs from a `finally`, so it must not * mask the run's real outcome. * * SAFETY: re-asserts the root is not a symlink immediately before removal (via * `lstat`, which does NOT follow the link). A root swapped to a symlink between * the run and cleanup is left in place rather than followed — `rmSync(recursive)` * could otherwise delete the symlink's target outside tmp. */ export declare function cleanupHarness(harnessRoot: string, opts: CleanupHarnessOptions): void; /** One unit of executor→grader work: an eval + which arm (with/without skill). */ export interface EvalWorkItem { entry: EvalEntry; arm: 'with' | 'without'; } /** * Build the per-eval work items for a set of evals (one tier's worth, or a whole * suite). Every eval gets a WITH arm (skill present). When `baseline` is set, * every eval ALSO gets a WITHOUT arm (skill absent) so vat can record an * informational A/B — the WITHOUT arm never contributes to the pass/fail verdict * (see {@link partitionFragmentsByArm}) and never drives tier gating. Pure + * unit-testable. */ export declare function buildEvalWorkItems(evals: readonly EvalEntry[], baseline: boolean): EvalWorkItem[]; /** * Partition graded fragments into the WITH arm (the authoritative verdict + * grading.json) and the WITHOUT arm (baseline.json, informational only). A * fragment with no `arm` (or `arm: 'with'`) is a WITH-arm fragment. Pure + * unit-testable. */ export declare function partitionFragmentsByArm(fragments: EvalFragment[]): { withArm: EvalFragment[]; withoutArm: EvalFragment[]; }; /** * The vat-only grader dir for a run: `/vat-skill-grade-/`. It is * deliberately OUTSIDE the harness root (the skill's `--add-dir` sandbox) and * created 0700, so it is invisible to Claude's own permission model and to any * OTHER OS user. Pure (derives a path only). * * SCOPE OF THE GUARANTEE — read honestly. `--add-dir`/`bypassPermissions` is * Claude's permission model, NOT an OS sandbox: the executor's skill code runs * as the SAME OS uid as vat, so it CAN read a 0700 dir this process owns. The * layered defenses here — dir outside the sandbox, named by an unpredictable * `dirToken` (distinct from the integrity nonce, which never touches the dir * name or any argv and travels only via grader stdin), the nonce echoed back * per fragment, and each fragment file unlinked the instant vat reads it — RAISE * THE BAR against same-uid forgery (a forger must now win a per-fragment * read→overwrite race against a secret it cannot predict, with no persisted copy * to harvest at leisure). They do NOT amount to true isolation from same-uid * code. The complete fix is running the grader under a SEPARATE OS uid / * container; that is tracked as a follow-up (see CHANGELOG "Security" notes) and * is the only thing that closes the residual race outright. */ export declare function resolveGraderOutDir(dirToken: string): string; /** * The executor working directory for one eval: its staged input workspace * `/` when the eval declares input `files`, else undefined * (the executor then defaults to the staged subject dir). Pure + unit-testable. */ export declare function resolvePerEvalWorkspaceDir(entry: EvalEntry, workspacesRoot: string): string | undefined; /** * Best-effort removal of a vat-only tmp dir that lives OUTSIDE the harness root — * the grader fragment dir and the held eval suite. Never throws (it runs from * cleanup, where masking the run's real outcome would be worse than a leftover 0700 * tmp dir) and refuses to follow a symlinked root. */ export declare function removeVatOnlyDir(dir: string | undefined): void; /** The results/ artifacts vat is the SOLE writer of, resolved for one run. */ export interface ArtifactPaths { gradingOut: string; frictionOut: string; baselineOut: string; toolEvalOut: string; } /** Resolve the run's grading/friction/baseline/tool-eval artifact paths under * `resultsDir`. Single source of truth for the filenames (used by the * pre-pipeline stale wipe AND the post-merge writer). */ export declare function resolveArtifactPaths(resultsDir: string): ArtifactPaths; /** * Remove any PRIOR run's artifacts before this run writes its own. The harness * root is deterministic per skill-set and reused across runs (`--keep`/`--out`, or * after a crash/SIGKILL that preempted cleanup), so a stale grading/friction/ * baseline/tool-eval.json can otherwise survive into a run that throws BEFORE the merge — * where the `finally` would then echo the PRIOR run's friction as if it were this * run's. Wiping all three up front closes that cross-run leak. Best-effort * (`force: true`) — a missing file is fine. */ export declare function wipeStaleArtifacts(paths: ArtifactPaths): void; /** * The COMPOSITE run verdict (issue #145 Phase T): the run passes only when BOTH * the prose-expectation grading passed AND every tool-expectation verdict passed. * Tool verdicts live in tool-eval.json (a SEPARATE channel — C2); this combines * the two at the exit-code layer WITHOUT mixing the channels' data. When no eval * declared `toolExpectations`, `toolEval.evals` is empty and this equals * `outputAllPassed`. Pure + unit-testable. */ export declare function computeCompositeVerdict(outputAllPassed: boolean, toolEval: ToolEvalReport): boolean; /** * The run's final pass/fail after cost-tiered fail-fast: the {@link * computeCompositeVerdict} of the tiers that RAN, AND no tiers were skipped. A * fail-fast run that gated higher tiers is NEVER a pass (skipped ≠ passed) — this * forces `false` so the exit code is EvalFailure (4), never downgraded to 0 by the * composite path alone. Pure + unit-testable. */ export declare function resolveCompositeAllPassed(outputAllPassed: boolean, toolEval: ToolEvalReport, skipped: SkippedEvalsSummary | undefined): boolean; /** * Running total of spend across every executor+grader session in a run (adopter * follow-up). `sessions` counts only sessions that REPORTED a `total_cost_usd`, so * `≈$${totalUsd} across ${sessions} sessions` is always internally consistent (the * sum is over exactly those sessions). Mutated in place by {@link recordSessionCost} * — safe under the cooperative (single-threaded) pipeline concurrency. */ export interface RunCostSummary { totalUsd: number; sessions: number; } /** Fold one session's `total_cost_usd` into the accumulator; a non-number (mock spawn / missing result) is ignored. */ export declare function recordSessionCost(acc: RunCostSummary, totalCostUsd: number | undefined): void; /** * The ` | ≈$0.42 across 6 sessions` spend suffix for the summary line, or `''` * when no session reported a cost (e.g. every spawn was a test mock) so the suffix * never adds noise to a run with no cost signal. Pure + unit-testable. */ export declare function formatRunCostSuffix(cost: RunCostSummary | undefined): string; /** * The run's human summary line, computed from the COMPOSITE verdict so an * output-pass with a failing tool verdict still reads FAIL. The prose-expectation * counts (`passed/total`) come from the grading verdict; when any tool-expectation * verdict failed, a `(N tool)` suffix names how many — so a composite FAIL whose * OUTPUT counts look all-green (e.g. `FAIL 3/3 (1 tool)`) is self-explaining. Pure. */ export declare function buildRunSummary(verdict: GradingVerdict, toolEval: ToolEvalReport, compositeAllPassed: boolean): string; /** * The run summary line, appending the fail-fast SKIPPED note when the tier gate * stopped higher tiers. Legibility is required — the skipped tiers are named on * their own line, never silently dropped. Pure. Note that a run with skipped * tiers ALWAYS reads FAIL (skipped ≠ passed forces `compositeAllPassed` false at * the call site), so the base line is already FAIL when the note is present. */ export declare function buildRunSummaryWithSkips(verdict: GradingVerdict, toolEval: ToolEvalReport, compositeAllPassed: boolean, skipped: SkippedEvalsSummary | undefined, cost?: RunCostSummary): string; /** * Domain orchestrator for `vat skill test run`. Pure of process.exit — all * exit-code decisions live in the caller (run.ts). */ export declare function runSkillTestHarness(opts: RunHarnessOptions): Promise; //# sourceMappingURL=run-harness.d.ts.map