Files
pragent/.opencode
Marcos 2b1cf750b7 fix(review): correct diff-compression line numbers, prior-review dedupe, triage skip
Four defects, all found reviewing PR #9 (two of them by pragent-bot's own
review of that PR, which the anchoring bug then misplaced):

* compress_diff dropped context lines but copied the original `@@` hunk
  header verbatim, so the header no longer described the lines beneath it.
  parse_diff_anchors then walked stale headers and produced anchor sets
  shifted by the number of elided lines, misplacing inline comments or
  demoting them to bullets. Each surviving run of lines is now re-emitted as
  its own hunk with a recomputed `@@ -a,b +c,d @@`, so the output stays a
  valid unified diff whose numbers describe the real post-change file. The
  pseudo-marker `@@ … N context line(s) omitted … @@` is gone; it parsed as
  a hunk header and reset the anchor counter to 0. Anchoring additionally
  runs on the raw diff now, so the prompt window can never shrink the
  anchorable set.

* compress_diff's `_FILE_HEADER` regex matched diff *body* lines: a removed
  YAML `---` separator or an added `++` line was read as a file header,
  truncating the hunk and dropping its `@@` header with it. Body detection
  is now prefix-based, with a full-shape hunk-header regex.

* extract_finding_bullets could not match the bullets pragent itself posts:
  summary_bullets renders an emoji severity badge between the `-` and the
  `[SEV]` tag, which the regex rejected, so compact_prior_reviews always
  returned [] and every re-review repeated its previous findings.

* triage returning `{"lenses":[]}` — documented in .opencode/agents/triage.md
  as "no lens has surface, skip the fan-out" — ran every lens instead, since
  _intersect_with_triage mapped an empty selection to "all" and the call site
  had a second `or reviewers` fallback. `[]` and None are now distinct
  outcomes: `[]` skips, None fails open. A roster naming only unknown lens
  ids now fails open rather than silencing the review. The skip path returns
  a well-formed empty-findings response instead of "", which had landed in
  ai_review's unparseable-output branch and posted "AI review produced no
  parseable output" — a malfunction message for a normal verdict.

Also: non-URL references (a CVE id, a doc title) rendered as
`[CVE-2024-1234](CVE-2024-1234)`, a broken relative link in Gitea — now
plain text. PRAGENT_DIFF_CONTEXT and friends parse through _int_env, so a
typo logs and falls back instead of killing a review mid-flight. Removed
format_usage_section, dead since the collapsible usage block replaced it and
carrying a duplicate copy of the price-target logic.

Tests: 290 -> 301. New coverage for hunk-header fidelity before/after
compression, header-shaped content lines, the bullet round-trip against the
real renderer, and triage's three outcomes (previously untested).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B11e8TZZxJyzHW7jj7KWUN
2026-08-20 23:05:03 +00:00
..

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.

  1. Create .opencode/agents/<id>.md with frontmatter:
    ---
    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
    ---
    
    Body = the system prompt. End it with the strict JSON contract in .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.
  2. Add one entry to the repo's .pr-review.json:reviewers[]:
    { "id": "<id>", "severity_floor": "low", "max_findings": 8 }
    
    No Python change. No image rebuild. The orchestrator picks it up next run.
  3. (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

  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://<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.