feat: live status-board comment — per-model/per-lens review progress #1
Reference in New Issue
Block a user
Delete Branch "feat/status-board"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 3 of the gadfly-games build.
When several models (each with several lenses) review a PR, mid-run all you'd see is a row of
⏳ Reviewing…placeholders across N per-model comments. This adds one consolidated, live-updating status-board comment that aggregates every model's per-lens progress (queued → running → finished + verdict), so the swarm's progress is visible at a glance — and a watcher (human or agent) can tell when everything is finished.What it looks like
How it works
cmd/gadfly/status.go— opt-instatusWriter(gated onGADFLY_STATUS_FILE) publishes this model's lenses to a JSON file, written atomically (temp + rename) asrunSpecialiststransitions each lens. Inert when unset, so plain runs and unit tests are unaffected.scripts/status-board.sh— background renderer: polls the status dir, renders + upserts one marker comment everyGADFLY_STATUS_POLL_SECS(default 12s), caching the comment id to PATCH in place rather than re-searching each tick.entrypoint.sh— pre-seeds every model asqueued(so the full swarm shows from t=0, even models waiting on a lane cap), launches the board, waits only on the review lanes, then signals.donefor one final render and reaps the board.Invariants
GADFLY_STATUS_BOARD=0.Docs
README config table + new "Live status board" section, example-stub note, CLAUDE.md architecture map.
Tests
gofmt -lclean ·go vetquiet ·go buildok ·go test -race ./...green (newstatus_test.gocovers the lifecycle, the disabled no-op, and unknown-lens handling).status-board.shrender + upsert verified locally under bash with a stubbed curl.🤖 Generated with Claude Code
🪰 Gadfly review —
m1/qwen3:14b(m1)Verdict: Blocking issues found — 3 reviewers: security, correctness, error-handling
🔒 Security — No material issues found
No material issues found
The implementation introduces a live status board for Gadfly reviews with the following security considerations addressed:
Environment Variable Handling
The
GITEA_TOKENis used with proper access controls (repo write access), and the script usescurlwith theAuthorizationheader securely. No direct exposure of secrets in the code or logs.Input Sanitization
The
status_file_forfunction inentrypoint.shsanitizes model IDs by replacing non-alphanumeric characters with underscores, preventing path traversal or injection attacks.Atomic Writes
The
statusWriterinstatus.gouses atomic writes (temp + rename) to ensure file integrity and prevent partial writes that could be exploited.JSON Handling
The
render_sectionfunction instatus-board.shgracefully handles malformed JSON files by skipping them, and thestatusWriterensures only defined lenses are updated.Access Control
The status board is advisory and does not interfere with the actual review process. It uses a separate comment from per-model findings, ensuring no impact on the binary's stdout or exit code.
No security vulnerabilities were found in the code changes.
🎯 Correctness — No material issues found
Verdict: No material issues found
The implementation correctly tracks per-model/per-lens progress through atomic JSON file updates and aggregation. Key correctness aspects verified:
Lens state transitions (queued → running → finished) are properly modeled in
modelStatusandlensStatusstructs, with tests confirming state persistence and mutation rules.Atomic writes in
statusWriter.flush()use temp files + rename to prevent partial writes, ensuring the status board never reads inconsistent data.Unknown lens handling in
set()safely ignores unrecognized lens names without modifying the status file, as validated byTestStatusWriterUnknownLens.Model pre-seeding in
entrypoint.shinitializes all models as queued, ensuring the status board shows full swarm progress from t=0.Error resilience in
flush()correctly swallows I/O errors (as required by the "advisory/best-effort" invariant).Status aggregation in
status-board.shaccurately combines per-model JSON files into a consolidated view, with proper handling of edge cases like zero lenses or incomplete data.All logic aligns with the PR's stated invariants and requirements.
🧯 Error handling & edge cases — Blocking issues found
Blocking issues found
cmd/gadfly/status.go:67—flush()swallows errors when creating the temp file directory. IfGADFLY_STATUS_FILEpoints to a non-existent directory,os.CreateTemp()will fail silently, preventing status updates without any feedback. Verified by reading the code.scripts/status-board.sh:73—render_section()silently skips malformed JSON files. If a model's status file is invalid (e.g., due to concurrent writes), the board will omit that model's progress without logging, leading to incomplete status updates. Verified by reading the code.scripts/status-board.sh:113—upsert()does not check HTTP response codes from the Gitea API. If the comment post/patch fails (e.g., invalid token, rate limit), the script will retry blindly without knowing the root cause. Verified by reading the code.entrypoint.sh:173—status_file_for()may generate excessively long filenames if the model name contains many non-alphanumeric characters. While not a critical failure, it could lead to filesystem path length limitations. Verified by reading the code.Automated adversarial review by Gadfly. Advisory only — does not block merge. · ⏱️ reviewed in 21m 52s
🪰 Gadfly review —
glm-5.2:cloud(ollama-cloud)Verdict: Minor issues — 3 reviewers: security, correctness, error-handling
🔒 Security — Minor issues
VERDICT: Minor issues
scripts/status-board.sh:33/:98-99— the board comment marker is a static, nonce-less literal, so comment discovery is bound to the PR only, not to the current run.find_existingpaginatesissues/${PR}/commentsand selects the first comment whosebodystarts with<!-- gadfly-status-board -->(line 33/98-99); the marker contains no run/commit/repo nonce. Theupsertpath then uses the write-scopedGITEA_TOKENto PATCH that comment (line 114). Impact: on a PR that previously ran Gadfly, a new run reuses/overwrites the stale board comment rather than creating a fresh one (this is at least intentional for restart recovery per the comment at lines 90-91), and — more relevantly — any comment in that PR whose body begins with that exact public literal would be PATCHed by the bot using its repo-write token. This is a minor authorization over-reach: the token is used to modify a comment the current run did not create, keyed solely on a guessable prefix. By contrast,run.sh:44's per-model marker<!-- gadfly-review:${PROVIDER}:${MODEL} -->at least binds to model identity; the board marker is fully static. Suggested fix: include a per-run nonce (e.g. commit SHA or a generated run id) inMARKER, e.g.<!-- gadfly-status-board:${GADFLY_RUN_ID} -->, and only PATCH comments whose body starts with the run-specific marker. No authn/authz gap in the token usage itself (it is used with the intended repo-write scope for comment CRUD, exactly asrun.shalready does); no injection or secret-leak path (URL path components are operator env, the token is sent only as anAuthorizationheader and never embedded in the posted body or logged).🎯 Correctness — Minor issues
Both findings are confirmed against the actual code.
Finding 1 verified:
run.shhas three paths that exit/skip before the binary runs: empty diff (exit 0at line 103), missing Ollama Cloud key (lines 145-146 setREVIEWand skip), and binary not found (lines 148-150). The entrypoint pre-seeds each model at lines 222-226 as{done:false, lenses:[]}. Only the binary'sstatusWriter(status.go) ever setsdone:true.status-board.shline 77 increments the "finished" counter only when.done == true, and line 58 renderslenses:[]+done:falseas "⏳ waiting to start". The final render (lines 127-129, one last upsert after.donesentinel) would perpetually show such a model as waiting and the headerN/Mwould never reachM/M.Finding 2 verified:
entrypoint.sh:162tr -c '[:alnum:]._-' '_'maps/and:to_, som1/qwen3:14b→m1_qwen3_14b.json, colliding with a model idm1_qwen3_14b. Real but low-probability.VERDICT: Minor issues
entrypoint.sh:222-226/scripts/run.sh:103,145-146,148-150— pre-seeded status file is never marked done when a model is skipped before the binary runs. The entrypoint pre-seeds every model as{lenses:[], done:false}. The board's "finished" counter (status-board.sh:77) only increments for files with.done == true, which is only ever set by the binary'sstatusWriter.set. Butrun.shhas several paths that never invoke the binary: empty diff (exit 0at run.sh:103), missing Ollama Cloud key (run.sh:145-146 →REVIEW="⚠️ …"and skips the binary), and binary not found (run.sh:148-150). In all of these the model's pre-seeded file stays{lenses:[], done:false}forever. Consequence: the final board render shows that model perpetually as "⏳ waiting to start" (status-board.sh:58) and the headerN/M reviewers finishedcan never reachM/M— directly breaking the PR's stated invariant that "a watcher can tell when everything is finished." Suggested fix: haverun.sh(orreview_one) write a terminal status (done:true, or a single errored lens) when the binary is skipped, or have the board treat a stalelenses:[]file as finished/skipped on the final render.entrypoint.sh:162—status_file_forsanitization can collide for distinct model ids.tr -c '[:alnum:]._-' '_'maps every non-alnum/._-char to_, so e.g.m1/qwen3:14band a hypotheticalm1_qwen3_14bboth becomem1_qwen3_14b.json, and one model's status file silently overwrites the other's. Unlikely with real model ids, but it's a real correctness hazard; the board would then show only one of the two models. Consider including a short hash of the original id in the filename to guarantee uniqueness.🧯 Error handling & edge cases — Minor issues
VERDICT: Minor issues
scripts/status-board.sh:31,130— busy-loop on invalidGADFLY_STATUS_POLL_SECS.POLL="${GADFLY_STATUS_POLL_SECS:-12}"(line 31) is never validated. The script usesset -uo pipefailwith noset -e(line 24), so if a user setsGADFLY_STATUS_POLL_SECS=""or a non-numeric value (e.g."12s"),sleep "$POLL"(line 130) fails and the error is silently swallowed. Thewhile :loop (line 127) then spins with no delay, callingupsert "$(render_body)"(line 128, 2+curlhits to the Gitea API) as fast as it can — an accidental API-hammering tight loop. Verified: there is no numeric check anywhere between line 31 and thesleepat line 130. Suggested fix: coerce to a positive int, e.g.[[ "$POLL" =~ ^[0-9]+$ ]] && [ "$POLL" -ge 1 ] || POLL=12.entrypoint.sh:162— status-file name collisions from over-aggressive sanitization.status_file_for()flattens the model id withtr -c '[:alnum:]._-' '_', so two distinct models can map to the same filename (e.g.m1/qwen:14bandm1_qwen:14bboth →m1_qwen_14b.json). Two model processes would then overwrite each other's status file, and the board would render a single flickering section instead of two. The JSON carries the real model/provider, but uniqueness is the documented contract (the comment at lines 158-160 states "this just needs to be unique per model"). Low severity for realistic model lists, but the edge case is real on unusual input. Suggested fix: hash the raw id into the filename (e.g...._$(printf %s "$1" | md5sum | cut -c1-8).json) while keeping a human-readable prefix.cmd/gadfly/main.go:236-240— a panicked lens is never marked finished.sw.set(sp.Name, lensRunning, ...)(line 236) is called beforereviewWithSpecialist, andsw.set(sp.Name, lensFinished, ...)(line 240) only runs after it returns. The goroutine's defers (lines 234-235) only dowg.Done/sem drain — there is norecover, and greppingcmd/gadflyconfirms none exists. If the specialist pass panics, the goroutine crashes the whole binary (pre-existing behavior) and that model's status file is left stuck in therunningstate — the board's final render shows it as neverdone. Since the process dies this is largely moot for that run, but it is an unhappy path the diff introduces into the status surface. Low severity; noting for completeness.No issues found with the atomic-write/cleanup discipline in
flush()or the.donefinal-render sequencing — those unhappy paths are handled correctly.Automated adversarial review by Gadfly. Advisory only — does not block merge. · ⏱️ reviewed in 6m 21s
🪰 Gadfly review —
m5/qwen3.6:35b-mlx(m5)Review incomplete — all lenses errored — 3 reviewers: security, correctness, error-handling
🔒 Security — ⚠️ could not complete
⚠️ This reviewer failed to complete: agent: step 10: context deadline exceeded
🎯 Correctness — ⚠️ could not complete
⚠️ This reviewer failed to complete: agent: step 3: context deadline exceeded
🧯 Error handling & edge cases — ⚠️ could not complete
⚠️ This reviewer failed to complete: agent: step 9: context deadline exceeded
Automated adversarial review by Gadfly. Advisory only — does not block merge. · ⏱️ reviewed in 30m 0s
🪰 Gadfly review —
deepseek-v4-flash:cloud(ollama-cloud)Verdict: No material issues found — 3 reviewers: security, correctness, error-handling
🔒 Security — No material issues found
🔒 Security — Verdict: No material issues found
After tracing the data flow end-to-end, I find no material security vulnerabilities introduced by this change. Here's what I verified:
Status file content is tightly controlled. The verdict strings written to the JSON come from
verdict.label()(consolidate.go:18-29), which returns one of four fixed strings ("No material issues found","Minor issues","Blocking issues found","Reviewed"). Lens names come from the configuredSpecialist.Namelist. Model/provider names come from environment variables set byentrypoint.sh. No user-supplied or model-generated content flows into the status file unchecked.Atomic writes prevent partial-read races.
status.go:111-130usesos.CreateTemp+os.Renamewithin the same directory, so the board script (status-board.sh) never reads a half-written file.API calls are injection-safe. The
upsertfunction instatus-board.sh:109-124encodes the comment body throughjq -n --arg b "$body" '{body:$b}', which properly JSON-escapes the content before sending it to the Gitea API. TheBOARD_IDandPRvalues used in URL paths come from the API response (numeric ID) and a required env var respectively — no injection vector.File paths are sanitized.
entrypoint.sh:162usestr -c '[:alnum:]._-' '_'on the model name before constructing the status file path, preventing any path-traversal attempt through model names.No new secret exposure. The
GITEA_TOKENis used incurlcommands with-H "Authorization: token ${GITEA_TOKEN}", the same pattern already used throughoutrun.shandentrypoint.sh. The status file itself contains only metadata (model name, provider, lens names, verdicts) — no credentials or tokens.The board is properly gated.
GADFLY_STATUS_BOARD=0disables it entirely (entrypoint.sh:217), and thestatusWriteris inert whenGADFLY_STATUS_FILEis empty (status.go:76-78), so plain runs and unit tests are unaffected.The only observation worth noting (not a finding) is that the
GITEA_TOKENis visible in the process list viacurlcommand arguments — but this is the same pattern used throughout the existing codebase (run.sh,entrypoint.sh) and is standard practice in single-container environments. No change here.🎯 Correctness — No material issues found
🎯 Correctness — Verdict: No material issues found
After tracing the full data flow through the Go binary, the shell scripts, and the JSON file format, I find no logic bugs, incorrect constants, or semantic errors introduced by this change.
What I verified:
statusWriteratomic-write pattern (temp file + rename) is correct and safe for concurrent goroutines.set()method'sdonecomputation correctly requires all lenses to be inlensFinishedstate.modelStatusJSON field tags (json:"name",json:"state", etc.) match the field names used by thejqfilter inrender_section().verdict.label()output (e.g."No material issues found") is correctly passed tosw.set()as the verdict string.lenses:[],done:false) are properly overwritten by the binary'snewStatusWriter, and the board'srender_sectionhandles the empty-lenses case with$tot == 0.provider_of()inentrypoint.shandmodelProvider()in Go produce the same provider string for a given model id, so the JSONproviderfield is consistent between pre-seed and binary-written files.GADFLY_STATUS_FILEenv var flows correctly:entrypoint.sh→review_one()→run.sh→ binary'sos.Getenv("GADFLY_STATUS_FILE").GADFLY_MODELenv var used bynewStatusWritermatches the model id used bystatus_file_for()to derive the filename, so the binary writes to the same path the board reads from.render_bodyprintfformat string has the correct number of%s/%dplaceholders (5) matching the 5 arguments.render_sectionjq filter correctly handleserroredbeingnull(via// false) whenomitemptyomits it.Minor cosmetic note (not a correctness issue):
scripts/status-board.sh:80— The$'\n\n'separator appended to each section, combined with the\n\nbefore%sin theprintfformat string on line 86, produces an extra blank line between model sections in the rendered comment. This is purely cosmetic and does not affect the board's function.🧯 Error handling & edge cases — No material issues found
🧯 Error handling & edge cases — Verdict: No material issues found
After reading the full source files (
status.go,status_test.go,main.go,consolidate.go,specialists.go,model.go) and the shell scripts, the code is well-defended for its advisory/best-effort contract. No blocking or material error-handling issues exist in this lens.What was checked and why it's clean:
set()guardsw == nil || w.path == ""at the top; the disabled-writer path is explicitly tested (TestStatusWriterDisabled).flush()usesos.CreateTemp+os.Renamewithin the same directory, so the board reader never sees a partial file even under concurrent lens goroutines. Temp files are cleaned up on every error path.set()holdsw.muacross the entire read-modify-write ofw.st, preventing data races between concurrent lens goroutines.set()silently ignores them (tested inTestStatusWriterUnknownLens); no panic, no append, no corruption.render_sectionpipes stderr to/dev/nullso a malformed/half-written JSON file (shouldn't happen due to atomic writes, but defensive) simply skips that model for one tick.upsertclearsBOARD_IDon PATCH failure so the next tick re-discovers or re-creates the comment.jq— the board script checks forjqand exits 0 immediately; the entrypoint's pre-seed loop uses2>/dev/null || true, so a missingjqsilently disables the board rather than crashing.LANE_PIDSis guarded with[ "${#LANE_PIDS[@]}" -gt 0 ]beforewait, and the board'srender_bodyhandlestotal == 0with a fallback message.os.CreateTempandos.Rename/os.Remove, a.status-*.tmpfile is left behind. These are in the status directory and have a.tmpextension so the board ignores them, but they could accumulate across crashes. This is consistent with the advisory/best-effort design and is not a functional bug.Nothing in this lens requires a code change.
Automated adversarial review by Gadfly. Advisory only — does not block merge. · ⏱️ reviewed in 10m 32s
🪰 Gadfly review —
minimax-m3:cloud(ollama-cloud)Verdict: Minor issues — 3 reviewers: security, correctness, error-handling
🔒 Security — No material issues found
VERDICT: No material issues found
I traced the security-relevant trust boundaries for this PR and verified them against the checked-out code:
scripts/status-board.sh:95,114,120reusesGITEA_TOKENto POST/PATCH exactly one comment under a hardcoded marker (<!-- gadfly-status-board -->). The token is already required to have repo write access for the existingrun.shflow; the board adds no new privilege. Verified by readingscripts/status-board.shandscripts/run.sh.say()atscripts/status-board.sh:37echoes only its own prefix + message to stderr, and the only runtime messages I see ("starting…","patch of comment ${BOARD_ID} failed","jq not found") contain no token material.GITEA_TOKENis never expanded intosay's$*. Verified.entrypoint.sh:162status_file_forpipes the model id throughtr -c '[:alnum:]._-' '_'before concatenation withSTATUS_DIR/, so/and:(which appear in real model ids likem1/qwen3:14b) are stripped to_and the result can never escape the directory. Verified.jquse--arg/-n:entrypoint.sh:223(--arg model,--arg provider),scripts/status-board.sh:44,77,98,111. Nothing is interpolated into ajqprogram string. Verified.${GITEA_API}and${PR}are operator-controlled (set by the consumer's stub workflow / entrypoint), not user-controlled, so there's no SSRF surface added by this PR. Verified by readingentrypoint.sh:26-29.cmd/gadfly/status.go:112-129usesos.CreateTempin the same dir +os.Rename, withos.Remove(tmpName)on every error path. No leftover tmp files, no half-written readers. Verified.GADFLY_STATUS_BOARDdefaults to1(on), but the gate atentrypoint.sh:217and the board script both check it explicitly, and the README documents the opt-out. The advisory nature of the board means even a misrender can't affect review correctness — only PR comment content, which is already trusted to operator-controlled env.One tiny non-security note I happened to spot while reading the file (and so will mention only as a courtesy, not a lens finding): the
tr -csanitizer is one-way and theoretically allows two distinct model ids (e.g.a/banda:b) to map to the sameSTATUS_DIR/*.jsonpath. The JSON inside carries the real model/provider so the comment content stays correct — only the on-disk write races. Not security-relevant.🎯 Correctness — Minor issues
VERDICT: Minor issues
cmd/gadfly/main.go:233-241— a panicked lens is silently stuck as "🔄 running" forever. The goroutine hasdefer wg.Done()anddefer func(){<-sem}, but norecover. IfreviewWithSpecialistpanics, the deferred calls run andwg.Wait()returns, butsw.set(...finished...)never executes; the lens stays atstate:runningin the JSON. BecauseDoneis only recomputed insideset, the model keeps reporting⏳ 0/N lenseson the board, and the overall "X/Y reviewers finished" counter never increments for it. The binary itself also exits without flipping the lens — the same status file is what the board keeps re-rendering after the entrypoint touches${STATUS_DIR}/.done. This is the exact failure mode the status board exists to surface, and it is the one it misrepresents most. Fix:defer func(){ if r := recover(); r != nil { sw.set(sp.Name, lensFinished, "panic: "+fmt.Sprint(r), true) } }()afterdefer wg.Done()(and ideally let the panic still propagate).entrypoint.sh:162— filename sanitizer can collide for distinct model ids.tr -c '[:alnum:]._-' '_'maps both/and:to_, som1/qwen3:14bandm1:qwen3/14b(or any pair differing only in those positions) both produce${STATUS_DIR}/m1_qwen3_14b.json. The second model's binary then overwrites the first's status file mid-run, and the board's "X/Y reviewers finished" counter and per-lens bullets silently drop a reviewer. The very example in the adjacent comment (m1/qwen3:14b) demonstrates the collision. Fix: keep/as a path separator (STATUS_DIR/<provider>/<model>.json) or append a short hash of the original id to disambiguate.🧯 Error handling & edge cases — Minor issues
VERDICT: Minor issues
cmd/gadfly/main.go:233— panicking lens goroutine leaves the lens stuck at "running" forever. InsiderunSpecialists, the goroutine setslensRunningbefore callingreviewWithSpecialistandlensFinishedafter; there is nodefer recover(). A panic in any goroutine terminates the entire Go process, so the binary exits non‑zero,run.sh's|| log "model $1 failed (continuing)"swallows the failure, and the status file is left with that lensrunningandDone=false. The board's final render (afterentrypoint.shtouches.done) shows the model as still in progress even though the review crashed. Adefer recover()that callssw.set(sp.Name, lensFinished, "panicked", true)would close the loop. No explicitpanic(...)exists in the reviewed code, so this is rare in practice — butagent.Run/majordomo could panic, and the gap is real.entrypoint.sh:162—status_file_forsanitization is lossy and can collide.tr -c '[:alnum:]._-' '_'collapses both/and_(and:, etc.) into_, so distinct model ids likem1/qwen3:14bandm1_qwen3_14bmap to the same${STATUS_DIR}/m1_qwen3_14b.json. The pre-seed loop overwrites the first entry's metadata with the second's, and whichever binary flushes last wins the shared path. The board then silently drops one model. The comment inentrypoint.sh:158-160claims "just needs to be unique per model" while the transformation isn't injective. Realistic collisions are rare with the default model list, but not impossible (the lane logic supports user-suppliedMODELS). Fix: hash the model id (printf '%s' "$1" | sha1sum | cut -c1-16) or use a delimiter-preserving substitution.scripts/status-board.sh:65—render_sectioninterpolates.nameinto Markdown without escaping. Lens names are interpolated verbatim inside**…**(and.model/.providerdirectly into the header). Built-in lens names are hard-coded and safe, butSpecialist.Nameis also sourced from.gadfly.ymlin the repo under review (cmd/gadfly/specialists.go:104,109) — i.e. PR-controlled. A PR that adds a.gadfly.ymldefining a specialist whose name contains**, backticks, or](http://…)would break the bold span or smuggle a link into the board comment. Same applies to.model/.provider(e.g.m1/qwen3:14b, fine in practice, but the principle holds). jq could pipe these throughgsub("[\\\\]|\*|\]\("; " ")` (or similar) before interpolation.scripts/status-board.sh:92—find_existinggives up after page 10 (500 comments). Past 500 PR comments,find_existingreturns empty, the upsert falls into the POST branch, and a second board comment is created. Within a single invocationBOARD_IDis cached after POST, so further ticks PATCH the new comment — duplicates do not multiply per tick — but on the next container restart (or any path that clearsBOARD_ID) the script will POST yet another comment, fragmenting the board across runs. Marker-based dedup is broken past that boundary. Probably fine for real PRs, but acontinuepast 500 is at odds with "one consolidated comment." Consider raising the cap, paging with a creation-time window, or accepting an explicitBOARD_COMMENT_IDenv var.Automated adversarial review by Gadfly. Advisory only — does not block merge. · ⏱️ reviewed in 8m 14s
The dogfood workflow had a truncated 5-model list (3 cloud + the 2 Macs) and was missing GADFLY_PROVIDER_LENS_CONCURRENCY. Restore mort's full fleet so gadfly reviews its own PRs with the same 11 reviewers and the model-quality scoreboard is comparable across both repos: 9 cloud: minimax-m3, glm-5.2, glm-5.1, kimi-k2.7-code, deepseek-v4-pro, nemotron-3-super, gpt-oss:120b, qwen3-coder:480b, gemma4 2 local: m1/qwen3:14b, m5/qwen3.6:35b-mlx GADFLY_MODELS / *_CONCURRENCY / *_LENS_CONCURRENCY now match mort's adversarial-review.yml verbatim. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>