/** * Shared v3 Node runtime — the scheduling main loop. * * Ties the pure pieces together against the SHARED contract: * load dag → freeze bot snapshots → init runDir → * { materialize journal → decideNext → dispatch ready work under caps → * await a settle → repeat } until terminal. * * Every side effect lives here (journal append, STATE checkpoint, dir layout, * goal/inputs/env materialization). The actual worker spawn (`runNode`) and * manifest validation (`validateManifest`) are INJECTED — codex's * `ephemeral-pool.ts` / `manifest.ts` provide them, but the runtime compiles * against the contract types alone so the two halves build independently. * * MVP scope: static DAG, fail-fast, no retry (always `attempts/001`). Retry * (`attempts/NNN`) and richer cancel semantics are deferred — see * `docs/design/2026-06-01-v3-mvp-engine-split.md`. */ import { type V3Dag, type V3Node, type V3ResultSchema } from './dag.js'; import { type V3ArtifactOutputs } from './artifact-contract-declarations.js'; export { matchLoopExitWhen, revisitBudgetStatus, } from './core-control.js'; import { type StoredEvent, type V3ErrorClass, type V3UncertainHostEffect } from './journal.js'; import type { RunChatBinding } from './grill-state.js'; import { type BotSnapshot, type GoalAsk, type Manifest, type RunNode, type ValidateManifest } from './contract.js'; import type { AttemptLeaseProvider, ExecutionContextSnapshot, GateResolver, HostExecutorRegistry, HostExecutorPolicy, ProviderReconciler } from './runtime-host-contract.js'; /** * Render the self-contained instruction file the goal-mode agent reads via * `$BOTMUX_GOAL_PATH`. The execution contract (read inputs / write products / * write the manifest) lives HERE — in a file — rather than inside the `/goal` * command text, because a long multi-line `/goal` argument trips Claude Code's * paste-detection (the TUI folds it into a "[Pasted text]" blob and the * slash-command parser never fires). The pool's `buildGoalCommand` therefore * sends only a short single-line `/goal` that points the agent at this file. * * Rendered from `contract.ts` constants so the manifest shape stays a single * source of truth shared with codex's validator. */ export declare function renderGoalFile(goal: string, resultSchema?: V3ResultSchema, loopCtx?: { loopId: string; iteration: number; maxIterations: number; }, nodeInstructions?: string, hasWorkflowParams?: boolean, outputs?: V3ArtifactOutputs): string; /** * Map a node's failure to its terminal kind (the blocked/failed split): * - `blocked` = semantic/contract failure — retryable via a new attempt * - `failed` = infrastructure / human-veto / budget — needs intervention * * `selfReportedFail` marks the special case where the manifest is structurally * VALID but declares `status:'fail'` — then the node's own `error.retryable` * decides (`false` → failed; `true`/absent → blocked, the agent presumably * knows a human can unblock it). */ export declare function classifyTerminal(errorClass: V3ErrorClass, opts?: { selfReportedFail?: boolean; retryable?: boolean; }): 'blocked' | 'failed'; /** * Read + validate a goal worker's `ask.json` (the runtime human-ask payload). * Defensive: a missing / malformed / out-of-bounds file yields `undefined`, so a * broken ask degrades to a plain blocked card rather than crashing the drive — * the manifest's `error.message` still carries the question text for the human. * Accepts either 2–6 concrete options or `freeText:true`. Exported for tests. */ export declare function readGoalAsk(askPath: string): GoalAsk | undefined; /** * Merge a node's capability override onto the bot's frozen snapshot (P2). * Workflow execution always uses CLI bypass permissions; per-node overrides * may redirect the model but cannot alter the permission posture. */ export declare function mergeNodeCapability(snap: BotSnapshot, override: V3Node['override']): BotSnapshot; /** Validation outcome for an opt-in `result.json` against its node schema. */ export interface ResultValidation { ok: boolean; problems?: string[]; } /** Read a worker's cross-node revisit request from `result.json` (if any). A * revisit is `{ "status": "revisit", "revisitTo": "", "reason"? }`. * Absent result.json / non-revisit status → `{ ok:true }` (no request). A * malformed revisit (missing/blank revisitTo, non-string reason) → `ok:false` * so the runtime blocks it as resultInvalid. The ancestor membership check * (toNodeId ∈ node.revisitTo) is the caller's (it has the node). */ export declare function readRevisitRequest(manifest: Manifest, outputDir: string): { ok: true; request?: { toNodeId: string; reason?: string; }; } | { ok: false; problems: string[]; }; /** * Validate a `result.json` against the node's (already dag-validated) result * schema subset. Top-level types only — see `V3ResultSchema`. Undeclared * extra properties are allowed (JSON-Schema default). */ export declare function validateResult(filePath: string, schema: V3ResultSchema): ResultValidation; /** * Compute the attemptId the NEXT dispatch of `nodeId` must use, from the * journal: an unconsumed `nodeRetryRequested` reservation wins (retry intent * is authoritative for the redrive); otherwise max(seen)+1 — which is 001 for * a first dispatch. Dispatch events are the authority for "seen"; a * reservation is consumed by a later `nodeDispatched` with the same number. */ export declare function nextAttemptIdFor(events: StoredEvent[], key: string): string; /** Latest dispatched attemptId for a dispatch `key` (the `previousAttemptId` a * retry entrypoint must reference). `key` is an instance (`A#001`), a loop * body expansion, or a legacy nodeId — matched by `(instanceId ?? nodeId)` so * a retry stays inside the same instance. Undefined when never dispatched. */ export declare function latestAttemptIdFor(events: StoredEvent[], key: string): string | undefined; export interface V3RuntimeDeps { /** Spawn an ephemeral worker for one goal node (codex's pool). */ runNode: RunNode; /** Validate a node's manifest after the worker exits (codex's manifest.ts). */ validateManifest: ValidateManifest; /** Freeze a node's bot spawn config at run start. Given `node.bot` (may be * undefined → the run's default bot), returns the snapshot persisted in the * runDir and threaded through `runNode` (never re-resolved mid-run). */ resolveBotSnapshot: (botId: string | undefined) => BotSnapshot; /** Host-specific execution profile validation. Botmux defaults this in its * facade; portable hosts may validate arbitrary executor ids. */ validateExecutionSnapshot?: (snapshot: BotSnapshot, selector: string) => void; /** Owns attempt resource acquisition, crash recovery, and close proof. */ attemptLeaseProvider: AttemptLeaseProvider; /** Trusted deterministic host executors. Required when the DAG has host nodes. */ hostExecutors?: HostExecutorRegistry; /** Authorize a fully parsed host input before its prepared artifact is * committed for approval/provider execution. Defaults to Botmux's existing * chat-bound identity policy. */ hostExecutorPolicy?: HostExecutorPolicy; /** Provider recovery capabilities keyed by executor.provider. */ hostReconcilers?: Map; /** Injectable wall clock for deterministic host idempotency-TTL recovery. */ now?: () => number; /** Resolve a humanGate. Required only if the DAG declares any gate; the * runtime throws if a gate is hit without a handler. (Wired by * `human-gate.ts` post-milestone.) */ resolveGate?: GateResolver; } export interface V3RuntimeOptions { /** The run lives in `${baseDir}/${dag.runId}`. */ baseDir: string; /** Gate handling model. `blocking` keeps the CLI/dev y/N path; `suspend` * writes the pending wait and returns `awaitingGate` for a daemon/card layer * to resolve and re-drive from disk. */ gateMode?: 'blocking' | 'suspend'; /** Concurrency caps (codex's three-layer cap; conservative defaults). */ globalConcurrency?: number; perBotConcurrency?: number; perCliConcurrency?: number; cancelSignal?: AbortSignal; /** Bot identities already pinned by an immutable run envelope. When set, * the runtime must use these exact snapshots instead of live bots.json. */ frozenBotSnapshots?: ReadonlyMap; /** `dag.json` / `bots.snapshot.json` are authorized exact-byte artifacts. * Runtime may read them but must never rewrite them on start/retry/resume. */ authorizedArtifacts?: boolean; /** Parsed from the exact verified params artifact. Each node receives only * the keys it explicitly references, via an attempt-local 0600 JSON file. */ executionContext?: ExecutionContextSnapshot; /** @deprecated Use `executionContext`. Kept for persisted/older host adapters. */ resolvedWorkflowData?: ExecutionContextSnapshot; /** How long the scheduler waits for the original host SDK promise before * detaching and reconciling the still-open durable intent with the same key. */ hostResponseWaitMs?: number; /** The run's authenticated chat binding — threaded verbatim into every * RunNodeRequest so worker CLI children get real BOTMUX_* identity env. * See RunNodeRequest.chatBinding. */ chatBinding?: RunChatBinding; } export interface V3PendingGate { nodeId: string; waitId: string; prompt: string; options: string[]; approveOptions: string[]; approvers: string[]; hostApproval?: { attemptId: string; approvalDigest: string; inputHash: string; }; } export type V3RunOutcome = { reason: 'terminal'; runStatus: 'succeeded' | 'failed' | 'blocked' | 'cancelled'; failedNodeId?: string; blockedNodeId?: string; failureReason?: 'allSinksSkipped'; failureDetail?: string; uncertainHostEffects?: V3UncertainHostEffect[]; runDir: string; } | { reason: 'awaitingGate'; pendingWaits: V3PendingGate[]; runDir: string; }; /** * Run a validated DAG to terminal. Resumable: if `journal.ndjson` already has * events (daemon restart), the loop picks up from the materialized state * instead of re-running completed nodes. */ export declare function runWorkflow(dag: V3Dag, deps: V3RuntimeDeps, opts: V3RuntimeOptions): Promise; //# sourceMappingURL=shared-node-runtime.d.ts.map