# Dippin Language Specification Complete reference for AI agents generating `.dip` workflow files. This document is the canonical, self-contained spec for the dippin DSL and CLI toolchain. Install: `go install github.com/2389-research/dippin-lang/cmd/dippin@latest` Generate from template: `dippin new minimal`, `dippin new parallel`, `dippin new conditional`, `dippin new review-loop`, `dippin new human-gate` ## Grammar (simplified BNF) ```dippin workflow goal: "" [requires: , , ...] start: exit: [inputs : [required: true] [default: ] [prompt: ""] [description: ""] [options: , ] [pattern: ""] [min: ] [max: ] [max_length: ] [multiline: true] ...] [defaults model: provider: max_total_tokens: max_cost_cents: max_wall_time: stall_timeout: on_failure: tool_commands_allow: "," tool_denylist_add: "," ...] [vars : ...] : : parallel -> , [, ...] fan_in <- , [, ...] edges -> [on | when ] [label: ] [choice: ] [weight: ] [loop] [override: true] [else -> ] # success-side default for any node with no matching guard / unconditional edge; at most one per block ``` `else` is reserved as the first token of an edges-block line, so a node cannot be used as an edge *source* under the ID `else` (the other contextual keywords `on`/`when`/`loop` are only special after `->` and remain usable as node IDs). --- ## Node Kinds | Kind | Required Fields | Optional Fields | |------|----------------|-----------------| | `agent` | `prompt` (or `prompt_file`) | `model`, `provider`, `backend`, `working_dir`, `tool_access` (`none` disables LLM tools; DIP139 warns on unknown), `auto_status`, `goal_gate`, `reasoning_effort`, `fidelity`, `max_turns`, `prompt_file` (external file for `prompt`; mutually exclusive with `prompt:`), `system_prompt`, `system_prompt_file` (external file for `system_prompt`; mutually exclusive with `system_prompt:`), `writable_paths` (CSV glob list bounding where this agent's tools may write, e.g. `workspace/**, .ai/**`; absent = unbounded; present-but-empty is a parse error; a malformed or runtime-unrecognized value fails closed at the runtime = deny-all) | | `human` | `mode` (freeform\|choice\|interview\|yes_no) | `default`, `timeout` (duration, e.g. 5m), `timeout_action` (string: fail\|default) | | `tool` | `command` (or `command_file`) | `timeout` (e.g. 30s, 5m), `outputs` (CSV), `marker_grep` (regex), `route_required` (bool), `output_limit` (bytes), `command_file` (path to external script, relative to .dip dir) | | `parallel` | `-> Target1, Target2` (inline) | — | | `fan_in` | `<- Source1, Source2` (inline) | — | | `subgraph` | `ref` | `params` (dip 1) / `inputs` (dip 2, #227) | All kinds also accept: `label`, `reads`, `writes`, `retry_policy`, `max_retries`, `base_delay`, `retry_target`, and the retry-exhaustion route — spelled `fallback_target` in `dip 1`, `fallback_retry_target` in `dip 2` (`dippin fmt --migrate` relabels it). These are the engine's retry channel, read from the node, not the `edges` block. --- ## Edge Conditions ```dippin when when and when or when not on # sugar: equality vs the source node's outcome channel ``` **Comparison operators:** `=`, `==`, `!=`, `contains`, `not contains`, `startswith`, `endswith`, `in` (all string comparison, no numeric ops) **`on ` shorthand:** desugars to `when = `, where the channel is the source node's natural outcome channel — `ctx.outcome` for agent nodes, `ctx.tool_marker` for tool nodes with `marker_grep`. IR-identical to the equivalent `when`; `dippin fmt` rewrites eligible `when` edges to `on`. The value must be a single bare identifier (`[A-Za-z0-9][A-Za-z0-9_-]*`); quoted, multi-token, or any other values require an explicit `when = ...`. Source nodes with no outcome channel must use `when`: human gates (which route on the choice/label, not `ctx.outcome`), `conditional` nodes, and tools without `marker_grep`. **Variables:** Always namespace-qualified: `ctx.outcome`, `ctx.status`, `graph.goal`, `params.*` (subgraph params), `inputs.*` (declared workflow inputs — a closed namespace; an undeclared reference is DIP156) --- ## Common LLM Mistakes | # | Mistake | Fix | |---|---------|-----| | 1 | Missing `start:` or `exit:` field | Every workflow needs both. They reference node IDs declared below. | | 2 | Edge references undeclared node | Every node in an edge must be declared as `agent`, `human`, `tool`, etc. | | 3 | `parallel` targets without matching `fan_in` sources | `parallel P -> A, B` requires `fan_in J <- A, B` with the same set. | | 4 | Bare variable names in conditions | Use `ctx.outcome`, not `outcome`. All variables need a namespace prefix. | | 5 | Agent node with empty prompt | Every `agent` node should have a `prompt:` field with content (except start/exit lifecycle nodes). | | 6 | Missing tool timeout | Add `timeout: 60s` (or appropriate duration) to every `tool` node. | | 7 | Exhaustive conditions flagged | `ctx.outcome = success` + `ctx.outcome = fail` is exhaustive — DIP101/DIP102 are auto-suppressed. No need to add a fallback edge. | | 8 | Verbose output sharing stdout with routing marker | When a tool's stdout drives routing, redirect verbose output to a sibling file and `printf` only the marker. Otherwise large output (test logs, stack traces) can crowd out the marker under runtime stdout caps. See `nodes.md` → Tool Nodes → Markers and Verbose Output. | | 9 | Hand-parsing tool stdout for routing | Use `marker_grep: ""` (and optionally `route_required: true`) instead of regexing `ctx.tool_stdout` in edge conditions. Populates `ctx.tool_marker` directly — typed routing is more reliable than substring matching on raw stdout. | | 10 | DIP101/DIP102 flagged on marker-routed tool node | If the tool already declares `marker_grep:`, the validator treats it as a safe routing source and suppresses both warnings. If you're still seeing them, the source node isn't a `tool`, or `marker_grep` is empty. | | 11 | Boolean field rejected as invalid | Boolean fields (`goal_gate`, `auto_status`, `cache_tools`, `route_required`) accept `true/false`, `1/0`, `yes/no`, `on/off`, case-insensitive. Anything else is a parse error — pre-v0.29 silently coerced unknown values to `false`. | --- ## Exhaustive Conditions When outgoing edges from a node cover all possible values, DIP101 and DIP102 warnings are automatically suppressed. Known exhaustive sets: - `ctx.outcome`: `{success, fail}` or `{success, failure}` - `outcome`: `{success, fail}` or `{success, failure}` Tool nodes that declare `marker_grep:` are also treated as exhaustive (typed routing via `ctx.tool_marker`). This means the common pattern below is valid with zero warnings: ```dippin Gate -> Fix when ctx.outcome = fail Gate -> Next when ctx.outcome = success ``` --- ## Example: Conditional Routing ```dippin workflow ReviewPipeline goal: "Review code and route by outcome" start: Analyze exit: Done agent Analyze auto_status: true prompt: Analyze the code changes. Set STATUS: success if approved, STATUS: fail if changes needed. agent Approve prompt: Finalize the approved changes. agent RequestChanges prompt: Describe what changes are needed. agent Done prompt: Summarize the review outcome. edges Analyze -> Approve when ctx.outcome = success Analyze -> RequestChanges when ctx.outcome = fail Analyze -> Done Approve -> Done RequestChanges -> Done ``` --- ## Identifiers & Reserved Words **Identifiers:** `[a-zA-Z0-9][a-zA-Z0-9_\-./]*` — letters, digits, underscore, dash, dot, slash. **Contextual keywords** (not reserved — usable as node IDs): `workflow`, `agent`, `human`, `tool`, `subgraph`, `parallel`, `fan_in`, `edges`, `defaults`, `inputs`, `vars`, `when`, `on`, `and`, `or`, `not`, `true`, `false`, `restart`, `loop`, `override`, `label`, `weight`. **Position-reserved keyword:** `else` is the one exception — it is reserved *only* as the first token of an `edges`-block line (where it introduces the section default), so it cannot be an edge *source* node ID there. Everywhere else `else` is an ordinary identifier and may be used as a node ID. --- ## Validation with `dippin check` Use `dippin check` in tool-calling loops to validate generated `.dip` files. It runs parse + validate + lint in one shot and outputs JSON to stdout: ```bash dippin check my_workflow.dip ``` ```json {"valid":true,"errors":0,"warnings":0,"diagnostics":[],"suggested_actions":[]} ``` ```json {"valid":false,"errors":1,"warnings":2,"diagnostics":[{"code":"DIP003","severity":"error","message":"unknown node reference \"Nope\" in edge","line":19,"fix":""}],"suggested_actions":[]} ``` Use `valid` to decide whether to retry generation. Use `diagnostics` to feed error details back to the LLM for correction. Use `suggested_actions` for actionable fixes when available. --- ## Bundles (`.dipx`) A `.dipx` is a deterministic ZIP that packages a `.dip` entry plus every transitively-reachable subgraph as one integrity-verified artifact. Every analysis command (`validate`, `lint`, `doctor`, `check`, `parse`, `cost`, `coverage`, `simulate`, `optimize`, `unused`, `graph`, `diff`, `explain`, `export-dot`) accepts either `.dip` or `.dipx` as input. - **Build a bundle**: `dippin pack pipeline.dip` → `pipeline.dipx` - **Inspect**: `dippin inspect pipeline.dipx` (prints manifest, sha256 identity, file list) - **Extract**: `dippin unpack pipeline.dipx -o ./out` (atomic via staging dir + rename) Workflow: author and lint as `.dip`; package with `dippin pack` for distribution to the runtime. `dippin check pipeline.dipx` validates the bundled entry workflow exactly as if it were on disk. Bundle commands return distinct exit codes (`0` ok, `1` user error, `2` integrity error, `3` I/O error, `4` cancelled) so tooling can disambiguate failures that the analysis-command `0/1/2` ladder collapses. --- ## Diagnostic Code Summary 72 diagnostic codes across two categories: - **DIP001–DIP010** (errors): start/exit missing, unknown refs, unreachable nodes, cycles, duplicates, parallel/fan_in mismatch, unparseable edge conditions - **DIP101–DIP162** (warnings): conditional reachability, missing defaults, overlapping conditions, unbounded retries, undefined variables, unknown models, empty prompts, missing timeouts, invalid policy/fidelity/reasoning_effort, stylesheet refs, namespace prefixes, condition type checking, structured output validation, manager_loop checks, tool-access safety, writable-paths safety, subgraph tool_access boundary, agent failure route, negative budget defaults, cross-file subgraph tool_access, restricted→tool-bearing info-flow (chain-attack), negative last_response_truncate, ambiguous routing (multiple unconditional edges), human-gate choice key (label routes without explicit choice), edge weight (unused by routing), marker coverage (marker_grep enumerates a marker no edge routes), redundant parallel/fan_in edge (edges-block re-declaration of an inline fork; the inline list is authoritative), prompt-cascade opt-out no-op (prompt_prefix/suffix: none with no defaults cascade), unknown input type (DIP155), reference to an undeclared input in a prompt or edge condition (DIP156), `${inputs.x}` inside a tool `command:` which never interpolates (DIP157), invalid or inapplicable input constraint — enum default ∉ options, min > max, bad pattern regex, or a constraint on a type that lacks it (DIP158), declared-but-unreferenced input / dead input (DIP159), subgraph params omitting a required input of the referenced child (cross-file, DIP160), agent pinned to a deprecated catalog model — retired first-party, still billed on passthrough (DIP161), agent `model:` is a family alias (`family@selector`, e.g. `opus@latest`) that resolves to no eligible model — unknown family/selector or all members deprecated/preview (DIP162). DIP155–DIP158 are error-severity — they cause `dippin lint` and `dippin check` to exit non-zero (DIP159 is a warning). --- ## File Structure (strict order) ``` workflow goal: "" requires: , # optional; environmental deps surfaced to runtimes start: exit: defaults model: claude-sonnet-4-6 provider: anthropic inputs idea: text required: true prompt: "What do you want built?" edges ``` Sections in canonical order: **header** (workflow name, goal, optional `requires`, start, exit) → **inputs** (optional) → **defaults** (optional) → **vars** (optional) → **nodes** (any order) → **stylesheet** (optional) → **edges** (optional). `inputs`, `defaults`, `vars`, and `edges` are bare keywords — no colon after them. `requires:` is a comma-separated list of environmental dependencies (e.g. `git, docker, jq`); semantics live in downstream consumers and unknown entries are accepted without a parser diagnostic. `inputs` declares the workflow's callee-side signature: entries are `name: type` (types: `text`, `number`, `bool`, `enum`, `file`, `secret`) with an optional indented block of attributes (`required`, `prompt`, `description`, `default`, `options`, `pattern`, `min`, `max`, `max_length`, `multiline`). Declaration order is significant and never reordered by the formatter. Reference a declared input as `${inputs.name}` in a prompt — never inside a tool `command:`, which never interpolates it (DIP157); an undeclared reference is DIP156, and an unrecognized type is DIP155. Indentation: 2 spaces. Comments: `#` line comments (literal inside multiline blocks). ## Node Types ### agent — LLM call ``` agent Review prompt: Analyze the code and produce a structured review. Rate quality from 1-10. model: claude-sonnet-4-6 provider: anthropic auto_status: true goal_gate: true retry_policy: standard max_retries: 3 ``` | Field | Type | Notes | |-------|------|-------| | `prompt` | multiline | Required (DIP110 if empty, start/exit exempt) | | `prompt_file` | string | Path (relative to `.dip` source directory) to an external file whose contents become the agent's `prompt`. Mutually exclusive with `prompt:`. See "Prompt File Directives" below. | | `system_prompt` | multiline | System message | | `system_prompt_file` | string | Path (relative to `.dip` source directory) to an external file whose contents become the agent's `system_prompt`. Mutually exclusive with `system_prompt:`. See "Prompt File Directives" below. | | `model` | string | Must be valid model ID (DIP108) | | `provider` | string | anthropic, openai, google, deepseek, xai, mistral, cohere | | `backend` | string | Per-node backend override (e.g., `native`, `claude-code`, `acp`) | | `working_dir` | string | Per-node working directory override for isolated execution. | | `tool_access` | string | LLM tool-catalog gate. Only one explicit value: `none` (no tools). Omitted = full catalog. Invalid values are fail-closed at runtime and warned by DIP139. An enforcing runtime is required. See "Agent Tool Access" below. | | `writable_paths` | CSV (globs) | Comma-separated glob list bounding where this agent's tools may write (e.g. `workspace/**, .ai/sprints/**`). Absent = unbounded. An enforcing runtime is required. See "Writable Paths" below. | | `max_turns` | int | Max conversation turns | | `cmd_timeout` | duration | e.g. `30s`, `5m` | | `auto_status` | bool | Parses `STATUS: success/fail` → `ctx.outcome` | | `goal_gate` | bool | Pipeline fails if gate fails. Add a failure route — an `on fail` edge (dip 2), or `retry_target`/`fallback_target` (v1). See DIP115 | | `response_format` | string | `json_object` or `json_schema` (DIP130) | | `response_schema` | multiline JSON | Must be valid JSON (DIP132). Requires `response_format: json_schema` (DIP131) | | `reasoning_effort` | string | `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max` (DIP119) | | `fidelity` | string | `full`, `summary:high`, `summary:medium`, `summary:low`, `compact`, `truncate` (DIP114) | | `cache_tools` | bool | Cache tool results | | `compaction` | string | Context compaction strategy | | `compaction_threshold` | float | 0.0-1.0 (DIP116) | | `params` | key: value | Custom parameters. Keys must not shadow field names (DIP133) | | `reads` | CSV | Context keys read (advisory) | | `writes` | CSV | Context keys written (advisory) | **Agent Tool Access (`tool_access:`)** — *added v0.32.0; an enforcing runtime is required.* A node-level gate on the LLM tool catalog. One explicit value: - `tool_access: none` — The runtime returns an empty tool registry to the model, strips the `tools` array from the request (e.g., Anthropic `tool_choice: none`), and scrubs tool-naming text from the system prompt. `Params` keys (`allowed_tools`, `disallowed_tools`, `tool_choice`, `permission_mode`) are ignored when the gate is set — Params cannot reopen it. - *omitted* — Full catalog (current behavior, unchanged). Invalid values fall back to no-tools at runtime (fail-closed) and are flagged by [DIP139](https://2389-research.github.io/dippin-lang/validation.html#dip139). Bad spelling reduces capability, never expands it. **Threat model bounded:** the v0.28.2 single-agent, multi-tool-call vector — an LLM emitting multiple tool calls in a single response to bypass per-call gating. Set `tool_access: none` on summarizer / reporter / status-only agents that should never execute tools. **Scope is per node, not per pipeline.** `tool_access: none` constrains the executor of *this one node* — it does not taint or restrict what downstream nodes do with this node's output. A bounded summarizer feeding a tool-capable agent is a normal, intended pattern: the restriction is fully intact (this node still cannot call tools), it simply does not extend across edges. Pipeline-level information-flow control is a separate, runtime-layer concern (see [#56](https://github.com/2389-research/dippin-lang/issues/56)). **Non-goals (deferred):** cross-node propagation / cascade ([#53](https://github.com/2389-research/dippin-lang/issues/53)). Chain attacks between agents ([#56](https://github.com/2389-research/dippin-lang/issues/56)) are *partially* addressed by [DIP147](https://2389-research.github.io/dippin-lang/validation.html#dip147) (Hint): a `tool_access: none` agent that declares a context key in `writes:` flowing into a downstream tool-bearing agent's `reads:`. DIP147 keys off dippin's existing declared-IO flow model (the same `reads:`/`writes:` analysis as DIP107/DIP112), **not** a runtime-specific data-flow model — which is why the broader cross-node *edge* lint ([#57](https://github.com/2389-research/dippin-lang/issues/57), the bare `none → full` / `${ctx.last_response}` auto-injection edge) remains rejected: it would have to assume the runtime's auto-injection semantics. The `${ctx.last_response}` vector and a `last_response_truncate:` mitigation remain #56 follow-ups; the v1 field bounds a single-agent vector. **Scope vs. tool-node safety:** `tool_access` gates *LLM-driven* tool calls on agent nodes. It is unrelated to `tool` nodes (shell commands authored directly in `.dip`), whose allowlist/denylist is controlled by the v0.28.x defaults `tool_commands_allow` and `tool_denylist_add`. `tool_access` may also be set per-branch on a block-form `parallel` node; an omitted branch value inherits the target agent's setting. **Subgraph boundary:** `tool_access` does not cross a `subgraph_ref` / `ref` file boundary — a referenced child `.dip` (`manager_loop` or `subgraph` node) is governed entirely by its own file. When a workflow declares `tool_access` and also references a subgraph, [DIP143](https://2389-research.github.io/dippin-lang/validation.html#dip143) (Hint) reminds you to give the child's agents their own `tool_access`. This is distinct from the in-file flow lints (DIP147, and the rejected #57 edge warning): it concerns the cross-*file* boundary. Native `dippin lint` now resolves the child across that boundary: [DIP146](https://2389-research.github.io/dippin-lang/validation.html#dip146) (Hint) fires when a resolved child restricts no agent's `tool_access` while a workflow on the path does, superseding DIP143 for boundaries it can resolve ([#89](https://github.com/2389-research/dippin-lang/issues/89)). DIP143 remains the filesystem-free advisory (e.g. the wasm playground) and the fallback when the child can't be resolved. **Writable Paths (`writable_paths:`)** — *added v0.35.0; an enforcing runtime is required.* A node-level glob list bounding where the agent's tools may write. Shape: comma-separated globs (e.g. `workspace/**, .ai/sprints/**`). - `writable_paths: workspace/**, .ai/sprints/**` — the runtime confines all file mutations (Write, Edit, ApplyPatch, Bash, and any process Bash spawns) to paths matching these globs, resolved against an **immutable session root**. `working_dir` and `Params` keys cannot relocate the anchor. - *omitted* — unbounded writes (current behavior, unchanged). - **Fail-closed:** A present-but-empty `writable_paths:` is rejected by `dippin validate`/`pack` (parse error — list at least one glob or omit the field). A `writable_paths` that is malformed or **unrecognized by a runtime that does not enforce this field** → the runtime must deny all writes or refuse to start. Never falls through to unbounded. **A runtime that does not enforce `writable_paths` must refuse to start rather than run unbounded — this is a safety requirement, not a suggestion.** **Enforcement scope (native backend only):** `writable_paths` is enforced on the `native` backend. On `claude-code` and `acp`, session creation **refuses to start** when `writable_paths` is set — fail-closed, never a silent no-op. **Residual escape classes (out of scope):** `writable_paths` bounds *where writes land*; it does **not** bound network (e.g. `curl`, `cargo fetch`), reads / read-based exfiltration, or *content* within an allowed path (an agent with `writable_paths: workspace/**` can still poison `workspace/Cargo.toml`). Chain laundering (writing an allowed file that a downstream unbounded agent reads) is tracked in [#56](https://github.com/2389-research/dippin-lang/issues/56). **Non-goals (deferred):** cross-node propagation / defaults cascade ([#53](https://github.com/2389-research/dippin-lang/issues/53)), tool-name allowlists ([#55](https://github.com/2389-research/dippin-lang/issues/55)), chain-attack mitigation ([#56](https://github.com/2389-research/dippin-lang/issues/56)). **Lint:** DIP141 fires when `writable_paths` is set alongside `tool_access: none` on the same object (dead config — no tools to bound). DIP142 fires on unsafe entries: absolute paths, `~`, Windows drive letters, `..` escapes, or brace-expansion fragments (`*.{md` from `*.{md,yaml}` being comma-split). Use workspace-relative globs (e.g. `.ai/sprints/**`). `writable_paths` may also be set per-branch on a block-form `parallel` node; an omitted branch value **inherits the target agent's** setting — it never resets to unbounded. ### human — user decision gate ``` human Approve mode: choice ``` | Field | Type | Notes | |-------|------|-------| | `mode` | string | **Required.** `choice`, `freeform`, `interview`, or `yes_no` (DIP127) | | `default` | string | Default choice (meaningless in interview mode — DIP128) | | `prompt` | multiline | Prompt text | | `questions_key` | string | Context key for interview questions | | `answers_key` | string | Context key for interview answers | | `timeout` | duration | e.g. `5m`, `1h`. How long to wait for human response. | | `timeout_action` | string | `fail` or `default`. Action on timeout (default: `fail`). | | `reads` | CSV | Context keys read | | `writes` | CSV | Context keys written | **Modes:** - `choice`: Outgoing edge labels become buttons. Human selects one. - `freeform`: Open text input → `ctx.human_response` - `interview`: Structured Q&A from upstream agent output. Don't combine with choice-style edges (DIP129). - `yes_no`: Binary Y/N prompt — two outgoing edges labeled `[Y]` and `[N]`. ### tool — shell command ``` tool RunTests command: npm test -- --coverage timeout: 60s outputs: pass, fail ``` | Field | Type | Notes | |-------|------|-------| | `command` | multiline | Shell command. Supports pipes, here-docs, case/esac. Required unless `command_file` is set. | | `command_file` | string | Path (relative to the `.dip` source directory) to an external script whose contents replace inline `command:`. Mutually exclusive with `command`. See "Tool Command File" below. | | `timeout` | duration | **Required** (DIP111). e.g. `30s`, `5m` | | `outputs` | CSV | Possible stdout values for condition checks | | `marker_grep` | string | Regex matched against stdout; sets `ctx.tool_marker`. The runtime validates and applies the regex. | | `route_required` | bool | When true, fails the node if the command emits no routing signal recognized by the runtime (the runtime defines the routing-signal format). | | `output_limit` | int | Per-node stdout byte cap (non-negative integer); 0 (or omitted) uses the engine default. | | `reads` | CSV | Context keys read | | `writes` | CSV | Context keys written | Do NOT use `${ctx.*}` in commands — they expand to empty at parse time (DIP124). Output is captured as `ctx.tool_stdout` and `ctx.tool_stderr`. **Tool Command File (`command_file:`)** — *added v0.33.0.* Reference an external file for a tool node's command instead of inlining a heredoc: ```dip tool Setup command_file: scripts/setup.sh ``` Path resolution: relative to the `.dip` source directory. Absolute paths rejected. Symlinks rejected. Parent-tree escape (`../../etc/passwd`) rejected. 4 MiB size cap. Mutually exclusive with `command:` — specifying both is a parse error. Loading: CLI entry points (`dippin lint`, `dippin pack`, `dippin validate`, `dippin doctor`) load the file contents into the IR after parse. The LSP and the playground skip loading; they show the path unresolved. The runtime reads `.dipx` bundles where content is already inlined, so it sees no difference from inline `command:`. Non-goals (deferred): configurable size cap ([#66](https://github.com/2389-research/dippin-lang/issues/66)), full-chain symlink resolution ([#67](https://github.com/2389-research/dippin-lang/issues/67)), glob expansion ([#68](https://github.com/2389-research/dippin-lang/issues/68)), DOT round-trip preservation of the directive form ([#69](https://github.com/2389-research/dippin-lang/issues/69)), graceful LSP/WASM not-loaded signal ([#70](https://github.com/2389-research/dippin-lang/issues/70)). See issue [#52](https://github.com/2389-research/dippin-lang/issues/52). **Prompt File Directives (`prompt_file:` and `system_prompt_file:`)** — *added v0.34.0.* Reference external prompt files from agent nodes: ```dip agent Reviewer model: claude-sonnet-4-6 system_prompt_file: prompts/persona.md prompt_file: prompts/task.md ``` Convention: keep prompt files in a `prompts/` directory alongside your `.dip`. Path resolution and security are identical to `command_file:` above: - Paths resolved relative to the `.dip` source directory - Absolute paths rejected - Parent-tree escapes (`..`) rejected - Symlinks rejected - 4 MiB size cap The two slots are independent — an agent may use any combination of inline `prompt:`, `prompt_file:`, inline `system_prompt:`, `system_prompt_file:`. Only same-slot conflicts (`prompt:` + `prompt_file:`, or `system_prompt:` + `system_prompt_file:`) are parser-time errors. Cross-slot mixes (e.g. `prompt:` + `system_prompt_file:`) are fine. **Pack-time loading:** `dippin pack` inlines the prompt content into the bundled `.dip` so the `.dipx` is self-contained. The runtime reads inline prompts from the bundle; no separate file lookup at runtime. **Shared prompt fragments (#175)** — single-source boilerplate shared across agents. In the `defaults` block, `prompt_prefix:`/`prompt_suffix:` (inline) or `prompt_prefix_file:`/`prompt_suffix_file:` (fragment file) cascade to **every agent**; a per-agent `prompt_include: ` appends an extra fragment. The effective prompt is composed at resolve time as `prefix → body → include → suffix` (suffix always last — satisfies "final line must be …"). An agent opts out with `prompt_suffix: none` / `prompt_prefix: none` (`DIP154` hints on an opt-out with no matching cascade). Fragment files use the same security envelope as `prompt_file`; `pack` inlines the composed prompt, or ships the fragment files under `--no-inline`. Parts are joined with a fixed blank line (`\n\n`); a body-less passthrough agent (no own prompt / include, e.g. a declared start:/exit: node) is skipped, so the cascade never synthesizes a prompt on it (#248). The `defaults` block also accepts **`system_prompt_file:`** (#72) — a shared system prompt (persona) that is a *fallback default*: agents that declare no `system_prompt`/`system_prompt_file` of their own inherit it, and any agent that sets its own overrides it (node wins). File form only. ### parallel / fan_in — concurrent execution ``` parallel FanOut -> WorkerA, WorkerB, WorkerC fan_in Merge <- WorkerA, WorkerB, WorkerC ``` Both inline (`parallel P -> A, B`) and block form (`parallel P` with `branch:` lines) are supported; block form additionally allows per-branch `model` / `provider` / `fidelity` / `tool_access` / `writable_paths` / `last_response_truncate` overrides (an omitted per-branch value inherits the target agent's setting). Every `parallel` must have a matching `fan_in` with identical target/source sets (DIP007) — this applies to both forms. Wire edges from each target to the `fan_in` node in the `edges` block. All targets execute concurrently with independent context copies. ### subgraph — embed another workflow ``` subgraph CodeReview ref: phases/code_review.dip params: repo: myproject branch: main reads: analysis writes: review_result ``` | Field | Type | Notes | |-------|------|-------| | `ref` | string | Path to .dip file (DIP126 if missing) | | `params` / `inputs` | key: value | Call-site binding (`params:` dip 1, `inputs:` dip 2). A key matching a declared child input seeds `${inputs.key}`; others seed `${params.key}` | | `reads` | CSV | Context keys read | | `writes` | CSV | Context keys written | ### manager_loop — supervised child pipeline Spawns a child `.dip` pipeline, polls it on a cadence, and can steer it by injecting context. Maps to `stack.manager_loop` in the runtime; DOT shape `house`. Full reference: [docs/nodes.md](https://github.com/2389-research/dippin-lang/blob/main/docs/nodes.md). ```dip manager_loop QualityGate label: "Quality Gate Supervisor" subgraph_ref: quality_loop.dip poll_interval: 30s max_cycles: 12 stop_condition: stack.child.outcome = success steer_condition: stack.child.cycles = 5 steer_context: hint: halfway_through priority: high ``` | Field | Type | Notes | |-------|------|-------| | `subgraph_ref` | string | **Required.** Path to child .dip file (DIP135 if missing/not found) | | `poll_interval` | duration | Poll cadence (e.g. `30s`). `0` = event-driven | | `max_cycles` | int | Max poll cycles. `0` = unbounded → DIP137 | | `stop_condition` | condition | Over `stack.child.*`; when true the loop exits | | `steer_condition` | condition | When true, inject `steer_context` into child | | `steer_context` | map[string]string | Inline `k=v, k=v` or block form. No commas in inline values | Runtime state: `stack.child.cycles`, `stack.child.outcome`, `stack.child.status`. Lint: DIP135 (bad ref), DIP136 (invalid field), DIP137 (unbounded). ## Common Fields (all block nodes) | Field | Notes | |-------|-------| | `label` | Display name (defaults to node ID) | | `class` | CSS class names (reserved) | | `retry_policy` | `standard`, `aggressive`, `patient`, `linear`, `none` (DIP113 if invalid) | | `max_retries` | Max retry attempts | | `base_delay` | Override base delay, e.g. `500ms`, `2s` | | `retry_target` | Node to jump to on retry — the engine's retry channel, read from the node (not an edge). Same spelling in `dip 1` and `dip 2`. | | `fallback_target` / `fallback_retry_target` | Node to route to when retries are exhausted (read from the node, not an edge). Spelled `fallback_target` in `dip 1`, `fallback_retry_target` in `dip 2`; `dippin fmt --migrate` relabels it. | **Every node must have at least one field.** An empty node body causes a parse error. ## Edges ``` edges Start -> Analyze Analyze -> Decide Decide -> Merge when ctx.outcome = success Decide -> Revise when ctx.outcome = fail Revise -> Analyze loop ``` | Attribute | Syntax | Notes | |-----------|--------|-------| | condition | `when ` | Guard expression | | outcome shorthand | `on ` | Sugar for `when ctx.outcome = ` (agent) or `when ctx.tool_marker = ` (tool + `marker_grep`); `fmt` rewrites eligible `when` to `on`. `` must be a single bare identifier (`[A-Za-z0-9][A-Za-z0-9_-]*`) — quoted or other values need `when`. Not for human gates (route on choice/label) or marker-less tools — use `when` | | label | `label: ` | Display text / human choice button | | choice | `choice: ` | Human-gate routing key; carried, not interpreted — `choice:` is preferred when present, and `label:` remains the fallback routing key when `choice:` is absent (DIP150) | | weight | `weight: ` | Soft-deprecated (DIP151) — parsed but ignored by routing; removal slated for dip 2 | | override | `override: true` | Carried, not interpreted by the parser | | else default | `else -> ` | Section-level success-side default route; at most one per `edges` block; no source node and no attributes (#157) | | loop | `loop` | Bare keyword marking a back-edge; **required on back-edges** to avoid DIP005 (unconditional cycle). Legacy `restart: true` still parses; `fmt` rewrites it to `loop` | ### Conditions Variables must have namespace prefix: `ctx.`, `params.`, or `graph.` (DIP120 if missing). | Operator | Example | |----------|---------| | `=` or `==` | `ctx.outcome = success` | | `!=` | `ctx.outcome != fail` | | `contains` | `ctx.response contains error` | | `not contains` | `ctx.response not contains error` | | `startswith` | `ctx.type startswith urgent` | | `endswith` | `ctx.name endswith _review` | | `in` | `ctx.tier in gold,silver,bronze` | | `and` / `or` | `ctx.outcome = success and ctx.score = high` | | `not` | `not ctx.flagged = true` | Parentheses control precedence. Operator priority: `not` > `and` > `or`. **Quoted values** *(lossless since v0.49.0)*: condition values may be double-quoted — `when ctx.msg = "hello world"`. Inside double quotes, escaped `\"` and `\\` are preserved losslessly, and operator- or comment-like text (`||`, `#`) is literal — only a real trailing `#` comment is stripped. An unterminated double quote is a parse error, rejected before validation (reported at the opening quote — not a DIP010, which is for conditions that tokenize but fail to parse). Quoting is required when the value is the reserved bare keyword `loop`: `when ctx.x = "loop"` (unquoted `loop` is taken as the back-edge flag). **Exhaustive detection:** The linter auto-detects exhaustive condition pairs (`success`/`fail`, complementary `contains`/`not contains`). Using `success`/`fail` as condition values suppresses DIP101/DIP102 warnings. ## Multiline Blocks Fields `prompt:`, `system_prompt:`, `command:`, `response_schema:` support indented content: ``` agent MyAgent prompt: First line sets the indentation baseline. All subsequent lines are de-indented by that amount. Empty lines are preserved. # This is literal content, not a comment. ``` ## Defaults Block ``` defaults model: claude-sonnet-4-6 provider: anthropic retry_policy: standard max_retries: 2 fidelity: medium max_restarts: 3 cache_tools: true compaction: auto max_total_tokens: 500000 max_cost_cents: 1000 max_wall_time: 30m on_failure: Escalate stall_timeout: 5m tool_commands_allow: "git *,make *" tool_denylist_add: "rm -rf /,dd *" ``` All defaults are inherited by nodes unless overridden at the node level. | Default Field | Type | Notes | |---------------|------|-------| | `max_total_tokens` | int | Budget cap on total tokens consumed. | | `max_cost_cents` | int | Budget cap in cents (e.g. 1000 = $10.00). | | `max_wall_time` | duration | Maximum wall-clock time for the workflow (e.g. `30m`, `2h`). | | `on_failure` | string | Graph-level default failure route — node to jump to when an agent has no explicit failure edge, no fallback_target, and no bounded retry. | | `stall_timeout` | duration | Wall-clock span with no forward progress before the run aborts/routes through on_failure (e.g. `5m`, `90s`). 0/unset = disabled. | | `tool_commands_allow` | string | Glob allowlist for tool-node shell commands (comma-separated; optional). | | `tool_denylist_add` | string | Glob patterns appended to the runtime's default denylist (comma-separated; optional). | ## CLI Reference **Global flag:** `--format text|json` — must come **before** the subcommand (e.g. `dippin --format json check file.dip`). Use `dippin help` (not `--help`) to see all commands. ### Authoring | Command | Purpose | |---------|---------| | `dippin parse ` | Output IR as JSON | | `dippin validate ` | Structural checks only (DIP001-DIP010) | | `dippin lint ` | Full validation + semantic warnings (DIP001–DIP162) | | `dippin check ` | All-in-one. JSON output by default — **use this for automated workflows** | | `dippin fmt ` | Print canonical format to stdout | | `dippin fmt --check ` | Exit 1 if not formatted | | `dippin fmt --write ` | Rewrite file in place | | `dippin fmt --migrate ` | Convert a v1 file to `dip 2` (edges own destinations). Combines with `--check`/`--write`. Exit 3 when the migration flags cases needing author review | | `dippin new