vet

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit f074273939ce18cf8935b040d093e9d15a91855a
parent e85b72901bbce9af9470099b45ad51bc7686ef95
Author: andrewlaack-collab <andrew.laack@imbue.com>
Date:   Tue, 10 Feb 2026 09:40:41 +0000

Update issues found to use github review format instead of pr comments. (#46)

* Update issues found to use github review format instead of pr comments.

* Update wording

* Better wording again

* Added tests for github formatting

* Added comment, updated wording to be more consistent

* whitespace

* Don't redirect errors, useful for debugging.

* Refactoring to make exit status codes more sensible (realized 1 was default error for python failures so shouldn't be using that for issues found).

* Fix comment fallback behavior

* Updated exit status code descriptors
Diffstat:
M.github/workflows/vet.yml | 25++++++++++++++++---------
MDEVELOPMENT.md | 5+++--
MREADME.md | 27+++++++++++++++++----------
Mskills/vet/SKILL.md | 2+-
Avet/__snapshots__/formatters_test.ambr | 35+++++++++++++++++++++++++++++++++++
Mvet/cli/main.py | 9++++++++-
Mvet/formatters.py | 46+++++++++++++++++++++++++++++++++++++++++++++-
Avet/formatters_test.py | 44++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 169 insertions(+), 24 deletions(-)

diff --git a/.github/workflows/vet.yml b/.github/workflows/vet.yml @@ -3,7 +3,6 @@ name: Vet permissions: contents: read pull-requests: write - issues: write on: pull_request: @@ -20,7 +19,9 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.11" - - run: pip install git+https://github.com/imbue-ai/vet.git + # we want to run the current version of vet against the PR, not necessarily the deployed / main version. + # this is the only difference between this vet.yml and the recommended one in the readme. + - run: pip install . - name: Run vet if: github.event.pull_request.head.repo.full_name == github.repository env: @@ -33,11 +34,17 @@ jobs: ${{ github.event.pull_request.body }} run: | set +e - vet "$VET_GOAL" --quiet --base-commit "${{ github.event.pull_request.base.sha }}" > "$RUNNER_TEMP/vet-output.txt" 2>&1 + vet "$VET_GOAL" --quiet --output-format github \ + --base-commit "${{ github.event.pull_request.base.sha }}" \ + > "$RUNNER_TEMP/review.json" status=$? - if [ ! -s "$RUNNER_TEMP/vet-output.txt" ]; then - echo "Vet found no issues." > "$RUNNER_TEMP/vet-output.txt" - fi - gh pr comment "${{ github.event.pull_request.number }}" --body-file "$RUNNER_TEMP/vet-output.txt" - if [ "$status" -eq 1 ]; then exit 0; fi - exit "$status" + if [ "$status" -ne 0 ] && [ "$status" -ne 10 ]; then exit "$status"; fi + + jq --arg sha "${{ github.event.pull_request.head.sha }}" \ + '. + {commit_id: $sha}' "$RUNNER_TEMP/review.json" > "$RUNNER_TEMP/review-final.json" + + gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews" \ + --method POST --input "$RUNNER_TEMP/review-final.json" || \ + gh pr comment "${{ github.event.pull_request.number }}" \ + --body "$(jq -r '[.body] + [.comments[] | "**\(.path):\(.line)**\n\n\(.body)"] | join("\n\n---\n\n")' "$RUNNER_TEMP/review-final.json")" + exit 0 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md @@ -74,9 +74,10 @@ Example configuration: The following are the **expected** exit status codes for vet: -- `0` - Success, no issues found -- `1` - Issues were found in the code +- `0` - No issues found +- `1` - Unexpected runtime error - `2` - Invalid arguments or configuration +- `10` - Issues were found in the code ## Concepts diff --git a/README.md b/README.md @@ -36,7 +36,6 @@ name: Vet permissions: contents: read pull-requests: write - issues: write on: pull_request: @@ -66,17 +65,23 @@ jobs: ${{ github.event.pull_request.body }} run: | set +e - vet "$VET_GOAL" --quiet --base-commit "${{ github.event.pull_request.base.sha }}" > "$RUNNER_TEMP/vet-output.txt" 2>&1 + vet "$VET_GOAL" --quiet --output-format github \ + --base-commit "${{ github.event.pull_request.base.sha }}" \ + > "$RUNNER_TEMP/review.json" status=$? - if [ ! -s "$RUNNER_TEMP/vet-output.txt" ]; then - echo "Vet found no issues." > "$RUNNER_TEMP/vet-output.txt" - fi - gh pr comment "${{ github.event.pull_request.number }}" --body-file "$RUNNER_TEMP/vet-output.txt" - if [ "$status" -eq 1 ]; then exit 0; fi - exit "$status" + if [ "$status" -ne 0 ] && [ "$status" -ne 10 ]; then exit "$status"; fi + + jq --arg sha "${{ github.event.pull_request.head.sha }}" \ + '. + {commit_id: $sha}' "$RUNNER_TEMP/review.json" > "$RUNNER_TEMP/review-final.json" + + gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews" \ + --method POST --input "$RUNNER_TEMP/review-final.json" || \ + gh pr comment "${{ github.event.pull_request.number }}" \ + --body "$(jq -r '[.body] + [.comments[] | "**\(.path):\(.line)**\n\n\(.body)"] | join("\n\n---\n\n")' "$RUNNER_TEMP/review-final.json")" + exit 0 ``` -NOTE: This will not fail in CI if Vet finds an issue. This will only add a comment to the PR. +NOTE: This will not fail in CI if Vet finds an issue. #### Environment variables @@ -119,12 +124,14 @@ Vet snapshots the repo and diff, optionally adds a goal and agent conversation, ## Output & exit codes - Exit code `0`: no issues found -- Exit code `1`: issues found +- Exit code `1`: unexpected runtime error - Exit code `2`: invalid usage/configuration error +- Exit code `10`: issues found Output formats: - `text` - `json` +- `github` ## Configuration diff --git a/skills/vet/SKILL.md b/skills/vet/SKILL.md @@ -70,6 +70,6 @@ Vet analyzes the full git diff from the base commit. This may include changes fr - `--base-commit REF`: Git ref for diff base (default: HEAD) - `--model MODEL`: LLM model to use (default: claude-4-6-opus) - `--confidence-threshold N`: Minimum confidence 0.0-1.0 (default: 0.8) -- `--output-format FORMAT`: Output as `text` or `json` +- `--output-format FORMAT`: Output as `text`, `json`, or `github` - `--quiet`: Suppress progress output - `--help`: Show comprehensive list of options diff --git a/vet/__snapshots__/formatters_test.ambr b/vet/__snapshots__/formatters_test.ambr @@ -0,0 +1,35 @@ +# serializer version: 1 +# name: test_format_github_review_mixed_issues + dict({ + 'body': ''' + **Vet found 2 issues.** + + --- + + **[incorrect_function_implementation]** (severity 2/5) (confidence 0.75) + + General architecture concern + ''', + 'comments': list([ + dict({ + 'body': ''' + **[incorrect_function_implementation]** (severity 2/5) (confidence 0.75) + + This function has a bug + ''', + 'line': 10, + 'path': 'src/app.py', + 'side': 'RIGHT', + }), + ]), + 'event': 'COMMENT', + }) +# --- +# name: test_format_github_review_no_issues + dict({ + 'body': '**Vet found 0 issues.**', + 'comments': list([ + ]), + 'event': 'COMMENT', + }) +# --- diff --git a/vet/cli/main.py b/vet/cli/main.py @@ -33,6 +33,7 @@ from vet.cli.models import get_models_by_provider from vet.cli.models import validate_model_id from vet.formatters import OUTPUT_FIELDS from vet.formatters import OUTPUT_FORMATS +from vet.formatters import format_github_review from vet.formatters import format_issue_text from vet.formatters import issue_to_dict from vet.formatters import validate_output_fields @@ -503,6 +504,9 @@ def main(argv: list[str] | None = None) -> int: if not issues: if args.output_format == "json": print(json.dumps({"issues": []}, indent=2), file=output_stream) + elif args.output_format == "github": + payload = format_github_review(issues, output_fields) + print(json.dumps(payload, indent=2), file=output_stream) elif not args.quiet: print("No issues found.", file=output_stream) return 0 @@ -510,12 +514,15 @@ def main(argv: list[str] | None = None) -> int: if args.output_format == "json": issues_list = [issue_to_dict(issue, output_fields) for issue in issues] print(json.dumps({"issues": issues_list}, indent=2), file=output_stream) + elif args.output_format == "github": + payload = format_github_review(issues, output_fields) + print(json.dumps(payload, indent=2), file=output_stream) else: for issue in issues: print(format_issue_text(issue, output_fields), file=output_stream) print(file=output_stream) - return 1 + return 10 finally: if output_file is not None: output_file.close() diff --git a/vet/formatters.py b/vet/formatters.py @@ -4,7 +4,7 @@ from pydantic import BaseModel from vet.imbue_core.data_types import IdentifiedVerifyIssue -OUTPUT_FORMATS = ["text", "json"] +OUTPUT_FORMATS = ["text", "json", "github"] OUTPUT_FIELDS = [ "issue_code", @@ -91,3 +91,47 @@ def issue_to_dict(issue: IdentifiedVerifyIssue, fields: list[str]) -> dict: if "line_number" in fields and output.line_number_end is not None: include_fields.add("line_number_end") return output.model_dump(mode="json", include=include_fields) + + +def _format_review_comment_body(issue: IdentifiedVerifyIssue, fields: list[str]) -> str: + parts: list[str] = [] + header = [] + if "issue_code" in fields: + header.append(f"**[{issue.code}]**") + if "severity" in fields and issue.severity_score: + header.append(f"(severity {issue.severity_score.raw:.0f}/5)") + if "confidence" in fields and issue.confidence_score: + header.append(f"(confidence {issue.confidence_score.normalized:.2f})") + if header: + parts.append(" ".join(header)) + if "description" in fields: + parts.append(issue.description) + return "\n\n".join(parts) + + +def format_github_review( + issues: tuple[IdentifiedVerifyIssue, ...], + fields: list[str], +) -> dict: + inline = [i for i in issues if i.location and i.location[0].filename] + body_only = [i for i in issues if not i.location or not i.location[0].filename] + + count = len(issues) + noun = "issue" if count == 1 else "issues" + body = f"**Vet found {count} {noun}.**" + if body_only: + sections = [_format_review_comment_body(i, fields) for i in body_only] + body += "\n\n---\n\n" + "\n\n".join(sections) + + comments = [] + for issue in inline: + loc = issue.location[0] + comment: dict = { + "path": loc.filename, + "line": loc.line_start, + "side": "RIGHT", + "body": _format_review_comment_body(issue, fields), + } + comments.append(comment) + + return {"body": body, "event": "COMMENT", "comments": comments} diff --git a/vet/formatters_test.py b/vet/formatters_test.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from syrupy.assertion import SnapshotAssertion + +from vet.formatters import OUTPUT_FIELDS, format_github_review +from vet.imbue_core.data_types import ( + ConfidenceScore, + IdentifiedVerifyIssue, + IssueCode, + IssueLocation, + SeverityScore, +) + + +def _make_issue( + *, + description: str = "Buffer overflow", + severity_raw: float = 2.5, + confidence_raw: float = 0.75, + filename: str | None = "src/foo.py", + line_start: int = 40, + line_end: int = 50, +) -> IdentifiedVerifyIssue: + location = () + if filename is not None: + location = (IssueLocation(line_start=line_start, line_end=line_end, filename=filename),) + return IdentifiedVerifyIssue( + issue_id="test-issue-id", + code=IssueCode.INCORRECT_FUNCTION_IMPLEMENTATION, + description=description, + severity_score=SeverityScore(raw=severity_raw, normalized=severity_raw / 5.0), + confidence_score=ConfidenceScore(raw=confidence_raw, normalized=confidence_raw), + location=location, + ) + + +def test_format_github_review_no_issues(snapshot: SnapshotAssertion) -> None: + assert format_github_review((), OUTPUT_FIELDS) == snapshot + + +def test_format_github_review_mixed_issues(snapshot: SnapshotAssertion) -> None: + inline_issue = _make_issue(description="This function has a bug", filename="src/app.py", line_start=10) + body_issue = _make_issue(description="General architecture concern", filename=None) + assert format_github_review((inline_issue, body_issue), OUTPUT_FIELDS) == snapshot