From 04cd260ff974c077044865276f80ddcb65df6a0e Mon Sep 17 00:00:00 2001 From: Steve Dudenhoeffer Date: Thu, 25 Jun 2026 19:06:08 -0400 Subject: [PATCH] docs: add CLAUDE.md + provider example configs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- CLAUDE.md | 113 +++++++++++++++++++++++++++++++++ README.md | 6 +- examples/README.md | 24 +++++++ examples/endpoint-aliases.yml | 54 ++++++++++++++++ examples/local-ollama.yml | 49 ++++++++++++++ examples/openai-compatible.yml | 52 +++++++++++++++ 6 files changed, 296 insertions(+), 2 deletions(-) create mode 100644 CLAUDE.md create mode 100644 examples/README.md create mode 100644 examples/endpoint-aliases.yml create mode 100644 examples/local-ollama.yml create mode 100644 examples/openai-compatible.yml diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ceebca4 --- /dev/null +++ b/CLAUDE.md @@ -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* β†’ :+: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-`). +- **Tag `v*`** β†’ publishes `:` (+ `: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_` / +`GADFLY_ALIAS_` (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_="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). diff --git a/README.md b/README.md index 2877dd2..21b74ed 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..c8d8919 --- /dev/null +++ b/examples/README.md @@ -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_` | + +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). diff --git a/examples/endpoint-aliases.yml b/examples/endpoint-aliases.yml new file mode 100644 index 0000000..62bc09f --- /dev/null +++ b/examples/endpoint-aliases.yml @@ -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_="|[|]" registers a provider you +# can then reference as "/" (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 }} diff --git a/examples/local-ollama.yml b/examples/local-ollama.yml new file mode 100644 index 0000000..42c5818 --- /dev/null +++ b/examples/local-ollama.yml @@ -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://: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 }} diff --git a/examples/openai-compatible.yml b/examples/openai-compatible.yml new file mode 100644 index 0000000..b2a6d82 --- /dev/null +++ b/examples/openai-compatible.yml @@ -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 }}