Files
pragent/docs/plans/2026-08-21-pragent-update-design.md
T
claude f9f6ab4bf0 docs(plans): 2026-08-21 pragent update design
- Repo opt-in (.pr-review.json:enabled) replaces AI-REVIEW/AI-USAGE labels.
- Token humanization (fmt_tokens), multi-provider equivalent cost (Claude,
  GPT, Gemini, Grok), richer review summary (walkthrough + risk verdict +
  test coverage), trivial/info severity levels, per-PR merge confidence 1-5.
- Defers reachability severity demotion, rules-mining from feedback, and
  heavy infra (sequence diagrams, T-rex, cross-repo) to future work.
2026-08-21 23:32:49 +00:00

411 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# pragent — Update Design
**Date:** 2026-08-21
**Status:** Approved (brainstorm, 2026-08-21)
**Replaces:** none — additive + behavioral. Existing `docs/plans/2026-08-04-pragent-design.md` stays authoritative on architecture.
## Problem
The pilot has been live long enough to surface pain that the original design didn't
cover:
1. **Two labels to remember.** `AI-REVIEW` (gate) + `AI-USAGE` (opt-in for the
cost block) are per-PR. Every new contributor reads the README wrong at least
once. Reviews that the team *wanted* are skipped because nobody labeled; reviews
we *don't* want still run because the label is sticky.
2. **Token numbers are unreadable.** `Total Tokens: 2071025 in / 17303 out`
requires a mental carry. The pilot already measures the tokens; the rendering
just doesn't help.
3. **Cost is anchored on one provider.** The pilot runs free (headroom/glm-5.2)
but the only equivalent-cost line is Claude Sonnet. We can't answer "what would
this have cost on GPT / Gemini / Grok?" without running the CLI on a different
model.
4. **The PR summary is operational, not useful.** A lens-fanout run posts
`Multi-lens review of repo#index (sha X). Lenses: security,perf. Findings:
critical=0 high=1 medium=2 low=1.` That tells a reviewer *how the bot worked*,
not *what they should look at*. Real products post a risk verdict, a
file-by-file walkthrough, and a test-coverage note.
5. **Triage noise is the dominant failure mode** in every competitor (CodeRabbit,
Qodo, Greptile, DoorDash). We already address most of it (severity_floor,
per-file cap, cross-lens agreement, tone-strip), but two cheap wins are left on
the table: a per-PR *merge confidence* badge, and a richer severity scale that
includes `trivial` / `info` (CodeRabbit's pattern).
This update also distills lessons from a 30-article survey of AI code review
products (CodeRabbit, Qodo/Merge + PR-Agent, Greptile, GitHub Copilot code
review, Gemini Code Assist, qodo-ai/pr-agent, anc95/ChatGPT-CodeReview, Sourcery,
Danger, plus the security literature around the April 2026 prompt-injection
disclosures). Where we already match the state of the art, this update notes it
and moves on; where a competitor's pattern is genuinely better, it lands here.
## Decisions
| Question | Decision | Why |
|---|---|---|
| Trigger | `.pr-review.json:enabled` on the PR's base branch | Repo opt-in replaces labels. No per-PR manual step. Trust stays on base. |
| Default when `.pr-review.json` is absent | Disabled | Explicit opt-in. Mirrors "labels fully removed." |
| Cost model | Always render when usage data is present | Drop the `report_usage` parameter + `AI-USAGE` label + `PRAGENT_USAGE_ALWAYS` env. |
| Token rendering | `1,234,567 (1.2M)` | Python `f"{n:,}"` + short suffix only when `n ≥ 1000`. |
| Multi-provider cost | Markdown table in the collapsible usage block | Replaces the single Sonnet line. Default compare set: Sonnet, GPT-5, Gemini 2.5 Pro, Grok 4.5. |
| Summary depth | Add `walkthrough` / `risk_verdict` / `test_coverage` to the agent JSON; Python fallback for lens synthesis | Agent produces the rich text; Python derives the same three when the lens fan-out is engaged. |
| Severity scale | Extend from 4 → 6 levels: add `trivial` + `info` | Matches CodeRabbit. Backward compat (unknown → medium). |
| Merge confidence | 15 integer in the review header. Python-computed. | Stole the badge idea from Greptile. |
| Reachability demotion | Defer | Needs the security graph. Note in §7. |
| Rules mining from feedback | Defer | `feedback_harvest` / `feedback_analyze` exist; distillation is a separate effort. |
| Sequence diagrams / T-rex / cross-repo | Skip | Too heavy for the pilot. |
## 1. Label removal + repo opt-in
### `.pr-review.json` schema delta
```diff
{
+ "enabled": true,
"focus": [...],
"exclude_paths": [...],
...
}
```
`enabled` is a top-level boolean, default `false`, read from the **base branch**
(unchanged trust rule — `fetch_repo_config(ref=base_ref)` already handles this).
### Webhook behavior (`pilot/webhook_server.py`)
- Remove constants `AI_REVIEW_LABEL`, `AI_USAGE_LABEL`. Remove
`_labels_have_ai_review`. Remove the `report_usage` plumbing from
`_handle_pull_request` and `_run_review`.
- New helper `is_repo_enabled(api, repo, ref, token) -> bool` in `webhook_server.py`
(or reused via `fetch_repo_config` — see below). `False` on any failure
(404, parse error, missing key, malformed value). Logs the reason to stderr.
- `_handle_pull_request` order of operations:
1. `action in SKIP_ACTIONS``200 ignore`
2. base_ref present + fetch config
3. `if not config.get("enabled")``200 "skip (repo not opted in)"`
4. claim in-flight slot
5. thread off `_run_review`
- Pre-claim gate keeps opted-out repos from consuming concurrency slots on
bursts. One extra `GET contents/.pr-review.json` per PR event (404 for
unconfigured repos) — negligible.
### `pilot/ai_review.py` cleanup
- Delete `AI_REVIEW_LABEL`, `AI_USAGE_LABEL` constants.
- Delete `pr_has_label()` helper (its only call sites were the AI-USAGE
re-reads at render time).
- Drop the `report_usage: bool` parameter from `review_pr()`. Always render
the collapsible usage block when `usage` is not None.
- Remove the two `PRAGENT_USAGE_ALWAYS` references (env reads).
- Extend `parse_repo_config()` to extract `enabled` (validate is bool,
default False).
- Extend `effective_config()` to preserve `enabled` through the style-defaults
merge.
### Docs
- `README.md`: rewrite "Label a PR `AI-REVIEW`" + "add the AI-REVIEW label" to
"commit `.pr-review.json: {"enabled": true}` to the default branch." Drop the
AI-USAGE paragraph. Update the flow diagram.
- `pilot/README-webhook.md`: replace onboarding steps. Drop the per-PR label
ceremony.
- `pilot/README.md` (CI-step path): if it still references labels, remove.
## 2. Token humanization
New helper in `pilot/ai_review.py`:
```
def fmt_tokens(n: int | None) -> str:
"""1234567 -> '1,234,567 (1.2M)'; 0 -> '0'; <1000 -> comma-form; None -> '?'."""
```
Rules:
- `None``"?"`.
- `n < 1000``f"{n:,}"` (no short suffix — most findings have ~tens of tokens).
- `1000 ≤ n < 1_000_000``f"{n:,} ({n/1000:.1f}K)"`, drop trailing `.0`.
- `1_000_000 ≤ n < 1_000_000_000``f"{n:,} ({n/1_000_000:.1f}M)"`.
- else `...B`.
- Negative inputs → `"?"` (defensive — never expected from usage dicts).
Apply in:
- `pilot/ai_review._render_collapsible_usage` — input, output, reasoning,
cache_read, cache_write, total.
- `pilot/ai_review.inline_comment_body` — the `🪙 ~N tok (...)` per-finding
line.
Tests: `test_fmt_tokens` golden vectors — `0`, `42`, `999`, `1000`, `1234`,
`1_234_567`, `1_234_567_890`, `None`, `-1`.
## 3. Multi-provider cost in usage section
### `pilot/cost_model.PRICES` — extend with real published rates
Source: Anthropic platform docs, OpenAI pricing, Gemini API pricing, xAI docs.
Fetched 2026-08-21. Numbers in USD per million tokens.
| key | input | output | cache_write | cache_read |
|---|---:|---:|---:|---:|
| `claude-opus-5` | 5.00 | 25.00 | 6.25 | 0.50 |
| `claude-sonnet-5` | 2.00 | 10.00 | 2.50 | 0.20 |
| `claude-haiku-4-5` | 1.00 | 5.00 | 1.25 | 0.10 |
| `gpt-5` | 1.25 | 10.00 | 1.25 | 0.125 |
| `gpt-5-mini` | 0.25 | 2.00 | 0.25 | 0.025 |
| `gemini-2.5-pro` | 1.875 | 12.50 | 1.875 | 0.1875 |
| `gemini-2.5-flash` | 0.30 | 2.50 | 0.30 | 0.03 |
| `grok-4.5` | 2.00 | 6.00 | 2.00 | 0.30 |
| `grok-4.3` | 1.25 | 2.50 | 1.25 | 0.20 |
Notes on derivation:
- Gemini 2.5 Pro publishes a tiered range (`$1.25$2.50` in, `$10$15` out,
`$0.125$0.25` cached). Midpoints are taken for a single line; the
`compare_against` field lets a repo override per-key if precision matters.
- Providers without a separate cache_write charge (OpenAI, Gemini, Grok) set
`cache_write = input` so the existing `cost()` formula continues to work
without a branch on provider.
- `cost_target` (the highlighted single line) and `compare_against` (the table)
are independent fields — see §3.2.
### 3.1 Render
Replace the single `**Est. cost on {provider}**: $X.XX` line in
`_render_collapsible_usage` with a compact markdown table:
```
**Equivalent cost on paid providers** (this run's measured tokens):
| Provider | Cost |
|---|---:|
| Claude Sonnet 5 | $4.32 |
| GPT-5 | $2.71 |
| Gemini 2.5 Pro | $4.04 |
| Grok 4.5 | $4.32 |
```
Sort cheapest-first. Skip rows whose cost is `$0.00`. Bold the row matching
`cost_target` (the user-selected highlight).
### 3.2 Config
`.pr-review.json`:
```json
{
"enabled": true,
"cost_target": "claude-sonnet-5",
"compare_against": ["claude-sonnet-5", "gpt-5", "gemini-2.5-pro", "grok-4.5"]
}
```
`parse_repo_config()`:
- Validate each key exists in `PRICES`. Drop unknowns to stderr (keeps
`cost_model._resolve_price_target`'s typo-reporting consistent).
- Cap the list at `CONFIG_MAX_LIST_ITEMS` (12).
- Default when absent: `["claude-sonnet-5", "gpt-5", "gemini-2.5-pro",
"grok-4.5"]`.
### 3.3 Tests
`tests/pilot/test_cost_model.py`:
- Add equivalent-cost golden vectors against the new price keys.
- Update `test_observed_report_prices_every_model` and
`test_report_renders_every_requested_model` to cover the new keys.
- Add `test_compare_against_parsing` (valid / unknown / over-cap / missing).
## 4. Richer review summary
### 4.1 Schema additions (agent prompts + `SYSTEM_PROMPT`)
```json
{
"walkthrough": [
"file X: does Y",
"file Z: refactors W"
],
"risk_verdict": "Medium risk: changes auth middleware without adding tests.",
"test_coverage": "No tests for behavioral change in pilot/foo.py."
}
```
Rules (added to `.opencode/agents/pragent.md`, each lens agent `.md`, and the
ollama `SYSTEM_PROMPT`):
- `walkthrough`: 26 bullets, file- or change-grouped, plain prose (no
severity emoji). Skip if the diff is one obvious line.
- `risk_verdict`: exactly one line. Lead with `Low|Medium|High|Critical risk:`
followed by a concrete reason grounded in the diff.
- `test_coverage`: short string. One of
`Tests added` / `Tests changed` / `No tests for behavioral change` /
`No test files in repo` / a repo-specific free-text override from
`instructions`.
### 4.2 Parsing
Extend `parse_review_output(text)` and the lens fan-out's synthetic-text
builder (`pilot/opencode_review.run_lenses_review`) to emit these three
fields in the final JSON block. Empty defaults preserve backward compat with
agents that haven't been re-deployed yet.
### 4.3 Python fallback (when fields are empty)
The multi-lens fan-out already synthesizes the findings JSON in Python today;
add a `_synthesize_summary_fields(findings, diff) -> dict` helper that
computes:
- `walkthrough`: group `merged` findings by `path`, one bullet per path
containing the peak severity emoji and the first-problem truncated to ~80
chars. If `merged` is empty, list `changed_files(diff)` with the size of
the diff as the body ("`pilot/foo.py` — +12 lines").
- `risk_verdict`: from `sev_counts` and `_multi_lens` flags:
- any critical → `Critical risk: <N> critical finding(s).`
- any high → `High risk: <N> high finding(s).`
- any medium → `Medium risk: <N> medium finding(s) (<lens> lens).`
- else `Low risk: clean or minor nits only.`
- `test_coverage`: scan `changed_files(diff)` with `is_test_path()`. Three
buckets:
- any test path changed alongside non-test paths → `Tests added` (or
`Tests changed`).
- non-test paths present, no test path → `No tests for behavioral change in
<first non-test path>.`
- no test paths at all and non-test paths present → `No tests for
behavioral change in <first non-test path>.` (same as above; the
distinction "no test files in repo" needs a tree scan — keep it simple
for v1).
### 4.4 Render
Extend `format_review_body()` to render three new sections between
`### Summary of Changes` and `### Key Risks & Concerns`:
```
### Risk Verdict
🟡 Medium risk: changes auth middleware without adding tests.
### Walkthrough
- `pilot/foo.py` — adds retry logic for transient Gitea API errors
- `pilot/bar.py` — extracts shared header parser
### Test Coverage
No tests for behavioral change in pilot/foo.py.
```
Each section renders an `_No <section> provided._` placeholder when empty
(matches the existing `Summary of Changes` / `Key Risks & Concerns` collapse
behavior).
### 4.5 Tests
`tests/pilot/test_ai_review.py`:
- Golden vectors for each new section (provided + Python-fallback paths).
- Combined body test: summary + walkthrough + risk + tests + table +
collapsible usage all render in the right order with no orphan markers.
## 6. Stolen ideas
### 6.1 Merge confidence 15 (Greptile)
New function `merge_confidence(findings: list[dict]) -> int` in
`pilot/ai_review.py`:
```
start at 5
-1 if any critical finding
-1 if any high finding
-1 if any medium finding
-1 if any _multi_lens: True finding (cross-lens agreement = harder to dismiss)
clamp to [1, 5]
```
Render in `REVIEW_HEADER`:
```
🤖 **AI Review** · pragent pilot · glm-5.2:cloud · `abc12345` · Merge confidence: 3/5 🟡
```
Badge map: 5/4 = 🟢, 3 = 🟡, 2 = 🟠, 1 = 🔴.
Tests: golden vectors for all 5 score branches.
### 6.2 Add `trivial` + `info` severity levels (CodeRabbit)
Extend `SEVERITIES` and `SEVERITY_RANK`:
```
SEVERITIES = ("critical", "high", "medium", "low", "trivial", "info")
SEVERITY_RANK = {"info": -1, "trivial": 0, "low": 1, "medium": 2, "high": 3, "critical": 4}
```
Update `_severity_badge` emoji map (`trivial`/`info` = ⚪). Update
`apply_repo_config` threshold semantics so `medium+` still means what it
meant (only `low` ranks below `medium` is unchanged). Update agent prompts
to permit emitting `trivial` / `info`. Backward compat: `_normalize_finding`
already coerces unknown severities to `medium`.
Tests: existing `test_apply_repo_config` cases keep passing; add
`test_severity_threshold_respects_new_levels` and
`test_unknown_severity_normalizes_to_medium`.
### 6.3 Reachability-aware severity demotion — DEFER
CodeRabbit Security demotes severity by one level when a vulnerability is
unreachable / only theoretically exploitable. We can't compute reachability
without the security graph. Document in §7 and revisit when a Code-Rabbit-
style graph index lands.
### 6.4 Rules mining from feedback — DEFER
`pilot/feedback_harvest.py` + `pilot/feedback_analyze.py` exist. A future
`pilot/learn_rules.py` cron job will distill FP-vote signals into
`.pr-review.learned.json` and merge into `instructions`. Document in §7.
### 6.5 Sequence diagrams / T-rex / cross-repo — SKIP
Too heavy for the pilot's footprint. Document in §7.
## 7. Deferred (not in this update)
- **Reachability-aware severity demotion.** Requires a Code-Rabbit-style
reachability graph over the repo.
- **Rules mining from feedback.** A `learn_rules.py` job that consumes the
feedback DB and writes `.pr-review.learned.json`. `feedback_harvest` /
`feedback_analyze` are the substrate.
- **Sequence diagrams / T-rex sandbox / cross-repo review.** Three features
Greptile / Qodo highlight. All require either a code graph index (heavy
precompute) or sandbox runtime execution (separate infra). Skip.
- **Per-finding confidence scores.** Greptile publishes a 05 score on every
comment. We deliberately stay on severity — confidence on findings
requires the agent to self-estimate, which is unreliable without a
cross-lens consensus check. The merge-confidence badge (§6.1) is the
higher-signal version of the same idea.
- **`Fix with Cursor` handoff.** Greptile ships a one-click "send all findings
to Cursor/Codex/Claude Code." Our users *are* the bot's host, not an
external coding IDE. Skip.
- **Cost-model batch column.** The cost model already prices batch at 50%;
the PR-review path will never use it (stateful agent loops aren't
batchable). Keep the column for completeness, no new work.
## 8. Risk register
| Risk | Mitigation |
|---|---|
| Webhook floods the API with `.pr-review.json` fetches on a large owner | One `GET` per PR event, mostly 404. Documented acceptable. The dedicated `/health` already reports inflight count. |
| `.pr-review.json:enabled` set on a high-traffic repo creates surprise review load | The README will document the opt-in explicitly. The webhook's `PRAGENT_MAX_CONCURRENT_REVIEWS` already bounds the spawn rate. |
| New severity levels (`trivial` / `info`) break repos that filter on `medium+` | `apply_repo_config` threshold semantics preserve the rank of `low` and `medium`. `trivial` ranks below `low`, `info` below `trivial`. New filters naturally include them. |
| Token humanization loses precision a maintainer relies on | `fmt_tokens` always keeps the full comma-separated number; the short suffix is a parenthetical. |
| Multi-provider cost table is misleading when a provider has tiered pricing | `compare_against` is a per-repo override. The README documents the midpoints for Gemini 2.5 Pro. |
| Agent prompt change for `walkthrough` / `risk_verdict` / `test_coverage` causes regressions on deployed agents | Python fallback (§4.3) synthesizes the same fields when the agent omits them. Backward compat preserved by empty defaults. |
| Removing `report_usage` breaks the `review_pr` tests that pass it | Test updates are part of this update. |
| Removing labels breaks users who still apply them | No Gitea API change is needed; the labels just stop being read. A one-paragraph README note acknowledges the change. |
## 9. Prioritized implementation list
| # | Item | Section | Effort |
|---|---|---|---|
| P0 | Label removal + repo opt-in (`enabled` in `.pr-review.json`) | §1 | M |
| P1 | `fmt_tokens()` helper + apply in usage + inline | §2 | S |
| P1 | Multi-provider cost table (extend `PRICES`, render table, `compare_against`) | §3 | M |
| P2 | Richer summary (`walkthrough` / `risk_verdict` / `test_coverage`) schema + Python fallback | §4 | L |
| P2 | `trivial` + `info` severity levels | §6.2 | S |
| P3 | Merge confidence 15 in review header | §6.1 | S |
| P3 | README + `pilot/README-webhook.md` rewrite | §1, §10 | S |
| P3 | Test updates across all sections | (each) | M |
P0 first because it changes webhook behavior (must land with the repo-opt-in
docs so onboarding isn't broken mid-rollout). P1 items are independent and
small — ship together. P2 ships the user-visible summary improvement.