import { type PreparedOutputRoot } from "./selected-output-paths.js"; export declare const RUN_STATUS_SCHEMA = "humanish.run-status.v1"; /** The file, relative to the run directory. */ export declare const RUN_STATUS_FILE = "status.json"; /** How often a live run touches `updatedAt`. */ export declare const RUN_STATUS_TOUCH_MS = 5000; /** * A `running` record whose `updatedAt` is older than this is INTERRUPTED, not alive: the process * died without finalizing (a dropped SSH, a killed terminal, a crash). Three touch intervals of * slack so an ordinary scheduling hiccup or a slow disk never mislabels a healthy run. */ export declare const RUN_STATUS_STALE_MS: number; /** Which manifest a run came from, when it came from one. */ export interface RunLabProvenance { /** The lab id as declared in its manifest (`config.id`). */ id: string; /** Repo-relative manifest path, when the run came from a file on disk. */ path?: string; /** `committed` = humanish/labs, `ignored` = a local overlay, `explicit` = a path the operator passed. */ origin?: "committed" | "ignored" | "explicit"; } export type RunStatusState = "running" | "finished"; /** The outcome summary a finalized record carries. Derived from the bundle; never authoritative. */ export interface RunStatusOutcome { /** `review.verdict` verbatim. */ verdict?: string; /** True when the run's own envelope reported success. */ ok?: boolean; /** `review.participants` counts, when the run recorded any. */ participants?: { total: number; reachedGoal: number; reportedFriction?: number; }; /** The run-level estimate, `null` when declared absent (never coerced to 0). */ estimatedCostUsd?: number | null; durationMs?: number; } export interface RunStatusRecord { schema: typeof RUN_STATUS_SCHEMA; runId: string; state: RunStatusState; mode: "dry-run" | "live"; /** Absent when the run did not come from a lab manifest (a library caller, a bare `run`). */ lab?: RunLabProvenance; /** The pid that owns the run, for local liveness and (later) cancellation. */ pid: number; startedAt: string; /** Refreshed on a fixed cadence while the run is alive; the staleness signal. */ updatedAt: string; completedAt?: string; outcome?: RunStatusOutcome; } export interface RunStatusHandle { /** Resolves once the initial record has landed on disk. The write itself is fire-and-forget — * starting a run must never block on its own index — but a caller that needs the record to * exist before proceeding (a test, or a launcher that hands the run id to another process) * can await this instead of polling. */ readonly started: Promise; /** Write `updatedAt` now. Called by the internal cadence; exposed for tests and for backends * that want to mark a phase boundary. Never throws. */ touch(): Promise; /** Finalize: state `finished`, `completedAt`, and the derived outcome. Stops the cadence. * Idempotent — a second call is a no-op, so a backend with several exit paths is safe. */ finish(outcome?: RunStatusOutcome): Promise; /** * Stop the cadence WITHOUT claiming an outcome. For a path that is abandoning the run: the record * stays `running` and goes stale, which is the honest reading. * * Resolves when any IN-FLIGHT write has settled, so a caller that is about to delete the run * directory can be sure nothing is still writing into it. Clearing the interval alone is not * enough — a write started microseconds earlier is still on its way to disk. */ stop(): Promise; } export interface BeginRunStatusOptions { runId: string; mode: "dry-run" | "live"; lab?: RunLabProvenance; /** Injectable clock (tests freeze it; the repo's `now()` convention). */ now?: () => number; /** Injectable pid so a test never depends on the real process id. */ pid?: number; /** Cadence override; 0 disables the interval entirely (tests drive `touch()` themselves). */ touchMs?: number; } /** A no-op handle, so a caller that cannot write status still has a uniform interface. */ export declare function inertRunStatus(): RunStatusHandle; /** * Bind a run's status records to the lifetime of the run itself. * * WHY THIS IS NOT A `finally` AT EACH BACKEND. A run function does not have one exit — the lab * backends have 18 early `return`s between opening the record and finalizing it, every one of them * a fail-closed path (bad subject, packing failure, missing key). Relying on each of those to * remember the record is the same per-call-site discipline that already failed once on this * contract, and the failure is silent: the run is over, the cadence keeps ticking, and the record * keeps saying `running` — a listing surface then shows a dead run as alive for as long as the * process lives. CI caught it as a deleted run directory racing a still-live writer. * * So the scope owns the lifetime. Control returning from the run function IS the run ending, * whatever path it took, and any record still open at that moment is finalized with NO outcome: * the run ended and we have no verdict to report. That is honest and it is different from both * neighbours — a backend that finalized properly carries its real outcome, and a process that * CRASHED never reaches here at all, leaving a `running` record to go stale and read as * `interrupted`, which is exactly what happened. */ export declare function withRunStatusScope(fn: () => Promise): Promise; /** * Start a run's status record and keep it fresh. Fire-and-forget by design: a status write that * fails must never fail the run it describes, so every write swallows its error. The interval is * `unref`'d — this file can never be the reason a process stays alive. */ export declare function beginRunStatus(runPaths: PreparedOutputRoot, options: BeginRunStatusOptions): RunStatusHandle; /** The three ways a run reads from disk. `interrupted` is a `running` record gone stale. */ export type RunLiveness = "running" | "interrupted" | "finished"; /** * Classify a status record. Pure, so the TUI, the CLI and tests share one definition of "alive". * `nowMs` is passed in rather than read, so a classification is reproducible. */ export declare function classifyRunStatus(record: Pick, nowMs: number, staleMs?: number): RunLiveness; /** Shape guard for a record read off disk. Unknown extra fields are tolerated (additive contract). */ export declare function isRunStatusRecord(value: unknown): value is RunStatusRecord; /** * The legacy bridge: infer a lab id for a bundle written BEFORE this contract, where the only * attribution was the `lab:` convention on persona/scenario source strings. Deliberately * conservative — it reads the convention and nothing else, and a `lab:` prefix with an empty * remainder is not an id. Ids may contain colons (`oss:meta`), so only the FIRST segment is * stripped. Returns undefined when the bundle carries no such marker. */ export declare function inferLegacyLabId(bundle: { persona?: { source?: string; }; scenario?: { source?: string; }; }): string | undefined; /** A monotonic elapsed-ms helper for callers that need a duration without trusting wall clocks. */ export declare function elapsedMsSince(startNs: bigint): number;