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

  • 6tasksthe plan falls out of the bindings
  • 4wavesparallel by construction
  • 3verbs exercisedexec · infer · invoke
  • T2the tierbuilders

The whole file

6 tasks · 4 waves

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

release-notes.nika.yamlsource
nika: v1workflow:  id: release-notes  description: "git log → typed release notes → CHANGELOG insert → team ping"# 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:  tools: ["nika:edit", "nika:notify", "nika:read", "nika:write"]  exec: ["git"]                   # the ONE program this workflow may run  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"]  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"]tasks:  history:    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 }}    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:    invoke:      tool: "nika:read"      args: { path: "${{ const.changelog_src }}" }  copy:    with:      existing: ${{ tasks.existing.output }}    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.106.1 · vendored graph, never re-derived

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