/** * v3 DAG definition — schema, loader, validator, topological order. * * The v3 runtime (LLM-driven workflow) loads a hand-written `dag.json`, * validates it, and walks it in topological order with deps gating. This * module is the *schema half* of the engine: pure data + validation. Node.js * filesystem loading lives in `dag-loader.ts`. * * Deliberately standalone from v0.2's `definition.ts` — v3 nodes are a much * smaller surface (goal / host, no loop / decision / fanout) and coupling the * two schemas would drag v0.2's complexity into the new engine. See * `docs/design/2026-06-01-v3-mvp-engine-split.md` §3 for the authored shape. */ import { type V3ArtifactOutputs } from './artifact-contract-declarations.js'; /** * `goal` — an LLM node driven by the `botmux-goal` skill (single goal, one * ephemeral worker). `host` — a deterministic side-effect node (feishu-send * / base write / schedule) that does NOT route through an LLM. MVP runs * `goal` nodes end to end; `host` is reserved in the schema so the runtime * can grow into it without a breaking change (it is rejected at validate * time until the executor lands — see `validateDag`). * `loop` — a composite node wrapping a bounded sub-pipeline (structured rework: * `code -> test` until the test's structured result passes). The outer DAG * stays acyclic — rework NEVER appears as a back-edge; it only exists inside * an explicit loop body. See docs/design/2026-06-06-v3-structured-loop-design.md. */ export type V3NodeType = 'goal' | 'host' | 'loop'; export declare const NODE_KINDS: readonly V3NodeType[]; /** First host slice: every registered executor is side-effecting and must be * approved against its frozen runtime input. Keep this list in lockstep with * the shared host-executor registry. */ export declare const V3_HOST_EXECUTORS: readonly ["feishu-send", "feishu-reply", "botmux-schedule"]; export type V3HostExecutorName = typeof V3_HOST_EXECUTORS[number]; /** Default per-node wall-clock budget when a node omits `timeoutSec`. * Generous on purpose: completion is detected by the manifest watcher * (seconds after the agent finishes), so the timeout only fires for hung * nodes — a long default costs nothing on the happy path. The architect is * prompted to set per-node `timeoutSec` explicitly for long tasks. */ export declare const DEFAULT_NODE_TIMEOUT_SEC = 1800; /** Hard ceiling for per-node `timeoutSec` (4h) — rejects runaway budgets the * architect might hallucinate while still allowing genuinely long tasks. */ export declare const MAX_NODE_TIMEOUT_SEC = 14400; /** A humanGate frozen at authoring time — the runtime never lets a node * add / skip a gate at runtime (design Q10). */ export interface V3HumanGate { /** Approval-card body shown to the human reviewer. */ prompt: string; /** Button option keys shown on the approval card. */ options?: string[]; /** Selecting any of these options maps to `resolution:'approved'`. */ approveOptions?: string[]; /** Empty = any operator allowed by the outer daemon permission gate. */ approvers?: string[]; } export declare const DEFAULT_HUMAN_GATE_OPTIONS: readonly string[]; export declare const MAX_HUMAN_GATE_OPTIONS = 8; export declare const MAX_HUMAN_GATE_OPTION_LENGTH = 32; /** * Declares that this node consumes an upstream node's products. MVP pulls the * upstream node's *whole* manifest (all files) into this node's `inputs.json`; * a per-file selector is deferred (design §2.3). Invariant: `from` MUST also * appear in the node's `depends` — you can only read outputs of a node you * wait for. */ export interface V3InputRef { /** Upstream nodeId whose manifest files become this node's inputs. */ from: string; /** P3 per-file selector: pull ONE named product instead of the whole * manifest. Exactly one of `name` (manifest logical name) / `path` * (manifest relative path) when present. A selector that matches nothing * at dispatch time is surfaced to the agent via `GoalInputs.omitted` * (reason 'selectorMiss') — absence reads as a contract gap, not silence. */ select?: { name?: string; path?: string; }; /** schemaVersion 2 stable public-output key. */ output?: string; } /** * A normalized incoming edge (edge-activation design 2026-06-06 §1.1). * Authored as either a plain string (`"build"`) or an object * (`{ "from": "review", "when": {...} }`); validateDag normalizes both to this * shape. No `when` = unconditional (source `done` ⇒ active). With `when`, * the edge's activation is decided ONCE by the runtime reading the source's * `result.json` and journaled as `edgeResolved` — never re-read afterwards. * * `from` values are deduped per node: P0 supports at most ONE edge per * (from, to) pair, so `(from, to)` is a stable idempotency key for * `edgeResolved`. Express OR over outcomes inside the source's structured * result instead of authoring parallel conditional edges. */ export interface V3DependRef { from: string; /** Predicate over the SOURCE node's structured result — same shape and * validation as a loop exit predicate (`result.` + exactly one * comparison operator, declared + required + type-compatible). */ when?: V3EdgeWhen; } /** Edge predicates reuse the loop-exit predicate shape verbatim. */ export type V3EdgeWhen = V3LoopExitWhen; /** * Join semantics over a node's incoming edges (design §1.2). Evaluated ONCE, * only after every incoming edge has settled (source done/skipped and any * predicate journaled) — no early release, no loser cancellation in P0. */ export type V3TriggerRule = 'all_success' | 'one_success' | { quorum: number; }; /** * Per-node capability override (P2, edge-activation follow-up). Merged onto * the bot's frozen `BotSnapshot` at dispatch time: * - `model` picks a different model for THIS node (cost control: cheap * models for research nodes, strong models for code nodes); * - `systemPromptAppend` adds node-specific instructions to the goal file. * Permission is deliberately not overridable: every workflow worker requires * CLI bypass permission, and bots configured to disable it are rejected. * `toolsSubset` is deferred — it needs a per-CLI capability matrix across the * daemon init/worker/adapter chain (P2b). */ export interface V3CapabilityOverride { model?: string; systemPromptAppend?: string; } export declare const MAX_OVERRIDE_MODEL_LENGTH = 64; export declare const MAX_OVERRIDE_SYSTEM_PROMPT_APPEND = 8000; /** * Opt-in structured-output contract — a deliberately TINY subset of * JSON-Schema (flat object, primitive-typed properties, optional required * list). Hand-validated (no deps, repo style); anything outside the subset * is rejected at validateDag time so the architect can never author a schema * the runtime's validator cannot execute. * * NOT supported (first slice): nested schemas, array item types, patterns. * `type:'array'|'object'` properties validate the TOP-LEVEL type only. * `enum` is supported on STRING properties only (edge-activation design §1.3) * — it is the decision-vocabulary anchor for edge predicates: validateDag * cross-checks `equals`/`notEquals` operands against the source field's enum, * so a typo'd decision value fails at validate time, not at runtime. */ export interface V3ResultSchema { type: 'object'; properties: Record; required?: string[]; } export type V3ResultFieldType = 'string' | 'number' | 'boolean' | 'array' | 'object'; /** Caps on the resultSchema subset (anti-runaway: a giant schema bloats the * goal prompt and the validator). Checked at validateDag time. */ export declare const RESULT_SCHEMA_MAX_PROPERTIES = 32; export declare const RESULT_SCHEMA_MAX_BYTES = 4096; /** Caps on a string property's `enum` (anti prompt-bloat; counted inside the * 4KB schema budget like everything else). */ export declare const RESULT_ENUM_MAX_VALUES = 16; export declare const RESULT_ENUM_MAX_VALUE_LENGTH = 64; /** Backstop ceiling for `maxIterations` — like the timeout cap, it rejects a * runaway budget the architect might hallucinate; a human can still grant * extra iterations one at a time once the loop blocks. */ export declare const MAX_LOOP_ITERATIONS = 20; /** Cross-node revisit budgets (anti-infinite-loop). Two tiers: * - PER-PAIR (source→target): how many times one node may revisit one ancestor * before the run blocks — default 1 (a node sends each ancestor back once; * expected multi-round rework belongs in a structured loop, not ad-hoc * revisit). Pinpoints which edge is ping-ponging. * - PER-RUN: total revisits across the whole run — a generous backstop so many * distinct pairs (or many nodes revisiting) can't run away. * Exhaustion blocks the run; a human grants +1 (revisitBudgetGranted). */ export declare const DEFAULT_REVISIT_BUDGET_PER_PAIR = 1; export declare const DEFAULT_REVISIT_BUDGET_PER_RUN = 8; /** * Exit predicate over the exit node's structured result. Deliberately tiny: * `path` is fixed to `result.` (the resultSchema subset is flat, so there * is nothing deeper to address) and exactly ONE comparison operator must be * set. validateDag cross-checks the key against the exit node's resultSchema * (declared AND required, operator type-compatible), so "field missing at * runtime" is a validate-time impossibility, not a runtime branch. * * No `continue.when` counterpart — when the predicate does not match, the loop * implicitly continues (until maxIterations). Two independent predicates * would create undefined both-match / neither-match states. */ export interface V3LoopExitWhen { /** `result.` — a key of the exit node's resultSchema. */ path: string; equals?: string | number | boolean; notEquals?: string | number | boolean; gt?: number; gte?: number; lt?: number; lte?: number; } export interface V3LoopExit { /** Body nodeId whose structured result decides the loop's exit. */ node: string; when: V3LoopExitWhen; } /** Which body node's final-iteration manifest is the loop's outward product * (what downstream `inputs: [{from: }]` reads). Defaults to the * exit node, but a repair loop usually exports the WORKER's product (`code`), * not the gate's (`test`). */ export interface V3LoopOutput { from: string; } export interface V3Node { /** Unique within the DAG; also used as a runDir path segment, so it is * constrained to `[A-Za-z0-9._-]`. */ id: string; type: V3NodeType; /** Required + non-empty for `goal` nodes; the single-sentence objective. */ goal?: string; /** Which bot/CLI runs this node. MVP dogfoods a single CLI, but the field * is per-node so a mixed-backend DAG is a non-breaking extension. */ bot?: string; /** Normalized incoming edges. Authored as `string | {from, when?}`; * validateDag normalizes to `V3DependRef[]` (edge-activation design §1.1). * Unconditional edges gate on source `done`; `when` edges additionally * gate on the journaled `edgeResolved` verdict. */ depends: V3DependRef[]; /** Join semantics over incoming edges; defaults to 'all_success' (exactly * today's behavior). Only meaningful on nodes with ≥1 incoming edge. */ triggerRule?: V3TriggerRule; /** Per-node capability override (restrict/redirect only — see * V3CapabilityOverride). Goal nodes (incl. loop body nodes) only; a loop * composite never spawns a worker, so it rejects this field. */ override?: V3CapabilityOverride; /** Upstream products to thread in as inputs (every `from` ⊆ `depends`). */ inputs: V3InputRef[]; /** schemaVersion 2 public products addressable by downstream output keys. */ outputs?: V3ArtifactOutputs; /** Wall-clock budget in seconds; falls back to DEFAULT_NODE_TIMEOUT_SEC. */ timeoutSec?: number; /** Optional human approval gate, evaluated *before* the node's work runs. */ humanGate?: V3HumanGate | null; /** Opt-in structured-output contract: when set, the node must write a * `result.json` (listed in its manifest files) matching this schema; a * violation blocks (not fails) the node. Absent → zero behavior change. */ resultSchema?: V3ResultSchema; /** Definition-level revisit exits (cross-node回溯). When this node's * `result.json` returns `{ "status": "revisit", "revisitTo": "" }`, the * runtime may revisit ancestor node `` — but ONLY if `` is listed * here. Default (absent / empty) = the node cannot revisit anything. * validateDag enforces every entry is an ANCESTOR (transitive `depends`), * so a revisit can never create a forward jump or a cycle in the run. */ revisitTo?: string[]; /** Deterministic executor invoked by the host runtime (never an LLM). */ executor?: V3HostExecutorName; /** Frozen before the runtime gate; supports typed host bindings. */ input?: unknown; /** Hard iteration bound; the loop blocks (recoverable, human can grant +1) * when it is exhausted without the exit predicate matching. */ maxIterations?: number; /** The per-iteration sub-pipeline. Goal nodes only — no nesting, no * humanGate inside a body (both first-cut restrictions). */ body?: { nodes: V3Node[]; }; /** Structured exit condition; not matching ⇒ implicit continue. */ exit?: V3LoopExit; /** Previous-iteration products threaded into the NEXT iteration's inputs. * Entries are `.result` | `.files` | `.manifest`. */ feedback?: string[]; /** Outward product projection (defaults to exit.node). */ output?: V3LoopOutput; /** Only supported value (and the default): 'blocked'. */ onExhausted?: 'blocked'; /** Only supported value (and the default): 'fresh' — every iteration's every * body node runs a fresh ephemeral worker. `resumeWithinLoop` is deferred. */ sessionPolicy?: 'fresh'; } /** A `V3Node` narrowed to a goal node — `goal` is guaranteed present. This is * what crosses into `runNode` (the pool only ever runs goal nodes in MVP). */ export interface V3GoalNode extends V3Node { type: 'goal'; goal: string; } /** Narrowing guard: a validated goal node always has a non-empty `goal`. */ export declare function isGoalNode(node: V3Node): node is V3GoalNode; export interface V3HostNode extends V3Node { type: 'host'; executor: V3HostExecutorName; input: unknown; humanGate: V3HumanGate; } export declare function isHostNode(node: V3Node): node is V3HostNode; /** A `V3Node` narrowed to a loop node — validateDag guarantees every loop * field is present and normalized (output defaulted to exit.node, feedback * defaulted to `[]`). */ export interface V3LoopNode extends V3Node { type: 'loop'; maxIterations: number; body: { nodes: V3Node[]; }; exit: V3LoopExit; feedback: string[]; output: V3LoopOutput; } /** Narrowing guard for validated loop nodes. */ export declare function isLoopNode(node: V3Node): node is V3LoopNode; /** * The expanded id a body node instance runs under in iteration N: * `repairLoop.i001.code`. Path-safe by construction (loopId/bodyId are * SEGMENT_RE, `.` is in the charset) and free of the `:` the blocked-card * nonce uses as a separator. OPAQUE — never parse this string back; journal * events carry a structured `loop: {loopId, iteration, bodyNodeId}` instead. */ export declare function loopInstanceId(loopId: string, iteration: number, bodyNodeId: string): string; export interface V3Dag { /** Missing means legacy v1. Newly authored DAGs use schemaVersion 2. */ schemaVersion?: 1 | 2; /** Stable id for this run; used as the runDir name, so path-segment safe. */ runId: string; nodes: V3Node[]; } /** Thrown by `validateDag` / `loadDag` with every problem found, not just the * first — authoring a DAG by hand is iterative, so surface the full list. */ export declare class DagValidationError extends Error { readonly problems: string[]; constructor(problems: string[]); } /** Node ids and runId double as filesystem path segments under the runDir. */ export declare const V3_DAG_SEGMENT_RE: RegExp; /** * Validate an untrusted parsed value into a `V3Dag`. Pure — throws * `DagValidationError` with the full problem list on any violation, otherwise * returns a normalized dag (defaults filled, `humanGate: undefined` → `null`). * * Checks: runId shape; non-empty unique path-safe node ids; known `type`; * `goal` non-empty for goal nodes; host executor/input/gate policy; * `depends` reference existing nodes, no self-dep, no dup `from` (P0: one * edge per (from,to)); edge predicates validated against the SOURCE's * resultSchema (goal-with-schema sources only); `triggerRule` shape/bounds; * `inputs.from` reference existing nodes AND appear in `depends`; acyclic * (delegated to `topologicalOrder`, conditional edges included). */ export declare function validateDag(raw: unknown): V3Dag; /** * Deterministic topological order via Kahn's algorithm. Ties (nodes with the * same remaining in-degree available at once) are broken by ascending id so * the schedule is stable across runs — important for reproducible journals. * Throws if the graph contains a cycle (lists the offending nodes). * * Assumes `depends` already reference existing nodes; `validateDag` enforces * that before calling here. */ export declare function topologicalOrder(dag: V3Dag): string[]; //# sourceMappingURL=dag.d.ts.map