AGPL-3.0-or-later · forever.

showcase · tier 4 · research

CEO Monday brief

Market + repo pulse + KPI sheet gathered in parallel, jq sums the revenue, thinking model writes.

The ping ends with the run’s own cost line. The workflow reports its own bill. · conformance-gated in nika-spec ↗ · re-proven at every push

  • 10tasksthe plan falls out of the bindings
  • 7wavesparallel by construction
  • 3verbs exercisedinvoke · exec · infer
  • T4the tierresearch

The whole file

10 tasks · 7 waves

The plan falls out of the bindings, nothing is scheduled by hand.

ceo-monday-brief.nika.yamlsource
nika: v1workflow:  id: ceo-monday-brief  description: "news + repo pulse + KPIs → one synthesis → dated brief → a ping on every outcome"model: ollama/qwen3.5:4b   # local · zero key · `--model mock/echo` rehearses the whole fileinputs:  alert:    type: bool    default: false    description: "Send the founders ping. OFF by default so a rehearsal touches no webhook."  kpi_sheet:    type: string    default: "./examples/fixtures/ceo-kpis.csv"    description: "The weekly numbers · columns incl. weekly_revenue"const:  watch_query: "AI workflow engines"secrets:  founders_webhook:    source: env    key: FOUNDERS_WEBHOOK_URL    egress:                       # sanction the one send · the secret IS the URL      - to: "nika:notify"        host_from_self: truepermits:  exec: ["git"]  tools: ["nika:convert", "nika:date", "nika:fetch", "nika:jq", "nika:notify", "nika:prompt", "nika:read", "nika:write"]  net:    http:      - "hn.algolia.com"          # the market-signal search · the only host this brief READS      # `host_from_self:` above sanctions the FLOW (the secret may be the URL) —      # it does not grant the capability. The host stays unknown at check, so it      # is judged at RUN against this bound: name the escalation host here or the      # send is refused mid-run, after the tokens are already spent.      - "hooks.slack.com"         # swap for wherever FOUNDERS_WEBHOOK_URL points  fs:    # The sheet arrives as an INPUT, so `check` reads past it — and the wall does    # not move when the input does: point --var kpi_sheet at anything else and the    # run refuses it NIKA-SEC-004. That asymmetry is the point of a permits block.    read: ["./examples/fixtures/ceo-kpis.csv"]    # One dated brief per run, written DIRECTLY in ./briefs/. `*` matches a single    # segment and never crosses `/`, so nothing here can steer the write into a    # subtree — which is why this is not `./briefs/**`.    write: ["./briefs/*-monday.md"]tasks:  # ── the human gate · FIRST, before anything untrusted enters ──  #  # This run holds all three legs of the lethal trifecta: a private read (the  # numbers), untrusted ingress (a public news API), and external egress (a  # write and a webhook). NEP-0002 asks for one blocking human decision that  # DOMINATES every path to every egress-capable task — which forces it here,  # at the root, not in front of the save.  #  # A gate placed after the fetch could never dominate: the brief still reaches  # `save` along its own data edge, a path the human is not standing on. And a  # human skimming a model's summary of an attacker's page is a weak check  # anyway — by then the payload is already inside the run. So the decision  # taken here is the honest one: authorize the CAPABILITY ENVELOPE up front.  approve:    invoke:      # blocking · no `default:` · a gate with a default is not a gate, and the      # checker enforces both directions: add `default: false` here and this      # file turns RED with NIKA-SEC-009, because a defaulted prompt dominates      # nothing. Leaving it blocking costs one [headless-prompt] hint instead —      # that hint is the price of a real gate, not a defect to paper over.      # The run pauses (exit 4, not a failure) — answer and resume with      #   nika run <file> --resume <trace> --answer approve=true      tool: "nika:prompt"      args:        message: |          This Monday brief will:            · fetch public news from hn.algolia.com            · run git in this repo            · read ${{ inputs.kpi_sheet }}            · write ./briefs/<date>-monday.md            · ping the founders webhook (armed: ${{ inputs.alert }})          Proceed? [yes/no]  # ── branch 1 · market signal · the untrusted leg ──  news:    with:      go: ${{ tasks.approve.output }}    when: ${{ with.go == true }}    invoke:      tool: "nika:fetch"      args:        url: "https://hn.algolia.com/api/v1/search?query=${{ const.watch_query }}"        mode: jq        jq: ".hits[:10] | map(.title)"    on_error:      # The honest dry run · offline the host resolves nowhere and this literal      # takes over, so the join below is exercised on both paths with the same      # binding. It is labelled in the brief so nobody mistakes it for signal.      recover: ["(offline rehearsal · no market signal was fetched)"]  # ── branch 2 · engineering pulse ──  pulse:    with:      go: ${{ tasks.approve.output }}    when: ${{ with.go == true }}    exec:      # Default capture (stdout), so a non-zero exit FAILS the task and the      # recovery below is what handles it. The tempting alternative — `capture:      # structured` — turns the exit code into data and the task reports success      # whatever happened; that is only honest when something downstream reads      # `exit_code`, and the checker flags the case where nothing does      # ([swallowed-exit]). Here the failure is visible in the run (« 1      # recovered ») and labelled in the brief, which is the same information      # with none of the silence.      command: ["git", "--no-pager", "shortlog", "-sn", "--since=1 week ago", "HEAD"]    on_error:      recover: "(no engineering pulse · git could not be read here)"  # ── branch 3 · the numbers · the private leg ──  sheet:    with:      go: ${{ tasks.approve.output }}    when: ${{ with.go == true }}    invoke:      tool: "nika:read"      args: { path: "${{ inputs.kpi_sheet }}" }  rows:    with:      csv: ${{ tasks.sheet.output }}    # A SKIPPED producer hands its consumer `null`, and the consumer runs anyway.    # Measured: without this line a « no » at the gate reaches nika:convert with    # nothing and the run FAILS (NIKA-BUILTIN-CONVERT-002) instead of landing    # nothing — a refusal must be a quiet no-op, never an error. Every task    # downstream of a gated one carries its own admission test.    when: ${{ with.csv != null }}    invoke:      tool: "nika:convert"      args:        input: "${{ with.csv }}"        from: csv        to: json        has_header: true  revenue:    with:      rows: ${{ tasks.rows.output }}    when: ${{ with.rows != null }}    invoke:      tool: "nika:jq"      args:        # CSV cells arrive as STRINGS whatever they look like, so `tonumber`        # before `add`. Money is arithmetic, never a model's opinion of it.        input: "${{ with.rows }}"        expression: "map(.weekly_revenue | tonumber) | add"  # ── the join · three branches land here, one call ──  brief:    with:      go: ${{ tasks.approve.output }}      # Every binding is SHALLOW (`.output`, never `.output.field`). Measured:      # a skipped producer's `.output` reads as `null` and the join survives it,      # while a deep field of the same skipped producer is NIKA-VAR-001 ·      # unresolved template reference — bindings resolve before `when:` decides,      # so a deep read into a « no » path kills the run the gate was meant to      # spare. Reach one level, gate, then reach deeper downstream.      news: ${{ tasks.news.output }}      pulse: ${{ tasks.pulse.output }}      rows: ${{ tasks.rows.output }}      revenue: ${{ tasks.revenue.output }}    when: ${{ with.go == true }}    infer:      max_tokens: 1200               # the cost report becomes a ceiling, not UNBOUNDED      prompt: |        Market signal · ${{ with.news }}        Engineering pulse · ${{ with.pulse }}        Weekly rows · ${{ with.rows }}        Revenue, summed by the engine · ${{ with.revenue }}        Write my Monday brief · 5 sections · market · product · numbers ·        risks · the ONE decision this week needs.      thinking:        # A reasoning budget, honoured by seats that support extended thinking        # and ignored by the ones that do not — declaring it never fails a run.        enabled: true        budget_tokens: 4000  stamp:    with:      go: ${{ tasks.approve.output }}    when: ${{ with.go == true }}    invoke:      tool: "nika:date"      args: { op: now }    output:      day: ".[:10]"                  # nika:date returns the ISO string itself · slice YYYY-MM-DD  save:    with:      go: ${{ tasks.approve.output }}   # every effect re-reads the decision · a « no » lands nothing      day: ${{ tasks.stamp.day }}      brief: ${{ tasks.brief.output }}    when: ${{ with.go == true }}    invoke:      tool: "nika:write"      args:        path: "./briefs/${{ with.day }}-monday.md"        content: "${{ with.brief }}"        create_dirs: true  # ── the ping · a TASK, not a cleanup hook ──  #  # `after: {save: terminal}` admits all four settled states, so this fires  # whether the brief landed, failed, or was skipped by a « no » at the gate.  # It is deliberately not an `on_finally:` hook: cleanup errors are LOGGED and  # DO NOT PROPAGATE (03 §on_finally · best-effort lane), so a webhook that  # dies there dies in silence — measured: an unresolved secret in a cleanup  # hook leaves the run green with nothing sent. An alert you cannot trust to  # fail loudly is not an alert.  alert:    after:      save: terminal    with:      armed: ${{ inputs.alert }}      outcome: ${{ tasks.save.status }}   # observe WHAT happened · same pass-set as the after edge      day: ${{ tasks.stamp.day }}    when: ${{ with.armed == true }}       # OFF by default · a rehearsal touches no webhook    invoke:      tool: "nika:notify"      args:        channel: webhook        target: "${{ secrets.founders_webhook }}"        message: "Monday brief · ${{ with.outcome }} · briefs/${{ with.day }}-monday.md"        severity: infooutputs:  brief: ${{ tasks.brief.output }}  revenue:    value: ${{ tasks.revenue.output }}    description: "Total weekly revenue, summed by jq — the number nobody guessed"

nika inspect · engine 0.106.1 · vendored graph, never re-derived

17 declared edges · 7 waves · the plan falls out of the bindings