AGPL-3.0-or-later · forever.

showcase · tier 3 · builders

PR review swarm

One read-only review agent per changed file, in parallel and under budget. jq merges the findings.

Big PRs get deep reviews, because attention now scales with the diff. · conformance-gated in nika-spec ↗ · re-proven at every push

The whole file

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

t3-pr-review-fanout.nika.yamlsource
nika: v1workflow:  id: pr-review-fanout  description: "changed files → one read-only review agent each → merged REVIEW.md"model: ollama/qwen3.5:4b   # local tool-calling model · swap for anthropic/claude-sonnet-4-6 for depthvars:  base_ref: "main"tasks:  changed:    exec:      command: ["git", "diff", "--name-only", "${{ vars.base_ref }}...HEAD"]  files:    with:      changed: ${{ tasks.changed.output }}    invoke:      tool: "nika:jq"      args:        input: "${{ with.changed }}"        expression: 'split("\n") | map(select(length > 0))'  todo_sweep:    invoke:      tool: "nika:grep"      args:        pattern: "TODO|FIXME|HACK"        path: "./src"  reviews:    with:      files: ${{ tasks.files.output }}    for_each: ${{ with.files }}    max_parallel: 4    fail_fast: false    on_error:      recover: null                    # a budget-exhausted review yields null · the swarm lives    agent:      system: "You are a precise code reviewer. Read the file, then finish with ONLY the schema'd object ({file, findings: [{severity, message, line}]} · severity exactly one of blocker|high|med|low), then call nika:done."      prompt: "Review ${{ item }} · bugs first, then risky patterns. Read it before judging."      tools:        - "nika:read"                  # read-only swarm · least privilege        - "nika:done"      max_turns: 6      max_tokens_total: 30000      schema:        type: object        required: [file, findings]        properties:          file: { type: string }          findings:            type: array            items:              type: object              required: [severity, message]              properties:                severity: { type: string, enum: [blocker, high, med, low] }                message: { type: string }                line: { type: integer }  merged:    with:      reviews: ${{ tasks.reviews.output }}    invoke:      tool: "nika:jq"      args:        input: "${{ with.reviews }}"        # `. // []` · a clean diff SKIPS the swarm (empty for_each → null        # upstream) and the fan-in must survive its own good news.        expression: '(. // []) | map(select(. != null)) | map(.findings[] + {file: .file}) | sort_by(.severity)'  summary:    with:      merged: ${{ tasks.merged.output }}      todo_sweep: ${{ tasks.todo_sweep.output }}    infer:      prompt: |        Merge these review findings into REVIEW.md · blockers first ·        ${{ with.merged }}        Open TODO debt found by grep ·        ${{ with.todo_sweep }}        End with a verdict · ship / fix-first / redesign.  save:    with:      summary: ${{ tasks.summary.output }}    invoke:      tool: "nika:write"      args:        path: "./REVIEW.md"        content: "${{ with.summary }}"outputs:  findings:    value: ${{ tasks.merged.output }}    description: "All findings, severity-sorted, with file attribution"  review: ${{ tasks.summary.output }}

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

6 declared edges · 6 waves · the plan falls out of the bindings