From b02b11d69139843665da4cdbf776bc0b3583490d Mon Sep 17 00:00:00 2001 From: steve Date: Sun, 28 Jun 2026 02:23:40 +0000 Subject: [PATCH] feat(reusable): ship the curated swarm as the default config consumers inherit (#10) --- .gitea/workflows/adversarial-review.yml | 23 ++++++-------- .gitea/workflows/review-reusable.yml | 42 ++++++++++++++++--------- CLAUDE.md | 13 +++++--- README.md | 3 +- examples/README.md | 2 +- examples/reusable.yml | 21 ++++++++----- 6 files changed, 64 insertions(+), 40 deletions(-) diff --git a/.gitea/workflows/adversarial-review.yml b/.gitea/workflows/adversarial-review.yml index 27dc5db..8ef119c 100644 --- a/.gitea/workflows/adversarial-review.yml +++ b/.gitea/workflows/adversarial-review.yml @@ -1,10 +1,11 @@ -# 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. +# Gadfly reviewing its OWN PRs — a thin CALLER of the reusable workflow +# (.gitea/workflows/review-reusable.yml), dogfooding the "subscribe" path. The +# reusable holds the image pin, env plumbing, AND the default swarm; this file +# holds only the triggers, the actor gate, secret forwarding, and allow-list. # -# Advisory only — never blocks a merge. Fleet: 6 cloud + Claude Code -# (sonnet, opus, opus:max) competitors. +# Advisory only — never blocks a merge. It inherits the default swarm: 3 cloud +# models + Claude Code (sonnet, opus, opus:max), 5-lens suite (claude models run +# one at a time, each with all 5 lenses at once). name: Adversarial Review (Gadfly) @@ -48,11 +49,7 @@ jobs: GADFLY_FINDINGS_URL: ${{ secrets.GADFLY_FINDINGS_URL }} GADFLY_FINDINGS_TOKEN: ${{ secrets.GADFLY_FINDINGS_TOKEN }} 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" + # Inherit the default swarm (3 cloud + Claude Code sonnet/opus/opus:max, + # 5-lens suite) from review-reusable.yml. Only the consumer-specific + # allow-list is set here. allowed_users: "steve,fizi,dazed" - timeout_minutes: 90 diff --git a/.gitea/workflows/review-reusable.yml b/.gitea/workflows/review-reusable.yml index fdb0921..3958d9c 100644 --- a/.gitea/workflows/review-reusable.yml +++ b/.gitea/workflows/review-reusable.yml @@ -10,11 +10,13 @@ # secrets: # forward ONLY what the reviewer needs # OLLAMA_CLOUD_API_KEY: ${{ secrets.OLLAMA_CLOUD_API_KEY }} # CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} -# with: { models: "...", allowed_users: "..." } # all optional +# with: { allowed_users: "..." } # config inputs are optional (see below) # -# 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 are DECLARED below (workflow_call.secrets) so a +# Inputs ship the DEFAULT swarm (see the inputs block): 3 cloud models + the +# Claude Code engine, 5-lens suite (claude models serial, 5 lenses each). A consumer +# inherits it by omitting `with:` entirely, or overrides any field (e.g. +# `models:` for a cloud-only / different-provider setup; "" falls back to the +# image's built-in default). Secrets are DECLARED below (workflow_call.secrets) so a # caller forwards only the credentials the reviewer actually uses — least # privilege — rather than `secrets: inherit`, which leaks every caller secret # (registry/deploy/db creds) into this workflow. `secrets: inherit` still works @@ -30,21 +32,33 @@ name: Gadfly review (reusable) on: workflow_call: + # Inputs ship the DEFAULT Gadfly swarm so a consumer can just call this + # workflow (no `with:` block) and inherit it. The default is opinionated — + # 3 strong cloud models + the Claude Code engine (sonnet/opus/opus:max), the + # 5-lens suite. Claude models run ONE at a time, but each runs all 5 lenses at + # once. It needs OLLAMA_CLOUD_API_KEY and CLAUDE_CODE_OAUTH_TOKEN; a consumer + # with only one (or a different provider) overrides `models:` (and forwards + # just the secrets it uses). Set any input to "" to fall back to the + # image/entrypoint built-in default. + # + # Peak claude concurrency = provider_concurrency × provider_lens_concurrency + # (1 model × 5 lenses = up to 5 concurrent `claude -p` per pass) — friendly to + # a single subscription. Raise claude-code in either knob for more parallelism. inputs: - models: { type: string, default: "" } # GADFLY_MODELS (csv) - specialists: { type: string, default: "" } # GADFLY_SPECIALISTS + models: { type: string, default: "minimax-m3:cloud,glm-5.2:cloud,deepseek-v4-pro:cloud,claude-code/sonnet,claude-code/opus,claude-code/opus:max" } # GADFLY_MODELS (csv) + specialists: { type: string, default: "security,correctness,maintainability,performance,error-handling" } # GADFLY_SPECIALISTS (5-lens default suite) 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 + provider_concurrency: { type: string, default: "ollama-cloud=3,claude-code=1" } # GADFLY_PROVIDER_CONCURRENCY (claude models one at a time) + provider_lens_concurrency: { type: string, default: "ollama-cloud=3,claude-code=5" } # GADFLY_PROVIDER_LENS_CONCURRENCY (each claude runs all 5 lenses at once) + timeout_secs: { type: string, default: "600" } # GADFLY_TIMEOUT_SECS (per lens) + max_steps: { type: string, default: "14" } # GADFLY_MAX_STEPS worker_model: { type: string, default: "" } # GADFLY_WORKER_MODEL - allowed_users: { type: string, default: "" } # GADFLY_ALLOWED_USERS + allowed_users: { type: string, default: "" } # GADFLY_ALLOWED_USERS (consumer-specific; set in your stub) 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 } + # Job wall-clock cap. 90 as a default: the 5-lens suite across a slow lane + # (claude-code with extended thinking) over two passes can run long. + timeout_minutes: { type: number, default: 90 } # Declared so callers can forward ONLY the secrets the reviewer needs # (least privilege) instead of `secrets: inherit`, which would hand this # workflow every secret in the caller's repo (registry/deploy/db creds the diff --git a/CLAUDE.md b/CLAUDE.md index 7d284c6..7c16db1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,9 +46,11 @@ 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 forwarding only the secrets the reviewer needs (Phase 4). gadfly's own - adversarial-review.yml is a thin caller of it (dogfoods the path). +.gitea/workflows/review-reusable.yml reusable (workflow_call) review job; ships the DEFAULT swarm as + input defaults (3 cloud + Claude Code sonnet/opus/opus:max, 5-lens suite; + claude models serial, 5 lenses each) so consumers inherit it by omitting `with:`. Consumers subscribe + with an ~8-line caller forwarding only the secrets the reviewer needs (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 ``` @@ -98,7 +100,10 @@ comment with a section each. Default suite = security/correctness/maintainabilit error-handling; opt-in built-ins = tests/docs/conventions/improvements. Select via `GADFLY_SPECIALISTS` (csv or `all`); define/override via `GADFLY_SPECIALIST_` env or a repo `.gadfly.yml` (`specialists:` + `define:`). See `cmd/gadfly/specialists.go`. Cost ≈ -specialists × models × 2 passes — keep the default model count low (entrypoint defaults to one). +specialists × models × 2 passes — the **image/entrypoint** default stays minimal (one model) for +that reason; the **reusable workflow** (`review-reusable.yml`) deliberately ships a heavier +opinionated default swarm (3 cloud + Claude Code, 5 lenses) for steve's own fleet, which consumers +inherit or override per-input. **Dynamic `auto`** (`GADFLY_SPECIALISTS=auto`): a selector (`GADFLY_SELECTOR_MODEL` or the review model) picks lenses per-diff and may invent ad-hoc ones (`cmd/gadfly/auto.go`). **Worker-tier** (`GADFLY_WORKER_MODEL`): a `delegate_investigation` tool offloads grep/read legwork to a cheap diff --git a/README.md b/README.md index 2f0bd92..577e574 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,8 @@ 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. 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@…`, - forwarding only the secrets the reviewer needs), best when you take the defaults — or the full self-contained + forwarding only the secrets the reviewer needs), which ships a **default swarm** (3 cloud models + + the Claude Code engine, 5-lens suite) you inherit by omitting `with:` or override per-input — 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). diff --git a/examples/README.md b/examples/README.md index 4af343e..22b170c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,7 +6,7 @@ set the secrets/vars it references. Gadfly is advisory only — it never blocks | File | Backend | Needs | |------|---------|-------| -| [`reusable.yml`](reusable.yml) | **slimmest stub** — calls Gadfly's reusable workflow, forwarding only the secrets the reviewer needs (least privilege, not `secrets: inherit`); take the defaults or override a few inputs | secret `OLLAMA_CLOUD_API_KEY` | +| [`reusable.yml`](reusable.yml) | **slimmest stub** — calls Gadfly's reusable workflow and inherits its **default swarm** (3 cloud + Claude Code, 5-lens suite), forwarding only the secrets it needs (least privilege, not `secrets: inherit`); the stub keeps a cloud-only `models:` override so it runs with just the Ollama key (drop it + add the Claude token to get the full default) | 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) | diff --git a/examples/reusable.yml b/examples/reusable.yml index 312376f..a582053 100644 --- a/examples/reusable.yml +++ b/examples/reusable.yml @@ -5,7 +5,12 @@ # 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). +# The reusable ships a DEFAULT swarm: 3 cloud models + the Claude Code engine +# (sonnet/opus/opus:max), 5-lens suite. That default needs BOTH +# OLLAMA_CLOUD_API_KEY and CLAUDE_CODE_OAUTH_TOKEN. This example overrides +# `models:` to a cloud-only set so it works with just OLLAMA_CLOUD_API_KEY — +# delete that override (and forward the Claude token) to inherit the full default. +# # Forward ONLY the secrets the reviewer uses (least privilege) — see the # `secrets:` block below. GITEA_TOKEN is automatic. `secrets: inherit` also works # but hands the reusable EVERY secret in your repo (registry/deploy/db creds the @@ -13,9 +18,8 @@ # Gadfly commit (not @main) so a push there can't change what runs with your # secrets. # -# 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. +# 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) @@ -56,7 +60,10 @@ jobs: # GADFLY_FINDINGS_URL: ${{ secrets.GADFLY_FINDINGS_URL }} # GADFLY_FINDINGS_TOKEN: ${{ secrets.GADFLY_FINDINGS_TOKEN }} with: - # All optional — omit to take Gadfly's defaults. Examples: - # models: "qwen3-coder:480b-cloud,gpt-oss:120b-cloud" - # specialists: "security,correctness,error-handling" + # Cloud-only override so this works with just OLLAMA_CLOUD_API_KEY. Delete + # this line (and forward CLAUDE_CODE_OAUTH_TOKEN above) to inherit the full + # default swarm (3 cloud + Claude Code sonnet/opus/opus:max, 5 lenses). + models: "minimax-m3:cloud,glm-5.2:cloud,deepseek-v4-pro:cloud" + # Other inputs inherit the default (5-lens suite, concurrency, 90-min cap); + # override any of them here (specialists, provider, base_url, timeout_secs…). allowed_users: "your-username"