docs: add CLAUDE.md + provider example configs
Build & push image / build-and-push (push) Successful in 6s

- CLAUDE.md: project goals (advisory-only, real-bugs-not-nits, easy-to-enable,
  provider-agnostic, portable), architecture map, build/test/release, and
  maintenance rules — incl. "keep README + examples/ current with any env/flag/
  provider/trigger change" and the advisory-only invariant.
- examples/: local-ollama.yml, openai-compatible.yml, endpoint-aliases.yml +
  an examples/README index; README setup step points at them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Steve Dudenhoeffer
2026-06-25 19:06:08 -04:00
parent bd76aa8286
commit 04cd260ff9
6 changed files with 296 additions and 2 deletions
+113
View File
@@ -0,0 +1,113 @@
# Gadfly — Developer Guide
Gadfly (🪰) is an **agentic adversarial code reviewer** that runs in Gitea Actions. On a pull
request it reads the *checked-out repository* with read-only tools, hunts for real problems,
verifies each one against the actual code, and posts its findings as a comment. It is
**advisory only** — it never blocks a merge.
> This is a public, **vibe-coded** project (built largely by an AI agent). Keep that framing
> honest in the README; don't oversell it.
## Project goals (keep changes aligned to these)
1. **Find *real* problems, not nits.** The whole point of the agentic tools + two-pass
recheck is to kill diff-only false positives. Anything that raises the false-positive rate
(or removes verification) works against the project.
2. **Advisory, never blocking.** Gadfly must never fail a CI job for review *content*, never
merge, never deploy. Non-zero exit only on usage/config errors; even then run.sh posts a
notice rather than failing. Do not add it to branch-protection required checks.
3. **Easy to turn on for any repo.** Consumers should need only a ~15-line stub workflow + a
couple of secrets/vars. All real logic lives in the image (`entrypoint.sh`), not in the
consumer's YAML (Gitea's act_runner has weak YAML expression support).
4. **Provider-agnostic.** Powered by [majordomo](https://gitea.stevedudenhoeffer.com/steve/majordomo),
so it can target Ollama (local/cloud), OpenAI, Anthropic, Google, or any
OpenAI/Ollama-compatible endpoint. Don't re-hardcode a single provider.
5. **Portable & self-contained.** `cmd/gadfly` depends only on the Go stdlib + majordomo. Keep
it that way — no heavyweight deps, no coupling to any one consumer repo (e.g. mort).
## Architecture
```
cmd/gadfly/ the reviewer binary — pure producer of review markdown (stdout)
main.go agent orchestration: review pass + adversarial recheck pass, budgets
model.go provider/model resolution (majordomo.Parse) + env endpoint aliases
tools.go the 5 read-only repo tools (read_file/list_dir/grep/find_files/get_diff)
recheck.go second-pass verification prompt + verdict recompute
*_test.go sandbox, recheck, wrap-up, and spec/endpoint-parse unit tests
scripts/run.sh fetch PR diff+meta, run the binary, upsert ONE labeled PR comment
scripts/system-prompt.txt the reviewer persona + verification discipline (generic, not repo-specific)
entrypoint.sh container brains: trigger gating, PR clone, model loop (the logic that
used to live in workflow YAML)
Dockerfile multi-stage; private-module creds via BuildKit secrets never reach the final image
.gitea/workflows/build-image.yml push main → :latest; tag v* → :<tag>+:latest; PR → build-only
examples/ copy-paste consumer stub workflows for different providers
```
**Data flow:** consumer stub workflow → container `entrypoint.sh` (gate + clone) →
`scripts/run.sh` (per model) → `cmd/gadfly` binary (agentic review) → markdown → run.sh
upserts a PR comment as `gitea-actions`.
**Two passes:** a *review* pass drafts findings; an adversarial *recheck* pass independently
re-verifies each finding against the code and drops the unconfirmed ones, recomputing the
verdict. Verdict is one of: `No material issues found` / `Minor issues` / `Blocking issues found`.
## Build / test
```sh
go build ./cmd/gadfly # needs read access to the private majordomo module
go test ./...
gofmt -l cmd/ # must be clean
docker build -t gadfly:dev --secret id=REGISTRY_USER,env=REGISTRY_USER --secret id=REGISTRY_PASSWORD,env=REGISTRY_PASSWORD .
```
Run it locally against a real diff without CI:
```sh
git -C /path/to/repo diff main > /tmp/x.diff
GADFLY_PROVIDER=ollama GADFLY_MODEL=qwen2.5-coder:7b \
GADFLY_REPO_DIR=/path/to/repo GADFLY_DIFF_FILE=/tmp/x.diff \
GADFLY_SYSTEM_FILE=scripts/system-prompt.txt ./gadfly
```
## Release / deploy
- **Push to `main`** → CI builds and pushes `:latest` (+ `:sha-<short>`).
- **Tag `v*`** → publishes `:<tag>` (+ `:latest`). Pin consumers to `:vN` for stability.
- Required CI secrets: `REGISTRY_USER` / `REGISTRY_PASSWORD` (registry push + read access to the
private majordomo module). Optional `DISCORD_WEBHOOK_URL`.
## Configuration
The full env reference lives in the **README** (`Models & providers` + `Configuration`).
Provider selection: `GADFLY_PROVIDER` (default `ollama-cloud`), `GADFLY_MODEL`/`GADFLY_MODELS`,
`GADFLY_BASE_URL`, `GADFLY_API_KEY`. Named endpoint aliases via `GADFLY_ENDPOINT_<NAME>` /
`GADFLY_ALIAS_<NAME>` (http-capable) and majordomo `LLM_*` DSNs (HTTPS-only).
**Tested vs untested:** only the Ollama paths (local + OpenAI-compatible pointed at Ollama)
are actually exercised. OpenAI/Anthropic/Google come from majordomo's abstraction and are
**untested** (no spend). Keep the README honest about this; update it if that changes.
## When making changes — maintenance rules
- **Keep the README and `examples/` current.** Any change to env vars, flags, defaults,
triggers, provider support, or the consumer stub MUST be reflected in `README.md` and the
relevant files under `examples/` in the *same* change. The README's `Configuration` table,
the `Models & providers` table, and the example workflows are the contract users rely on —
stale docs are a bug.
- **Preserve the advisory-only invariant** (goal #2). If you touch exit codes or the workflow,
re-confirm a review can never fail/block a consumer's CI.
- **Don't add mort-specific (or any single-consumer) assumptions** to the binary or system
prompt. The system prompt is intentionally generic; repo-specific conventions should be
discovered by the agent at runtime (it can read the repo's own CONTRIBUTING/CLAUDE.md), not
hardcoded here.
- **Keep secrets out of image layers.** Private-module creds flow via BuildKit `--mount=type=secret`
in the build stage only; never bake them into the final image or commit them.
- Add a test when you add logic (see the `*_test.go` patterns). Keep `gofmt` clean and `go vet` quiet.
## Lessons
- majordomo's `LLM_*` env DSNs are **HTTPS-only** (`DSN.BaseURL()` forces `https://`), so they
can't express a plaintext local Ollama. That's why Gadfly adds the http-capable
`GADFLY_ENDPOINT_<NAME>="provider|base-url[|key]"` mechanism (see `cmd/gadfly/model.go`).
- Gitea `vars`/`secrets` are **not** auto-exposed as env in a job — the consumer stub must map
each one explicitly in its `env:` block (dynamic alias names can't be auto-enumerated).
+4 -2
View File
@@ -36,8 +36,10 @@ or **Blocking issues found**.
Gadfly ships as a container image, so consuming repos don't build anything — they just run
it. Drop one file in your repo and set a couple of secrets/vars:
1. Copy [`examples/adversarial-review.yml`](examples/adversarial-review.yml) to
`.gitea/workflows/adversarial-review.yml` in your repo.
1. Copy a stub from [`examples/`](examples/) to `.gitea/workflows/adversarial-review.yml` in
your repo — [`adversarial-review.yml`](examples/adversarial-review.yml) for the Ollama Cloud
default, or a provider-specific one (local Ollama, OpenAI-compatible, endpoint aliases). See
the [examples index](examples/README.md).
2. Add repo config:
- **secret** `OLLAMA_CLOUD_API_KEY` — your [Ollama Cloud](https://ollama.com) key (empty
⇒ Gadfly posts a harmless "not configured" notice instead of reviewing). *Not needed if
+24
View File
@@ -0,0 +1,24 @@
# Example consumer workflows
Each file here is a complete, copy-paste **stub workflow**. Pick the one that matches your
setup, copy it to `.gitea/workflows/adversarial-review.yml` in the repo you want reviewed, and
set the secrets/vars it references. Gadfly is advisory only — it never blocks a merge.
| File | Backend | Needs |
|------|---------|-------|
| [`adversarial-review.yml`](adversarial-review.yml) | **Ollama Cloud** (default) + inline notes for every provider | secret `OLLAMA_CLOUD_API_KEY` |
| [`local-ollama.yml`](local-ollama.yml) | a **local/LAN Ollama** daemon | nothing (or `GADFLY_BASE_URL` for a remote host) |
| [`openai-compatible.yml`](openai-compatible.yml) | any **OpenAI-compatible** endpoint (local Ollama `/v1`, gateway, vLLM, OpenRouter…) | `GADFLY_BASE_URL` (+ a key for most gateways) |
| [`endpoint-aliases.yml`](endpoint-aliases.yml) | **several named backends** at once (one comment each) | repo vars `GADFLY_ENDPOINT_<NAME>` |
Common to all:
- **Triggers:** new/reopened/ready non-draft PR (auto), `@gadfly review` comment (allowed users),
or manual `workflow_dispatch` with a `pr_number`.
- `GITEA_TOKEN` is provided automatically; comments post as `gitea-actions`.
- Tested backends are the **Ollama** ones; OpenAI/Anthropic/Google are wired via majordomo but
untested. See the repo [README](../README.md#models--providers) for the full config reference
and the honest tested/untested status.
> **Gitea note:** repo `vars`/`secrets` are not auto-exposed as env — anything you reference
> via `${{ vars.X }}` / `${{ secrets.X }}` must appear in the step's `env:` block (already wired
> in these examples).
+54
View File
@@ -0,0 +1,54 @@
# Gadfly with named ENDPOINT ALIASES — review with several backends at once,
# each posting its own comment. Copy to .gitea/workflows/adversarial-review.yml.
#
# GADFLY_ENDPOINT_<NAME>="<provider>|<base-url>[|<key>]" registers a provider you
# can then reference as "<name>/<model>" (NAME lowercases: BIGBOX -> bigbox).
# The base URL is used verbatim, so plaintext http LAN endpoints work.
#
# Gitea note: vars/secrets aren't auto-exposed as env, so map each alias here.
# Suggested repo vars:
# GADFLY_ENDPOINT_BIGBOX = ollama|http://192.168.1.50:11434
# GADFLY_ENDPOINT_GPU = openai|http://gpu.lan:8000/v1
name: Adversarial Review (Gadfly)
on:
pull_request:
types: [opened, reopened, ready_for_review]
issue_comment:
types: [created]
workflow_dispatch:
inputs:
pr_number: { description: "PR number to review", required: true }
permissions:
contents: read
issues: write
pull-requests: write
concurrency:
group: gadfly-${{ github.event.issue.number || github.event.pull_request.number || github.event.inputs.pr_number }}
cancel-in-progress: true
jobs:
review:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: docker://gitea.stevedudenhoeffer.com/steve/gadfly:latest
env:
GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
# --- named endpoints (mapped from repo vars) ---
GADFLY_ENDPOINT_BIGBOX: ${{ vars.GADFLY_ENDPOINT_BIGBOX }} # "ollama|http://192.168.1.50:11434"
GADFLY_ENDPOINT_GPU: ${{ vars.GADFLY_ENDPOINT_GPU }} # "openai|http://gpu.lan:8000/v1"
# one reviewer (one comment) per model, across the aliased endpoints:
GADFLY_MODELS: "bigbox/qwen2.5-coder:7b,gpu/llama3.1"
# --- event context (leave as-is) ---
EVENT_NAME: ${{ github.event_name }}
PR: ${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}
PR_BRANCH: ${{ github.head_ref }}
IS_DRAFT: ${{ github.event.pull_request.draft }}
COMMENT_BODY: ${{ github.event.comment.body }}
COMMENT_ID: ${{ github.event.comment.id }}
ACTOR: ${{ github.actor }}
+49
View File
@@ -0,0 +1,49 @@
# Gadfly using a LOCAL Ollama daemon (no API key needed).
# Copy to .gitea/workflows/adversarial-review.yml in your repo.
#
# The runner must be able to reach the Ollama host. For localhost on the runner,
# leave GADFLY_BASE_URL unset; for a LAN box set it to http://<host>:11434.
#
# Pick a model you've pulled into Ollama (e.g. `ollama pull qwen2.5-coder:7b`).
name: Adversarial Review (Gadfly)
on:
pull_request:
types: [opened, reopened, ready_for_review]
issue_comment:
types: [created]
workflow_dispatch:
inputs:
pr_number: { description: "PR number to review", required: true }
permissions:
contents: read
issues: write
pull-requests: write
concurrency:
group: gadfly-${{ github.event.issue.number || github.event.pull_request.number || github.event.inputs.pr_number }}
cancel-in-progress: true
jobs:
review:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: docker://gitea.stevedudenhoeffer.com/steve/gadfly:latest
env:
GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
# --- local Ollama ---
GADFLY_PROVIDER: ollama
GADFLY_MODELS: qwen2.5-coder:7b
# GADFLY_BASE_URL: http://192.168.1.50:11434 # uncomment for a remote/LAN daemon
# --- event context (leave as-is) ---
EVENT_NAME: ${{ github.event_name }}
PR: ${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}
PR_BRANCH: ${{ github.head_ref }}
IS_DRAFT: ${{ github.event.pull_request.draft }}
COMMENT_BODY: ${{ github.event.comment.body }}
COMMENT_ID: ${{ github.event.comment.id }}
ACTOR: ${{ github.actor }}
+52
View File
@@ -0,0 +1,52 @@
# Gadfly against an OpenAI-COMPATIBLE endpoint.
# Copy to .gitea/workflows/adversarial-review.yml in your repo.
#
# Works for: a local Ollama's OpenAI bridge (http://localhost:11434/v1), an
# in-house gateway, OpenRouter, vLLM, LM Studio, etc. This is the same code path
# the real OpenAI API uses, so it's a free way to exercise the OpenAI provider.
#
# Set GADFLY_API_KEY (or OPENAI_API_KEY) — Ollama ignores it, but most gateways
# require some value.
name: Adversarial Review (Gadfly)
on:
pull_request:
types: [opened, reopened, ready_for_review]
issue_comment:
types: [created]
workflow_dispatch:
inputs:
pr_number: { description: "PR number to review", required: true }
permissions:
contents: read
issues: write
pull-requests: write
concurrency:
group: gadfly-${{ github.event.issue.number || github.event.pull_request.number || github.event.inputs.pr_number }}
cancel-in-progress: true
jobs:
review:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: docker://gitea.stevedudenhoeffer.com/steve/gadfly:latest
env:
GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
# --- OpenAI-compatible endpoint ---
GADFLY_PROVIDER: openai
GADFLY_BASE_URL: http://localhost:11434/v1 # e.g. local Ollama /v1, or your gateway
GADFLY_API_KEY: ${{ secrets.OPENAI_API_KEY }} # any non-empty value for Ollama
GADFLY_MODELS: qwen2.5-coder:7b
# --- event context (leave as-is) ---
EVENT_NAME: ${{ github.event_name }}
PR: ${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}
PR_BRANCH: ${{ github.head_ref }}
IS_DRAFT: ${{ github.event.pull_request.draft }}
COMMENT_BODY: ${{ github.event.comment.body }}
COMMENT_ID: ${{ github.event.comment.id }}
ACTOR: ${{ github.actor }}