{
  "version": "0.2.0-draft",
  "templates": [
    {
      "name": "chain",
      "file": "chain.nika",
      "intent": "take data, produce words, save them",
      "patterns": [
        "deterministic gather",
        "one model job",
        "explicit persist"
      ],
      "slots": 10,
      "sha256": "c09adee68ad5548be7f3de4ea3d749293fb8312d8fd32d42f181a21307bce520",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · chain · gather facts → one model step → persist.\n#\n#   [gather]────▶[think]────▶[persist]\n#   nika:read     infer       nika:write\n#\n# The everyday shape: pull in something real, run exactly ONE model job over\n# it, write the result somewhere you can find it again. Most useful workflows\n# are this and nothing more — reach for a bigger skeleton only when this one\n# genuinely cannot hold the job.\n#\n# Demonstrates ·\n#   - the three-verb spine · invoke → infer → invoke\n#   - `with:` as the DAG edge · a task reads its input from a binding, and the\n#     binding IS what orders the graph (nobody writes the arrows)\n#   - `on_error: recover:` · the skeleton rehearses green in an empty\n#     directory, before it is pointed at real data\n#\n# Needs · nothing. Scaffolds and runs green in an empty directory.\n#\n# Run · nika run <file> --model mock/echo    # offline rehearsal · zero keys\nnika: chain-template       # SLOT: kebab-case workflow id\n\n# This skeleton keeps its authored offline model. Creating a workflow does\n# not select a paid provider from ambient credentials. Choose a different\n# model explicitly for a real run; `--model mock/echo` remains offline.\nmodel: mock/echo\n\nconst:\n  source: \"./README.md\"             # SLOT: your input · README.md exists in most repos\n  destination: \"./output.md\"        # SLOT: where the result lands\n\npermits:                            # the blast radius · default-deny once present\n  tools: [\"nika:read\", \"nika:write\"]\n  # Two literal paths, one each way. `check` can read these (they come from\n  # `const:`) and proves them against the boundary before anything runs — keep\n  # them in step with the two `const:` entries above.\n  fs:\n    read: [\"./README.md\"]     # SLOT: same path as const.source\n    write: [\"./output.md\"]    # SLOT: same path as const.destination\n\ntasks:\n  gather:\n    invoke:                         # SLOT: the fact source · nika:read / nika:fetch / exec\n      tool: \"nika:read\"\n      args: { path: \"${{ const.source }}\" }\n    on_error:\n      # Offline rehearsal. A freshly scaffolded directory has no README, and a\n      # skeleton that dies on its first run teaches nothing — so a not-found\n      # recovers into a literal standing in for the real document. `on_codes:`\n      # keeps that narrow: ONLY not-found is forgiven, a permission error\n      # still fails loudly. Delete this block once the source really exists.\n      # In an EMPTY directory check prints one [inputs] hint that this read\n      # would fail — the run does not: this recover carries it (measured ·\n      # rc=0 · « 1 recovered »). The hint retires once your source exists.\n      on_codes: [NIKA-BUILTIN-READ-001]\n      recover: \"REHEARSAL · no source document here yet.\"\n\n  think:\n    with:\n      gather: ${{ tasks.gather.output }}\n    infer:\n      max_tokens: 800               # SLOT: the spend ceiling · sized for the SEAT, not the answer\n      # The marker below is a VALUE, not a comment. The parser drops\n      # comments, so a `# SLOT:` line could never make `check` refuse —\n      # which is how an unfilled scaffold ran green and left a file that\n      # read like a result (#1066). Replace the line, keep the binding.\n      prompt: |\n        <SLOT: what should the model do with the gathered text?>\n\n        ${{ with.gather }}\n\n  persist:\n    with:\n      think: ${{ tasks.think.output }}\n    invoke:\n      tool: \"nika:write\"\n      args:\n        path: \"${{ const.destination }}\"   # SLOT: destination · same path as permits.fs.write\n        content: \"${{ with.think }}\"       # ALWAYS pass content · a write without it writes nothing\n    on_error:\n      # Golden rehearsal. Under `nika test` the mock plane simulates the\n      # MODEL, not effects — this write is refused (NIKA-452) so the pin\n      # lane needs this recover to walk its own scaffold. `nika run` never\n      # fires it on success (a recover only runs on failure). Delete this\n      # block once pointed at real data, so a genuine write failure is loud.\n      recover: \"REHEARSAL · write refused under nika test — the real run persists to the destination\"\n\noutputs:\n  result: ${{ tasks.think.output }}  # SLOT: the callable contract\n"
    },
    {
      "name": "gate-and-act",
      "file": "gate-and-act.nika",
      "intent": "watch X, act when Y",
      "patterns": [
        "jq extraction",
        "CEL skip-gate",
        "often zero model calls"
      ],
      "slots": 9,
      "sha256": "4870c70f7a6198f86b1da8587e7e1345bbece86ce07446d74e16866cbe514610",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · gate-and-act · check a condition, act only when it holds.\n#\n#   [check]────with · when(cond)────▶[act]\n#   nika:fetch                        nika:notify\n#\n# The watcher. Read a number, stay silent unless it crosses a line. Most runs\n# do nothing at all — and \"nothing happened\" is a SUCCESS here, not a failure.\n# Often zero model calls, so it is cheap enough to run on a schedule.\n#\n# Demonstrates ·\n#   - `when:` as a SKIP gate · routing, not failure · a skipped task is not an\n#     error and does not fail the run\n#   - flow vs capability · `egress:` sanctions the SEND, `permits.net.http`\n#     grants the REACH · an alert path needs BOTH (see `permits.net`)\n#   - `on_error: recover:` · the gate rehearses CLOSED, so the skeleton is\n#     green before the real source is wired\n#\n# Needs · nothing to rehearse. To alert FOR REAL: ALERTS_WEBHOOK_URL in the\n#   environment and a live source in `const.source_url` — then delete the\n#   `on_error:` so a dead source is loud.\n#\n# Run · nika run <file>          # offline rehearsal · the gate stays closed\nnika: gate-and-act-template       # SLOT: kebab-case workflow id\n\nconst:\n  source_url: \"https://api.example.com/v1/value\"   # SLOT: what to watch\n  threshold: 100                    # SLOT: the trigger condition value\n\npermits:                            # the blast radius · default-deny once present\n  tools: [\"nika:fetch\", \"nika:notify\"]\n  net:\n    http:\n      - \"api.example.com\"           # SLOT: the watched host from const.source_url\n      - \"hooks.slack.com\"           # SLOT: the host ALERTS_WEBHOOK_URL points at.\n                                    #   `host_from_self` below means the host is not\n                                    #   knowable at check — it is judged at RUN against\n                                    #   this list, so an unnamed host fails mid-flight.\n\nsecrets:\n  webhook:\n    source: env\n    key: ALERTS_WEBHOOK_URL         # SLOT: where the act lands\n    egress:\n      - to: \"nika:notify\"\n        host_from_self: true        # the secret value IS the destination URL\n\ntasks:\n  check:\n    invoke:\n      tool: \"nika:fetch\"\n      args:\n        url: \"${{ const.source_url }}\"\n        mode: jq\n        jq: \".\"\n    extract:\n      value: \".value\"               # SLOT: the jq path to the watched field\n    on_error:\n      # Offline rehearsal · a sample UNDER the threshold — the gate stays\n      # closed, the skeleton runs green before you wire the real source.\n      recover: { value: 42 }\n\n  act:\n    with:\n      value: ${{ tasks.check.value }}   # the binding IS the edge · check → act\n    # when: is a SKIP gate — routing, not failure (a skipped task is not an error)\n    when: ${{ with.value > const.threshold }}   # SLOT: the CEL condition\n    invoke:\n      tool: \"nika:notify\"           # SLOT: the action · notify / write / exec\n      args:\n        channel: webhook\n        target: \"${{ secrets.webhook }}\"\n        message: \"Threshold crossed · ${{ with.value }}\"   # SLOT\n        severity: warning\n\noutputs:\n  value: ${{ tasks.check.value }}\n"
    },
    {
      "name": "fanout",
      "file": "fanout.nika",
      "intent": "do this for EVERY item",
      "patterns": [
        "runtime collection",
        "the full leash (`for_each.max_parallel`",
        "`for_each.fail_fast`",
        "retry)"
      ],
      "slots": 10,
      "sha256": "e45da5bcc8080771eb279b8a347715f5ba50bb428bb7b9493167a909c21729db",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · fanout · discover a collection → process every item in parallel → merge.\n#\n#   [discover]──▶[process ∥×N]───▶[survivors]──▶[merge]\n#   nika:glob     for_each·infer    nika:jq        infer\n#   [prove]──▶[law_holds]────────────┘  the fan-in law, proven on a fixture first\n#   nika:jq    nika:assert\n#\n# The \"do this for EVERY one of them\" job, where you do not know how many there\n# are until the run starts. Discovery yields a collection, `for_each` fans it\n# out under a real leash (bounded parallelism, per-item timeout, retries), one\n# bad item cannot sink the batch, and the survivors merge into a single answer.\n#\n# Demonstrates ·\n#   - `for_each:` over a RUNTIME collection · the fan width is discovered, never\n#     written down\n#   - the full leash · `for_each.max_parallel` + `timeout` + `retry` +\n#     `for_each.fail_fast: false`\n#   - `on_error: recover: null` + a null-aware fan-in · a failed item becomes a\n#     hole at its index, order is preserved, the batch survives\n#   - pin the glob · `*.md` also matches README.md (hint `glob-readme`)\n#   - `--resume` cache-hits the whole fan when items + definition match,\n#     including prompts that read `item.field`\n#   - extract facts (`type: integer`), then `nika:jq` is the law —\n#     do not pay a second infer to name the level (`13-extract-then-law`)\n#   - prove the fan-in law on a const fixture + `nika:assert` BEFORE it meets\n#     real data (`prove` → `law_holds` · the `unproven-law` hint stays silent)\n#\n# Needs · nothing. Scaffolds and runs green in an empty directory — an empty\n#   discovery skips the fan-out and the fan-in still returns a value.\n#\n# COST · `check` reads `process` as UNBOUNDED by construction: the fan width\n#   is discovered at run, so no static ceiling exists (measured on 0.118.7 ·\n#   « for_each over an expression (unknown count) · 1 uncapped task »). The\n#   per-item ceiling below bounds each CALL; the RUN bounds the FAN. The cap\n#   admits this file (measured on a real seat with `--max-cost-usd 0.10`: the\n#   static floor stays under the cap because the width is unknown) and a\n#   crossing mid-run is NIKA-1704 — the crossing call completes and counts,\n#   nothing new starts.\n#\n# Run · nika run <file> --model mock/echo                            # offline rehearsal\n#       nika run <file> --model <provider/model> --max-cost-usd 0.25     # a real seat · the fan's cap\nnika: fanout-template       # SLOT: kebab-case workflow id\n\n# This skeleton keeps its authored offline model. Creating a workflow does\n# not select a paid provider from ambient credentials. Choose a different\n# model explicitly for a real run; `--model mock/echo` remains offline.\nmodel: mock/echo\n\nrun:\n  # A `timeout:` is a deadline, and a deadline needs a clock. Declaring it says\n  # out loud which one: `system` is the ambient wall clock (the honest default);\n  # `virtual` drives a simulated clock for deterministic tests.\n  clock: system\n\nconst:\n  collection_source: \"./items\"      # SLOT: where the collection comes from\n  # Known answers for the fan-in law — computed by hand, never by a model.\n  # `prove` feeds them through the SAME jq `survivors` uses; a drift between\n  # the two is a red assert, not a quietly wrong report.\n  law_cases: [\"kept\", null, \"also kept\"]\n\npermits:\n  tools: [\"nika:assert\", \"nika:glob\", \"nika:jq\", \"nika:read\"]\n  fs:\n    # TWO entries, and both earn their place. `nika:glob` is gated on the\n    # DIRECTORY it walks, not on the files it returns — measured: with\n    # `./items/*.md` here (the pattern) the run dies `NIKA-SEC-004 ·\n    # ./items resolves outside the declared permits.fs.read boundary`,\n    # because `*` never crosses `/` and so never matches `./items` itself.\n    # And `nika:read` is gated on the FILES — a directory grant does not\n    # cover its children (measured: dir-only here made every per-item read\n    # die SEC-004, `recover: null` swallowed each, and the model was\n    # handed nulls — a green run inventing content). A deeper pattern\n    # (`./items/**/*.md`) walks deeper and needs `./items/**` for both.\n    read: [\"./items\", \"./items/*\"]\n\ntasks:\n  discover:\n    invoke:                         # SLOT: glob / fetch sitemap / exec + jq split\n      tool: \"nika:glob\"\n      args: { pattern: \"${{ const.collection_source }}/*.md\" }\n\n  # `discover` hands back PATH STRINGS — a prompt that interpolates the raw\n  # item shows the model a FILENAME, never the file (measured: a green run\n  # whose fluent « report » was invented from the names alone — check, mock\n  # rehearsal and run all green around it). Reading the item is its own fan.\n  # Delete this task — and fan `process` over `discover` — when your items\n  # already ARE the content (an inline list · fetched records). Need each\n  # content paired with its path? `resume-screener` shows the transpose.\n  read:\n    with:\n      discover: ${{ tasks.discover.output }}\n    for_each:\n      items: ${{ with.discover }}\n      max_parallel: 8\n      fail_fast: false\n    on_error:\n      recover: null                 # an unreadable item yields null · the batch lives\n    invoke:\n      tool: \"nika:read\"\n      args: { path: \"${{ item }}\" }\n\n  process:\n    with:\n      read: ${{ tasks.read.output }}\n    # The quiet-day guard: an EMPTY discovery skips the `read` fan, and a\n    # skipped task hands NULL — which a bare for_each refuses (NIKA-VAR-006).\n    # The ternary keeps the no-items day green end to end.\n    for_each:\n      items: \"${{ with.read == null ? [] : with.read }}\"\n      max_parallel: 4                 # SLOT: the polite ceiling\n      fail_fast: false\n    timeout: \"60s\"                  # SLOT: per-iteration bound\n    retry:\n      max_attempts: 3\n      backoff_strategy: exponential\n      jitter: true\n    on_error:\n      recover: null                 # a failed item yields null at its index · the batch lives\n    infer:                          # SLOT: the per-item job (any verb)\n      max_tokens: 2000              # SLOT: the per-ITEM ceiling · think + answer · multiply by the fan width\n      # The marker below is a VALUE, not a comment. The parser drops\n      # comments, so a `# SLOT:` line could never make `check` refuse —\n      # which is how an unfilled scaffold ran green and left a file that\n      # read like a result (#1066). Replace the line, keep the binding.\n      prompt: |\n        <SLOT: what to do with each item>\n\n        ${{ item }}\n\n  # The fan-in below is a LAW: a jq over what a model produced. A law is\n  # proven on known answers first — `prove` runs the SAME expression over\n  # `const.law_cases`, `law_holds` refuses the run if it drifted. Measured on\n  # 0.118.7: without this pair `check` hints `[unproven-law]` on `survivors`.\n  prove:\n    invoke:\n      tool: \"nika:jq\"\n      args:\n        input: ${{ const.law_cases }}\n        expression: \"((. // []) | [ .[] | select(. != null) ]) == [\\\"kept\\\", \\\"also kept\\\"]\"\n\n  law_holds:\n    with:\n      ok: ${{ tasks.prove.output }}\n    invoke:\n      tool: \"nika:assert\"\n      args:\n        condition: \"${{ with.ok }}\"\n        message: \"the fan-in law drifted from its hand-computed fixture\"\n\n  survivors:\n    after:\n      law_holds: success            # state, no data · the law is proven before it meets real data\n    with:\n      process: ${{ tasks.process.output }}\n    invoke:                         # the null-aware fan-in · order preserved\n      tool: \"nika:jq\"\n      args:\n        input: ${{ with.process }}\n        # `. // []` · an EMPTY discovery skips the whole for_each (null\n        # upstream) — the fan-in must survive its own quiet day.\n        expression: \"(. // []) | [ .[] | select(. != null) ]\"\n\n  merge:\n    with:\n      survivors: ${{ tasks.survivors.output }}\n    infer:\n      max_tokens: 3000              # SLOT: the fan-in ceiling · think + answer\n      # SLOT: the fan-in · the survivors array (failed items filtered). Keep\n      # slot markers out of the block below — it is prompt TEXT, not YAML.\n      prompt: |\n        Merge these results into one report · ${{ with.survivors }}\n\noutputs:\n  report: ${{ tasks.merge.output }}\n"
    },
    {
      "name": "etl-state",
      "file": "etl-state.nika",
      "intent": "only what changed since last run · survive bad input",
      "patterns": [
        "state read→parse→diff→write",
        "`on_error: on_codes:` quarantine"
      ],
      "slots": 8,
      "sha256": "64d090ea10a33ab224d16feea6b80bbfeb4e1c8396d514361ae0740399958136",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · etl-state · incremental data job with a state file.\n#\n#   [approve]──▶[fresh]──┬───────────────────────▶[save_state]   when approved\n#    prompt      fetch   │                         nika:write\n#   [previous_raw]─▶[previous]──┴──▶[delta]──when ≠∅──▶[process]\n#    nika:read      nika:jq          json_diff          nika:jq\n#\n# The \"only what changed since last time\" job. A cursor file on disk remembers\n# the last run, a fetch brings the current truth, and an RFC-6902 diff says\n# exactly what moved — so downstream work is proportional to the CHANGE, not to\n# the size of the dataset.\n#\n# Demonstrates ·\n#   - a state file read→diff→write cycle · one path, both directions\n#   - `nika:read` returns TEXT · it is parsed with `fromjson` before diffing\n#     (skip that and the diff is meaningless — see `previous`)\n#   - a blocking human gate that DOMINATES every path to the network and the\n#     write · NEP-0002's Rule of Two, as a check\n#   - `on_error: on_codes:` · forgive not-found ONLY, never a permission error\n#\n# TWO HINTS THIS FILE KEEPS · one is right, one is the checker's limit:\n#   [headless-prompt] RIGHT · the gate is deliberately blocking · a `default:`\n#     here completes the lethal trifecta and lights NIKA-SEC-009 (measured —\n#     add one and watch the TRIFECTA rung flip). It needs a human; that is\n#     the point.\n#   [inputs] WRONG ABOUT THE RUN · « `previous_raw` reads ./state/etl-state.json\n#     which does not exist here … the run would fail at that wave ». It does\n#     not: the `on_error: on_codes: [NIKA-BUILTIN-READ-001]` below carries the\n#     first run (measured on 0.118.7 · the run reaches the gate with\n#     `previous_raw` recovered). The checker names static read paths without\n#     modelling recovery — a checker limitation, not a defect of this file.\n#     The hint retires the moment the cursor file exists.\n#\n# GOLDEN LANE · the gate stays blocking and default-free. Pin the declined\n#   branch with `nika test <file> --answer approve=false --update`: the answer\n#   belongs to this invocation, never to the workflow. Adding `default:` here\n#   still completes the lethal trifecta and opens NIKA-SEC-009.\n#\n# Needs · nothing to rehearse. For REAL use: a reachable source in\n#   `const.source_url` — then delete the `on_error:` on `fresh`.\n#\n# Run · nika run <file> --output json\n#     The run PAUSES at the gate (exit 4 · durable, not a failure) and prints\n#     its own resume command. Answer it to continue.\nnika: etl-state-template       # SLOT: kebab-case workflow id\n\nconst:\n  source_url: \"https://api.example.com/v1/records\"   # SLOT: the data source\n  state_path: \"./state/etl-state.json\"               # SLOT: the cursor file\n\npermits:                            # the blast radius · default-deny once present\n  tools: [\"nika:fetch\", \"nika:jq\", \"nika:json_diff\", \"nika:prompt\", \"nika:read\", \"nika:write\"]\n  net: { http: [\"api.example.com\"] }    # SLOT: the source host from const.source_url\n  fs:                               # the cursor file, read then rewritten — one path, both ways\n    read: [\"./state/etl-state.json\"]    # SLOT: keep in step with const.state_path\n    write: [\"./state/etl-state.json\"]   # SLOT: idem — the job writes nothing else\n\ntasks:\n  # NEP-0002 · the Rule of Two, as a check. This run holds all three legs at\n  # once: it reads a private file, ingests UNTRUSTED network content, and\n  # persists that content into the very file the NEXT run reads as trusted\n  # state. One human decision has to dominate EVERY path to that write — so the\n  # gate sits before the first network touch, not next to the write (a gate the\n  # fetch can route around dominates nothing). Blocking on purpose: a `default:`\n  # here would disarm it, and the checker knows — measured, adding `default:`\n  # flips TRIFECTA to `✖ NIKA-SEC-009 lethal trifecta complete`.\n  approve:\n    invoke:\n      tool: \"nika:prompt\"\n      args:\n        message: \"Fetch ${{ const.source_url }} and persist it into ${{ const.state_path }}?\"\n\n  previous_raw:\n    invoke:\n      tool: \"nika:read\"\n      args: { path: \"${{ const.state_path }}\" }\n    on_error:\n      on_codes: [NIKA-BUILTIN-READ-001]   # not-found ONLY · a permission error still fails loudly\n      recover: \"[]\"                       # first run · the STRING \"[]\", so `previous` parses it\n                                          # exactly like a real file — one code path, not two\n\n  previous:\n    with:\n      raw: ${{ tasks.previous_raw.output }}\n    invoke:\n      # `nika:read` hands back TEXT, and `nika:json_diff` compares VALUES. Feed\n      # it the raw string and the diff degrades to a single\n      # `{\"op\":\"replace\",\"path\":\"\"}` carrying the entire new document — it looks\n      # like it works, and it reports \"everything changed\" forever. Measured.\n      # `fromjson` is what makes the next task an actual diff.\n      tool: \"nika:jq\"\n      args:\n        input: \"${{ with.raw }}\"\n        expression: \"fromjson\"\n\n  fresh:\n    with:\n      go: ${{ tasks.approve.output }}   # the binding IS the edge · the gate dominates the fetch\n    when: ${{ with.go == true }}        # declined → no network touch at all\n    invoke:\n      tool: \"nika:fetch\"            # SLOT: fetch / read / exec · the fresh data\n      args:\n        url: \"${{ const.source_url }}\"\n        mode: jq\n        jq: \".records\"\n    on_error:\n      recover: []                   # offline rehearsal · an empty batch, the delta stays quiet\n\n  delta:\n    with:                           # the bindings ARE the edges · previous + fresh → delta\n      previous: ${{ tasks.previous.output }}\n      fresh: ${{ tasks.fresh.output }}\n    invoke:\n      tool: \"nika:json_diff\"        # RFC 6902 · empty patch = nothing new\n      args:\n        before: \"${{ with.previous }}\"\n        after: \"${{ with.fresh }}\"\n\n  process:\n    with:\n      delta: ${{ tasks.delta.output }}\n    when: ${{ size(with.delta) > 0 }}\n    invoke:\n      tool: \"nika:jq\"               # SLOT: the delta job (jq · infer · write…)\n      args:\n        input: \"${{ with.delta }}\"\n        expression: \"length\"\n\n  save_state:\n    with:\n      fresh: ${{ tasks.fresh.output }}\n      go: ${{ tasks.approve.output }}   # the binding IS the edge · the gate dominates the write\n    when: ${{ with.go == true }}        # a refusal is a VALUE · the write is skipped, never failed\n    invoke:\n      tool: \"nika:write\"\n      args:\n        path: \"${{ const.state_path }}\"\n        content: \"${{ with.fresh }}\"    # a VALUE · the engine serializes it, and the next run's\n                                        # `fromjson` reads it straight back\n        create_dirs: true\n        overwrite: true\n\noutputs:\n  changes:\n    value: ${{ tasks.delta.output }}\n    description: \"RFC 6902 ops since last run · empty = no-op run\"\n"
    },
    {
      "name": "agent-loop",
      "file": "agent-loop.nika",
      "intent": "research / review / open-ended",
      "patterns": [
        "plan-then-execute",
        "default-deny tools",
        "budgets",
        "engine-owned typed result"
      ],
      "slots": 6,
      "sha256": "4bac2151575df3fc3342ea19cdadf5b5e416eeaaa528bc3ec5f012acfb98da8e",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · agent-loop · plan fast → execute with a leashed agent → validate the typed result.\n#\n#   [plan]────▶[execute]─────▶[confirm]\n#   infer       agent·leashed  nika:assert\n#\n# The open-ended job — research, review, triage — where the steps cannot be\n# written in advance. A cheap `infer` drafts the plan, a LEASHED agent works\n# it (bounded turns, bounded spend, a default-deny tool grant), and an assert\n# refuses to pass an empty result downstream.\n#\n# Demonstrates ·\n#   - `agent:` under the full leash · `max_turns` + `max_tokens_total` + an\n#     explicit `tools:` list · nothing outside it is reachable\n#   - `schema:` on the agent · the engine owns the final shape — a free-text\n#     answer that misses it is re-asked; a `nika:done` result that misses it\n#     is judged as it is on 0.118.7 (see `execute`) — never hand-instruct it\n#   - `nika:assert` as a trust boundary · an empty run must not read as a\n#     successful one\n#\n# Needs · nothing. Scaffolds and runs green in an empty directory.\n#\n# Run · nika run <file> --model mock/echo                    # offline rehearsal\n#     · nika run <file> --var goal=\"triage the open bugs\"    # a real seat\nnika: agent-loop-template       # SLOT: kebab-case workflow id\n\n# This skeleton keeps its authored offline model. Creating a workflow does\n# not select a paid provider from ambient credentials. Choose a different\n# model explicitly for a real run; `--model mock/echo` remains offline.\nmodel: mock/echo\n\ninputs:\n  goal:\n    type: string\n    required: true\n    # A `required:` input still carries a `default:` here so the skeleton\n    # RUNS the moment it is scaffolded. Replace the default with your job;\n    # `--var goal=…` overrides it at any time.\n    default: \"<SLOT: what the agent must accomplish>\"\n    description: \"What the agent must accomplish\"   # SLOT\n\npermits:                            # the blast radius · default-deny once present\n  # Exactly what the body invokes: `nika:assert` (the confirm task) plus the\n  # two the agent may call. No `fs:` — see the note on `tools:` below.\n  tools: [\"nika:assert\", \"nika:done\", \"nika:jq\"]\n\ntasks:\n  plan:\n    infer:\n      prompt: \"Break '${{ inputs.goal }}' into at most 4 concrete steps.\"   # SLOT\n      # SLOT: the spend ceiling for the planning call. Size it for the SEAT,\n      # not for the answer: a reasoning model spends tokens thinking before\n      # it emits the first brace, and a ceiling that cuts it off mid-thought\n      # fails NIKA-INFER-002 (\"no JSON value found · the reply was cut off at\n      # the token limit\"). Measured — 400 starves qwen3.5:4b on this prompt.\n      max_tokens: 2000\n      schema:\n        type: object\n        additionalProperties: false\n        required: [steps]\n        properties:\n          steps: { type: array, items: { type: string } }\n\n  execute:\n    with:\n      # BOTH bindings, on purpose. The plan is a summary of the goal, never a\n      # substitute for it: measured on five real seats, a prompt that carried\n      # only the plan produced three GREEN runs with an invented or an empty\n      # answer — the agent was asked to compute over data it never received,\n      # and the assert below only proves non-emptiness. The goal rides in.\n      goal: ${{ inputs.goal }}\n      steps: ${{ tasks.plan.output.steps }}\n    agent:\n      # Describe the job in the prompt; declare its result shape in schema:.\n      # The granted nika:done sentinel is required to finish this loop.\n      # Text-only plans receive feedback within the turn/token budgets.\n      # The engine validates the final result and can request a schema\n      # repair within its configured retry allowance and task budgets.\n      # A valid result shape does not prove the promised work was performed;\n      # retain an independent check of the facts or artifacts you require.\n      system: \"You are a careful analyst. Work the plan step by step and report what you actually found.\"   # SLOT\n      prompt: |\n        Goal · ${{ with.goal }}\n        Plan · ${{ with.steps }}\n      tools:                        # SLOT: the MINIMUM grant for the job\n        # Pure compute — this pair needs no filesystem, so the skeleton runs\n        # anywhere. Granting the agent a tool here is only HALF a grant: every\n        # call still crosses the workflow boundary above. Two halves, two\n        # clocks — `tools:` vs `permits.tools` is judged STATICALLY at check\n        # (since 0.115); adding `nika:read` to this list WITHOUT adding\n        # `permits.fs.read` stays the run's verdict and fails at run with\n        # `NIKA-SEC-004 · agent tool \"nika:read\" refused by the security\n        # boundary` — measured, mid-loop, after the turns are paid for.\n        - \"nika:jq\"\n        - \"nika:done\"               # explicit completion · loop-owned\n      max_turns: 15                 # SLOT: the loop bound\n      max_tokens_total: 80000       # SLOT: cumulative input + output tokens\n      schema:                       # SLOT: the typed final-message contract\n        # Closed on purpose: declare every allowed result field here.\n        # Keep extra fields out of the result; schema repair is bounded,\n        # and exhausting that allowance is a failure.\n        type: object\n        additionalProperties: false\n        required: [findings]\n        properties:\n          findings:\n            type: array\n            minItems: 1               # an empty list is a schema miss, never a green run\n            items: { type: string, minLength: 1 }\n\n  confirm:\n    with:\n      # The schema above already refuses an empty list and an empty string;\n      # this assert is the trust boundary downstream code can SEE, and the\n      # place for YOUR law once the goal fixes one (a count · a required id ·\n      # a value you computed by hand). `size` counts the collection in this\n      # expression here — a per-item rule belongs in the schema\n      # (as above) or in a `nika:jq` law. No static law catches an INVENTED\n      # answer: that gap closes upstream, by binding the goal into the agent.\n      has_findings: ${{ size(tasks.execute.output.findings) > 0 }}   # the check crosses as ONE boundary expression · SLOT: your law\n    invoke:\n      tool: \"nika:assert\"\n      args:\n        condition: \"${{ with.has_findings }}\"\n        message: \"Agent returned no findings, do not trust an empty run\"   # SLOT\n\noutputs:\n  findings:\n    value: ${{ tasks.execute.output.findings }}\n    description: \"The agent's typed findings\"   # SLOT\n"
    },
    {
      "name": "human-gated-ship",
      "file": "human-gated-ship.nika",
      "intent": "anything irreversible (deploy · send · publish)",
      "patterns": [
        "parallel gates (`nika:grep` evidence",
        "`exec:` command)",
        "assert",
        "a BLOCKING `nika:prompt`",
        "`after: {…: terminal}` record"
      ],
      "slots": 11,
      "sha256": "e447e644d2e34327ab03f5fee8bed54fd0cfc2293e1379e3dcdb3a6b050bf65c",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · human-gated-ship · gates in parallel → a human signs → ship · evidence always lands.\n#\n#   [check_a]─┐                             ┌──▶[act]──▶[record]\n#   [check_b]─┴─▶[gates]──▶[human ⏸]──yes──┘    exec     nika:notify (always)\n#   grep · exec   assert     nika:prompt\n#\n# Anything you cannot take back — a deploy, a send, a publish. The machine\n# proves the preconditions in parallel, an assert refuses to even ASK when a\n# gate is red, the human decides once with the full picture, and the evidence\n# lands whether the answer was yes, no, or the ship blew up.\n#\n# Demonstrates ·\n#   - two gate kinds, each in its native form · FILE evidence is `nika:grep`\n#     (structured · no subprocess · no confinement trap) · a COMMAND is `exec:`\n#     in argv form\n#   - `nika:assert` before the question · never ask a human to rubber-stamp a\n#     red board\n#   - `after: <task>: success` · a STATE edge (ordering, no data) vs `with:` (a\n#     value edge) — two different kinds of arrow\n#   - `after: <task>: terminal` · the always-runs pattern · evidence on success,\n#     failure OR refusal\n#   - a BLOCKING gate that dominates every effect · fail-closed lives in the\n#     invocation, never in the file (see THE TRADE)\n#\n# THE TRADE · real gates read private files, the ship is a program, the record\n#   leaves the machine: that is the lethal trifecta (NEP-0002), and only a\n#   blocking `nika:prompt` that dominates every path to those effects clears\n#   it. A `default: false` on the gate LOOKS fail-closed — unattended it answers\n#   NO — but a defaulted gate is not blocking and dominates nothing: with the\n#   gates below the file is refused at check, `NIKA-SEC-009 lethal trifecta\n#   complete · human gate required` (measured on 0.118.7 · pinned beside this\n#   file in `human-gated-ship.negative.yaml`). So the gate here has NO default:\n#   the check is green, and headless the run PAUSES (exit 4 · durable, not a\n#   failure) — the ONE hint this file keeps, `[headless-prompt]`, is the checker\n#   naming that pause. Unattended NO is one flag away, and it belongs to the\n#   invocation, never to the workflow ·\n#     nika run <file> --answer human=false\n#\n# Needs · nothing to rehearse. To ship FOR REAL: the evidence the gate greps\n#   (under `const.evidence_dir`), TEAM_WEBHOOK_URL in the environment, a real\n#   command in `check_b` and the real ship in `act` — then delete the three\n#   `on_error:` rehearsal blocks, so missing evidence, a refused command and a\n#   lost audit trail are all loud.\n#\n# Run · nika run <file>                        # asks at a terminal · PAUSES headless (exit 4)\n#       nika run <file> --answer human=false   # unattended · fail closed · act settles skipped\n#       nika run <file> --resume <trace> --answer human=true   # the resume line the pause prints\n#\n# GOLDEN LANE · nika test <file> --answer human=false --update · the answer\n#   belongs to this invocation, never to the workflow.\nnika: human-gated-ship-template       # SLOT: kebab-case workflow id\n\nconst:\n  evidence_dir: \"./release\"         # SLOT: the DIRECTORY the file gate walks (grep walks a tree, never one file)\n  version_pattern: '1\\.4\\.2'        # SLOT: the version you are shipping · a regex, so the dots are escaped\n\npermits:                            # SLOT: the blast radius · default-deny once present\n  exec: [\"echo\"]                    # SLOT: ONLY the programs check_b + act run (argv form)\n  tools: [\"nika:grep\", \"nika:assert\", \"nika:prompt\", \"nika:notify\"]\n  # `host_from_self` below sanctions the FLOW (the secret may BE the URL) — it\n  # does not grant the capability to reach anyone. The host stays unknown at\n  # check, so it is judged at RUN against this list: name the webhook host here\n  # or the record is refused mid-run, after the ship already happened.\n  net: { http: [\"hooks.slack.com\"] }   # SLOT: the webhook host · nothing else may leave\n  fs:\n    # TWO entries, both measured on 0.118.7. The grep walk is judged on its\n    # ROOT at check — `./release/*` alone is refused NIKA-SEC-004 — and reads\n    # its FILES at run: `./release` alone checks green and then returns NO\n    # MATCH for a pattern that is there (the files stay unreadable, the walk\n    # reports nothing) — a gate that reads RED with no error to tell you why.\n    read: [\"./release\", \"./release/*\"]   # SLOT: keep in step with const.evidence_dir\n\nsecrets:\n  webhook:\n    source: env\n    key: TEAM_WEBHOOK_URL           # SLOT: where the record lands\n    egress:\n      - to: \"nika:notify\"\n        host_from_self: true        # the secret value IS the destination URL\n\ntasks:\n  # ── the verification wave · both gates run in parallel ──\n  check_a:\n    # FILE evidence, the native way. `nika:grep` returns `[{path, line, match}]`,\n    # already structured. An `exec: [\"grep\", …]` here draws the native-first/006\n    # hint (exit 2 under `--native-strict`) AND needs the same fs grant: an exec\n    # child is confined to `permits.fs.read` too (measured on 0.118.7 · without\n    # the grant `grep` dies NIKA-SEC-001 « seatbelt refused the confined\n    # process » and the message names no path). GREEN = the release notes under\n    # `evidence_dir` name the version being shipped.\n    invoke:\n      tool: \"nika:grep\"\n      args:\n        pattern: \"${{ const.version_pattern }}\"\n        path: \"${{ const.evidence_dir }}\"\n    on_error:\n      # Rehearsal · in an EMPTY directory the walk finds no `./release` and\n      # fails NIKA-BUILTIN-GREP-001 (measured); recovering to one match keeps\n      # the scaffold green the moment it is scaffolded. `on_codes:` keeps that\n      # narrow — a permission refusal still fails loudly. DELETE this block once\n      # the evidence exists: a ship gate must fail CLOSED on missing evidence.\n      on_codes: [NIKA-BUILTIN-GREP-001]\n      recover: [{ path: \"REHEARSAL\", line: 0, match: \"REHEARSAL\" }]\n\n  check_b:\n    # A COMMAND gate · a test suite, a CLI probe · argv form, never a shell.\n    exec:\n      command: [\"echo\", \"ok\"]        # SLOT: gate 2 · the program must be in permits.exec\n      capture: structured\n    on_error:\n      # Golden rehearsal · `nika test` refuses exec (NIKA-SEC-001 · the mock\n      # plane simulates the model, not effects). Catch-all ON PURPOSE — never\n      # `on_codes: [NIKA-SEC-001]`: that code is also a real blocklist stop,\n      # and a narrow forgiveness would swallow a security refusal. The shape\n      # matches `capture: structured` so the gates expression reads it.\n      # Delete once the gate runs a real check.\n      recover: { exit_code: 0, stdout: \"REHEARSAL\", stderr: \"\" }\n\n  gates:\n    with:\n      all_green: ${{ size(tasks.check_a.output) > 0 && tasks.check_b.output.exit_code == 0 }}   # two value edges · one boundary expression\n    invoke:\n      tool: \"nika:assert\"\n      args:\n        condition: \"${{ with.all_green }}\"\n        message: \"A gate is RED: refusing to proceed\"   # SLOT\n\n  human:\n    after:\n      gates: success              # state, no data · no question until the board is green\n    invoke:\n      # NO `default:` — on purpose (THE TRADE above). At a terminal this ASKS;\n      # headless it PAUSES (exit 4 · durable, not a failure) and prints its own\n      # resume line. A `default: false` would answer NO unattended AND disarm\n      # the gate: measured on 0.118.7, this file is then refused NIKA-SEC-009\n      # at check. The unattended answer is the invocation's, `--answer`.\n      tool: \"nika:prompt\"\n      args:\n        message: \"All gates GREEN. Proceed?\"   # SLOT: the decision, fully informed\n\n  act:\n    with:\n      go: ${{ tasks.human.output }}   # the answer crosses as a value edge\n    when: ${{ with.go == true }}\n    exec:\n      command: [\"echo\", \"shipped\"]   # SLOT: the irreversible action (argv · program must be in permits.exec)\n      # default capture · a failing ship fails LOUDLY (NIKA-EXEC-001):\n      # never `capture: structured` on the irreversible step (exit codes\n      # would become data and a red ship would read as success)\n\n  record:\n    after:\n      act: terminal                 # the always-pattern · runs on success, failure, OR refusal\n    with:\n      acted: ${{ tasks.act.status }}   # observe the outcome · same pass-set as the after edge\n    invoke:\n      tool: \"nika:notify\"\n      args:\n        channel: webhook\n        target: \"${{ secrets.webhook }}\"\n        message: \"Run finished · act=${{ with.acted }}\"   # SLOT · success | failure | skipped\n        severity: info\n    on_error:\n      # Rehearsal only. With TEAM_WEBHOOK_URL unset the reference cannot resolve\n      # and this task fails NIKA-VAR-001 (measured) — which would make the\n      # skeleton red on a machine that has no webhook. Recovering keeps the\n      # scaffold runnable; DELETE this block for real use, because a ship whose\n      # audit trail silently vanished is exactly what this task exists to prevent.\n      recover: \"REHEARSAL · no webhook configured · nothing was sent\"\n\noutputs:\n  acted: ${{ tasks.act.status }}\n"
    },
    {
      "name": "website-brief",
      "file": "website-brief.nika",
      "intent": "understand a site (domain · theme · assets) from a URL",
      "patterns": [
        "fetch `traverse:` crawl",
        "one typed infer",
        "explicit persist",
        "zero exec"
      ],
      "slots": 9,
      "sha256": "52ff867ab56ab2aef8c3a86a69cccaab0c68681bdf96dec53bf2f8cdc37d4bac",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · website-brief · crawl a site → understand it → persist the brief.\n#\n#   [crawl_site]────▶[brief]────▶[persist]\n#   nika:fetch        infer       nika:write\n#\n# \"Here is a URL, tell me what this company actually does.\" One bounded crawl,\n# one typed model pass, one JSON file downstream tools can read. The output is\n# a SHAPE, not prose — which is what makes it composable.\n#\n# Demonstrates ·\n#   - `traverse:` · a bounded same-origin crawl from a single URL · one fetch\n#     task, N pages, no loop to write\n#   - `schema:` on `infer` · the brief is typed, so the next workflow can bind\n#     `.colors` without parsing anything\n#   - a JSON artifact written as a VALUE · `content:` takes one `${{ … }}`, never\n#     hand-typed braces\n#\n# Needs · nothing to rehearse. For a REAL brief: network access and a live site\n#   in `const.site_url` — then delete the `on_error:` block so a dead host is loud.\n#\n# Run · nika run <file> --model mock/echo    # rehearsal · green even with no network\n#   (on a networked machine the crawl really fetches const.site_url — it is permitted)\nnika: website-brief-template       # SLOT: kebab-case workflow id\n\n# This skeleton keeps its authored offline model. Creating a workflow does\n# not select a paid provider from ambient credentials. Choose a different\n# model explicitly for a real run; `--model mock/echo` remains offline.\nmodel: mock/echo\n\nconst:\n  site_url: \"https://example.com\"   # SLOT: the site to understand\n  out_path: \"./out/brief.json\"      # SLOT: where the brief lands\n\npermits:                            # the blast radius · default-deny once present\n  tools: [\"nika:fetch\", \"nika:write\"]\n  net: { http: [\"example.com\"] }    # SLOT: the host from const.site_url. ONE entry is\n                                    #   enough — `traverse:` is a same-origin BFS (+ the\n                                    #   robots probe), so the crawl never leaves this host.\n  fs: { write: [\"./out/brief.json\"] }   # SLOT: the one file · keep in step with const.out_path\n                                        #   (the infer step needs no grant — it is pure compute)\n\ntasks:\n  crawl_site:\n    invoke:\n      tool: \"nika:fetch\"\n      args:\n        url: \"${{ const.site_url }}\"\n        traverse: { max_pages: 5 }  # SLOT: crawl bound · 1..=25 (robots honored)\n    on_error:\n      # Offline rehearsal · a literal standing in for the crawl digest, so the\n      # brief step runs with no network at all. Delete once the site is real.\n      recover: \"REHEARSAL DIGEST · Example Co · a small tool company · plain blue and white pages · buttons, a pricing table, one logo.\"\n\n  brief:\n    with:\n      crawl_site: ${{ tasks.crawl_site.output }}\n    infer:\n      max_tokens: 1200\n      # SLOT: the one model job · what should the brief capture? Keep slot\n      # markers out of the block below — it is prompt TEXT, not YAML, so a\n      # stray comment line is sent to the model verbatim.\n      prompt: |\n        From this site crawl, produce a creative brief: the domain of\n        activity, the dominant visual theme, the audience, the usable\n        colors and image assets.\n        Crawl digest · ${{ with.crawl_site }}\n      schema:                       # SLOT: the typed shape downstream tasks rely on\n        type: object\n        additionalProperties: false\n        properties:\n          domain: { type: string }\n          theme: { type: string }\n          audience: { type: string }\n          colors: { type: array, items: { type: string } }\n          assets: { type: array, items: { type: string } }\n        required: [domain, theme, audience, colors, assets]\n\n  persist:\n    with:\n      brief: ${{ tasks.brief.output }}\n    invoke:\n      tool: \"nika:write\"\n      args:\n        path: \"${{ const.out_path }}\"\n        create_dirs: true\n        # The path ends `.json`, so `content:` is ONE interpolation of a value\n        # the engine serializes. Typing `{ \"domain\": ${{ … }} }` by hand emits\n        # unquoted fields and the artifact stops being JSON.\n        content: \"${{ with.brief }}\"\n    on_error:\n      # Golden rehearsal · `nika test` refuses effects (NIKA-452 · the mock\n      # plane simulates the model, not effects) — this recover lets the pin\n      # lane walk the scaffold. Never fires on a successful real write.\n      # Delete once wired, so a genuine write failure is loud.\n      recover: \"REHEARSAL · write refused under nika test — the real run persists the brief\"\n\noutputs:\n  brief: ${{ tasks.brief.output }}   # SLOT: the callable contract\n"
    },
    {
      "name": "media-asset-pack",
      "file": "media-asset-pack.nika",
      "intent": "generate image/audio assets from a brief",
      "patterns": [
        "`nika:image_generate`",
        "`nika:jq` manifest",
        "local/mock provider first"
      ],
      "slots": 8,
      "sha256": "9358b5a04ba81ec9b3c3b9c7195f3f87e1777274d1721ce4428cffb0c901d519",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · media-asset-pack · brief → generate assets → manifest.\n#\n#   [brief]──▶[render]────────────▶[manifest]──▶[persist]\n#   infer      nika:image_generate  nika:jq       nika:write\n#\n# Turning a sentence into shippable art plus the paperwork that makes it\n# usable: a model writes the image prompt, the generator renders it, and a\n# manifest records WHAT was made from WHICH brief — so the pack can be\n# regenerated, audited, or handed to someone else.\n#\n# Demonstrates ·\n#   - `nika:image_generate` with `provider: mock` · byte-real assets, zero key\n#   - the directory-AND-children permit · the one shape that passes `check` and\n#     survives the run (see `permits.fs.write`)\n#   - a JSON artifact built as a VALUE by `nika:jq`, never typed by hand — and\n#     built from the TOOL's own record (the prompt the generator received),\n#     not from the model's words (see `manifest`)\n#\n# Needs · nothing. Scaffolds and runs green in an empty directory.\n#\n# Run · nika run <file> --model mock/echo    # offline rehearsal · assets land in ./out/assets\nnika: media-asset-pack-template       # SLOT: kebab-case workflow id\n\n# This skeleton keeps its authored offline model. Creating a workflow does\n# not select a paid provider from ambient credentials. Choose a different\n# model explicitly for a real run; `--model mock/echo` remains offline.\nmodel: mock/echo\n\nconst:\n  subject: \"<SLOT: what the asset is about>\"\n  out_dir: \"./out/assets\"           # SLOT: where assets land\n\npermits:                            # the blast radius · default-deny once present\n  tools: [\"nika:image_generate\", \"nika:jq\", \"nika:write\"]\n  fs:\n    write:\n      # Two entries, and both earn their place: `check` judges the `output_dir:`\n      # ARGUMENT (`./out/assets`), while the RUN gates every FINAL file path under\n      # it — the asset, its provenance manifest, and manifest.json. Grant only the\n      # directory and the file sails through check, then dies at run on the first\n      # asset. `*` is one segment and never crosses `/`, which is all this needs:\n      # image_generate lands its files flat, so no subtree grant is warranted.\n      - \"./out/assets\"              # SLOT: keep in step with const.out_dir\n      - \"./out/assets/*\"            # SLOT: idem · the files that land inside it\n\ntasks:\n  brief:\n    infer:\n      max_tokens: 600\n      # SLOT: the creative direction · style · constraints. Keep slot markers\n      # out of the block below — it is prompt TEXT, not YAML, so a stray\n      # comment line is sent to the model verbatim.\n      prompt: |\n        Write one vivid, concrete image prompt for: ${{ const.subject }}.\n        No text in the image · no watermark · a calm central zone.\n      schema:\n        type: object\n        additionalProperties: false\n        properties:\n          image_prompt: { type: string }\n        required: [image_prompt]\n\n  render:\n    with:\n      brief_image_prompt: ${{ tasks.brief.output.image_prompt }}\n    invoke:\n      tool: \"nika:image_generate\"\n      args:\n        provider: mock              # SLOT: local | openai | gemini | xai (local/mock first)\n        prompt: \"${{ with.brief_image_prompt }}\"\n        output_dir: \"${{ const.out_dir }}\"\n        filename_prefix: \"asset\"    # SLOT: filename stem\n    on_error:\n      # Golden rehearsal · the image tool is an EFFECT, refused under\n      # `nika test` even on `provider: mock` (the plane simulates the\n      # model, not effects). The literal keeps the shape `manifest` reads\n      # (`.images` · `.prompt`) intact — a recover value interpolates `with.`\n      # (measured on 0.118.7), so the rehearsal manifest still records the\n      # brief. Delete once a real seat renders.\n      recover: { images: [], prompt: \"${{ with.brief_image_prompt }}\" }\n\n  manifest:\n    with:\n      render: ${{ tasks.render.output }}\n    invoke:\n      # ONE input, the generator's own record: `nika:image_generate` returns\n      # the `prompt` it received beside its `images` (measured on 0.118.7), so\n      # the manifest is a projection of a TOOL output — not a law over what a\n      # model produced. Feed it the model's words directly and it becomes one,\n      # and `check` asks for a const-fixture proof (`[unproven-law]` · the\n      # shape is `nika try 13-extract-then-law`). Here nothing is judged, only\n      # recorded — from the side that actually rendered.\n      tool: \"nika:jq\"\n      args:\n        input: ${{ with.render }}\n        expression: \"{ brief: { image_prompt: .prompt }, images: .images }\"\n\n  persist:\n    with:\n      manifest: ${{ tasks.manifest.output }}\n    invoke:\n      tool: \"nika:write\"\n      args:\n        path: \"${{ const.out_dir }}/manifest.json\"\n        create_dirs: true\n        # `.json` path → `content:` is ONE interpolation of a value the engine\n        # serializes. `nika:jq` above BUILT that value; hand-typing braces\n        # around an interpolation emits unquoted fields and breaks the artifact.\n        content: \"${{ with.manifest }}\"\n    on_error:\n      # Golden rehearsal · same NIKA-452 refusal as `render` above. Never\n      # fires on a successful real write. Delete once wired.\n      recover: \"REHEARSAL · write refused under nika test — the real run lands the manifest\"\n\noutputs:\n  manifest: ${{ tasks.manifest.output }}  # SLOT: the callable contract\n"
    },
    {
      "name": "api-upload-and-create",
      "file": "api-upload-and-create.nika",
      "intent": "call a product API: upload a file and create from it",
      "patterns": [
        "fetch `multipart:` (file + text parts)",
        "masked secrets header",
        "mode/jq extraction"
      ],
      "slots": 11,
      "sha256": "1e06897b41686f7c51f97e70893bd16e09dba869df85378cb3c89913e1d20952",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · api-upload-and-create · upload a file and create the resource in one authenticated call.\n#\n#   [create]\n#   nika:fetch · multipart\n#\n# The product-API job: push the bytes AND the metadata that describe them, get\n# the created resource back, keep only the fields you need. One round trip —\n# `multipart:` carries a file part and text parts together, which is what most\n# create-with-upload endpoints actually want.\n#\n# Demonstrates ·\n#   - `multipart:` · a file part (`path:`) and text parts (`value:`) in one\n#     request · exactly one of the two per part\n#   - a masked `secrets:` entry in a header, with `egress:` naming the two\n#     places it may travel (the call, and the returned value)\n#   - `mode: jq` response narrowing · the workflow carries fields, not payloads\n#   - `on_error: on_codes:` + `recover:` · rehearses green while the KEY is not\n#     configured, and forgives nothing else — a wired key the API rejects, or a\n#     dead endpoint, is loud; the same bindings work once it is wired\n#\n# WHY ONE CALL, not upload-then-create · a file part is `permits.fs.read`-gated,\n#   and a private read + untrusted ingress + an onward authenticated POST is the\n#   lethal trifecta (NEP-0002): chaining a second call onto the first response\n#   fails NIKA-SEC-009 and demands a human gate. Measured both orderings. One\n#   round trip needs no ceremony — reach for `human-gated-ship` when the job\n#   genuinely wants a person in the loop.\n#\n# Needs · nothing to rehearse. To run it FOR REAL: a reachable API, the file at\n#   `const.asset_path`, and EXAMPLE_API_KEY in the environment — then delete the\n#   `on_error:` block, so that an unset key is loud too (a rejected key already\n#   is: the block only forgives the not-configured case).\n#\n# Run · nika run <file>          # offline rehearsal · no key exported\nnika: api-upload-and-create-template       # SLOT: kebab-case workflow id\n\nconst:\n  api_base: \"https://api.example.com\"     # SLOT: the product API base\n  asset_path: \"./out/assets/asset-1.png\"  # SLOT: the file to upload\n\nsecrets:\n  API_KEY:\n    source: env\n    key: EXAMPLE_API_KEY            # SLOT: the OS env var holding the key\n    egress:\n      - to: \"nika:fetch\"            # the send · default-deny otherwise\n      - to: \"outputs\"               # the return value derives from the authed response\n\npermits:                            # the blast radius · default-deny once present\n  tools: [\"nika:fetch\"]\n  # `egress:` above sanctions the FLOW (this secret may ride a fetch); it does\n  # NOT grant the capability to reach anyone. The host is the separate, required\n  # half — an unlisted host is refused at RUN, mid-flight, with the bytes\n  # already on the wire.\n  net: { http: [\"api.example.com\"] }   # SLOT: the host from const.api_base\n  fs:\n    # A `multipart:` file part names a path, and that read crosses the boundary\n    # like any other: measured, without this entry the call dies `NIKA-SEC-004 ·\n    # ./out/assets/asset-1.png resolves outside the declared permits.fs.read\n    # boundary`. One exact file, never the tree it sits in. The drift detector\n    # models a multipart part as a read (2026-07-29) — the former NIKA-DRIFT-001\n    # false hint is closed.\n    read: [\"./out/assets/asset-1.png\"]   # SLOT: keep in step with const.asset_path\n\ntasks:\n  create:\n    invoke:\n      tool: \"nika:fetch\"\n      args:\n        url: \"${{ const.api_base }}/items\"   # SLOT: the create endpoint\n        method: POST\n        headers:\n          x-api-key: \"${{ secrets.API_KEY }}\"  # SLOT: the auth header name\n        multipart:\n          # Exactly one of `path:` (file) or `value:` (text) per part — a part\n          # carrying both, or neither, is refused before anything is sent.\n          - { name: file, path: \"${{ const.asset_path }}\" }\n          - { name: title, value: \"Rehearsal item\" }   # SLOT: the metadata fields\n        mode: jq\n        jq: \"{ id: .id, url: .url }\"   # SLOT: the fields downstream needs\n    on_error:\n      # Offline rehearsal, NARROWED to the one thing it may forgive: the key is\n      # not configured. A `secrets:` entry whose env var is unset fails\n      # NIKA-VAR-001 before any byte leaves (measured on 0.118.7 · the run\n      # settles « 1 recovered » on that code) — that absence IS the rehearsal\n      # switch. Everything else stays loud: a wired key the API rejects is\n      # NIKA-BUILTIN-FETCH-001 naming the 401 (measured), a dead endpoint too.\n      # A catch-all here turned a 401 into a GREEN run carrying the id below —\n      # a result that reads like a created item (measured against a fixture API\n      # answering 401). The literal is shaped EXACTLY like what the jq above\n      # projects, so `outputs.result` has the same shape on both paths. Delete\n      # the block once the key is wired, so an unset key is loud as well.\n      on_codes: [NIKA-VAR-001]\n      recover: { id: \"rehearsal-0001\", url: \"https://app.example.com/items/rehearsal-0001\" }\n\noutputs:\n  result: ${{ tasks.create.output }}   # SLOT: the callable contract\n"
    },
    {
      "name": "docker-report",
      "file": "docker-report.nika",
      "intent": "read a system's state (docker · kubectl · gh), explain it, keep the report",
      "patterns": [
        "argv-array exec (provable allowlist)",
        "parallel reads",
        "one artifact"
      ],
      "slots": 8,
      "sha256": "6e2a3211a39b960cc170efe70964a5e0b50bf99eeba78752cae27acadb64e9c6",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · docker-report · read real state via a pinned CLI → explain it → keep the report.\n#\n#   [ps]────┐\n#   [df]────┴──▶[diagnose]────▶[keep]\n#   exec ×2      infer          nika:write\n#\n# The \"ask the machine what is actually true, then explain it\" job. Two reads\n# run in parallel because nothing connects them, one model pass turns raw CLI\n# output into a report a human can act on, and the report is kept on disk. Swap\n# `docker` for `kubectl`, `gh`, `systemctl` — the shape does not change.\n#\n# Demonstrates ·\n#   - argv-ARRAY `exec:` · one program, exactly these arguments, no shell to\n#     inject into · and `permits.exec` names the single program allowed\n#   - implicit parallelism · `ps` and `df` share no edge, so the scheduler runs\n#     them together · nobody writes \"in parallel\" anywhere\n#   - `on_error: recover:` on an exec · the skeleton rehearses green on a host\n#     with no Docker at all\n#\n# Needs · nothing to rehearse. For a REAL report: a running Docker daemon —\n#   then delete the two `on_error:` blocks so a broken daemon is loud.\n#\n# Run · nika run <file> --model mock/echo    # offline rehearsal · no daemon\nnika: docker-report-template       # SLOT: kebab-case workflow id\n\n# This skeleton keeps its authored offline model. Creating a workflow does\n# not select a paid provider from ambient credentials. Choose a different\n# model explicitly for a real run; `--model mock/echo` remains offline.\nmodel: mock/echo\n\npermits:                            # the blast radius · default-deny once present\n  exec:\n    - \"docker\"                      # SLOT: the ONE program the reads may launch\n  tools:\n    - \"nika:write\"\n  fs:\n    write:\n      - \"./docker-health.md\"        # SLOT: where the report lands (must match `keep`)\n\ntasks:\n  # The reads run IN PARALLEL (no edges between them) — the\n  # scheduler proves it from the DAG, nobody orders it.\n  ps:\n    exec:\n      # SLOT: argv ARRAY form — one program, exactly these arguments. The array\n      # is why there is no shell here: no word-splitting, no globbing, nothing\n      # to quote wrong. `permits.exec` above is the provable allowlist.\n      command: [\"docker\", \"ps\", \"--all\", \"--format\", \"{{.Names}}\\t{{.Status}}\\t{{.Image}}\"]\n    on_error:\n      # Offline rehearsal · a host with no daemon answers with a literal that\n      # LOOKS like the real thing, so `diagnose` reads the same shape either\n      # way. Delete this once the daemon is really there.\n      recover: \"REHEARSAL\\tno docker daemon on this host\\tn/a\"\n\n  df:\n    exec:\n      command: [\"docker\", \"system\", \"df\"]   # SLOT: the second read (drop the task if one suffices)\n    on_error:\n      recover: \"REHEARSAL · disk usage unavailable without a daemon\"\n\n  diagnose:\n    with:\n      ps: ${{ tasks.ps.output }}\n      df: ${{ tasks.df.output }}\n    infer:\n      max_tokens: 2000              # SLOT: the spend ceiling for this call · think block + report\n      # SLOT: what should the model DO with the readings? Keep slot markers out\n      # of the block below — everything indented under `prompt: |` is prompt\n      # TEXT, and a stray comment line is sent to the model verbatim.\n      prompt: |\n        You are reading a Docker host's state. Containers (name·status·image):\n        ${{ with.ps }}\n\n        Disk usage:\n        ${{ with.df }}\n\n        Write a short health report: what is running, what exited, what\n        looks unhealthy (restart loops · old exits), and whether disk\n        usage needs attention. Plain prose, no preamble.\n\n  keep:\n    with:\n      diagnose: ${{ tasks.diagnose.output }}\n    invoke:\n      tool: \"nika:write\"\n      args:\n        path: \"./docker-health.md\"  # SLOT: same path as permits.fs.write\n        content: \"${{ with.diagnose }}\"\n    on_error:\n      # Golden rehearsal · `nika test` refuses effects (NIKA-452) — the pin\n      # lane recovers into this marker (outputs.report pins it · honest and\n      # deterministic). Never fires on a successful real write. Delete once\n      # the report really lands, so a genuine write failure is loud.\n      recover: \"REHEARSAL · write refused under nika test — the real run lands ./docker-health.md\"\n\noutputs:\n  report:\n    value: ${{ tasks.keep.output }}\n    description: \"Where the report landed — nika:write hands back the path it wrote\"\n"
    },
    {
      "name": "document-to-fields",
      "file": "document-to-fields.nika",
      "intent": "extract fields from a document and keep the evidence",
      "patterns": [
        "trim",
        "nonempty assertion",
        "typed fields",
        "exact source anchors"
      ],
      "slots": 2,
      "sha256": "a3916e19ae3dc85f95413690768e25fbdc7e0cc01dabf823891b5b619d98f837",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · document-to-fields · source text → anchored facts → nonempty proof.\n#\n# The source is trimmed and proved nonempty before inference. The extraction\n# is typed, and every value carries the exact source fragment that supports it.\n# An empty extraction is refused before it can look like a successful read.\n#\n# Needs · nothing. Runs offline in an empty directory with mock/echo.\n# Run · nika run <file> --model mock/echo\nnika: document-to-fields       # SLOT: kebab-case workflow id\n\n# This skeleton keeps its authored offline model. Creating a workflow does\n# not select a paid provider from ambient credentials. Choose a different\n# model explicitly for a real run; `--model mock/echo` remains offline.\nmodel: mock/echo\n\nconst:\n  # NOT a slot, deliberately. This literal is the REHEARSAL FIXTURE the\n  # skeleton's own `nika:assert` step reads — hole it and the template\n  # stops rehearsing green, without closing anything: this workflow\n  # persists nothing, so it leaves behind no file that reads like a\n  # result (#1066's actual failure mode). Replace it with your document,\n  # or bind this const to a `nika:read` / `nika:fetch` task's output.\n  document: |\n    Invoice INV-204\n    Supplier: Northwind Tools\n    Total: EUR 84.50\n    Rehearsal marker: mock\n\npermits:\n  tools: [\"nika:jq\", \"nika:assert\"]\n\ntasks:\n  normalize_document:\n    invoke:\n      tool: nika:jq\n      args:\n        input: ${{ const.document }}\n        expression: 'gsub(\"^\\\\s+|\\\\s+$\"; \"\")'\n\n  document_nonempty:\n    with:\n      document: ${{ tasks.normalize_document.output }}\n    invoke:\n      tool: nika:assert\n      args:\n        condition: ${{ size(with.document) > 0 }}\n        message: \"Document is empty after whitespace is trimmed\"\n\n  extract:\n    with:\n      document: ${{ tasks.normalize_document.output }}\n    after:\n      document_nonempty: success\n    infer:\n      max_tokens: 600                # SLOT: extraction spend ceiling\n      prompt: |\n        Extract only facts explicitly present in this document. For every\n        value, copy the shortest exact source fragment that anchors it.\n\n        ${{ with.document }}\n      schema:\n        type: object\n        additionalProperties: false\n        required: [fields]\n        properties:\n          fields:\n            type: array\n            minItems: 1\n            items:\n              type: object\n              additionalProperties: false\n              required: [name, value, anchor]\n              properties:\n                name: { type: string, minLength: 1 }\n                value: { type: string, minLength: 1 }\n                anchor: { type: string, minLength: 1 }\n\n  anchors_present:\n    with:\n      fields: ${{ tasks.extract.output.fields }}\n      document: ${{ tasks.normalize_document.output }}\n    invoke:\n      tool: nika:jq\n      args:\n        input: { fields: \"${{ with.fields }}\", document: \"${{ with.document }}\" }\n        expression: '. as $root | ($root.fields | length) > 0 and all($root.fields[]; . as $field | (.name | length) > 0 and (.value | length) > 0 and (.anchor | length) > 0 and ($root.document | contains($field.anchor)))'\n\n  compiled:\n    with:\n      anchors_present: ${{ tasks.anchors_present.output }}\n    invoke:\n      tool: nika:assert\n      args:\n        condition: ${{ with.anchors_present }}\n        message: \"Document extraction returned no anchored fields\"\n\noutputs:\n  fields: ${{ tasks.extract.output.fields }}\n"
    },
    {
      "name": "corpus-qa",
      "file": "corpus-qa.nika",
      "intent": "answer from a corpus, or say that the answer is unknown",
      "patterns": [
        "conflicting-id refusal",
        "idempotent sort/unique index",
        "empty citations on unknown"
      ],
      "slots": 3,
      "sha256": "0c219f7765a1df77ae6af91281129f82a1e677496e348bbb2d0a34e5141cce72",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · corpus-qa · canonicalize corpus → retrieve → answer or abstain.\n#\n# Identical duplicates are idempotent, while one id carrying conflicting text\n# is refused before indexing. An empty retrieval is a valid answer with empty\n# citations, never a fabricated model response.\n#\n# Needs · nothing. Runs offline in an empty directory with mock/echo.\n# Run · nika run <file> --model mock/echo\nnika: corpus-qa       # SLOT: kebab-case workflow id\n\n# This skeleton keeps its authored offline model. Creating a workflow does\n# not select a paid provider from ambient credentials. Choose a different\n# model explicitly for a real run; `--model mock/echo` remains offline.\nmodel: mock/echo\n\nconst:\n  query: \"<SLOT: the question to answer from the corpus>\"\n  documents:                         # SLOT: replace with retrieved source chunks\n    - { id: handbook, text: \"Support hours are 09:00 to 17:00 UTC.\" }\n    - { id: security, text: \"Security incidents are reported within one hour.\" }\n    - { id: handbook, text: \"Support hours are 09:00 to 17:00 UTC.\" }\n\npermits:\n  tools: [\"nika:jq\", \"nika:assert\"]\n\ntasks:\n  conflicting_ids:\n    invoke:\n      tool: nika:jq\n      args:\n        input: ${{ const.documents }}\n        expression: 'sort_by(.id) | group_by(.id) | map(select((map(.text) | unique | length) > 1) | .[0].id)'\n\n  duplicates_consistent:\n    with:\n      conflicting_ids: ${{ tasks.conflicting_ids.output }}\n    invoke:\n      tool: nika:assert\n      args:\n        condition: ${{ size(with.conflicting_ids) == 0 }}\n        message: \"One corpus id names conflicting source text\"\n\n  index_once:\n    after:\n      duplicates_consistent: success\n    invoke:\n      tool: nika:jq\n      args:\n        input: ${{ const.documents }}\n        expression: \"sort_by(.id) | unique_by(.id)\"\n\n  index_twice:\n    with:\n      index: ${{ tasks.index_once.output }}\n    invoke:\n      tool: nika:jq\n      args:\n        input: ${{ with.index }}\n        expression: \"sort_by(.id) | unique_by(.id)\"\n\n  index_is_idempotent:\n    with:\n      first: ${{ tasks.index_once.output }}\n      second: ${{ tasks.index_twice.output }}\n    invoke:\n      tool: nika:jq\n      args:\n        input: { first: \"${{ with.first }}\", second: \"${{ with.second }}\" }\n        expression: \".first == .second\"\n\n  compiled:\n    with:\n      idempotent: ${{ tasks.index_is_idempotent.output }}\n    invoke:\n      tool: nika:assert\n      args:\n        condition: ${{ with.idempotent }}\n        message: \"Corpus index changed when canonicalized twice\"\n\n  retrieve:\n    with:\n      index: ${{ tasks.index_once.output }}\n    invoke:\n      tool: nika:jq\n      args:\n        input: { documents: \"${{ with.index }}\", query: \"${{ const.query }}\" }\n        expression: '.documents as $docs | .query as $query | ($docs | map(select((.text | ascii_downcase) | contains($query | ascii_downcase))))'\n\n  answer:\n    with:\n      hits: ${{ tasks.retrieve.output }}\n    when: ${{ size(with.hits) > 0 }}\n    infer:\n      max_tokens: 500                # SLOT: answer spend ceiling\n      prompt: |\n        Answer from these passages only and cite their ids.\n        Question: ${{ const.query }}\n        Passages: ${{ with.hits }}\n      schema:\n        type: object\n        additionalProperties: false\n        required: [answer, citations]\n        properties:\n          answer: { type: string, minLength: 1 }\n          citations: { type: array, minItems: 1, items: { type: string } }\n\n  finalize:\n    with:\n      hits: ${{ tasks.retrieve.output }}\n      answer: ${{ tasks.answer.output }}\n    invoke:\n      tool: nika:jq\n      args:\n        input: { hits: \"${{ with.hits }}\", answer: \"${{ with.answer }}\" }\n        expression: 'if (.hits | length) == 0 then {known: false, answer: \"I do not know from this corpus.\", citations: []} else {known: true, answer: .answer.answer, citations: .answer.citations} end'\n\noutputs:\n  result: ${{ tasks.finalize.output }}\n"
    },
    {
      "name": "classify-and-route",
      "file": "classify-and-route.nika",
      "intent": "classify facts and route them by a governed law",
      "patterns": [
        "typed facts",
        "EvidenceSnapshot",
        "`nika:decide`",
        "fixture proof"
      ],
      "slots": 2,
      "sha256": "4528338b033f53a849ca0c7fa7f0151792442ace3789f890b3ef230beb3d737c",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · classify-and-route · typed facts → evidence → decision → queue.\n#\n# The model extracts facts. The deterministic nika:decide kernel owns the\n# routing law and emits a receipt. A known fixture proves the law separately.\n#\n# Needs · nothing. Runs offline in an empty directory with mock/echo.\n# Run · nika run <file> --model mock/echo\nnika: classify-and-route       # SLOT: kebab-case workflow id\n\n# This skeleton keeps its authored offline model. Creating a workflow does\n# not select a paid provider from ambient credentials. Choose a different\n# model explicitly for a real run; `--model mock/echo` remains offline.\nmodel: mock/echo\n\nconst:\n  request: \"<SLOT: the request to classify and route>\"\n  bundle:\n    decision_bundle_format: 1\n    manifest:\n      id: incident-route\n      version: \"1.0.0\"\n      owner: example-org\n      license: Apache-2.0\n      valid_until: \"2028-01-01T00:00:00Z\"\n    evidence_schema:\n      urgency:\n        type: integer\n        required: true\n        sources: [classifier]\n        integrity: observed\n    transforms:\n      urgency_step:\n        kind: bucket\n        min: 0\n        max: 1\n        edges: [1]\n        values: [0, 10000]\n    rules:\n      dimensions:\n        incident_priority:\n          terms:\n            - { evidence: urgency, transform: urgency_step, weight_bp: 10000, monotonicity: increases }\n      thresholds:\n        - { dimension: incident_priority, recommend_gte_bp: 5000 }\n    governance:\n      never_automatic: [recommend]\n      override: { append_only: true, reason_required: true }\n      appeal: \"ask the incident commander\"\n      human_required_triggers: [conflict_on_required]\n    fixtures:\n      - { name: urgent, class: positive, evidence: { urgency: 1 } }\n      - { name: routine, class: negative, evidence: { urgency: 0 } }\n      - { name: edge, class: ambiguous, evidence: { urgency: 0 } }\n      - { name: conflict, class: contradictory, evidence: { urgency: 1 } }\n      - { name: probe, class: adversarial, evidence: { urgency: 1 } }\n  urgent_snapshot:\n    t: \"2027-01-01T00:00:00Z\"\n    evidence:\n      - key: urgency\n        value: 1\n        source: classifier\n        observed_at: \"2027-01-01T00:00:00Z\"\n        digest: fixture-urgency\n        confidentiality: internal\n        integrity: observed\n        quality: { freshness: fresh, completeness: complete, independence_group: classifier }\n\npermits:\n  tools: [\"nika:jq\", \"nika:decide\", \"nika:assert\"]\n\ntasks:\n  extract_facts:\n    infer:\n      max_tokens: 300                # SLOT: classification spend ceiling\n      prompt: |\n        Extract the urgency fact from this request. Use 1 only for an active\n        widespread outage, otherwise use 0.\n\n        ${{ const.request }}\n      schema:\n        type: object\n        additionalProperties: false\n        required: [urgency]\n        properties:\n          urgency: { type: integer, enum: [0, 1] }\n\n  snapshot:\n    with:\n      facts: ${{ tasks.extract_facts.output }}\n    invoke:\n      tool: nika:jq\n      args:\n        input: ${{ with.facts }}\n        expression: '{t: \"2027-01-01T00:00:00Z\", evidence: [{key: \"urgency\", value: .urgency, source: \"classifier\", observed_at: \"2027-01-01T00:00:00Z\", digest: \"runtime-urgency\", confidentiality: \"internal\", integrity: \"observed\", quality: {freshness: \"fresh\", completeness: \"complete\", independence_group: \"classifier\"}}]}'\n\n  decide:\n    with:\n      evidence: ${{ tasks.snapshot.output }}\n    invoke:\n      tool: nika:decide\n      args:\n        bundle: ${{ const.bundle }}\n        evidence: ${{ with.evidence }}\n\n  queue:\n    with:\n      receipt: ${{ tasks.decide.output }}\n    invoke:\n      tool: nika:jq\n      args:\n        input: ${{ with.receipt }}\n        expression: 'if .outcome == \"human_required\" then \"manual-review\" elif .outcome == \"recommend\" then \"priority\" else \"standard\" end'\n\n  fixture_decide:\n    invoke:\n      tool: nika:decide\n      args:\n        bundle: ${{ const.bundle }}\n        evidence: ${{ const.urgent_snapshot }}\n\n  compiled:\n    with:\n      receipt: ${{ tasks.fixture_decide.output }}\n    invoke:\n      tool: nika:assert\n      args:\n        condition: ${{ with.receipt.outcome == \"human_required\" }}\n        message: \"Urgent fixture must abstain to manual review\"\n\noutputs:\n  queue: ${{ tasks.queue.output }}\n  receipt: ${{ tasks.decide.output }}\n"
    },
    {
      "name": "evaluate-and-optimize",
      "file": "evaluate-and-optimize.nika",
      "intent": "critique and improve a draft for a fixed number of rounds",
      "patterns": [
        "two unrolled revisions",
        "score-only final evaluation",
        "every infer capped"
      ],
      "slots": 7,
      "sha256": "6abb970d905444b07d0df63e6330f7eb6d0a6c636cc49a0f19c5d9d817552513",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · evaluate-and-optimize · draft → two fixed critique/revision rounds.\n#\n# The bound is structural: exactly two revision rounds are unrolled in the DAG,\n# followed by one score-only measurement of the final result. The model cannot\n# add another turn, and every infer has its own token ceiling.\n#\n# Needs · nothing. Runs offline in an empty directory with mock/echo.\n#\n# CEILINGS · every infer is capped, and a cap is sized for the SEAT, not for\n#   the answer: a reasoning seat thinks inside `max_tokens`, so a cap that fits\n#   the visible answer alone starves it (the thinking trace eats the cap and\n#   the answer is blank · NIKA-INFER-004 at run). The capacity gate names the\n#   floor BEFORE any spend, but only for the seat it is asked about — this\n#   file's envelope seat is `mock/echo`, which checks clean with ANY cap.\n#   Measured on 0.118.7 across five real seats: `max_tokens: 200` on the three\n#   critique steps was refused at check for the three reasoning seats («too\n#   small for reasoning seat … at least 256»). Pin the seat when you check:\n#     nika check <file> --model <provider/model>\n#\n# Run · nika run <file> --model mock/echo\nnika: evaluate-and-optimize       # SLOT: kebab-case workflow id\n\n# This skeleton keeps its authored offline model. Creating a workflow does\n# not select a paid provider from ambient credentials. Choose a different\n# model explicitly for a real run; `--model mock/echo` remains offline.\nmodel: mock/echo\n\nconst:\n  brief: \"<SLOT: the deliverable to draft, critique and revise>\"\n\npermits: {}\n\ntasks:\n  draft:\n    infer:\n      max_tokens: 300               # SLOT: the draft ceiling · sized for the SEAT (see CEILINGS)\n      prompt: \"Draft this deliverable: ${{ const.brief }}\"\n      schema:\n        type: object\n        additionalProperties: false\n        required: [text]\n        properties:\n          text: { type: string, minLength: 1 }\n\n  evaluate_1:\n    with:\n      candidate: ${{ tasks.draft.output.text }}\n    infer:\n      max_tokens: 400               # SLOT: the critique ceiling · a reasoning seat's floor is 256 here (measured)\n      prompt: |\n        Critique this candidate against the brief. Name one concrete repair.\n        Brief: ${{ const.brief }}\n        Candidate: ${{ with.candidate }}\n      schema:\n        type: object\n        additionalProperties: false\n        required: [score, repair]\n        properties:\n          score: { type: integer, minimum: 0, maximum: 100 }\n          repair: { type: string, minLength: 1 }\n\n  optimize_1:\n    with:\n      candidate: ${{ tasks.draft.output.text }}\n      repair: ${{ tasks.evaluate_1.output.repair }}\n    infer:\n      max_tokens: 300               # SLOT: the revision ceiling · sized for the SEAT\n      prompt: |\n        Revise the candidate using the repair, without changing the brief.\n        Candidate: ${{ with.candidate }}\n        Repair: ${{ with.repair }}\n      schema:\n        type: object\n        additionalProperties: false\n        required: [text]\n        properties:\n          text: { type: string, minLength: 1 }\n\n  evaluate_2:\n    with:\n      candidate: ${{ tasks.optimize_1.output.text }}\n    infer:\n      max_tokens: 400               # SLOT: idem · the same floor applies\n      prompt: |\n        Critique this revision against the same brief. Name one final repair.\n        Brief: ${{ const.brief }}\n        Candidate: ${{ with.candidate }}\n      schema:\n        type: object\n        additionalProperties: false\n        required: [score, repair]\n        properties:\n          score: { type: integer, minimum: 0, maximum: 100 }\n          repair: { type: string, minLength: 1 }\n\n  optimize_2:\n    with:\n      candidate: ${{ tasks.optimize_1.output.text }}\n      repair: ${{ tasks.evaluate_2.output.repair }}\n    infer:\n      max_tokens: 300               # SLOT: the final revision ceiling · sized for the SEAT\n      prompt: |\n        Produce the final revision using the last repair.\n        Candidate: ${{ with.candidate }}\n        Repair: ${{ with.repair }}\n      schema:\n        type: object\n        additionalProperties: false\n        required: [text]\n        properties:\n          text: { type: string, minLength: 1 }\n\n  final_evaluate:\n    with:\n      candidate: ${{ tasks.optimize_2.output.text }}\n    infer:\n      max_tokens: 400               # SLOT: the score-only ceiling · the same floor applies\n      prompt: |\n        Score the final candidate against the brief. Evaluate the candidate\n        exactly as written without proposing another revision.\n        Brief: ${{ const.brief }}\n        Candidate: ${{ with.candidate }}\n      schema:\n        type: object\n        additionalProperties: false\n        required: [score, reason]\n        properties:\n          score: { type: integer, minimum: 0, maximum: 100 }\n          reason: { type: string, minLength: 1 }\n\noutputs:\n  result: ${{ tasks.optimize_2.output.text }}\n  final_score: ${{ tasks.final_evaluate.output.score }}\n"
    },
    {
      "name": "bounded-batch",
      "file": "bounded-batch.nika",
      "intent": "admit a finite batch before starting model calls",
      "patterns": [
        "bounded-batch",
        "authoring",
        "bounded"
      ],
      "slots": 1,
      "sha256": "5333556fdb46fd16bd6e48f9a267d470558f6e663e2510df6322066eb75833d2",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · bounded-batch · admit a finite batch before starting model calls\n#\n# At most eight items, two concurrent calls, 2048 tokens and 30s per item. Empty input returns []. max_parallel alone does not bound item count.\n# Demonstrates · explicit input admission, narrow permits and typed results.\n# Needs · fill the SLOT; no services or credentials for mock rehearsal.\n# Run · nika run <file> --model mock/echo\nnika: bounded-batch\nmodel: ollama/qwen3.5:4b\nrun:\n  clock: system\nconst:\n  brief: '<SLOT: the per-item instruction>'\n  items:\n  - first note\n  - second note\npermits:\n  tools:\n  - nika:assert\n  - nika:validate\ntasks:\n  validate:\n    invoke:\n      tool: nika:validate\n      args:\n        data: ${{ const.items }}\n        format: json\n        schema:\n          type: array\n          maxItems: 8\n          items:\n            type: string\n            minLength: 1\n            maxLength: 4000\n  admit:\n    with:\n      valid: ${{ tasks.validate.output.valid }}\n    invoke:\n      tool: nika:assert\n      args:\n        condition: ${{ with.valid }}\n        message: Input violates the declared shape or size bound\n  process:\n    after:\n      admit: success\n    for_each:\n      items: ${{ const.items }}\n      max_parallel: 2\n      fail_fast: true\n    timeout: 30s\n    infer:\n      prompt: '${{ const.brief }}\n\n        Item: ${{ item }}'\n      max_tokens: 2048\n      schema:\n        type: object\n        additionalProperties: false\n        required:\n        - summary\n        properties:\n          summary:\n            type: string\n            minLength: 1\n            maxLength: 4000\noutputs:\n  results: '${{ tasks.process.output == null ? [] : tasks.process.output }}'\n"
    },
    {
      "name": "validate-records",
      "file": "validate-records.nika",
      "intent": "reject malformed records before producing totals",
      "patterns": [
        "validate-records",
        "authoring",
        "bounded"
      ],
      "slots": 1,
      "sha256": "fa9c9f0ea60b2d73b45d14ea5f55053b70ff01eadf70f11ab5aab6f53cb41d2c",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · validate-records · reject malformed records before producing totals\n#\n# At most eight records. Closed objects reject unknown fields; integer amounts avoid string coercion. Validation returns a verdict; assert makes rejection terminal.\n# Demonstrates · explicit input admission, narrow permits and typed results.\n# Needs · fill the SLOT; no services or credentials for mock rehearsal.\n# Run · nika run <file> --model mock/echo\nnika: validate-records\nconst:\n  batch: '<SLOT: the batch label>'\n  records:\n  - id: a\n    amount: 10\n  - id: b\n    amount: 20\npermits:\n  tools:\n  - nika:assert\n  - nika:jq\n  - nika:validate\ntasks:\n  validate:\n    invoke:\n      tool: nika:validate\n      args:\n        data: ${{ const.records }}\n        format: json\n        schema:\n          type: array\n          maxItems: 8\n          items:\n            type: object\n            additionalProperties: false\n            required:\n            - id\n            - amount\n            properties:\n              id:\n                type: string\n                minLength: 1\n                maxLength: 64\n              amount:\n                type: integer\n                minimum: 0\n                maximum: 1000000\n  admit:\n    with:\n      valid: ${{ tasks.validate.output.valid }}\n    invoke:\n      tool: nika:assert\n      args:\n        condition: ${{ with.valid }}\n        message: Input violates the declared shape or size bound\n  report:\n    after:\n      admit: success\n    invoke:\n      tool: nika:jq\n      args:\n        input:\n          batch: ${{ const.batch }}\n          records: ${{ const.records }}\n        expression: '{batch, count: (.records | length), total: ([.records[].amount] | add // 0)}'\noutputs:\n  report: ${{ tasks.report.output }}\n"
    },
    {
      "name": "snapshot-diff",
      "file": "snapshot-diff.nika",
      "intent": "compare two snapshots without model judgment",
      "patterns": [
        "snapshot-diff",
        "authoring",
        "bounded"
      ],
      "slots": 1,
      "sha256": "ec5772c13742e1ce2a8555663ce1d6832a17f35aa52c679b91b1750f81b86837",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · snapshot-diff · compare two snapshots without model judgment\n#\n# Each snapshot has at most eight bounded strings. Set semantics remove duplicates and sort the output; an empty snapshot is meaningful.\n# Demonstrates · explicit input admission, narrow permits and typed results.\n# Needs · fill the SLOT; no services or credentials for mock rehearsal.\n# Run · nika run <file> --model mock/echo\nnika: snapshot-diff\nconst:\n  resource: '<SLOT: the resource being compared>'\n  before:\n  - alpha\n  - beta\n  after:\n  - beta\n  - gamma\npermits:\n  tools:\n  - nika:assert\n  - nika:jq\n  - nika:validate\ntasks:\n  validate:\n    invoke:\n      tool: nika:validate\n      args:\n        data:\n          before: ${{ const.before }}\n          after: ${{ const.after }}\n        format: json\n        schema:\n          type: object\n          additionalProperties: false\n          required:\n          - before\n          - after\n          properties:\n            before:\n              type: array\n              maxItems: 8\n              items:\n                type: string\n                minLength: 1\n                maxLength: 4000\n            after:\n              type: array\n              maxItems: 8\n              items:\n                type: string\n                minLength: 1\n                maxLength: 4000\n  admit:\n    with:\n      valid: ${{ tasks.validate.output.valid }}\n    invoke:\n      tool: nika:assert\n      args:\n        condition: ${{ with.valid }}\n        message: Input violates the declared shape or size bound\n  diff:\n    after:\n      admit: success\n    invoke:\n      tool: nika:jq\n      args:\n        input:\n          resource: ${{ const.resource }}\n          before: ${{ const.before }}\n          after: ${{ const.after }}\n        expression: '{resource, added: ((.after - .before) | unique), removed: ((.before - .after) | unique),\n          kept: ([.after[] as $id | select(.before | index($id)) | $id] | unique)}'\noutputs:\n  diff: ${{ tasks.diff.output }}\n"
    },
    {
      "name": "deduplicate-records",
      "file": "deduplicate-records.nika",
      "intent": "collapse identical duplicates and refuse conflicts",
      "patterns": [
        "deduplicate-records",
        "authoring",
        "bounded"
      ],
      "slots": 1,
      "sha256": "268f72d69e07053f22042ff69e34071092c9c349be060fed7811763bf083c073",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · deduplicate-records · collapse identical duplicates and refuse conflicts\n#\n# At most eight records. Matching ids with different amounts fail before deduplication; exact duplicates are idempotent.\n# Demonstrates · explicit input admission, narrow permits and typed results.\n# Needs · fill the SLOT; no services or credentials for mock rehearsal.\n# Run · nika run <file> --model mock/echo\nnika: deduplicate-records\nconst:\n  dataset: '<SLOT: the dataset label>'\n  records:\n  - id: a\n    amount: 10\n  - id: a\n    amount: 10\n  - id: b\n    amount: 20\npermits:\n  tools:\n  - nika:assert\n  - nika:jq\n  - nika:validate\ntasks:\n  validate:\n    invoke:\n      tool: nika:validate\n      args:\n        data: ${{ const.records }}\n        format: json\n        schema:\n          type: array\n          maxItems: 8\n          items:\n            type: object\n            additionalProperties: false\n            required:\n            - id\n            - amount\n            properties:\n              id:\n                type: string\n                minLength: 1\n                maxLength: 64\n              amount:\n                type: integer\n                minimum: 0\n                maximum: 1000000\n  admit:\n    with:\n      valid: ${{ tasks.validate.output.valid }}\n    invoke:\n      tool: nika:assert\n      args:\n        condition: ${{ with.valid }}\n        message: Input violates the declared shape or size bound\n  consistent:\n    after:\n      admit: success\n    invoke:\n      tool: nika:jq\n      args:\n        input: ${{ const.records }}\n        expression: group_by(.id) | all(.[]; (map(.amount) | unique | length) == 1)\n  agree:\n    with:\n      consistent: ${{ tasks.consistent.output }}\n    invoke:\n      tool: nika:assert\n      args:\n        condition: ${{ with.consistent }}\n        message: Conflicting records share an id; do not silently keep the first\n  deduplicate:\n    after:\n      agree: success\n    invoke:\n      tool: nika:jq\n      args:\n        input:\n          dataset: ${{ const.dataset }}\n          records: ${{ const.records }}\n        expression: '{dataset, records: (.records | unique_by(.id))}'\noutputs:\n  result: ${{ tasks.deduplicate.output }}\n"
    },
    {
      "name": "project-public-fields",
      "file": "project-public-fields.nika",
      "intent": "select allowed fields before sending data to a model",
      "patterns": [
        "project-public-fields",
        "authoring",
        "bounded"
      ],
      "slots": 1,
      "sha256": "c4bc776cf730f13543c0379eec178ebf90dfb465c51b1d356ed6628c0f4fc614",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · project-public-fields · select allowed fields before sending data to a model\n#\n# A closed source schema and an explicit field allowlist bound the prompt. This is structural minimization, not free-text PII detection. Source data remains in local traces.\n# Demonstrates · explicit input admission, narrow permits and typed results.\n# Needs · fill the SLOT; no services or credentials for mock rehearsal.\n# Run · nika run <file> --model mock/echo\nnika: project-public-fields\nmodel: ollama/qwen3.5:4b\nrun:\n  clock: system\nconst:\n  instruction: '<SLOT: the public summary instruction>'\n  record:\n    topic: queue latency\n    severity: 2\n    email: private@example.invalid\n    internal_note: internal-only-fixture\npermits:\n  tools:\n  - nika:assert\n  - nika:jq\n  - nika:validate\ntasks:\n  validate:\n    invoke:\n      tool: nika:validate\n      args:\n        data: ${{ const.record }}\n        format: json\n        schema:\n          type: object\n          additionalProperties: false\n          required:\n          - topic\n          - severity\n          - email\n          - internal_note\n          properties:\n            topic:\n              type: string\n              minLength: 1\n              maxLength: 4000\n            severity:\n              type: integer\n              minimum: 0\n              maximum: 5\n            email:\n              type: string\n              minLength: 1\n              maxLength: 4000\n            internal_note:\n              type: string\n              minLength: 1\n              maxLength: 4000\n  admit:\n    with:\n      valid: ${{ tasks.validate.output.valid }}\n    invoke:\n      tool: nika:assert\n      args:\n        condition: ${{ with.valid }}\n        message: Input violates the declared shape or size bound\n  public:\n    after:\n      admit: success\n    invoke:\n      tool: nika:jq\n      args:\n        input: ${{ const.record }}\n        expression: '{topic, severity}'\n  summarize:\n    with:\n      public: ${{ tasks.public.output }}\n    timeout: 30s\n    infer:\n      prompt: '${{ const.instruction }}\n\n        Allowed fields only: ${{ with.public }}'\n      max_tokens: 2048\n      schema:\n        type: object\n        additionalProperties: false\n        required:\n        - summary\n        properties:\n          summary:\n            type: string\n            minLength: 1\n            maxLength: 4000\noutputs:\n  public: ${{ tasks.public.output }}\n  summary: ${{ tasks.summarize.output.summary }}\n"
    },
    {
      "name": "parallel-review",
      "file": "parallel-review.nika",
      "intent": "collect independent reviews with a deterministic conjunction",
      "patterns": [
        "parallel-review",
        "authoring",
        "bounded"
      ],
      "slots": 1,
      "sha256": "d02b45349edb57fe66b0692584c822f5bab0a7620c0d5d6ec445d96d332f43db",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · parallel-review · collect independent reviews with a deterministic conjunction\n#\n# Two fixed review calls, each capped at 2048 tokens and 30s. Agreement is a routing signal, not proof that the document is correct; this workflow does not publish.\n# Demonstrates · explicit input admission, narrow permits and typed results.\n# Needs · fill the SLOT; no services or credentials for mock rehearsal.\n# Run · nika run <file> --model mock/echo\nnika: parallel-review\nmodel: ollama/qwen3.5:4b\nrun:\n  clock: system\nconst:\n  document: '<SLOT: the document to review>'\n  law: '{ready: (.clarity.ready and .accuracy.ready), reviews: [.clarity, .accuracy]}'\n  cases:\n  - clarity:\n      ready: false\n      reason: fixture\n    accuracy:\n      ready: false\n      reason: fixture\n    expected: false\n  - clarity:\n      ready: false\n      reason: fixture\n    accuracy:\n      ready: true\n      reason: fixture\n    expected: false\n  - clarity:\n      ready: true\n      reason: fixture\n    accuracy:\n      ready: false\n      reason: fixture\n    expected: false\n  - clarity:\n      ready: true\n      reason: fixture\n    accuracy:\n      ready: true\n      reason: fixture\n    expected: true\npermits:\n  tools:\n  - nika:assert\n  - nika:jq\n  - nika:validate\ntasks:\n  validate:\n    invoke:\n      tool: nika:validate\n      args:\n        data: ${{ const.document }}\n        format: json\n        schema:\n          type: string\n          minLength: 1\n          maxLength: 4000\n  admit:\n    with:\n      valid: ${{ tasks.validate.output.valid }}\n    invoke:\n      tool: nika:assert\n      args:\n        condition: ${{ with.valid }}\n        message: Input violates the declared shape or size bound\n  clarity:\n    after:\n      admit: success\n      law_holds: success\n    timeout: 30s\n    infer:\n      prompt: 'Review clarity and missing explanations. Document: ${{ const.document }}'\n      max_tokens: 2048\n      schema:\n        type: object\n        additionalProperties: false\n        required:\n        - ready\n        - reason\n        properties:\n          ready:\n            type: boolean\n          reason:\n            type: string\n            minLength: 1\n            maxLength: 4000\n  accuracy:\n    after:\n      admit: success\n      law_holds: success\n    timeout: 30s\n    infer:\n      prompt: 'Review unsupported claims and internal contradictions. Document: ${{ const.document }}'\n      max_tokens: 2048\n      schema:\n        type: object\n        additionalProperties: false\n        required:\n        - ready\n        - reason\n        properties:\n          ready:\n            type: boolean\n          reason:\n            type: string\n            minLength: 1\n            maxLength: 4000\n  verdict:\n    with:\n      clarity: ${{ tasks.clarity.output }}\n      accuracy: ${{ tasks.accuracy.output }}\n    invoke:\n      tool: nika:jq\n      args:\n        input:\n          clarity: ${{ with.clarity }}\n          accuracy: ${{ with.accuracy }}\n        expression: ${{ const.law }}\n    after:\n      law_holds: success\n  prove:\n    for_each:\n      items: ${{ const.cases }}\n      max_parallel: 1\n      fail_fast: true\n    invoke:\n      tool: nika:jq\n      args:\n        input: ${{ item }}\n        expression: . as $case | (${{ const.law }}) | .ready == $case.expected\n  law_holds:\n    with:\n      cases: ${{ tasks.prove.output }}\n    invoke:\n      tool: nika:assert\n      args:\n        condition: ${{ with.cases == [true, true, true, true] }}\n        message: All four conjunction truth-table cases must pass\noutputs:\n  verdict: ${{ tasks.verdict.output }}\n"
    },
    {
      "name": "recover-optional-file",
      "file": "recover-optional-file.nika",
      "intent": "recover a missing optional file without hiding other failures",
      "patterns": [
        "recover-optional-file",
        "authoring",
        "bounded"
      ],
      "slots": 1,
      "sha256": "923ed35265aadfbf46cb6be76e86655d0cc03c37950128d3c05664e665c467de",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · recover-optional-file · recover a missing optional file without hiding other failures\n#\n# The read has two attempts and a 5s per-attempt timeout. Only not-found recovers; permission, decoding and timeout failures remain failures. The content check occurs after reading, not before allocation.\n# Demonstrates · explicit input admission, narrow permits and typed results.\n# Needs · fill the SLOT; no services or credentials for mock rehearsal.\n# Run · nika run <file> --model mock/echo\nnika: recover-optional-file\nrun:\n  clock: system\nconst:\n  fallback: '<SLOT: the explicit fallback content>'\npermits:\n  tools:\n  - nika:assert\n  - nika:jq\n  - nika:read\n  - nika:validate\n  fs:\n    read:\n    - ./optional.txt\ntasks:\n  read:\n    invoke:\n      tool: nika:read\n      args:\n        path: ./optional.txt\n    timeout: 5s\n    retry:\n      max_attempts: 2\n      backoff_strategy: exponential\n      jitter: false\n    on_error:\n      on_codes:\n      - NIKA-BUILTIN-READ-001\n      recover: ${{ const.fallback }}\n  validate:\n    with:\n      content: ${{ tasks.read.output }}\n    invoke:\n      tool: nika:validate\n      args:\n        data: ${{ with.content }}\n        format: json\n        schema:\n          type: string\n          minLength: 1\n          maxLength: 4000\n  admit:\n    with:\n      valid: ${{ tasks.validate.output.valid }}\n    invoke:\n      tool: nika:assert\n      args:\n        condition: ${{ with.valid }}\n        message: Content must be nonempty and at most 4000 characters\n  result:\n    after:\n      admit: success\n    with:\n      content: ${{ tasks.read.output }}\n    invoke:\n      tool: nika:jq\n      args:\n        input: ${{ with.content }}\n        expression: .\noutputs:\n  content: ${{ tasks.result.output }}\n"
    },
    {
      "name": "aggregate-by-key",
      "file": "aggregate-by-key.nika",
      "intent": "group validated integer amounts with exact totals",
      "patterns": [
        "aggregate-by-key",
        "authoring",
        "bounded"
      ],
      "slots": 1,
      "sha256": "a7a34d9415e3187ff32607d79272da49e790500f1998593730c5047dc8a111fd",
      "yaml": "# SPDX-License-Identifier: Apache-2.0\n# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json\n#\n# TEMPLATE · aggregate-by-key · group validated integer amounts with exact totals\n#\n# At most eight rows, finite region vocabulary and nonnegative integer minor units. Empty input returns no groups and total zero; no model computes the arithmetic.\n# Demonstrates · explicit input admission, narrow permits and typed results.\n# Needs · fill the SLOT; no services or credentials for mock rehearsal.\n# Run · nika run <file> --model mock/echo\nnika: aggregate-by-key\nconst:\n  currency: '<SLOT: the currency code>'\n  records:\n  - region: north\n    amount: 10\n  - region: south\n    amount: 20\n  - region: north\n    amount: 5\npermits:\n  tools:\n  - nika:assert\n  - nika:jq\n  - nika:validate\ntasks:\n  validate:\n    invoke:\n      tool: nika:validate\n      args:\n        data: ${{ const.records }}\n        format: json\n        schema:\n          type: array\n          maxItems: 8\n          items:\n            type: object\n            additionalProperties: false\n            required:\n            - region\n            - amount\n            properties:\n              region:\n                type: string\n                enum:\n                - north\n                - south\n              amount:\n                type: integer\n                minimum: 0\n                maximum: 1000000\n  admit:\n    with:\n      valid: ${{ tasks.validate.output.valid }}\n    invoke:\n      tool: nika:assert\n      args:\n        condition: ${{ with.valid }}\n        message: Input violates the declared shape or size bound\n  aggregate:\n    after:\n      admit: success\n    invoke:\n      tool: nika:jq\n      args:\n        input:\n          currency: ${{ const.currency }}\n          records: ${{ const.records }}\n        expression: '{currency, groups: (.records | group_by(.region) | map({region: .[0].region, count:\n          length, total: (map(.amount) | add)})), total: ([.records[].amount] | add // 0)}'\noutputs:\n  report: ${{ tasks.aggregate.output }}\n"
    }
  ]
}
