/** * Scenario → CampaignNode mapping (TH-9; DES-CAMPAIGN-001 §8). * * `POST /campaigns` accepts a batch of SCENARIOS (the qe-campaign vocabulary: garden's * campaign-recon rungs, or any caller's batch of checks) and this module maps them onto the * engine's `CampaignDef` — nodes, `on_success`/`on_terminal` edges, and one composed * single-phase workflow per deterministic scenario. Pure data-in/data-out: no engine handle, * no I/O, so every rule below is unit-testable without spawning a Core. * * ## The two scenario shapes, and where the work actually lives * * - **`tool` (deterministic)** — the scenario is a spec FILE (garden's runner spec, a script, * any argv-runnable check) and the node's workflow is one Tool-executor phase running that * argv verbatim: no council, no LLM, near-zero cost. The engine's Tool path (`run_tool_cmd`) * spawns the command directly. * - **`agent` (exploratory / authoring)** — a governed agent run over a problem statement; * seats are spent only where judgment is the work (test-R9). * * ## The 1022-byte rule (load-bearing, enforced here) * * PTY worker prompts longer than 1022 bytes are SILENTLY DISCARDED by the canonical line * discipline — the worker waits on input that never arrives and the unit burns its full * timeout (the S8 campaign finding; memory: "PTY canonical line limit"). The mapping * therefore refuses, loudly and per-token, anything that smells like a scenario BODY inlined * where a FILE PATH belongs: every Tool argv token and every agent problem statement must be * a single line of at most {@link MAX_INLINE_BYTES} bytes. The fix is always the same and the * error says it: persist the content as a file and pass its path. */ import type { CampaignDef, CampaignScenario, LaunchCampaignBody, WorkflowDef } from '../core/types.js'; /** * Byte ceiling for a single inline string handed to a node (a Tool argv token, an agent * problem). 1022 is the PTY canonical-line limit — the longest line a worker's stdin * accepts before the kernel silently discards it. Tool argv does not itself traverse a PTY, * but the ceiling is enforced uniformly because the failure it guards against is the same * SHAPE either way: scenario bodies belong in files, and a token this long is a body. */ export declare const MAX_INLINE_BYTES = 1022; /** Composed node workflows carry this prefix — the adapter keeps them OUT of the workflow * catalog (`listWorkflows`) while still persisting them to the overlay dir so a fresh-process * `resumeCampaign` can re-dispatch pending nodes by workflow id. */ export declare const CAMPAIGN_WORKFLOW_PREFIX = "campaign-"; /** The engine's preset-name cap (wicked-core `preset::validate_name`): a node's preset is named by it. */ export declare const MAX_PRESET_NAME = 64; /** * The composed workflow id for one deterministic scenario node: `campaign--`, or * when that would pass the engine's 64-char preset-name cap (X-MIG M11 arms node workflows as * presets; codex r1 on crew A), `campaign-<16 hex of sha256(campaign NUL scenario)>`. Deterministic, * and never equal to a composed name (that one always holds a `-` after the prefix; hex has none). */ export declare function scenarioWorkflowId(campaignId: string, scenarioId: string): string; /** A built campaign: the engine def plus the per-node Tool workflows the adapter must arm * (hot-register + overlay-persist) BEFORE the def launches. */ /** The engine's `CampaignDef.denial_gate` (wicked-core ≥ 0.7.27; DES-L1 PR-1D). Typed HERE until * wicked-crew-api-types carries `CampaignDef.denial_gate?` / `LaunchCampaignBody.denialGate?` * (0.39.0) — the engine's serde reads it today; `engineCampaignDef` spreads the def verbatim. */ export type CampaignDenialGate = 'hold' | 'auto_reject'; export type EngineCampaignDef = CampaignDef & { denial_gate?: CampaignDenialGate; }; export type LaunchCampaignBodyWithDenialGate = LaunchCampaignBody & { denialGate?: CampaignDenialGate; }; export interface BuiltCampaign { def: EngineCampaignDef; workflows: WorkflowDef[]; } /** A resolved repo the fan targets — `id` rides `run_spec.repo_ref`, `name` labels titles. */ export interface FanRepo { id: string; name: string; } /** The fanned scenario list plus the node-id order the route's `runIds` answer follows. */ export interface FannedScenarios { scenarios: CampaignScenario[]; /** * Node ids in the PINNED `runIds` order: repo-major over the fanned scenarios (repos in the * caller's resolved order, scenarios in input order within each repo), then the scenarios * that pinned their own `repoRef` (never fanned — an explicit per-scenario pin wins over the * launch-level scope) in input order. Empty exactly when `repos` was empty (legacy launch). */ runOrder: string[]; } /** * Fan a scenario batch across the launch's resolved repos (multiscope `projectId`/`repoRefs`). * * - `repos` empty ⇒ the batch is returned untouched (`runOrder: []`) — the legacy launch. * - one repo ⇒ no copies: scenarios without their own `repoRef` are assigned it, ids * unchanged (a single-codebase launch must look exactly like today's per-scenario spelling). * - several repos ⇒ each unpinned scenario becomes one copy per repo (`--r`, titled * `