feat: opencode review engine + .opencode factory
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>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
# pragent `.opencode/` — the review factory
|
||||
|
||||
pragent's review runs on **opencode** (the AI-coding-agent CLI). This directory is
|
||||
a portable **factory**: the `opencode.json` + `.opencode/` are dropped into a
|
||||
checked-out copy of the target repo at the PR head sha, then `opencode run` is
|
||||
launched there. The `pragent` primary agent reviews the diff with real tools
|
||||
(subagents, LSP/linters via bash, webfetch references) and emits a structured
|
||||
findings JSON. A thin Python shell posts that JSON back to Gitea as inline
|
||||
comments + ```suggestion blocks + a summary (dedupe + anchor validation stay
|
||||
deterministic in Python).
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
opencode.json provider (headroom → glm-5.2:cloud), model, lsp, permission, default_agent
|
||||
.opencode/
|
||||
agents/
|
||||
pragent.md PRIMARY reviewer — reads .pragent/brief.md, runs linters, emits findings JSON
|
||||
security.md subagent — injection/auth/secrets/supply-chain lens (dormant)
|
||||
tests.md subagent — missing/weak test coverage lens (dormant)
|
||||
perf.md subagent — N+1 / O(n²) / hot-path lens (dormant)
|
||||
skills/
|
||||
review-methodology/SKILL.md severity rubric, what to report, anchoring rules
|
||||
findings-schema/SKILL.md the exact output JSON shape
|
||||
commands/
|
||||
review.md /review slash command (local interactive use)
|
||||
README.md this file
|
||||
```
|
||||
|
||||
## How a review runs
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
WH["webhook_server.py<br/>HMAC + AI-REVIEW gate + dedupe"] --> RP["ai_review.review_pr"]
|
||||
RP --> ARCH["fetch repo archive @ head sha<br/>→ /tmp/pragent-work/<repo>-<sha>"]
|
||||
ARCH --> BRIEF["write .pragent/brief.md<br/>(title, body, diff, config, prior, sha)"]
|
||||
BRIEF --> DROP["drop opencode.json + .opencode/ into workdir"]
|
||||
DROP --> OC["opencode run --pure --agent pragent --dir <workdir><br/>--model headroom/glm-5.2:cloud"]
|
||||
OC --> PR["pragent primary<br/>load skills · run linters · read code · delegate lenses"]
|
||||
PR --> JSON["final message: summary + ```json findings```"]
|
||||
JSON --> PARSE["ai_review.extract_findings<br/>{summary, findings}"]
|
||||
PARSE --> ANCHOR["parse_diff_anchors → split_findings"]
|
||||
ANCHOR --> POST["post_inline_review<br/>summary + inline ```suggestion + ref links + sha marker"]
|
||||
```
|
||||
|
||||
## Lean by default
|
||||
|
||||
The `pragent` primary does the whole review in one pass for small/medium diffs
|
||||
(no subagent calls). It delegates to `@security` / `@tests` / `@perf` subagents
|
||||
ONLY on large (>~400 lines) or security-sensitive diffs. Token cost scales with
|
||||
PR size. `subagent_depth: 2` caps recursion.
|
||||
|
||||
`--pure` is passed at runtime so the reviewer doesn't load the host user's heavy
|
||||
global opencode plugins (supermemory/dcp/morph/pty) which hang cold-start. In the
|
||||
deploy pod there's no global config, so `--pure` is a no-op there — but it keeps
|
||||
host-local runs deterministic.
|
||||
|
||||
## Extending the factory
|
||||
|
||||
### Add a review lens (subagent)
|
||||
|
||||
1. Create `.opencode/agents/<name>.md` with `mode: subagent`, `hidden: true`, a
|
||||
`description`, and a read-only `permission` (deny edit/write, allow bash/webfetch,
|
||||
`task: deny` so it can't recurse). The body is its system prompt; end it by
|
||||
requiring the same findings-JSON shape.
|
||||
2. Allow it in the primary's `permission.task` list in `pragent.md`:
|
||||
```yaml
|
||||
task:
|
||||
"*": "deny"
|
||||
"security": "allow"
|
||||
"tests": "allow"
|
||||
"<name>": "allow" # add this
|
||||
```
|
||||
3. Mention in `pragent.md`'s "Delegate on heavy diffs" step when to invoke it.
|
||||
|
||||
That's it — the primary can now `@<name>` it via the Task tool. It stays dormant
|
||||
(the primary decides when), so adding it costs nothing for small PRs.
|
||||
|
||||
### Add a skill
|
||||
|
||||
1. `mkdir .opencode/skills/<name> && touch .opencode/skills/<name>/SKILL.md`
|
||||
2. Frontmatter: `name: <name>` (kebab-case, matches dir), `description:` (specific
|
||||
enough for the agent to pick it). Body = the knowledge.
|
||||
3. Refer to it from `pragent.md` ("Call the `skill` tool for `<name>`").
|
||||
|
||||
Per-language expertise is free: the host user already has 29 global skills
|
||||
(golang-*, react-*, k8s, terraform, testing, typescript, …). opencode
|
||||
auto-discovers them via the `skill` tool — the pragent primary loads a matching
|
||||
one when the repo's language fits. To ship a pragent-specific one, just drop it
|
||||
here.
|
||||
|
||||
### Change the output shape
|
||||
|
||||
Edit `.opencode/skills/findings-schema/SKILL.md` (the schema doc) AND the Python
|
||||
parser in `pilot/ai_review.py` (`parse_findings`) + the renderers
|
||||
(`inline_comment_body`, `summary_bullets`, `format_review_body`). Keep them in
|
||||
sync — the parser is tolerant but the agent and parser must agree on field names.
|
||||
|
||||
### Switch model / provider
|
||||
|
||||
Edit `opencode.json` `provider` + `model`. The provider points at the on-network
|
||||
headroom proxy (`http://100.74.17.70:8789/v1`, Anthropic `/v1/messages` format,
|
||||
`apiKey: ollama`) → `glm-5.2:cloud`. To use a different model, add a provider and
|
||||
reference it as `<provider>/<model-id>`.
|
||||
|
||||
## Local one-shot review (no webhook)
|
||||
|
||||
```bash
|
||||
cd ~/Projects/pragent
|
||||
opencode run --pure --agent pragent --dir . \
|
||||
--model headroom/glm-5.2:cloud \
|
||||
"Read .pragent/brief.md if present, else review \`git diff HEAD\`, and output findings."
|
||||
```
|
||||
|
||||
Or in the TUI: `/review` (uses `.opencode/commands/review.md`).
|
||||
|
||||
## Engine flag
|
||||
|
||||
`PRAGENT_ENGINE=opencode` (default once wired) uses this factory. `=ollama`
|
||||
falls back to the legacy direct model call in `pilot/ai_review.py`. The two
|
||||
share all Gitea I/O, dedupe, and posting logic.
|
||||
Reference in New Issue
Block a user