AGPL-3.0-or-later · forever.

showcase · tier 2 · builders

Release notes

git log in, typed notes out, the CHANGELOG edited in place and the team pinged with the headline.

Release day: one command, a changelog in your voice, zero copy-paste. · conformance-gated in nika-spec ↗ · re-proven at every push

  • 7tasksthe plan falls out of the bindings
  • 5wavesparallel by construction
  • 3verbs exercisedinvoke · exec · infer
  • T2the tierbuilders

The whole file

7 tasks · 5 waves

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

release-notes.nika.yamlsource
nika: release-notes# A NON-thinking local model, deliberately: a thinking model can spend the# whole `max_tokens` budget in its think block and return before the JSON# (engine#428). Schema showcases pick a model that answers directly.model: ollama/llama3.2:3binputs:  announce:    type: bool    default: false    description: "Actually send the team ping (needs TEAM_WEBHOOK_URL)"const:  since_tag: "v0.80.0"  changelog_src: "examples/fixtures/CHANGELOG.md" # point this at YOUR CHANGELOG.md  changelog_out: "out/CHANGELOG.md"                        # the working copy this run editssecrets:  team_webhook:    source: env    key: TEAM_WEBHOOK_URL    egress:                       # sanction the one send · the secret IS the URL      - to: "nika:notify"        host_from_self: truepermits:  exec: ["git"]                   # the ONE program this workflow may run  tools: ["nika:edit", "nika:notify", "nika:prompt", "nika:read", "nika:write"]  net:    # `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 announce host here or the    # send is refused mid-run, after the tokens are already spent.    http: ["hooks.slack.com"]  fs:    # The workflow never edits your changelog in place — it copies it and    # edits the copy, so you move the result when the diff looks right.    #    # The working copy appears in BOTH lists, and that is not redundancy:    # `nika:edit` reads the file before it writes it, so a write-only grant    # is refused with NIKA-SEC-004 · outside permits.fs.read. Every in-place    # builtin owes both halves.    read: ["examples/fixtures/CHANGELOG.md", "out/CHANGELOG.md"]    write: ["out/CHANGELOG.md"]tasks:  # The human gate (NEP-0002 · v2.2). `git log` output is UNTRUSTED content —  # a hostile branch writes its own commit subjects — and this workflow pairs  # it with a private `fs.read` and a webhook: the lethal trifecta, unless a  # human dominates the path. The gate comes FIRST and BOTH roots hang off it  # (`history` carries the untrusted content, `existing` carries the private  # read); a gate on a sibling branch dominates nothing. And each root  # CONSUMES the answer affirmatively (NEP-0020 — a bare `after:` admits the  # refusal: it settles success with value false), the downstream null-guards  # passing the « no » along the value edges. Non-interactive runs answer it  # in one pass with `--answer approve=true`.  approve:    invoke:      tool: "nika:prompt"      args:        mode: "confirm"        message: "read the git history since ${{ const.since_tag }}, rewrite the changelog copy, and ping the team if announce is on?"  history:    after: { approve: success }    with:      go: ${{ tasks.approve.output }}    when: ${{ with.go == true }}       # « no » at the gate → no history read at all    exec:      command: ["git", "log", "${{ const.since_tag }}..HEAD", "--oneline", "--no-merges"]    on_error:      # No such tag here (or not a repo at all) → a sample log takes over and      # the rest of the chain is exercised exactly as it would be on the day.      recover: |        a1b2c3d feat(api): pagination cursors on every list endpoint        d4e5f6a fix(auth): refresh tokens no longer rotate on read        7g8h9i0 refactor(store): drop the legacy write path  notes:    with:      history: ${{ tasks.history.output }}    when: ${{ with.history != null && size(with.history) > 0 }}  # a refused gate skips history (null) · an empty log has nothing to draft either    infer:      max_tokens: 1000      prompt: |        Write release notes from these commits ·        ${{ with.history }}        Tone · plain, direct, no marketing fluff.      schema:        type: object        additionalProperties: false        required: [headline, body]        properties:          headline: { type: string }          breaking: { type: array, items: { type: string } }          body: { type: string }  # Copy-then-edit · `nika:edit` is strictly in place and throws when `find:`  # matches nothing (NIKA-BUILTIN-EDIT-001), so its target must exist and must  # carry the anchor. Read yours, write the working copy, edit THAT.  existing:    after: { approve: success }    with:      go: ${{ tasks.approve.output }}    when: ${{ with.go == true }}       # the same « no » closes the private-read branch    invoke:      tool: "nika:read"      args: { path: "${{ const.changelog_src }}" }  copy:    with:      existing: ${{ tasks.existing.output }}    when: ${{ with.existing != null && size(with.existing) > 0 }}  # a refused gate skips the read (null) · an empty file has nothing to copy    invoke:      tool: "nika:write"      args:        path: "${{ const.changelog_out }}"        content: "${{ with.existing }}"        create_dirs: true        overwrite: true  changelog:    with:      notes_headline: ${{ tasks.notes.output.headline }}      notes_body: ${{ tasks.notes.output.body }}    after:      copy: success              # state, no data · the anchor is on disk before we edit    invoke:      tool: "nika:edit"      args:        path: "${{ const.changelog_out }}"        find: "# Changelog"      # a LITERAL string · not a regex        count: 1                 # the top anchor only · never every occurrence        replace: |          # Changelog          ## ${{ const.since_tag }}..HEAD · ${{ with.notes_headline }}          ${{ with.notes_body }}  announce:    with:      notes_headline: ${{ tasks.notes.output.headline }}    when: ${{ inputs.announce == true }}   # OFF by default · a rehearsal must not ping the team    after:      changelog: success    invoke:      tool: "nika:notify"      args:        channel: webhook        target: "${{ secrets.team_webhook }}"        message: "Release notes ready · ${{ with.notes_headline }}"        severity: infooutputs:  headline: ${{ tasks.notes.output.headline }}  body: ${{ tasks.notes.output.body }}

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

11 declared edges · 5 waves · the plan falls out of the bindings