import { type SubmissionIssue } from "audit-tools/shared"; import type { RemediationState } from "../../state/store.js"; import { type RemediationBlock, type RemediationHostHandoffRecord, type RemediationItemState, type RemediationPlan } from "../../state/types.js"; import { REMEDIATION_HOST_WORKLOAD_CONTRACT_VERSION as WORKLOAD_CONTRACT_VERSION } from "../types.js"; declare const STATE_CONTRACT_VERSION: "remediate-code-state/v1alpha1"; export type UnsupportedRetiredRemediationState = "unsupported_retired_state"; export type CurrentRemediationHostState = RemediationState & { readonly contract_version: typeof STATE_CONTRACT_VERSION; readonly status: "implementing"; readonly plan: RemediationPlan; readonly items: Record; }; export interface RemediationHostWorkItem { readonly id: string; readonly finding_ids: readonly string[]; readonly allowed_files: readonly string[]; readonly baseline_commit: string; readonly prompt: { readonly text: string; readonly sha256: string; }; readonly required_tests: readonly string[]; readonly result_path: string; readonly token_estimate: number; } export interface RemediationHostWorkload { readonly contract_version: typeof WORKLOAD_CONTRACT_VERSION; readonly run_id: string; readonly work_items: readonly RemediationHostWorkItem[]; } export interface PreparedRemediationHostHandoff { readonly workload: RemediationHostWorkload; readonly workload_path: string; /** Persist this in RemediationState before exposing the workload to the host. */ readonly handoff_record: RemediationHostHandoffRecord; } /** * Remediation's issue vocabulary: the SHARED submission codes plus this draw's * own domain corroboration codes. * * The submission half is imported, never restated — a submission that is * missing, unparseable, contract-invalid, or a duplicate identity means exactly * the same thing on both sides of the pipeline, and the two used to spell it * differently (`result_missing` here, a bare `null` in the audit ingest). The * git/worktree/test half stays here: audit has no analogue and dragging * `commit_not_landed` into the shared core would suggest it could emit one. */ export declare const REMEDIATION_ISSUE_CODES: readonly ["submission_missing", "submission_malformed", "submission_contract_invalid", "submission_rejected", "duplicate_submission_id", "workload_missing", "workload_invalid", "trusted_binding_missing", "commit_missing", "commit_not_landed", "baseline_not_ancestor", "changed_files_mismatch", "run_start_dirty_overlap", "required_test_failed", "required_test_timed_out", "required_test_output_overflow", "dependency_missing", "block_contract_invalid", "recovery_unrecorded", "tree_moved_between_phases"]; export type RemediationIssueCode = (typeof REMEDIATION_ISSUE_CODES)[number]; export type RemediationHostIngestIssue = SubmissionIssue; export interface RemediationHostIngestSummary { readonly accepted_count: number; readonly completed_work_item_ids: readonly string[]; readonly pending_work_item_ids: readonly string[]; readonly issues: readonly RemediationHostIngestIssue[]; readonly state_changed: boolean; readonly state: CurrentRemediationHostState; } /** * Resolve the submission validator the INGEST applies to one work item, plus * the directory that work item's submission is bound to. * * The hand-recovery verb draws from this rather than carrying a check of its * own: `parseResult` is the ingest's contract gate, so a rescued submission has * to satisfy exactly what a host-written one would. Everything downstream of * the shape gate — git corroboration, write-scope, the required-test rerun — * still runs at the next ingest, so recovery lands a submission, it does not * accept one. * * Returns `null` when there is no live workload naming that work item; a lane * with no contract to check against must never read as "passes". */ export declare function remediationSubmissionBinding(params: { readonly root: string; readonly artifactsDir: string; readonly runId: string; readonly workItemId: string; }): Promise<{ readonly submissionDir: string; readonly validate: (value: unknown) => SubmissionIssue | null; } | null>; /** Absolute result-file path owned by the current host-handoff boundary. */ export declare function remediationHostResultFilePath(params: { readonly root: string; readonly artifactsDir: string; readonly runId: string; readonly workItemId: string; }): string; /** * Pure dependency/phase partitioning shared by next-step and the host workload * boundary. Only level zero is safe to emit before the host has landed and * verified its prerequisites. */ export declare function hostDependencyLevels(state: Pick): RemediationBlock[][]; /** * Pre-computed required-test verdicts, keyed by `root` + command: `null` = * green, a string = the failure detail. * * This is the recovery path's ANSWER TABLE, not a lazy cache. A required-test * rerun is a `spawnSync`, which blocks the event loop for its whole duration — * so running one inside the state lock would starve the lock's own heartbeat * timer and let a second acquirer reclaim the lock as stale mid-hold. The * recovery verb therefore runs every distinct command ONCE, up front and * unlocked ({@link precomputeRecoveryTestVerdicts}), and hands the finished * table to the locked phase, which only ever READS it. * * Two consequences are deliberate. A command absent from the table is treated * as FAILED, never spawned — fail-closed is the only answer that keeps the * no-spawn-under-the-lock property mechanical rather than remembered. And the * table is recovery-only: a `targeted_command` is host-authored and need not be * idempotent (one that appends to a log, bumps a counter, or is flaky produces * a genuinely different second run), so collapsing spawns is a behavior change. * The normal lane passes `null` and stays byte-identical to the pre-recovery * behavior — every command spawns once per work item, exactly as before. */ export type RemediationRequiredTestVerdicts = ReadonlyMap; /** * A required-test rerun that did not pass, CLASSIFIED. * * `outcome` is the whole point. A suite that exceeded its deadline, a suite that * outran the capture buffer, and a suite that returned non-zero are different * facts — the first two are environment signals, only the last is the work being * wrong — and they used to arrive as one joined string (`" (exit 1)"` / * `" (ETIMEDOUT)"`) that a caller could only tell apart by parsing prose. * Output was not captured at all (`stdio: "ignore"`), so an operator staring at * a red ingest had nothing to read. * * `output_overflow` is separate from `timed_out` because node kills BOTH an * over-deadline and an over-`maxBuffer` child with a signal: a discriminator * that read `signal !== null` as "the deadline fired" reported a command that * was running fine and merely verbose as a hang. `output_overflow` does * NOT claim the tests were fine — see {@link describeRequiredTestFailure}; the * verdict is simply unknown, because a child killed mid-stream may equally have * been on its way to exit 3. */ export interface RequiredTestFailure { readonly command: string; readonly outcome: "failed" | "timed_out" | "output_overflow" | "spawn_error"; readonly exit_code: number | null; readonly stdout: string; readonly stderr: string; /** * The signal that killed the child, when one did and the runner's own caps did * not (an operator `kill`, an OOM reaper). Absent on every other outcome — * there is no signal to report — which is why it is optional rather than * `string | null`: a caller that reads it gets a name or nothing, never a * placeholder to special-case. */ readonly signal?: string; } /** * The ONE place a required-test command is spawned. * * `timeoutMs` is a parameter so the deadline is exercisable: a hang is a * first-class outcome of this function, and an outcome that can only be reached * by waiting ten real minutes is an outcome nothing ever tests. */ export declare function runRequiredTest(root: string, command: string, timeoutMs?: number): Promise; /** * Run every required-test command a recovery ingest could need, ONCE each, and * return the finished verdict table. Call this OUTSIDE the state lock — that is * the entire point (see {@link RemediationRequiredTestVerdicts}). * * Candidates are the work items with at least one still-pending finding whose * result file is present and parses as JSON; an item with no result file is * refused before its tests would ever run, so spawning for it is pure cost. The * filter is deliberately generous otherwise — over-inclusion costs one spawn, * while under-inclusion becomes a fail-closed refusal of a good result. */ export declare function precomputeRecoveryTestVerdicts(params: { readonly root: string; readonly artifactsDir: string; readonly runId: string; readonly state: unknown; }): Promise; export declare function prepareRemediationHostHandoff(params: { readonly root: string; readonly artifactsDir: string; readonly runId: string; readonly baselineCommit: string; readonly state: unknown; }): Promise; /** * Consume the host's landed results for the trusted workload. * * ## The `recovery` option, and what it actually buys * * A trusted binding can be stranded: a post-prepare `git commit --amend` (or * any history rewrite) re-mints the baseline the workload was bound to, leaving * it ORPHANED — contained by no ref and unreachable from HEAD. Every commit the * host then lands sits on the re-minted line, so `baseline → landed` ancestry is * false for all of them, and re-preparing does not help: a fresh binding must be * minted at HEAD, and HEAD is a DESCENDANT of the landed work. The items are * unacceptable under every preparable binding, with real, reachable, * correctly-scoped commits on disk. * * `recovery` waives ONE check — baseline→landed ancestry — and only when * the baseline is genuinely orphaned by BOTH probes in `gitCommitIsOrphaned`: no * branch/tag/remote ref contains it, and it is not reachable from HEAD. A * baseline the repository still keeps (an unmerged feature branch, a tag, a * remote ref) also fails the ancestry test when work lands elsewhere, and that * is the ordinary stale-worker case — recovery refuses it. Every other * corroboration check runs unchanged (the landed commit exists and is reachable * from HEAD; its mechanically derived changed files exactly equal * `changed_files` and lie within the prompt-bound `allowed_files`; no overlap * with run-start dirt; the required tests rerun green), `parseResult` stays * fully strict, and dependency/phase eligibility is enforced exactly as on the * normal lane. * * RESIDUAL RISK, stated plainly: under an orphaned baseline the evidence bar * drops to "the claimed commit is reachable from the current green HEAD and * matches this item's scope exactly". That CANNOT prove the work was built on * the trusted baseline — a commit landed from a stale or unrelated starting * tree satisfies it as long as its own file set stays in scope. Ancestry is the * check that would have caught that, and it is the one being waived. Which is * precisely why the relaxation costs an explicit operator verb, is gated on the * orphan precondition, and is marked `accepted_via_recovery` on the submission * ledger before the item lands — and why the normal lane keeps the full check. * * In recovery mode this function performs NO required-test spawn: the verdicts * arrive pre-computed on the `recovery` option, and a command missing from that * table is treated as failed. Its caller runs the tests first, unlocked — see * `recoverIngestHostResults`. */ export declare function ingestRemediationHostResults(params: { readonly root: string; readonly artifactsDir: string; readonly runId: string; readonly state: unknown; /** * Operator-explicit recovery mode (the `recover-ingest` verb). ABSENT on * every normal-lane call, where behavior is unchanged. * * It carries the pre-computed required-test verdicts rather than a bare * boolean so the no-spawn-while-locked property is structural: there is no * way to ask for recovery without having already run the tests outside the * lock. See {@link precomputeRecoveryTestVerdicts}. */ readonly recovery?: { readonly requiredTestVerdicts: RemediationRequiredTestVerdicts; }; }): Promise; export {}; //# sourceMappingURL=hostHandoff.d.ts.map