Skip to content

Lint-Aware Reviews

Linters are precise and models are articulate. Give Vizzini a golangci-lint report and it adds the diagnostics on lines your diff adds to the review prompt. The model then explains each one in plain terms and proposes a fix, without inventing rules of its own.

Run golangci-lint from the repository root, so its paths match the diff:

Terminal window
golangci-lint run --output.json.path=stdout --output.text.path=/dev/null --show-stats=false ./... > lint.json
git diff origin/main...HEAD | pr-review --mode review --lint-json lint.json

golangci-lint v1’s --out-format=json works as well, because both versions write the same Issues[] shape. Without --show-stats=false, v2 appends a plain-text summary after the JSON. Vizzini ignores it, but other JSON tools may not.

Any tool that writes SARIF 2.1.0 works: ruff for Python, Roslyn analyzers for C#, Semgrep, CodeQL and many more. Pass the report the same way; the format is detected automatically, and --lint-json can be repeated for repositories with several linters:

Terminal window
ruff check --output-format=sarif . > ruff.sarif
golangci-lint run --output.json.path=stdout --output.text.path=/dev/null --show-stats=false ./... > go.json
git diff main...HEAD | pr-review --mode review --lint-json go.json --lint-json ruff.sarif

These were captured and proven end to end (make e2e-lint-python, make e2e-lint-csharp, make e2e-lint-js):

Language Tool (version verified) Produce SARIF with Notes
Python ruff 0.17.4 ruff check --output-format=sarif . Writes absolute file:// URIs. Run pr-review from the same repo root. Security rules are the S group (select = ["F", "S"])
C# Roslyn, C# compiler 5.9.0 (.NET SDK 10.0.404) <ErrorLog>roslyn.sarif,version=2.1</ErrorLog> in the project, then dotnet build Writes absolute file:// URIs. Security analyzers are off by default; enable them with <AnalysisLevelSecurity>latest-all</AnalysisLevelSecurity>. CA2100 fires on DbCommand.CommandText, not through IDbCommand
TypeScript / JavaScript ESLint 10.13.0 + typescript-eslint 8.72.1 (TypeScript 6.0.3, Node 24.22.1) npm i -D @microsoft/eslint-formatter-sarif@3.1.0, then npx eslint -f @microsoft/eslint-formatter-sarif -o eslint.sarif . ESLint has no built-in SARIF; Microsoft’s formatter (3.1.0) works with ESLint 10 but pulls in an old ESLint 8 as its own dependency. Writes absolute file:// URIs. eslint-disable comments become suppressed results, which Vizzini skips. ESLint exits 1 when it reports errors, so don’t treat that as a failure. eval, new Function and string setTimeout need no-eval and no-new-func on top of recommendedTypeChecked (which brings @typescript-eslint/no-implied-eval). typescript-eslint 8.72 needs TypeScript < 6.1

If dotnet build or eslint runs in a container, mount the repository at the same absolute path (-v "$PWD:$PWD" -w "$PWD"), so the URIs the tool writes match where pr-review runs.

How a SARIF result is read:

SARIF Becomes
tool.driver.name the linter name
ruleId + message.text S608: Possible SQL injection… (falls back to the rule’s description)
first physicalLocation file, line and column
file:// URIs, uriBaseId + originalUriBaseIds repository-relative paths (pr-review resolves against its working directory, so run it from the repo root)
kind: pass / notApplicable, suppressions in effect, no line skipped

Absolute locations outside the repository, such as SDK files, are ignored.

Only diagnostics on a + line of the diff:

Diagnostic on… Sent?
A line the diff adds Yes
A context line (unchanged) No
A line outside every hunk No
A file the diff doesn’t touch No

When a large patch is split into chunks, each chunk gets only the diagnostics on its own hunks. In the prompt they appear as a block before the diff:

<lint>
internal/api/handler.go:4:13 errcheck: Error return value of `r.ParseForm` is not checked
internal/store/users.go:44:7 gosec: G202: SQL string concatenation
</lint>

The JSON output reports how many were included in "diagnostics".

goLint, _ := os.Open("go.json")
ruff, _ := os.Open("ruff.sarif")
out, err := engine.Review(ctx, patch, reviewer.ReviewOptions{
LintReports: []io.Reader{goLint, ruff},
LintRoot: repoRoot, // absolute; resolves SARIF file:// URIs
})

A malformed report fails the review before any request is made.

Terminal window
make e2e-lint # Go: golangci-lint JSON
make e2e-lint-python # Python: ruff SARIF (uses uvx ruff if ruff isn't installed)
make e2e-lint-csharp # C#: Roslyn SARIF from dotnet build in the .NET SDK container
make e2e-lint-js # TypeScript: ESLint + typescript-eslint SARIF in the node container

This builds a throwaway Go repo with a pre-existing lint issue, makes a change that adds new ones, runs the real golangci-lint and pr-review, and checks four things: only the new issues reach the prompt, every finding is anchored to its line, the gate exits 2, and auto-approve stays off. Without VIZZINI_LITELLM_URL, a stand-in model answers from the <lint> block alone. Point it at your proxy to see a real model explain the issues.