6e3a9eb5b0
Replace the single Python model-call reviewer with an opencode agent
factory. A primary 'pragent' agent reads a brief (title/body/diff/config/
prior reviews), inspects the checked-out repo, runs the repo's own linters
via bash, loads review-methodology + findings-schema skills, and emits a
{summary, findings} JSON with per-finding severity/path/line/problem/fix/
suggestion/reference. Dormant security/tests/perf subagent lenses fan out
only on large/risky diffs (lean by default).
pilot/opencode_review.py: fetches the repo archive at the head sha into a
temp workdir, writes .pragent/brief.md, drops the factory, runs
'opencode run --pure --agent pragent --dir <workdir>' headlessly. Isolates
HOME (shared, warmed), strips ANTHROPIC_* env (leaked host vars caused
ProviderModelNotFoundError), stdin=DEVNULL (opencode blocks on stdin),
maps the bare OLLAMA_MODEL to the provider-prefixed ref. No Gitea I/O —
ai_review.review_pr parses + anchors + posts (reuses all v2 logic/tests).
PRAGENT_ENGINE=opencode (default) selects it; =ollama keeps the legacy
direct-call path. Verified end-to-end: posts a real review with a summary
section, inline [CRITICAL]/[HIGH] comments + apply-able suggestions +
reference links, and the sha dedupe marker. 49 tests pass.
Co-Authored-By: Claude <noreply@anthropic.com>
2.3 KiB
2.3 KiB
name, description
| name | description |
|---|---|
| findings-schema | The exact JSON output shape pragent must emit at the end of a review. Load this before producing findings. |
pragent findings schema
The review's FINAL message is a short prose summary followed by ONE fenced
json code block. The Python shell parses the LAST json fenced block in the
message — so the JSON must be the last thing, and it must be valid.
Shape
{
"summary": "One-paragraph overview of the change and its risk, plus severity counts.",
"findings": [
{
"severity": "critical|high|medium|low",
"path": "path exactly as it appears in the diff's `+++ b/` side",
"line": 12,
"problem": "one line: what is wrong",
"fix": "one line: how to fix it",
"suggestion": "exact replacement lines, indented as in the file, or \"\"",
"reference": "https://... or \"\""
}
]
}
Field rules
severity— one ofcritical,high,medium,low. Anything else is coerced tomediumby the parser.path— the post-change path, exactly as in the diff (+++ b/foo/bar.ts→foo/bar.ts). Required; a finding without a real path is dropped.line— a post-change line number (int ≥ 1) that exists inpathafter the PR. Required; bad/missing line → the finding becomes a summary bullet instead of an inline comment.problem— one line, concrete: what is wrong and why it matters.fix— one line, the remedy. Empty string if the fix is architectural.suggestion— the literal new code replacing the flagged line(s). Minimal, just the changed lines, indented as they appear in the file. Empty string when no safe textual replacement exists (missing test, architectural note, a fix that needs context beyond one hunk). This is wrapped in a ```suggestion fence → Gitea renders an apply button.reference— a URL (CVE, library docs, spec) backing the finding, or"". Only link authoritative sources; don't fabricate URLs.
Clean diff
If there's nothing to report: {"summary":"<what it does, why it's fine>","findings":[]}.
Don't
- No prose after the closing ``` of the JSON block.
- No extra keys — unknown keys are ignored by the parser, so don't rely on them.
- Don't repeat findings from
prior_reviews.