AGPL-3.0-or-later · forever.

showcase · tier 4 · ops & support

Incident war room

Logs, status history and the runbook gathered in parallel, a typed timeline, and a recovery check before any draft.

The postmortem draft is ready before the retro is scheduled. · conformance-gated in nika-spec ↗ · re-proven at every push

The whole file

10 tasks · 8 waves · the plan falls out of the bindings, nothing is scheduled by hand.

t4-incident-war-room.nika.yamlsource
nika: v1workflow:  id: incident-war-room  description: "parallel evidence → typed timeline → settle + recheck → postmortem draft"model: ollama/qwen3.5:4b   # local default · the synthesis task below overrides to a stronger modelvars:  service: "checkout-api"  status_url: "https://status.internal.example.com/v1/services/checkout-api"  log_window: "90 minutes ago"secrets:  oncall_webhook:    source: env    key: ONCALL_WEBHOOK_URL    egress:                       # sanction the on_finally ping · the secret IS the URL      - to: "nika:notify"        host_from_self: truetasks:  # ── the gather wave · all three run in parallel ──  logs:    exec:      command: ["journalctl", "-u", "${{ vars.service }}", "--since", "${{ vars.log_window }}", "--no-pager"]      capture: structured  status_history:    invoke:      tool: "nika:fetch"      args:        url: "${{ vars.status_url }}"        mode: jq        jq: ".history"    retry:      max_attempts: 4      backoff_strategy: exponential      jitter: true  runbook:    invoke:      tool: "nika:read"      args: { path: "./runbooks/${{ vars.service }}.md" }  # ── reconstruct · typed timeline ──  timeline:    with:                               # three value edges · the gather wave feeds the synthesis      logs: ${{ tasks.logs.output.stdout }}      history: ${{ tasks.status_history.output }}      runbook: ${{ tasks.runbook.output }}    infer:      prompt: |        Service logs ·        ${{ with.logs }}        Status history · ${{ with.history }}        Runbook · ${{ with.runbook }}        Reconstruct the incident timeline as events.      schema:        type: object        required: [events]        properties:          events:            type: array            items:              type: object              required: [at, what]              properties:                at: { type: string }                what: { type: string }                evidence: { type: string }  # ── settle, then confirm recovery before claiming it ──  settle:    after:      timeline: succeeded    invoke:      tool: "nika:wait"      args: { duration: "60s" }  recheck:    after:      settle: succeeded    invoke:      tool: "nika:fetch"      args:        url: "${{ vars.status_url }}"        mode: jq        jq: ".current.state"  confirmed:    with:      recovered: ${{ tasks.recheck.output == 'operational' }}   # the whole check crosses as ONE boundary expression    invoke:      tool: "nika:assert"      args:        condition: "${{ with.recovered }}"        message: "Service is NOT back to operational: postmortem draft blocked"  # ── the draft · only after recovery is proven ──  postmortem:    with:      events: ${{ tasks.timeline.output.events }}    after:      confirmed: succeeded             # state, no data · recovery proven or no draft    infer:      model: anthropic/claude-sonnet-4-6   # per-task override · thinking budget      prompt: |        Timeline · ${{ with.events }}        Write the postmortem draft · summary · impact · root-cause        hypotheses · 3 follow-up actions with owners left blank.      thinking:        enabled: true        budget_tokens: 6000  save:    with:      postmortem: ${{ tasks.postmortem.output }}    invoke:      tool: "nika:write"      args:        path: "./incidents/${{ vars.service }}-postmortem.md"        content: "${{ with.postmortem }}"        create_dirs: true  # the always-pattern · the on-call ping fires on EVERY outcome:  # including the designed failure path (recovery NOT confirmed → the  # assert fails → save is cancelled → this still runs · `after:  # terminal` admits all four settled states · 03 §gate algebra).  ping:    after:      save: terminal    with:      outcome: ${{ tasks.save.status }}   # observe WHAT happened · same pass-set as the after edge    invoke:      tool: "nika:emit"      args:        event_type: "incident.postmortem.drafted"        payload:          service: "${{ vars.service }}"          status: "${{ with.outcome }}"    on_finally:      - invoke:          tool: "nika:notify"          args:            channel: webhook            target: "${{ secrets.oncall_webhook }}"            message: "Postmortem draft run finished for ${{ vars.service }} · ${{ with.outcome }}"            severity: infooutputs:  events:    value: ${{ tasks.timeline.output.events }}    description: "The reconstructed, typed incident timeline"  postmortem: ${{ tasks.postmortem.output }}

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

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