a87e7d2c72
All 3 cloud models converged (all "minor" — example code, no blocking): - Consolidate: a model whose every lens errored now reads "review incomplete", not a misleading "no issues found" (all 3 models). + test. - Consolidate: swarm-cancelled (unattributed) cells now surface a "swarm cancelled — N cell(s) did not run" banner instead of vanishing (all 3). + test. - main: io.ReadAll(os.Stdin) error is surfaced (all 3); a TTY stdin no longer hangs forever (TTY guard, minimax). - providerOf: a bare tier name now keys its own PerKey bucket instead of all bare tiers collapsing onto "tier" (minimax, glm-5.2) — distinct tiers throttle independently. - Review doc reworded (the closure, not fanout, carries per-cell errors). Left as documented example-scope behavior: no per-cell timeout (caller supplies ctx), unknown-severity → lowest rank (no crash). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
111 lines
3.3 KiB
Go
111 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"gitea.stevedudenhoeffer.com/steve/executus/config"
|
|
"gitea.stevedudenhoeffer.com/steve/executus/fanout"
|
|
"gitea.stevedudenhoeffer.com/steve/executus/model"
|
|
)
|
|
|
|
// DefaultLenses is the canary's review suite (mirrors gadfly's default).
|
|
var DefaultLenses = []Lens{
|
|
{Name: "security", Focus: "auth, injection, secret leakage, unsafe deserialization, SSRF."},
|
|
{Name: "correctness", Focus: "logic errors, broken invariants, off-by-one, contract violations."},
|
|
{Name: "error-handling", Focus: "swallowed errors, missing timeouts, races, unhandled edge cases."},
|
|
}
|
|
|
|
// Reviewer is configured entirely from the environment (the GADFLY_*-style light
|
|
// host): REVIEWER_MODELS (csv of tier/spec), REVIEWER_MODEL_TIER_<NAME> overrides,
|
|
// REVIEWER_MAX_CONCURRENT, REVIEWER_PROVIDER_CONCURRENCY. The diff is read from
|
|
// -diff or stdin.
|
|
//
|
|
// REVIEWER_MODELS=fast,thinking ANTHROPIC_API_KEY=... go run ./examples/reviewer < my.diff
|
|
func main() {
|
|
cfg := config.Env("REVIEWER_")
|
|
|
|
// Tier table from env, with code defaults.
|
|
model.Configure(cfg, map[string]string{
|
|
"fast": "anthropic/claude-haiku-4-5",
|
|
"thinking": "anthropic/claude-opus-4-8",
|
|
}, 0)
|
|
|
|
fleet := splitCSV(cfg.String("models", "fast"))
|
|
maxConc := cfg.Int("max_concurrent", 6)
|
|
perProvider := cfg.Int("provider_concurrency", 3)
|
|
|
|
diffFlag := flag.String("diff", "", "diff text to review; reads stdin when empty")
|
|
flag.Parse()
|
|
diff := *diffFlag
|
|
if strings.TrimSpace(diff) == "" {
|
|
// Guard against blocking forever on an interactive TTY (no piped input).
|
|
if fi, _ := os.Stdin.Stat(); fi != nil && fi.Mode()&os.ModeCharDevice != 0 {
|
|
fmt.Fprintln(os.Stderr, "reviewer: no diff (pass -diff or pipe one on stdin)")
|
|
os.Exit(2)
|
|
}
|
|
b, err := io.ReadAll(os.Stdin)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "reviewer: reading stdin: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
diff = string(b)
|
|
}
|
|
if strings.TrimSpace(diff) == "" {
|
|
fmt.Fprintln(os.Stderr, "reviewer: no diff (pass -diff or pipe one on stdin)")
|
|
os.Exit(2)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
var models []NamedModel
|
|
for _, spec := range fleet {
|
|
_, m, err := model.ParseModelForContext(ctx, spec)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "reviewer: resolve model %q: %v\n", spec, err)
|
|
os.Exit(1)
|
|
}
|
|
models = append(models, NamedModel{Name: spec, Provider: providerOf(spec), Model: m})
|
|
}
|
|
|
|
results := Review(ctx, models, DefaultLenses, diff, fanout.Options[cell]{
|
|
MaxConcurrent: maxConc,
|
|
PerKey: perKeyCaps(models, perProvider),
|
|
})
|
|
fmt.Print(Consolidate(results))
|
|
}
|
|
|
|
func splitCSV(s string) []string {
|
|
var out []string
|
|
for _, p := range strings.Split(s, ",") {
|
|
if p = strings.TrimSpace(p); p != "" {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// providerOf returns a model spec's provider (the first path segment, e.g.
|
|
// "anthropic/claude-…" → "anthropic"; a bare tier name → itself).
|
|
func providerOf(spec string) string {
|
|
if i := strings.IndexByte(spec, '/'); i > 0 {
|
|
return spec[:i]
|
|
}
|
|
return spec // bare tier name → its own bucket (don't collapse distinct tiers)
|
|
}
|
|
|
|
// perKeyCaps builds the PerKey map: each distinct provider capped at perProvider.
|
|
func perKeyCaps(models []NamedModel, perProvider int) map[string]int {
|
|
if perProvider <= 0 {
|
|
return nil
|
|
}
|
|
caps := map[string]int{}
|
|
for _, m := range models {
|
|
caps[m.Provider] = perProvider
|
|
}
|
|
return caps
|
|
}
|