On by default, opt-out via "reviewers": []. 290 tests pass.
- pilot/opencode_review.py: ReviewerSpec dataclass, default_reviewers(),
parse_reviewers_config(), parse_triage_config(), resolve_reviewers(),
_normalize_lens_finding(), posthash() (matches feedback.py scheme),
_agreement_hash() (severity-free for cross-lens promotion), _tone_strip(),
synthesize() 7-stage (severity_floor → tone-strip → length cap → per-lens
max → per-file cap → dedup by _posthash → cross-lens severity promote →
per-PR cap), run_lenses() (ThreadPoolExecutor pool=4), triage(),
_intersect_with_triage(), _filter_by_skip_if(), run_lenses_review().
run() routes to fan-out when config.reviewers[] present or PRAGENT_REVIEWERS=1.
- pilot/ai_review.py: parse_repo_config learns reviewers[] and triage objects
(id regex /^[a-z0-9][a-z0-9-]{0,31}$/, 8-entry cap, agent_file/model/
severity_floor/max_findings/activation/skip_if_all_changed_paths/hotpath_globs).
review_pr branches to opencode_review.run_lenses_review when configured.
_render_collapsible_usage shows lenses: ... line when present.
- .opencode/agents/{docs,code-quality,triage}.md: 3 new lens subagents.
- .opencode/skills/lens-orchestration/SKILL.md: strict-JSON contract every
lens subagent MUST honor.
- .opencode/agents/pragent.md: slim to coordinator; no more hardcoded
@security/@tests/@perf delegation; loads lens-orchestration skill.
- .opencode/README.md: rewrite 'Add a review lens' recipe for multi-lens.
- pilot/README-webhook.md: new 'Multi-lens pipeline' section (diagram +
default roster + config schema + env vars + cross-lens dedup contract).
- tests: 36 new tests (test_ai_review.py +12 reviewers/triage/usage,
test_opencode_review.py +24 orchestration). posthash golden-vector matches
feedback.py exactly across 5 severity × 2 line cases.
7.0 KiB
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 + language-highlighted suggested-fix 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, trust boundary
findings-schema/SKILL.md the exact output JSON shape
attention-tiering/SKILL.md trivial/lite/full/oversized + the budget each tier gets
linter-playbook/SKILL.md per-ecosystem check commands + turning diagnostics into findings
security-lens/SKILL.md inline security checklist + the source→sink test
malicious-change/SKILL.md hostile-PR detection: injection at the reviewer, install hooks, backdoors
comment-craft/SKILL.md how to write problem/fix/suggestion so a maintainer can act
commands/
review.md /review slash command (local interactive use)
README.md this file
How a review runs
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.parse_review_output<br/>{summary, findings}"]
PARSE --> ANCHOR["parse_diff_anchors → split_findings"]
ANCHOR --> POST["post_inline_review<br/>summary + inline lang-tagged fix block + ref links + sha marker"]
Lean by default
attention-tiering is the cost governor: it classifies every PR as trivial /
lite / full / oversized before any file is read, and each tier caps file
reads, linter runs, and subagent fan-out. The pragent primary does the whole
review in one pass for small/medium diffs (no subagent calls) and delegates to
@security / @tests / @perf ONLY at full/oversized when the lens has
real surface. Skills are loaded conditionally for the same reason — each one is
input tokens. Subagent recursion is capped by the primary's steps budget.
pilot/cost_model.py turns those tier assumptions into a per-PR and per-month
cost figure for any provider — run it after changing the factory to see what the
change costs.
--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)
Multi-lens orchestration is now Python-side (pilot/opencode_review.py).
Each lens is just a .md file; the Python side spawns one subprocess per
lens in parallel and synthesises the merged findings.
- Create
.opencode/agents/<id>.mdwith frontmatter:Body = the system prompt. End it with the strict JSON contract in--- description: One line that names what this lens catches. mode: subagent hidden: true model: headroom/glm-5.2:cloud temperature: 0.1 permission: edit: deny write: deny bash: "allow" # or narrow: "<cmd>": "allow" webfetch: allow task: deny ---.opencode/skills/lens-orchestration/SKILL.md(findings shape,severity_floor, no writes outside workdir, prompt-injection reporting). A real lens has target paths + output schema + tool budget + example findings — not just a different system prompt. - Add one entry to the repo's
.pr-review.json:reviewers[]:No Python change. No image rebuild. The orchestrator picks it up next run.{ "id": "<id>", "severity_floor": "low", "max_findings": 8 } - (Optional) Tighter defaults:
activation: "off"to ship-disabled,skip_if_all_changed_paths: "docs/**"to skip when only docs changed,hotpath_globs: ["**/queries/**"]to help triage recognise the lens.
Built-in lenses (you can override any of them): security, docs,
code-quality, tests, perf. Set reviewers: [] to opt out.
Add a skill
mkdir .opencode/skills/<name> && touch .opencode/skills/<name>/SKILL.md- Frontmatter:
name: <name>(kebab-case, matches dir),description:(specific enough for the agent to pick it). Body = the knowledge. - Refer to it from
pragent.md("Call theskilltool 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://<model-proxy-host>: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)
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.