diff --git a/.gitea/workflows/adversarial-review.yml b/.gitea/workflows/adversarial-review.yml index 7c97f74..4c1ea7a 100644 --- a/.gitea/workflows/adversarial-review.yml +++ b/.gitea/workflows/adversarial-review.yml @@ -1,11 +1,10 @@ -# Gadfly — agentic adversarial PR reviewer (https://gitea.stevedudenhoeffer.com/steve/gadfly). +# Gadfly reviewing its OWN PRs — now a thin CALLER of the reusable workflow +# (.gitea/workflows/review-reusable.yml), dogfooding the Phase-4 "subscribe" +# path. The reusable holds the image pin + env plumbing; this file holds only +# the triggers, the actor gate, and gadfly's specific swarm config. # -# Runs the published Gadfly image (pinned to an immutable :sha- tag — act_runner -# caches :latest, and this build is what carries foreman provider-type support) -# as a specialist swarm and posts -# ONE consolidated review comment as gitea-actions. Advisory only — never blocks a -# merge. Gadfly reviewing its OWN PRs — dogfooding: 6 cloud models + the Claude -# Code engine (sonnet) as a competitor. Local Macs and weak cloud models dropped. +# Advisory only — never blocks a merge. Fleet: 6 cloud + Claude Code +# (sonnet, opus, opus:max) competitors. name: Adversarial Review (Gadfly) @@ -33,57 +32,21 @@ jobs: review: # Security: only trusted users may trigger a secret-bearing run via a PR # comment (pull_request + workflow_dispatch are already trusted). Mirrors - # GADFLY_ALLOWED_USERS, the in-container belt-and-suspenders check. + # the allowed_users input below, the in-container belt-and-suspenders check. if: >- github.event_name != 'issue_comment' || (github.event.issue.pull_request && (github.actor == 'steve' || github.actor == 'fizi' || github.actor == 'dazed')) - runs-on: ubuntu-latest - # Fleet (6 cloud + claude-code/sonnet, all concurrent) reviewing every PR - # with the 3-lens suite. All cloud now, so runs are fast. - timeout-minutes: 90 - steps: - - uses: docker://gitea.stevedudenhoeffer.com/steve/gadfly:sha-c342bdb - env: - GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} - GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} - OLLAMA_CLOUD_API_KEY: ${{ secrets.OLLAMA_CLOUD_API_KEY }} - # Claude Code engine auth (for the claude-code/* entries in GADFLY_MODELS - # below): Pro/Max subscription token. Dogfoods the Phase-1 engine on - # gadfly's own PRs as a competitor alongside the Ollama models. - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - # Fleet: 6 cloud (3 at a time) + Claude Code (sonnet, opus, and opus - # with max extended thinking) — one - # consolidated comment each, all cloud now. The local Macs (m1/m5) and - # the weaker cloud models (gemma4, gpt-oss:120b, kimi-k2.7-code) were - # dropped as low-signal. The claude-code/* entries run the Phase-1 - # engine as competitors in their own lane (need CLAUDE_CODE_OAUTH_TOKEN). - GADFLY_MODELS: "minimax-m3:cloud,glm-5.2:cloud,glm-5.1:cloud,deepseek-v4-pro:cloud,nemotron-3-super:cloud,qwen3-coder:480b-cloud,claude-code/sonnet,claude-code/opus,claude-code/opus:max" - # cloud runs 3 at once; claude-code 2 at a time; both lanes parallel. - GADFLY_PROVIDER_CONCURRENCY: "ollama-cloud=3,claude-code=2" - # 3 cloud models x 3 lenses = 9 concurrent ollama-cloud queries (under the 10 budget). - GADFLY_PROVIDER_LENS_CONCURRENCY: "ollama-cloud=3" - # Default => the 3-lens suite (security, correctness, error-handling). - # Set the repo var GADFLY_SPECIALISTS to override (csv / "all" / "auto"). - GADFLY_SPECIALISTS: ${{ vars.GADFLY_SPECIALISTS || 'security,correctness,error-handling' }} - # Per-lens deadline + bounded steps (also bounds the claude-code lane). - GADFLY_TIMEOUT_SECS: "600" - GADFLY_MAX_STEPS: "14" - # Allow-list for the comment trigger (mirrors the job-level if: guard). - GADFLY_ALLOWED_USERS: "steve,fizi,dazed" - # --- findings telemetry: POST runs + findings to the gadfly-reports store --- - # Advisory & off unless GADFLY_FINDINGS_URL is set; failures only log to - # stderr and never affect the review. GADFLY_REPO / GADFLY_PR are derived - # in-container; the URL + token are user-scope secrets. - GADFLY_FINDINGS_URL: ${{ secrets.GADFLY_FINDINGS_URL }} - GADFLY_FINDINGS_TOKEN: ${{ secrets.GADFLY_FINDINGS_TOKEN }} - # --- 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 }} + uses: ./.gitea/workflows/review-reusable.yml + secrets: inherit + with: + models: "minimax-m3:cloud,glm-5.2:cloud,glm-5.1:cloud,deepseek-v4-pro:cloud,nemotron-3-super:cloud,qwen3-coder:480b-cloud,claude-code/sonnet,claude-code/opus,claude-code/opus:max" + specialists: "security,correctness,error-handling" + provider_concurrency: "ollama-cloud=3,claude-code=2" + provider_lens_concurrency: "ollama-cloud=3" + timeout_secs: "600" + max_steps: "14" + allowed_users: "steve,fizi,dazed" + timeout_minutes: 90 diff --git a/.gitea/workflows/review-reusable.yml b/.gitea/workflows/review-reusable.yml new file mode 100644 index 0000000..470b519 --- /dev/null +++ b/.gitea/workflows/review-reusable.yml @@ -0,0 +1,93 @@ +# Gadfly — REUSABLE adversarial-review workflow (Gitea `workflow_call`). +# +# Centralizes the ~90-line consumer stub so a repo can subscribe to Gadfly with +# a tiny caller. A consumer workflow does: +# +# jobs: +# review: +# if: ... # actor gate for the comment trigger +# uses: steve/gadfly/.gitea/workflows/review-reusable.yml@main +# secrets: inherit # passes OLLAMA_CLOUD_API_KEY etc. through +# with: { models: "...", allowed_users: "..." } # all optional +# +# Inputs are all optional and default to "" — an empty env value makes the +# image/entrypoint use its own built-in default, so the caller only sets what it +# wants to override. Secrets come via `secrets: inherit` (verified working on +# this Gitea's act_runner); an undefined secret resolves to empty, so optional +# ones (Claude Code token, foreman endpoints, findings store) are harmless when +# the consumer hasn't set them. +# +# Advisory only — never blocks a merge. The image is pinned to an immutable +# :sha- tag here (act_runner caches :latest); bump it per Gadfly release. + +name: Gadfly review (reusable) + +on: + workflow_call: + inputs: + models: { type: string, default: "" } # GADFLY_MODELS (csv) + specialists: { type: string, default: "" } # GADFLY_SPECIALISTS + provider: { type: string, default: "" } # GADFLY_PROVIDER + base_url: { type: string, default: "" } # GADFLY_BASE_URL + provider_concurrency: { type: string, default: "" } # GADFLY_PROVIDER_CONCURRENCY + provider_lens_concurrency: { type: string, default: "" } # GADFLY_PROVIDER_LENS_CONCURRENCY + timeout_secs: { type: string, default: "" } # GADFLY_TIMEOUT_SECS (per lens) + max_steps: { type: string, default: "" } # GADFLY_MAX_STEPS + worker_model: { type: string, default: "" } # GADFLY_WORKER_MODEL + allowed_users: { type: string, default: "" } # GADFLY_ALLOWED_USERS + trigger_phrase: { type: string, default: "" } # GADFLY_TRIGGER_PHRASE + # Job wall-clock cap. 45 > 30 as a default: a multi-model swarm or a slow + # lens (e.g. claude-code with extended thinking) can exceed 30 minutes. + timeout_minutes: { type: number, default: 45 } + +# The reusable job posts the review comment, so it needs issues/PR write. Gitea +# caps these by the caller's granted permissions; declaring them here is explicit. +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + review: + runs-on: ubuntu-latest + timeout-minutes: ${{ inputs.timeout_minutes }} + steps: + - uses: docker://gitea.stevedudenhoeffer.com/steve/gadfly:sha-c342bdb + env: + # --- event context (from the CALLER's github.*) ------------------- + GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + 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 }} + # --- provider auth (via secrets: inherit; empty if consumer unset) - + OLLAMA_CLOUD_API_KEY: ${{ secrets.OLLAMA_CLOUD_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} + GADFLY_API_KEY: ${{ secrets.GADFLY_API_KEY }} + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # Common named foreman/LAN endpoints (optional). Consumers with other + # GADFLY_ENDPOINT_s need the full stub (examples/), since a + # reusable workflow can't enumerate arbitrary names. + GADFLY_ENDPOINT_M1: ${{ secrets.GADFLY_ENDPOINT_M1 }} + GADFLY_ENDPOINT_M5: ${{ secrets.GADFLY_ENDPOINT_M5 }} + # --- findings telemetry (optional) -------------------------------- + GADFLY_FINDINGS_URL: ${{ secrets.GADFLY_FINDINGS_URL }} + GADFLY_FINDINGS_TOKEN: ${{ secrets.GADFLY_FINDINGS_TOKEN }} + # --- config (from inputs; empty => image default) ----------------- + GADFLY_MODELS: ${{ inputs.models }} + GADFLY_SPECIALISTS: ${{ inputs.specialists }} + GADFLY_PROVIDER: ${{ inputs.provider }} + GADFLY_BASE_URL: ${{ inputs.base_url }} + GADFLY_PROVIDER_CONCURRENCY: ${{ inputs.provider_concurrency }} + GADFLY_PROVIDER_LENS_CONCURRENCY: ${{ inputs.provider_lens_concurrency }} + GADFLY_TIMEOUT_SECS: ${{ inputs.timeout_secs }} + GADFLY_MAX_STEPS: ${{ inputs.max_steps }} + GADFLY_WORKER_MODEL: ${{ inputs.worker_model }} + GADFLY_ALLOWED_USERS: ${{ inputs.allowed_users }} + GADFLY_TRIGGER_PHRASE: ${{ inputs.trigger_phrase }} diff --git a/CLAUDE.md b/CLAUDE.md index b689971..42b887d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,6 +46,9 @@ entrypoint.sh container brains: trigger gating, PR clone, model loop (t 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 +.gitea/workflows/review-reusable.yml reusable (workflow_call) review job; consumers subscribe with + an ~8-line caller + `secrets: inherit` (Phase 4). gadfly's own + adversarial-review.yml is a thin caller of it (dogfoods the path). examples/ copy-paste consumer stub workflows for different providers ``` diff --git a/README.md b/README.md index 4711563..a36ec11 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,12 @@ Gadfly ships as a container image, so consuming repos don't build anything — t it. Drop one file in your repo and set a couple of secrets/vars: 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). + your repo. Two flavors: the slim [`reusable.yml`](examples/reusable.yml) — a tiny caller of + Gadfly's **reusable workflow** (`uses: steve/gadfly/.gitea/workflows/review-reusable.yml@…` + + `secrets: inherit`), best when you take the defaults — or the full self-contained + [`adversarial-review.yml`](examples/adversarial-review.yml) (Ollama Cloud default, with inline + notes for every provider / 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 index 55589d5..d5d41a5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,7 +6,8 @@ set the secrets/vars it references. Gadfly is advisory only — it never blocks | File | Backend | Needs | |------|---------|-------| -| [`adversarial-review.yml`](adversarial-review.yml) | **Ollama Cloud** (default) + inline notes for every provider | secret `OLLAMA_CLOUD_API_KEY` | +| [`reusable.yml`](reusable.yml) | **slimmest stub** — calls Gadfly's reusable workflow (`secrets: inherit`); take the defaults or override a few inputs | secret `OLLAMA_CLOUD_API_KEY` | +| [`adversarial-review.yml`](adversarial-review.yml) | **Ollama Cloud** (default) + inline notes for every provider; full self-contained stub | 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_` | diff --git a/examples/reusable.yml b/examples/reusable.yml new file mode 100644 index 0000000..fe0b284 --- /dev/null +++ b/examples/reusable.yml @@ -0,0 +1,50 @@ +# Gadfly — SLIM consumer stub via the reusable workflow. +# Copy to .gitea/workflows/adversarial-review.yml in your repo. +# +# This is the shortest way to subscribe: it calls Gadfly's centralized reusable +# workflow, which holds the image pin + all the env plumbing. You only declare +# the triggers, the comment-trigger actor gate, and any overrides you want. +# +# Needs: secret OLLAMA_CLOUD_API_KEY (the default Ollama Cloud provider). +# `secrets: inherit` passes your repo/org/user secrets through to the reusable +# workflow (GITEA_TOKEN is automatic). Pin @ to a Gadfly tag/branch. +# +# Prefer this when you're happy with the defaults. For custom named endpoints +# (GADFLY_ENDPOINT_) or a provider the reusable doesn't map, use the full +# stub in adversarial-review.yml instead. + +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: + # Only let your maintainers re-trigger via a PR comment (keep in sync with + # the allowed_users override below). + if: >- + github.event_name != 'issue_comment' + || (github.event.issue.pull_request && github.actor == 'your-username') + # Pin @ to a Gadfly release tag for stability (@main tracks latest). + uses: steve/gadfly/.gitea/workflows/review-reusable.yml@main + secrets: inherit + with: + # All optional — omit to take Gadfly's defaults. Examples: + # models: "qwen3-coder:480b-cloud,gpt-oss:120b-cloud" + # specialists: "security,correctness,error-handling" + allowed_users: "your-username"