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 it
Section titled “Run it”Run golangci-lint from the repository root, so its paths match the diff:
golangci-lint run --output.json.path=stdout --output.text.path=/dev/null --show-stats=false ./... > lint.jsongit diff origin/main...HEAD | pr-review --mode review --lint-json lint.jsongolangci-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.
Other languages: SARIF
Section titled “Other languages: SARIF”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:
ruff check --output-format=sarif . > ruff.sarifgolangci-lint run --output.json.path=stdout --output.text.path=/dev/null --show-stats=false ./... > go.jsongit diff main...HEAD | pr-review --mode review --lint-json go.json --lint-json ruff.sarifVerified tools
Section titled “Verified tools”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.
What reaches the model
Section titled “What reaches the model”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 checkedinternal/store/users.go:44:7 gosec: G202: SQL string concatenation</lint>The JSON output reports how many were included in "diagnostics".
From Go
Section titled “From Go”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.
See it work
Section titled “See it work”make e2e-lint # Go: golangci-lint JSONmake 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 containermake e2e-lint-js # TypeScript: ESLint + typescript-eslint SARIF in the node containerThis 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.