/** * Batch Runner module (batch-runner DESIGN D5, D6, D7, D8, D9). * * Executes an already-parsed manifest (D2) against an already-computed * provider assignment (D4, `batch-assign.ts`): per op it builds * `{...handlerDeps, provider, invocation}` where `invocation` is a * capture adapter buffering stdout/stderr, and calls the SAME handler * the main dispatch switch calls, forced to data mode, through a * bounded worker pool (manifest-order scheduling, drain-safe fail-fast). * * Process effects: the runner performs exactly ONE stdout write — the * summary envelope (D6) through `formatSuccessOutput` for the ambient * output mode — on the invocation adapter it is handed (the global * one). Everything the ops emit stays inside their per-op records. * Batch-level failures (validation, alignment) throw BEFORE any write, * so the caller (`handleBatch`, Ticket 4) owns the process-level error * envelope. * * The pool task wraps every handler invocation in try/catch: handlers * that throw `ValidationError` synchronously BEFORE entering * `invokeCommand` (repo parse-level checks, search `--type`/`--topic` * mutual exclusion, vision's operation switch) are converted into a * per-op failure whose `stderr` is byte-identical to what * `invokeCommand`'s own catch would have produced (`redactSecrets` → * `formatErrorOutput` in the op's data mode). * * D9: after a SUCCESSFUL op that declared an `output` target, the * captured stdout is persisted through a write-temp-then-rename seam so * readers never observe a half-written file. A write failure never * flips `ok` or the envelope counters — it is recorded as * `outputWriteError` on the op's record, so `total = ok + failed` * always holds. Ops that failed, and ops never scheduled because of * `--fail-fast`, write nothing. */ import type { OutputMode } from "./output.js"; import type { AllowedBatchCommand, BatchManifest } from "./batch-manifest.js"; import type { BatchProviderAssignment } from "./batch-assign.js"; import type { CommandInvocationAdapter } from "../command-invocation.js"; import type { HandlerDependencies } from "../index.js"; import type { ProviderId } from "../providers/types.js"; /** Parallel-by-default posture (DESIGN D8); `vision batch` overrides to 1. */ export declare const BATCH_DEFAULT_CONCURRENCY = 4; /** Hard ceiling for `--concurrency` (validation, not clamping). */ export declare const BATCH_MAX_CONCURRENCY = 8; /** `stderr` of an op that was never scheduled because of `--fail-fast`. */ export declare const BATCH_NOT_RUN_STDERR = "not run (--fail-fast)"; /** * A batch operation handler: the same shape the main dispatch switch * calls (`handleX(commandArgs, outputMode, handlerDeps) → exit code`). */ export type BatchOperationHandler = (args: string[], outputMode: OutputMode, deps: HandlerDependencies) => Promise; /** Injected dependencies. All of them are doubles in tests. */ export interface BatchRunnerDeps { /** The same object the main switch passes (`handlerDepsWithSelection`). */ readonly handlerDeps: HandlerDependencies; /** One handler per allowed command; the spread-overrides seam is D5. */ readonly handlers: Readonly>; /** The GLOBAL invocation adapter — receives exactly one summary write. */ readonly invocation: CommandInvocationAdapter; /** Ambient `--output-format`; the summary is formatted through it. */ readonly outputMode: OutputMode; readonly now?: () => number; /** * D9 per-op output write seam (temp file write). Defaults to the real * `node:fs/promises` `writeFile`; tests inject doubles to observe or * fail the write. */ readonly writeOutputFile?: (path: string, data: string) => Promise; /** * D9 per-op output rename seam (atomic land over the target). * Defaults to the real `node:fs/promises` `rename`. */ readonly renameOutputFile?: (from: string, to: string) => Promise; /** * D9 temp-file cleanup seam (best-effort removal after a failed * write or rename). Defaults to the real `node:fs/promises` `rm` * with `force: true`. */ readonly removeOutputFile?: (path: string) => Promise; } export interface BatchRunOptions { /** Integer in 1..8 (D8); defaults to {@link BATCH_DEFAULT_CONCURRENCY}. */ readonly concurrency?: number; /** Stop scheduling on the first failed completion; drain in-flight ops. */ readonly failFast?: boolean; /** * D7: preview the assignment and run the pre-dispatch gates WITHOUT * executing anything — no transport (`descriptor.create()`), no cache * reads/writes, no per-op output files. Records become * {@link BatchDryRunRecord}s and the envelope carries `dryRun: true`. */ readonly dryRun?: boolean; } /** One manifest operation's outcome. `results[]` stays 1:1 with the manifest. */ export interface BatchRunRecord { readonly name: string; readonly command: AllowedBatchCommand; readonly ok: boolean; readonly exitCode: number; /** The assignment made visible (D6): pin or round-robin distribution. */ readonly resolvedProvider: ProviderId; readonly stdout?: string; readonly stderr?: string; readonly durationMs: number; /** * Per-op output file target, present on every SCHEDULED op that * declared one (the file exists only after a successful write, D9). */ readonly output?: string; /** * D9 write-path failure message (a failed write/rename, or a * successful op that emitted no stdout so nothing was written); * never flips `ok` or the counters. */ readonly outputWriteError?: string; } /** D7 dry-run gate outcome for one resolved provider. */ export type BatchDryRunReason = "ready" | "provider not configured" | "capability not advertised"; /** * D6 `DryRunRecord` — replaces {@link BatchRunRecord} element-for-element * when `envelope.dryRun === true` (D7): the assignment ran and the * resolved provider passed (or failed) the pre-dispatch gates, but no * transport was built, no handler ran, and nothing was written — hence * no `stdout`/`stderr`/`output`/`outputWriteError` keys at all. */ export interface BatchDryRunRecord { readonly name: string; readonly command: AllowedBatchCommand; readonly ok: boolean; readonly exitCode: number; /** The assignment preview: pin or round-robin distribution (D4). */ readonly resolvedProvider: ProviderId; readonly reason: BatchDryRunReason; readonly durationMs: number; } /** The stable v1 summary envelope (D6, D12). Written to stdout exactly once. */ export interface BatchRunEnvelope { readonly schemaVersion: 1; readonly total: number; readonly ok: number; readonly failed: number; readonly durationMs: number; readonly concurrency: number; /** Present only in dry runs (Ticket 6, D7). */ readonly dryRun?: true; /** Present only when `--fail-fast` was set AND triggered (D6). */ readonly failFast?: true; readonly results: readonly (BatchRunRecord | BatchDryRunRecord)[]; } /** * Execute a parsed manifest under a computed assignment (DESIGN D5/D6/D8). * * Scheduling is manifest order with a free-worker-takes-next-op pool; * max-active never exceeds `concurrency`. `--fail-fast` stops scheduling * after the first failed completion, drains in-flight ops (never * aborts), and backfills never-scheduled ops with * `{ok: false, exitCode: 1, stderr: "not run (--fail-fast)"}` so * `results[]` stays 1:1 with the manifest and `total = ok + failed` * always holds. * * Returns the envelope (for callers like the `vision batch` wrapper that * also persist it) and the process exit code: any failed op → 1. The * single stdout write happens here, before returning. */ export declare function runBatch(manifest: BatchManifest, assignments: readonly BatchProviderAssignment[], deps: BatchRunnerDeps, options?: BatchRunOptions): Promise<{ envelope: BatchRunEnvelope; exitCode: number; }>; //# sourceMappingURL=batch-runner.d.ts.map