import { type AutomaticAnalysisHooks, type AutomaticAnalysisResult } from "./automatic-analysis-completion.js"; import type { ActorRuntimeProvenance } from "./actor-contract.js"; import type { ActorCompletionReason, ActorPersonaRef, ActorStatus, ActorTrace } from "./actor-contract.js"; import { type RunLabProvenance } from "./run-status.js"; import type { LabConfig, LabScenarioCaps, LabRuntimeAuth } from "./lab-config.js"; import { type E2BDesktopModule } from "./e2b-desktop-launch.js"; import { renderObserver, type ObserverResult } from "./observer.js"; import { type ResolvedPersona } from "./persona.js"; import { type PreparedSelectedOutputDirectory } from "./selected-output-paths.js"; import { type RunAdapterScore, type RunBundle, type RunFeedbackCandidate, type RunScorerProvenance } from "./run.js"; import { TERMINAL_AGENT_NOT_IMPLEMENTED_CODE } from "./terminal-agent-actor.js"; /** Provider-neutral metadata constant: the lane's non-secret tag (mirrors CUA_ACTOR_LAB_PROVIDER_METADATA). */ export declare const TERMINAL_PRODUCT_LAB_PROVIDER_METADATA: { readonly mode: "terminal-product-lab"; readonly tool: "humanish"; }; export declare const TERMINAL_EVENTS_ARTIFACT = "terminal-events.ndjson"; export declare const TERMINAL_TRANSCRIPT_ARTIFACT = "terminal-transcript.txt"; export declare const TERMINAL_LEDGERS_ARTIFACT = "terminal-ledgers.json"; export declare const TERMINAL_PRODUCT_LAB_SCHEMA = "humanish.terminal-lab-result.v1"; /** * Library-level hooks: the DI seams that drive the full live path against a fake sandbox + mock * CLI at zero spend. The deterministic merge-gate test wires loadModule (a fake @e2b/desktop * module) + env (the operator key source) + now (an injected clock); the live rung uses none of * them (it loads the real module and reads the real environment). */ /** * The read-only evidence a thin adapter's scorer/feedback hook sees (the layer-6 extension seam, * issue #154 acceptance #8). It is the FULLY-ASSEMBLED, redacted, verifiable evidence — the live run * bundle, the provider-neutral actor trace, and the persisted ledgers (substrate/command/ * interventions/cleanup/cost/no-spend). Every member is an EXPORTED public type, so a thin adapter * types against `import("humanish")` alone — never a deep `src/` import. The adapter reads this * to score the product attempt and derive feedback; it cannot mutate core's evidence (the lane * attaches only the namespaced `RunAdapterScore` it returns + the feedback candidates it derives). */ export interface TerminalProductScoringContext { /** The assembled live run bundle (already redacted/scrubbed + verifiable). Read-only to the adapter. */ bundle: RunBundle; /** The provider-neutral actor trace for the in-sandbox agent session. */ trace: ActorTrace; /** The persisted terminal-product ledgers (lifecycle/command/interventions/cleanup/cost/no-spend). */ ledgers: TerminalLedgers; /** * The FULL normalized transcript of the in-sandbox agent session — scrubbed (literal known * values) then redacted (shape patterns) AT THE SOURCE, capped at MAX_TRANSCRIPT_BYTES, and * byte-identical to the persisted terminal-transcript.txt artifact. The trace's transcriptTail * is a ~2KB projection of this; a scorer needs the whole session so a rubric can find * command-tier evidence anywhere in it, not only in the tail window (#341). */ transcript: string; /** The studied product name (public-safe). */ product: string; /** The lab id (the run's scenario scope). */ labId: string; /** The run id (for building namespaced idempotency keys + evidence pointers). */ runId: string; } export interface TerminalProductLabHooks { /** Lazy-load the E2B module (tests inject a fake; default loadE2BDesktopModule). */ loadModule?: () => Promise; /** * The operator environment the lane reads the runtime key from (and from which it asserts no * banned credential is requested). Defaults to process.env. The runtime key is injected ONLY * into command-scoped `codex` env or an external header transform — NEVER Sandbox.create envs (the credential * boundary); tests plant a fake key here and assert it never reaches metadata/global env/artifacts. */ env?: Record; renderObserverFn?: typeof renderObserver; /** Injected clock for deterministic timestamps + wall-clock arithmetic (tests only). */ now?: () => number; /** * Optional cost-ledger seam. Core has no product/media/payment spend signal, so it populates only * the provider line from trace tokenUsage when present. Tests and adapters can inject KNOWN spend * lines; absent signals retain the null-discipline default. */ costProbe?: (context: { tokenCostUsd?: number; }) => Partial> | undefined; /** * THE LAYER-6 EXTENSION SEAM (issue #154 acceptance #8: "product-adapter hooks WITHOUT forking * core"). A thin in-repo/out-of-tree adapter registers a product scorer here. The lane calls it * (when provided) over the fully-assembled evidence and attaches the returned, ADAPTER-NAMESPACED * `RunAdapterScore` to `bundle.adapterScore` WITHOUT core knowing any product noun (the score is * namespaced + its component breakdown rides in `data`). When NO scorer is given, the default * mission-based verdict (`review`) is unchanged. This is the SEAM the adopter's scorecard plugs * into — NOT a built-in product scorer (that lives in the adopter's repo). */ score?: (ctx: TerminalProductScoringContext) => RunAdapterScore | Promise; /** * Companion seam: derive product-feedback candidates from the same assembled evidence. The lane * appends the returned candidates to `bundle.feedbackCandidates`. The adapter records its * product-specific concepts (public CLI command observed, hosted success-or-blocker, feedback id, * media/job ids, no-spend proof, defection/friction risk) under each candidate's ADAPTER-NAMESPACED * `adapter` block — never as core enums (issue #154's "record product-specific concepts as * NON-core nouns" list). The candidates must still satisfy core's feedback-candidate shape (which * the bundle verifier enforces), so a malformed adapter candidate fails closed. */ deriveFeedback?: (ctx: TerminalProductScoringContext) => RunFeedbackCandidate[] | Promise; } export interface RunTerminalProductLabOptions { automaticAnalysis?: AutomaticAnalysisHooks; /** Which manifest produced this run (#455); threaded into the status record + bundle. */ lab?: RunLabProvenance; cwd: string; config: LabConfig; /** Resolved upstream (scenario.mode + CLI override); defaults safe (dry-run). */ dryRun: boolean; open?: boolean; runId?: string; hooks?: TerminalProductLabHooks; /** * Present ONLY when the scorer hooks were CONFIG-DECLARED and loaded by the CLI (#316). Its presence * is the "declared" marker: a config-declared terminal scorer returning status:"fail" FLIPS * bundle.review.verdict (like the browser routes), and one that throws becomes a visible review.gaps * entry. A LIBRARY caller passing `hooks` directly leaves this ABSENT and keeps today's purely * additive terminal behavior (verdict unchanged). Core-computed, never adopter-supplied. */ scorerProvenance?: RunScorerProvenance; } export interface TerminalProductLabResult extends AutomaticAnalysisResult { schema: typeof TERMINAL_PRODUCT_LAB_SCHEMA; /** True when the bundle verified AND (dry-run, or the live session reached a terminal verdict * without a harness error + cleanup was proven). The agent's pass/fail is evidence, not the * lab's exit code. */ ok: boolean; cwd: string; labId: string; /** The registry-resolved actor id that ran (or would run) the session. */ actor: string; /** The studied product name (public-safe). */ product: string; dryRun: boolean; runId: string; /** Live-only: the in-sandbox agent session verdict (omitted on dry-run / pre-session failure). */ session?: { status: ActorStatus; completionReason: ActorCompletionReason; reason: string; }; /** Live-only: the sandbox lifecycle proof (the key/auth value is NEVER surfaced here). */ sandbox?: { sandboxId: string; killed: boolean; /** BY-ID proof (never a re-list): 0 = confirmed reclaimed, 1 = still present (unconfirmed), * -1 = kill(id) itself failed or was unavailable. See TerminalLedgers["cleanup"]. */ remaining: number; }; /** Live-only: the spend ledger surfaced on the result — unknowns are null, never guessed. * Lets a programmatic caller read spend without parsing the bundle. */ cost?: { knownTotalUsd: number; fullyMeasured: boolean; /** Per-category USD: a known number, or null = NOT MEASURED (never coerced to 0). */ lines: Record<"product" | "media" | "payment" | "provider", number | null>; }; /** Live-only: the no-spend proof DERIVED from the ledger. */ noSpend?: { satisfied: boolean; maxUsd: number | null; knownZeroLines: string[]; unmeasuredLines: string[]; }; observer?: ObserverResult; warnings: string[]; error?: { code: "HUMANISH_LAB_ANALYSIS_INVALID" | "HUMANISH_LAB_TASKS_UNSUPPORTED" | "HUMANISH_TERMINAL_LAB_FAILED" | "HUMANISH_TERMINAL_LAB_ACTOR_UNSUPPORTED" | "HUMANISH_TERMINAL_LAB_SUBJECT_INVALID" | "HUMANISH_TERMINAL_LAB_KEYPLACEMENT_INVALID" | "HUMANISH_TERMINAL_LAB_RUNTIME_AUTH_MISSING" | "HUMANISH_TERMINAL_LAB_CAPS_MISSING" | "HUMANISH_TERMINAL_LAB_CAPS_EXCEEDED" | "HUMANISH_TERMINAL_LAB_CREDENTIAL_DENIED" | "HUMANISH_TERMINAL_LAB_CLEANUP_UNPROVEN" | typeof TERMINAL_AGENT_NOT_IMPLEMENTED_CODE; message: string; }; } /** * Wrapped so a DIRECT library caller gets the same status-record lifetime the CLI does: returning * from this function finalizes any record the run opened, whichever of its fail-closed exits it * took. `runLab` establishes a scope too and nesting is harmless — the inner scope owns what it * opened. Without this a test or an adopter calling the backend directly leaves the 5s cadence * ticking into a directory something else is deleting, which surfaces as an unrelated ENOTEMPTY. */ export declare function runTerminalProductLab(options: RunTerminalProductLabOptions): Promise; /** Substrate lifecycle ledger entry (create/readiness/exec/cleanup events with timestamps). */ export interface LifecycleRecord { at: string; event: string; /** Redacted+scrubbed before persisting (it never carries a secret, but the harness never trusts that). */ message: string; } /** Command-log ledger entry: which command ran, with what exit/duration (NEVER its env values). */ export interface CommandLogRecord { at: string; /** A public-safe label for the command (e.g. "codex-exec"); the full argv is bound by digest only. */ label: string; /** sha256-12 of the exact command string — pins "same recipe" without persisting it. */ commandDigest: string; /** The env var NAMES injected command-scoped (values NEVER persisted) — the credential evidence. */ envNames: string[]; exitCode?: number; timedOut?: boolean; durationMs: number; } /** * One operator intervention (assisted-input event). The current route ships NO assisted-input * path, so this ledger is ALWAYS empty — but always PRESENT (the safety contract: empty-present is * the contract, an absent ledger fails verify). A future assisted path can fill this shape. */ export interface InterventionRecord { at: string; kind: "stdin"; /** Redacted+scrubbed digest of the injected input (never the raw bytes). */ inputDigest: string; } /** * One cost line of the spend ledger. THE NULL DISCIPLINE (issue #154, the cost/no-spend asks): * three distinct states are crisply modeled and NEVER conflated — * - `usd: 0` => KNOWN to be zero. A measured-and-zero spend (we metered this category and it * billed nothing). The no-spend proof may legitimately assert this is zero. * - `usd: null` => NOT MEASURED. This run carries no spend signal for the category. `null` is * written explicitly (never undefined-omitted, never guessed to 0). The no-spend * proof must list this line as UNMEASURED and must NOT claim it is zero. * - line ABSENT => NOT APPLICABLE to this lane/run (n/a). The line simply does not appear in * `lines`. (The current route emits all four lines, so absence is reserved for * future lanes that genuinely have no such category.) * `null` vs missing-key is the load-bearing distinction: a missing key means "this category does not * exist for this run"; a present key with `null` means "this category exists but we did not measure * it". A no-spend proof that claimed zero on a `null` line would claim more than it measured. */ export interface CostLine { /** known zero (0) | not measured (null). The key is ALWAYS present when the line is applicable. */ usd: number | null; /** Optional billable-unit count, same discipline: a known count, or null = not measured. */ count?: number | null; /** How this line's value was established (provenance for the verifier + the human reviewer). */ source: "provider-token-usage" | "no-spend-signal" | "operator-cap" | "unmeasured" /** Tokens were COUNTED but no rate could price them, so `usd` stays null while the note * carries the measured token totals (#531). Distinct from "unmeasured", which means no * signal at all. */ | "unpriced-token-usage"; /** A short, public-safe note (never a secret value). */ note: string; } /** The cost categories the lane meters. product/media/payment are adapter signals; core can * populate the provider line from the actor trace's tokenUsage.costUsd when present. */ export type CostCategory = "product" | "media" | "payment" | "provider"; /** * The spend ledger (a block of `TerminalLedgers`). The no-spend PROOF is DERIVED from this — never * asserted independently. Every applicable category appears as a line; unknowns are `null`. */ export interface TerminalCostLedger { schema: "humanish.terminal-cost-ledger.v1"; /** USD currency unit (recorded explicitly so a future multi-currency lane is unambiguous). */ currency: "usd"; lines: Record; /** Sum of the KNOWN (non-null) lines. null lines contribute NOTHING and are NOT guessed as 0. */ knownTotalUsd: number; /** True when every applicable line is measured (no null). When false, knownTotalUsd is a LOWER * bound, not the full spend — the no-spend proof says so honestly. */ fullyMeasured: boolean; } /** * The no-spend proof, DERIVED from the cost ledger (issue #154: "derived from a ledger, not * asserted"). It is honest about what it knows: it lists the KNOWN-zero lines it can vouch for and, * separately, the UNMEASURED (null) lines it CANNOT vouch for. `satisfied` is true only when every * KNOWN line is zero (a known non-zero line fails it); but a proof with unmeasured lines explicitly * says it could not measure them — it never claims zero on a line the ledger marks null. */ export interface NoSpendProof { schema: "humanish.terminal-no-spend-proof.v1"; /** The maxUsd cap this proof was evaluated against (the no-spend scenario declares maxUsd: 0). */ maxUsd: number | null; /** True iff every KNOWN (measured) line is <= maxUsd (for a no-spend run, == 0). */ satisfied: boolean; /** Categories the ledger MEASURED and found at (known) zero — the proof CAN vouch for these. */ knownZeroLines: CostCategory[]; /** Categories the ledger measured with a known NON-zero spend (these break `satisfied`). */ knownNonZeroLines: CostCategory[]; /** Categories the ledger marks `null` (NOT MEASURED). The proof explicitly lists these and does * NOT claim they are zero — it claims only that this run could not measure them. */ unmeasuredLines: CostCategory[]; /** Sum of the known lines (== 0 for a satisfied no-spend run). */ knownTotalUsd: number; /** Human-readable honesty statement covering both what is proven and what is unmeasured. */ statement: string; } /** The persisted terminal-product ledgers artifact (substrate lifecycle + command log + interventions + cleanup + cost). */ export interface TerminalLedgers { schema: "humanish.terminal-ledgers.v1"; runtime?: ActorRuntimeProvenance; lifecycle: LifecycleRecord[]; commandLog: CommandLogRecord[]; /** ALWAYS present; ALWAYS empty while no assisted-input path ships — the safety contract. */ interventions: InterventionRecord[]; cleanup: { /** True when exact-id kill resolved, including the startup guard's acquired-instance kill * (found-and-killed or already gone both prove absence; see `remaining`/`reason`). */ killed: boolean; /** BY-ID proof, NEVER derived from Sandbox.list: 0 = confirmed reclaimed (kill(id) RESOLVED * -- returned true "found and killed" OR false "404, exact id already gone" -- and, when the * SDK exposes it, getInfo(id) did not report a live sandbox); 1 = getInfo(id) still reports * this exact sandbox running/paused (NOT reclaimed); -1 = kill(id) itself failed, threw, or * was unavailable (the server-side kill-on-timeout is the backstop). */ remaining: number; /** Honest, human-readable statement of which by-id signal produced `remaining`. */ reason: string; }; /** The spend ledger. Unknowns are `null`, never guessed; the no-spend proof * below is DERIVED from it. */ cost: TerminalCostLedger; /** The no-spend proof DERIVED from `cost`. Never an independent assertion. */ noSpendProof: NoSpendProof; } /** * Build the sandbox metadata from a POSITIVE ALLOWLIST (safety contract item 6). This is the ONLY * way metadata is set on the terminal lane — it carries solely non-secret labels and rejects any * value that is not a plain short label. A verifier check asserts the persisted metadata has no * prompt/token/secret shapes; this builder makes that true by construction. */ export declare function buildSandboxMetadata(allowlist: { labId: string; simId: string; runId: string; }): Record; /** * Project the terminal-product lab run into a humanish.run-bundle.v1 (no schema change — a new * producer only). DRY-RUN: a contract bundle. The terminal stream is a contract placeholder * (stdin disabled, no captured tail — honest: nothing ran), the subject is declared UNPINNED, and * the caps/policies/runtime-auth declarations are recorded without pretending that live ledgers * exist. The shipped live builder fills the same evidence contract. Exported for tests. */ export declare function buildTerminalProductBundle(args: { /** Lab provenance for the bundle\'s own `lab` field (#455). */ lab?: RunLabProvenance; actorId: string; createdAt: string; dryRun: boolean; labId: string; labTitle?: string; mission: string; persona: ActorPersonaRef; productName: string; publicSurfaces: string[]; caps?: LabScenarioCaps; runtimeAuth?: string; stdin: "disabled" | "planned" | "sent"; policies: { allowPrivateRepoAccess: boolean; allowProviderCredentials: boolean; allowPaymentCredentials: boolean; allowGitHubMutation: boolean; }; runId: string; source: RunBundle["source"]; }): RunBundle; /** * Build the LIVE terminal-product run bundle (mode "live") from the captured session: the actor * trace seam (stream.actor = trace), the substrate-lifecycle events, the terminal stream with the * redacted transcript tail, and references to the written evidence artifacts (terminal event * stream, transcript, ledgers, actor trace). verifyRun's terminal-product check (gated on * mode==="live") enforces the ledgers + proven cleanup + interventions-present over this bundle. */ export declare function buildLiveTerminalProductBundle(args: { /** Lab provenance for the bundle\'s own `lab` field (#455). */ lab?: RunLabProvenance; actorId: string; createdAt: string; labId: string; labTitle?: string; mission: string; persona: ActorPersonaRef; productName: string; publicSurfaces: string[]; caps?: LabScenarioCaps; runtimeAuthKeyName: string; runtimeAuth?: LabRuntimeAuth; policies: { allowPrivateRepoAccess: boolean; allowProviderCredentials: boolean; allowPaymentCredentials: boolean; allowGitHubMutation: boolean; }; runId: string; source: RunBundle["source"]; trace: ActorTrace; ledgers: TerminalLedgers; sandboxId?: string; sessionError?: string; sessionReason: string; }): RunBundle; /** * Resolve a committed persona (humanish/personas/.yaml) into behavioral directives so the * terminal agent runs IN CHARACTER (#308). Fail-SAFE, never fail-closed: an unsafe id, a missing * file, or unparseable YAML returns `persona: null`, and the caller keeps the legacy bare-id prompt * with a truthful empty traitsApplied — a persona that DECLARED nothing must not receive fabricated * traits. Reads are containment-guarded exactly like scenario.ref (readContainedRegularFile). */ export declare function resolveTerminalPersona(projectRoot: PreparedSelectedOutputDirectory, personaId: string): Promise<{ persona: ResolvedPersona | null; warnings: string[]; }>;