Files
pragent/pilot
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 pilot — AI Review bot

A minimal AI code-review bot for Gitea, running as a CI step on the existing act-runner. This is the pilot — a small, self-contained reviewer that predates the full pragent framework (whose design lives in docs/plans/2026-08-04-pragent-design.md). The framework will later absorb this; until then, this is what runs.

How it works

  1. You add pragent-bot to a repo and commit .gitea/workflows/ai-review.yml.
  2. On a PR, you add the AI-REVIEW label.
  3. Gitea Actions runs the workflow on the act-runner; it fetches the PR diff, asks glm-5.2:cloud (on-network via the headroom proxy) to review it, and posts the findings back as a PR review authored by pragent-bot.
  4. Remove the label to stop re-reviews on further pushes.

Fail-open: the job always exits 0 and never blocks CI. Errors become a short "review failed" comment.

Onboard a repo (3 steps)

1. Add pragent-bot as collaborator

Repo → Settings → Collaborators → Add → pragent-bot → permission Write. (Write is required to post reviews/comments.)

Or via API (with an admin/owner token):

curl -X PUT -H "Authorization: token $OWNER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"permission":"write"}' \
  "http://<gitea-host>:3000/api/v1/repos/OWNER/REPO/collaborators/pragent-bot"

2. Add the PRAGENT_BOT_TOKEN secret

Repo → Settings → Actions → Secrets → New secret → name PRAGENT_BOT_TOKEN, value = the bot's access token (ask the platform admin; stored mode-600 at ~/.claude/.pragent-bot-token on the admin host).

3. Commit the workflow

Copy pilot/workflow-template.yml into the target repo as .gitea/workflows/ai-review.yml and commit it. That's it.

Use it

Open a PR (or push to an open one), add the AI-REVIEW label. The review appears within ~3090s depending on diff size and model latency.

What's intentionally NOT in the pilot

Deferred to the full framework (by design, see the design doc):

  • Attention tiering (trivial/lite/full/oversized) and per-tier cost control.
  • Multiple analyzer fan-out over a shared cached prompt prefix.
  • Prior-comment synthesis (so each push re-posts; the latest review is tagged with the head SHA so it's easy to spot).
  • Inline line comments and status checks.
  • pragent explain / replay / analytics JSONL.
  • A second forge (GitLab) and the provider matrix.

Pieces

File Role
pilot/ai_review.py The reviewer script (stdlib only). Single source of truth — fetched at runtime by each repo's workflow.
pilot/workflow-template.yml The Gitea Action consumers copy into .gitea/workflows/ai-review.yml.
tests/pilot/test_ai_review.py Unit tests for the pure helpers (no network).

Run the tests

cd ~/Projects/pragent
PYTHONPATH=pilot python3 -m pytest tests/pilot/    # if pytest available
# or, without pytest:
python3 - <<'PY'
import os, sys, importlib.util
sys.path.insert(0, os.path.abspath("pilot"))
import ai_review  # noqa: F401
spec = importlib.util.spec_from_file_location("t", "tests/pilot/test_ai_review.py")
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
fails = 0
for n in sorted(x for x in dir(m) if x.startswith("test_")):
    try: getattr(m, n)(); print("PASS", n)
    except Exception as e: fails += 1; print("FAIL", n, e)
print("failed:", fails)
PY

Configuration knobs (env in the workflow)

Env Default Purpose
OLLAMA_MODEL glm-5.2:cloud Model id passed to the headroom proxy.
OLLAMA_MAX_TOKENS 6000 Output token cap.
DIFF_MAX_CHARS 150000 Diff truncation cap (with a noted truncation marker).
OLLAMA_URL http://<model-proxy-host>:8789 headroom proxy (tailnet). If the act-runner can't reach the tailnet IP, expose 8789 as an in-cluster Service+Endpoints and set this to the cluster DNS name.