Skip to content

Fix/benchmark broker and viz#275

Merged
robdmac merged 35 commits into
mainfrom
fix/benchmark-broker-and-viz
Jul 21, 2026
Merged

Fix/benchmark broker and viz#275
robdmac merged 35 commits into
mainfrom
fix/benchmark-broker-and-viz

Conversation

@robdmac

@robdmac robdmac commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

No description provided.

robdmac and others added 30 commits July 15, 2026 09:35
…es + no key nag

Smoke-testing slopcodebench end-to-end surfaced three issues; all fixed in
scb-visualize `run` (applied in the runner PTY so they can't be skipped and use
THIS session's broker) + the template guide:

1. OpenRouter never reached the model. providers.yaml pointed opencode straight at
   openrouter.ai with the broker PLACEHOLDER key -> auth fail (total_cost=0,
   "OpenCode error"). Now prepare_configs() rewrites the openrouter openai api_base
   to $OPENROUTER_BASE_URL (the session's 127.0.0.1 broker, which injects the real
   key server-side). Idempotent across runs in the shared /workspace via regex.

2. The live agent viewer never surfaced. The tmux executor wrote runs.jsonl + the
   per-run logfiles into the run's temp working dir (cleaned up), not the checkout
   .scb_tmux that scb-visualize watches. prepare_configs() pins tmux_log_dir to
   <checkout>/.scb_tmux so they persist and are found.

3. Orchestrator nagged for OPENROUTER_API_KEY though it was already set (no tool to
   list secrets). Guide now says: don't ask up front, proceed, and only ask if the
   run errors with an auth/credit failure. Step 3 (manual providers.yaml patch) is
   gone — it's automatic now.

Validated live (localhost + Playwright + Docker sandbox): providers.yaml rewritten
to the broker URL, runs.jsonl/logfile persist in the checkout, the agent viewer
appears on the canvas, and the run reaches the model (opencode/kimi-k2.6 making
glob/read tool calls, net=$0.01288 real spend vs $0.00 before).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
A multi-problem run surfaces one read-only PTY per problem, which clutters the
canvas. Add --no-viewers (and SCB_VIEWERS=0) to skip the per-run terminals and
rely on the scb-live browser dashboard as the low-noise overview. Default stays
on (current behavior); the run + dashboard are unaffected by the flag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
…run + scb-live)

The template bypassed the fork's purpose-built tooling by calling slop-code directly,
so the results browser was blank and the broker had to be re-patched by scb-visualize.
Rewire step 4 to the intended pipeline and fix the browser:

- Run via bin/scb-run (restores a pristine providers.yaml.orig, rewrites the openrouter
  api_base to $OPENROUTER_BASE_URL, launches the harness detached to .scb-run.log).
- Start bin/scb-live and point the results browser block at :8051 (was :8050, the Dash
  port that needs evaluated runs) — it renders a live per-problem tally (checkpoint /
  steps / cost / state + cumulative cost).
- bin/scb-visualize watch --skip-existing surfaces the optional per-problem viewers.
- Setup now saves configs/providers.yaml.orig so scb-run can re-patch a clean copy each
  run (the per-session broker URL differs per run).
- scb-visualize: add --skip-existing to watch mode (standalone use in the pipeline).

Validated live (localhost + Playwright + Docker sandbox): scb-run patched the broker,
scb-live served :8051 (HTTP 200, live table), the browser component rendered the
dashboard, the per-run viewer surfaced, and the run reached the model (cost=0.01015).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
…, workers)

Phase 1 of "let the user configure the benchmark". A run is a matrix of
(harness x model) arms, each running all `problems`, `workers` arms in parallel.

- scb-matrix (new): reads /workspace/.scb-config.yaml (harnesses, models, problems,
  workers, prompt, thinking, evaluate; created with defaults if missing), or CLI
  overrides for a one-off (--harness/--model/--problem repeatable, --workers, ...).
  Expands to arms, patches the broker, runs W-parallel, tee'ing each arm to
  .scb-run.<arm>.<ts>.log with tmux mirroring (per-arm viewer). Dependency-free.
- scb-live: matrix-aware — aggregates all logs in the latest batch into a per-ARM
  tally (arm | problem | checkpoint | steps | cost | state) + cumulative cost.
- template: setup seeds the config; step 4 runs scb-matrix (+ scb-live + scb-visualize
  watch); guide teaches chat to edit the config or pass overrides. Both the chat and
  (later) a config panel edit the same .scb-config.yaml source of truth.

Validated live (localhost + Playwright + Docker): "compare kimi-k2.6 and glm-4.6 on
file_backup, 2 workers" -> chat built `scb-matrix --model … --model … --workers 2`;
2 arms launched via the broker, 2 per-arm logs, 2 viewers, and the results browser
showed both arms with per-arm cost.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
Control panel — a new "benchmark" canvas block (BenchmarkBlock) with dropdowns/
multi-selects for harnesses, models (OpenRouter catalog autocomplete), skills,
problems, workers, thinking, prompt, evaluate, and a Run button. It reads/writes the
SAME /workspace/.scb-config.yaml the chat + scb-matrix use (single source of truth):
syncs from the file on mount, writes it on Run, then creates the runner terminal with
the scb-matrix pipeline command. Wiring: DashboardItemType + BenchmarkContent, Canvas
nodeTypes + sessionId injection + onCreateTerminalBlock callback, page blockTools/
defaultSizes/handleCreateTerminalBlock, files.ts read/writeSessionFile (PUT), and the
controlplane VALID_ITEM_TYPES allowlist. Template ships a config panel item.

Skills — a run is now a matrix of (harness x model x skill). A "skill" is a public
agent-skill pack (baseline/gsd/omc/superpowers/karpathy) that swaps slop-code's
--environment + --prompt (per the fork's REPRODUCE_PUBLIC_SKILLS.md). scb-matrix maps
each skill to its (env, prompt), stages the skill plugin (clones the public repo,
sets SCB_SKILL_<NAME>_DIR), and names per-arm logs with the skill. baseline + gsd run
in the sandbox VM (local envs); omc/superpowers/karpathy have Docker-only envs (panel
marks them ⚙ — need Docker-in-sandbox). scb-live's arm label includes the skill.

Verified live: the panel renders on the template dashboard (harness pills, models/
problems chips, workers/thinking/prompt, and the Skills selector with local vs ⚙
Docker skills); config parser/expansion unit-tested (1h x 2m x 2s = 4 arms with the
right env/prompt per skill); frontend + controlplane typecheck clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
…trix

- addyosmani: bring in the new public-skill from the fork's reproduce-public-skills
  branch (Docker env docker-python3.12-uv-with-addyosmani.yaml + trigger prompt).
  Added to scb-matrix SKILL_ENV/SKILL_PROMPT and the panel Skills list (⚙ Docker).
- Broker patch robustness: scb-matrix restored providers.yaml.orig then only replaced
  the openrouter.ai URL — so a STALE broker URL baked into a non-pristine .orig (from
  a prior session) survived, and opencode hit a dead broker path (401 -> cost=0.0).
  Now also regex-rewrite any 127.0.0.1:PORT/broker/<sid>/openrouter to THIS session's
  broker (mirrors scb-visualize). Verified: after the fix the baseline arm bills
  (cost=0.175) via the current session's broker.

Smoke test (panel-driven, localhost + Playwright + Docker): selecting baseline+gsd in
the Benchmark panel and hitting Run wrote .scb-config.yaml, launched 2 arms with the
correct per-skill env+prompt (baseline: local-tmux-py/just-solve; gsd: local-tmux-py-
with-gsd/gsd-local-trigger), cloned the GSD plugin, and scb-live showed both arms.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
…ern)

Previously only baseline + gsd had local (in-VM) slop-code envs; omc, superpowers,
karpathy, addyosmani were Docker-only (bind-mount the skill from a host path), so
they couldn't run in the Orcabot sandbox. Generalize GSD's local pattern to all of
them:

- scb-matrix SKILL_ENV/SKILL_PROMPT now point every skill at a local env
  (local-tmux-py-with-<skill>.yaml) + a `-local-trigger` prompt (relative
  .claude/skills/<dir> path instead of the Docker /tmp/agent_home mount).
- SKILL_PLUGIN gains all 5 skills as (repo, subpath, envvar). stage_skill clones
  the public plugin once and points SCB_SKILL_<NAME>_DIR at repo/<subpath> — so a
  plugin that lives in a repo subdir (karpathy: skills/karpathy-guidelines) stages
  exactly that dir. The env yaml's setup `cp -R`s it into the run's .claude/skills/.
- Panel: drop the ⚙ Docker marker + LOCAL_SKILLS gating — all skills run in the VM.
- template.json guide + comments updated to match.

The env yamls + `-local-trigger` prompts live in the fork (cloned at setup).

Smoke test — all 5 skills validated two ways:
1. Deterministic (no spend): clone → subpath resolve → cp into .claude/skills/<dir>
   → each local trigger's referenced entry file resolves. Uses the real SKILL_*
   maps from this file. All 5 PASS (incl. karpathy's nested subpath).
2. Live billing (panel → broker PTY → opencode, ~$0.16 total): every skill clones,
   stages, and its local trigger drives opencode to the model —
   baseline $0.059, superpowers $0.032, karpathy $0.048 (subpath), omc $0.009,
   addyosmani $0.019 (fresh clone of addyosmani/agent-skills).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
Adds a "subscription" auth path for Codex arms so they run on a ChatGPT/Codex
plan instead of a brokered API key. Orcabot's broker is API-key-only, but slop-code's
codex agent already supports a FILE credential (auth.json); this wires that up for
the in-VM local runtime.

scb-matrix: new `codex_auth: broker|subscription` config (+ `--codex-auth` /
`--subscription` flags). For a codex-subscription arm it (1) stages the located
auth.json into $AGENT_HOME/.codex/auth.json (searches $SCB_CODEX_AUTH_FILE,
$WS/.scb-codex-auth.json, ~/.codex/auth.json), (2) flips the openai provider in
providers.yaml from env_var to a file credential pointing at it, and (3) strips the
brokered OPENAI_*/OPENROUTER_* env so the codex CLI uses the subscription, not a
placeholder key. Model is passed as openai/gpt-5.5-codex (slop-code {provider}/{model}).

Panel: a "Codex: use my subscription" checkbox (shown only when codex is selected,
flagged ⚠ that the credential lives in the VM). BenchmarkContent gains codexAuth.

Validated in-VM with a dummy auth.json: staging + provider flip + env-strip + model
parsing all work; codex launches with CODEX_HOME=/tmp/agent_home/.codex, reads the
staged token, and reaches the subscription-auth handshake (rejected the dummy only on
`missing field id_token` — a real `codex login` auth.json has it). Live billing on a
real subscription credential is gated on user consent (credential exposure in the VM).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
Adds the SlopCodeBench Runner to starter-templates.json so it's seeded into every
fresh desktop DB like the other starters, instead of being DB-only (which meant a
rebuilt/reset local DB lost it). Also wires setup_guide into seedStarterTemplates'
INSERT — the SlopCodeBench template relies on its setup_guide (the chat-orchestrator
walkthrough), which the starter seeder previously dropped. Bumps the seed marker
v2 -> v3 so existing desktop DBs re-seed and pick it up on next /init-db (existing
starters are ON CONFLICT(id) DO NOTHING, so it's purely additive).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
Two setup papercuts:

1. No preselected problem. `problems` defaulted to [file_backup] in scb-matrix,
   the panel, and the template, so hitting Run could silently spend money on a
   benchmark the user never chose. Now it defaults to empty everywhere and
   scb-matrix exits(2) with a clear message listing the known problems instead of
   guessing. The panel's Run stays disabled until one is picked, and the setup
   guide tells chat to ASK which problem to run.

2. uv re-downloaded on every setup. The boot command ran `command -v uv ||
   install`, only adding $HOME/.local/bin to PATH *after* installing — so the
   check always missed and re-downloaded, even though the binary persists at
   /workspace/.home/.local/bin/uv. Export PATH before the check.
   (Note: uv IS baked into the current Dockerfile at /usr/local/bin/uv as of
   972e443, but images built before that — like the one currently shipping —
   don't have it, so the fallback still matters.)

starter-templates.json's copy is regenerated from template.json so the shipped
starter and the benchmarks/ source stay identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
1. Empty `problems` now means RUN THE FULL SET, not an error (corrects the previous
   commit, which wrongly made it exit(2)). slop-code already auto-discovers every
   valid problem when no --problem is passed (_resolve_problem_names -> discovery),
   so scb-matrix simply omits the flags and logs "ALL problem(s)". The panel no
   longer blocks Run on an empty problem list and shows "runs the full benchmark
   set, one after another"; the arm summary reads "all problems". No single problem
   is preselected anywhere.

2. list_secret_names chat tool — fixes chat nagging for a key that is already set
   (it had no way to check, so it guessed). Guardrails:
   - NAMES ONLY. Never selects `value`, not even a prefix — a "helpful" preview is
     a real leak. Returns strictly set/not-set metadata (+type/broker_protected/
     scope), so the broker guarantee (LLM never sees values) is preserved.
   - owner/editor gate, matching run_command; viewers cannot enumerate.
   - Scoped to the dashboard + '_global'.
   This grants no new capability: brokered secret NAMES are already visible in the
   PTY env as `NAME=[BROKERED]…` placeholders, and chat can already run `env` via
   run_command — this just makes it reliable instead of a hacky shell call.
   System prompt + the benchmark setup guide now require checking before asking.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
The slop-code-bench fork work was done in an ephemeral scratch clone and exported
here as a patch series + bundle so it wouldn't be lost (the scratch dir was indeed
cleared). Those commits now live where they belong — on feat/host-tmux-executor in
the fork checkout at ~/work/hyper/slop-code-bench — so duplicating fork code inside
this repo is no longer needed.
The setup guard was `[ -e .../bin/scb-visualize ] || git clone …`. On a VM whose
/workspace/slop-code-bench predated that script, the guard fired, git refused to
clone into a non-empty directory ("fatal: destination path already exists"), and
setup fell through to print SETUP_OK anyway — leaving a stale checkout with no
bin/scb-matrix, permanently. Observed on a real VM (a Jul-5 copy, not even a git
repo, missing scb-matrix entirely), so the runner could never start.

Now: if it's a git repo -> fetch + reset --hard to the branch tip; otherwise move
any existing directory aside to <dir>.stale.<ts> (nothing is deleted; prior
outputs/ are preserved) and clone fresh. Idempotent — verified against a replica
of the broken VM state: first run repairs + preserves, second takes the
fetch/reset path with no duplicate stale dirs.

Also: clone/fetch are now --depth 1 (a full-history clone timed out at 2min in
testing; setup needs no history), and providers.yaml.orig is refreshed with cp -f
now that clone/reset guarantees a pristine providers.yaml — removing the
broker-URL staleness bug at its source rather than only patching around it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
…iew race

Setup no longer needs a PTY or the user. Chat now runs it with run_command
(timeout_s=300), which blocks and returns an exit code, so it continues to the next
step automatically instead of the old "watch the setup terminal for SETUP_OK, then
say go" dance. The script is unchanged and idempotent; the guide tells chat how to
read the result (SETUP_OK -> proceed same turn; timed_out -> re-call, uv resumes;
egress hold -> ask user to Allow). create_terminal is now reserved for genuinely
long-running things (the benchmark run), matching run_command's own guidance.

Also removes the runbook note block (2 items now: benchmark panel + results
browser, laid out side by side), and adds list_secret_names / run_command to the
guide's tool list.

Live-view race fix: the results browser showed about:blank because the block's
Chromium launches once (observed: chromium 11:30:54, scb-live 11:35:56) and never
retries, so its single navigation hit a dead port. scb-live now starts during SETUP
so :8051 serves from the beginning, both start sites are guarded with pgrep (a blind
second start just fails with EADDRINUSE), and the guide tells chat to point the user
at the results browser + mention refresh if it's blank. Confirmed by the user: the
URL was always fine — manually loading localhost:8051 worked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
…run note

The pgrep guard I added never worked: `pgrep -f bin/scb-live` also matches the boot
command's OWN shell, whose command line contains that string. So it always reported
"already running", short-circuited, and scb-live was never started — port 8051 refused
and the results browser showed ERR_CONNECTION_REFUSED. Verified live in the VM: the
old guard claims running while no python3 bin/scb-live exists; probing the PORT
instead starts it and 8051 returns 200. Fixed in all three call sites (setup script,
run boot command, panel buildBootCommand).

scb-visualize no longer creates a note block per run — the terminal title
("agent: <problem>") already names it, and the note rendered empty. Grid reflows to
one terminal per slot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
The template shipped a results browser AND chat created a second one (create_browser)
during setup — hence two blocks, neither connecting. A browser block navigates exactly
once and never retries, so any block opened before scb-live has a run to show is stuck
on ERR_CONNECTION_REFUSED permanently; the user confirmed both only worked after
manually reloading post-launch.

The template now ships no browser at all (just the benchmark panel), and the guide
tells chat to create exactly ONE, at 127.0.0.1:8051, immediately AFTER launching a run
— reusing an existing one rather than adding a second.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
… zooms canvas

The chat-created results browser landed with EMPTY content: create_browser is an
MCP-UI tool taking {dashboard_id, url, position, size}, and the guide called it with a
bogus `name` — an omitted/empty url silently yields a blank browser block (the
frontend stores `content: cmd.url` verbatim). Relying on the LLM to get that right is
also fragile, and it left panel-launched runs with no results view at all.

So the Benchmark panel's Run now opens it deterministically via the already-wired
onCreateBrowserBlock, at 127.0.0.1:8051, only once a run exists — a browser block
navigates exactly once and never retries, so opening it earlier parks it on
ERR_CONNECTION_REFUSED. handleCreateBrowserBlock now reuses an existing browser on the
same URL instead of stacking duplicates (the two-browser report). The guide documents
the real create_browser signature and notes the panel already handles it.

Also: Cmd/Ctrl +/- now zooms the canvas instead of the webview, Cmd+0 resets to 100%,
ignored while typing in an input/textarea.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
Blocks were landing on top of each other. Three independent causes:

1. Stale state. computePlacement closes over `items` from React state, so blocks
   created back-to-back (scb-visualize surfacing several agent viewers, or chat
   creating a terminal then a browser) were all placed against the SAME pre-update
   list and collided. Placements are now reserved for 5s until the real item appears.

2. scb-visualize used its own fixed grid from a hardcoded origin, blind to existing
   canvas contents — so viewers overlapped the benchmark panel / runner terminal.
   It no longer sends a position; auto-placement handles it.

3. The search took the FIRST fit on a coarse newSize/2 grid, so it missed real gaps
   and fell through to "right of everything" more often than it should.

findAvailableSpace is extracted to src/lib/canvas/placement.ts (it's pure geometry —
it belongs in a testable module, not buried in a 4.4k-line page) and rewritten to
pack row-major on a fine grid, with clearance only as a tie-break. Two earlier
attempts were measurably worse and are documented in comments: scoring by clearance
parked blocks in the middle of open space and stranded gaps; minimising bounding-box
AREA growth always favoured extending the longer axis, producing a runaway
single-row strip. The fallback now minimises how far you must zoom out
(max(w/viewW, h/viewH)) instead. On the real dashboard sequence that turns a
2784x532 strip into a compact 1308x1396 layout.

8 vitest cases cover: no overlap, gap respected, roomier-side choice, N-in-a-row,
minimal-growth fallback, empty canvas, sidebar inset, and a regression replaying the
exact panel/runner/viewers/browser sequence that overlapped in D1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
An empty "Setup Terminal" kept appearing. Cause: sessions.item_id is NOT NULL
REFERENCES dashboard_items — a sandbox session cannot exist without a canvas item.
Having stripped every terminal from the template, a fresh dashboard had no sandbox,
so run_command failed with "No sandbox is running for this dashboard yet. Start a
terminal first." — and chat did exactly that, creating a useless empty PTY.

Rather than add a schema change to security-sensitive session code, setup is now
folded into the runner terminal's boot command (it is idempotent: fetch/reset, reuse
the venv). The ONE terminal you already expect does its own setup, so there is no
second component, and first-run dependency installation is visible instead of silent.
The guide tells chat not to run setup separately and that "no sandbox yet" on a new
dashboard is expected — launch the run, don't paper over it with an empty terminal.

Results browser now opens at 1200x750 (150% of the stock 800x500) — the live tally is
a wide table and was cramped. onCreateBrowserBlock takes an optional size override.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
All six findings verified against the code before fixing; all were real.

P1 results browser raced scb-live. Folding setup into the run made this worse: the
prewarmed Chromium navigated while the clone/venv was still going, and browser
navigation is one-shot with URL dedup preventing a retry. scb-live now starts
immediately after the checkout, BEFORE the slow venv sync, shrinking the window from
minutes to roughly clone time.

P1 per-run viewers were missing for every non-baseline skill. prepare_configs pinned
tmux_log_dir only in local-tmux-py.yaml, so the five skill variants kept the default
(a temp run dir); their runs.jsonl never appeared where watch() looks, so gsd/omc/
superpowers/karpathy/addyosmani surfaced no viewer at all. Now globs
local-tmux-py*.yaml.

P2 first-run config was not persisted: the panel wrote .scb-config.yaml only when a
session already existed, which is never true on a new dashboard, so the launched
config lived only in CLI flags while the documented "source of truth" file kept
defaults. The boot command now writes the config with printf (no session needed) and
scb-matrix reads the file — flags dropped entirely.

P2 export erased benchmark config: scrubber.ts had no 'benchmark' case so it hit
`default: return ''`. Root cause was that 'benchmark' was missing from
DashboardItemType; added, plus a case preserving the (non-sensitive) run config.

P2 toolbar-created benchmark still defaulted to problems:["file_backup"],
contradicting the empty/full-suite default.

P2 the v3 seed marker would have re-inserted the whole starter catalog, resurrecting
starters a user deliberately deleted. Split into two markers: v2 still guards the
original catalog, and newly-added starters seed under their own marker.

Boot command verified with bash -n and by printing the generated shell.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
… skill fix

P1 results-browser race: the earlier reorder only shrank the window (scb-live still
starts after fetch/clone) while the browser was created immediately, so a cold first
run could still land on ERR_CONNECTION_REFUSED with no retry. Replaced hope with a
handshake: the run clears /workspace/.scb-live.ready at the start and writes it from
a background loop once :8051 actually answers; the panel polls for that marker via
the session file API and only then opens the browser (giving up after ~5min with a
message pointing at the runner terminal).

P1 skill-viewer fix was unreachable: prepare_configs() only runs in `scb-visualize
run` mode, but the benchmark launches `watch`. Baseline worked only because
local-tmux-py.yaml carries tmux_log_dir statically. Pinned it statically in all five
local-tmux-py-with-*.yaml too, so the fix no longer depends on a code path the
benchmark never takes. (The glob patch stays for `run` mode.)

P2 removed committed Python bytecode (bin/__pycache__/*.pyc) and added
__pycache__/ + *.pyc to .gitignore — they were re-added by an earlier `git add -A`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
…owser

The readiness handshake was only wired into the Benchmark panel. The setup guide
still told chat to create the results browser "immediately AFTER" launching a run,
so chat-launched runs kept the original race: a browser block navigates once and
never retries, so one created while the harness is still cloning/building stays on
ERR_CONNECTION_REFUSED forever.

The guide now has chat read_file(".scb-live.ready") first and only create the browser
once that succeeds; if it errors, say the run is still preparing and re-check on the
next message. Also states explicitly that chat cannot poll in the background, so it
must not claim to be monitoring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
…scription)

The auth choice was effectively invisible. Codex-subscription mode existed only as a
checkbox rendered when `codex` was already in Harnesses — and the default harness is
opencode, so it was never shown. OpenRouter was never an option at all; it was implied
by a model name starting with "openrouter/".

Adds a Provider row at the top of the panel with two explicit choices:
- OpenRouter (brokered key) — routed through the session secrets broker
- Codex subscription — ~/.codex/auth.json, no API key

Because the choice constrains both harness and model, selecting it fixes them up:
subscription pins the harness to codex and swaps an openrouter/* model for
openai/gpt-5.5-codex; switching back restores the OpenRouter default. Model
suggestions and the placeholder follow the selected provider, and subscription mode
shows where auth.json must be staged plus the warning that the credential is readable
by the agent-under-test inside the VM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
…reflight + Stop

Root cause of the failed subscription run: the model, not the auth. The VM had a
valid ChatGPT login all along (auth_mode=chatgpt, id_token+access_token present, and
scb-matrix staged it correctly) — but I had picked openai/gpt-5.5-codex, and the
"*-codex" variants are API-key only. codex rejected every instance with "The
'gpt-5.5-codex' model is not supported when using Codex with a ChatGPT account".
The subscription-capable model is plain gpt-5.5, which is what the SWE-bench Pro
reproduce-public-skills harness uses (MODEL="gpt-5.5"). Added configs/models/gpt-5.5.yaml
and pointed the panel at openai/gpt-5.5.

Because problems defaults to the FULL suite, that bad config didn't fail once — it
failed identically for every problem while scb-matrix kept going, spawning a dead
viewer terminal per run. Added a preflight that refuses to start a subscription run
with no auth.json or a '*-codex' model, so this class of misconfiguration can't
stampede.

Stop button: writes /workspace/.scb-stop, which scb-matrix checks before each arm.
Honest limitation — it stops launching further work, it can't interrupt an agent
mid-problem.

Guide: pick the provider BEFORE any credential check. Chat was asking about
OPENROUTER_API_KEY even in subscription mode, where no API key is used at all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
Correcting an overreach in the previous commit. From a single error message I claimed
all "*-codex" model variants are API-key only and hard-blocked them. That's not
supported by the evidence: Codex CLI on a ChatGPT subscription demonstrably works, and
which model ids a given plan exposes is account-dependent. The defensible fact is
narrower: "gpt-5.5-codex" was an id I invented (mirroring gpt-5.3-codex from the fork's
configs) and this account rejected it; gpt-5.5 is what the SWE-bench Pro harness drives.

So: no model-id blocklist. Preflight now only checks the unambiguous case (subscription
with no auth.json). The stampede is handled where it belongs — a watchdog that tails an
arm's log and aborts when the failure will repeat for every problem (unsupported model,
401, bad auth, exhausted quota), setting the stop flag. Verified: injecting the real
error message into a live arm's log terminates the process and writes the flag. This
also covers failure modes a model whitelist never would.

Panel/guide reworded to present gpt-5.5 as a known-good default, not a rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
179 chars against 30-41 for the other starters, so it dominated the New Dashboard
grid. It was also stale: it promised "One-time VM setup required (fork + venv)",
which stopped being true once setup was folded into the run.

Now "Compare coding agents on slop-code-bench" (39), matching the house style.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
…ding it as failure

Verified a live subscription run works: codex authenticated, reached the model and
did real work on cfgpipe (py_compile, venv creation, schema fixtures, 12+ tool calls),
with no auth or model errors. But the dashboard showed total \$0.0000 / steps 0/0 —
which is exactly the signature of the earlier never-reached-the-model failures.

Cause: codex on a ChatGPT/Codex plan emits no token_count/total_cost events at all, so
cost is legitimately \$0 forever. Only broker/API-key runs bill per token. Our
"success = cost>0" heuristic was therefore wrong for subscription runs and would have
had chat reporting a healthy run as broken.

scb-live now renders "plan" instead of \$0.0000 when codex_auth=subscription, and the
guide judges success by checkpoint progression + real tool calls in the agent viewer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
…ee staging

Four review findings, all reproduced before fixing.

P1 a failed matrix exited 0. run_arm discarded rc and swallowed launch exceptions, so
main always printed "matrix complete" — a matrix where nothing ran was indistinguishable
from success. Arms now return a status, main lists per-arm failures and exits 1, and the
boot command surfaces it as "[matrix done rc=$rc]". Verified: a doomed launch now exits 1.

P1 one bad arm cancelled the whole matrix. The fatal watchdog wrote the GLOBAL stop flag,
terminating every other watcher and skipping queued arms — so a model-specific failure
killed the comparison arms it existed to be compared against. Fatal state is now per-arm;
only the user's Stop button writes the global flag. Verified: a fatal arm terminates
itself and leaves the flag unset.

P2 Run re-enabled immediately, allowing a competing runner against the same checkout,
config, stop flag and timestamped logs. The run now writes /workspace/.scb-running
(trapped so it clears on kill) and the panel polls it, showing "Running…" and keeping Run
disabled for the actual duration rather than for the second it took to create a terminal.

P2 parallel skill arms raced on clone. Workers each tested-and-cloned the same dest with
rc ignored and success logged unconditionally, so a worker could see a partial checkout
or silently run with no skill. Skills are staged once up front, cloned via temp dir +
atomic rename, rc and non-empty plugin dir validated; an arm whose skill failed to stage
is failed rather than quietly running without it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
P1 the guard was neither early nor atomic. It was created AFTER clone/venv/setup —
minutes on a first run, during which Run stayed enabled — and used `: >`, which never
rejects an existing file. Two clicks (or a click plus a chat-launched run) both entered
the same checkout. The lock is now the FIRST thing the boot command does, taken with
`set -o noclobber` so creation is atomic, and a losing run aborts instead of proceeding.
Verified: concurrent starts -> second refused with "a benchmark run is already active".

P2 a crash could disable Run permanently. Traps don't run after SIGKILL/VM crash/power
loss, so a stale lock would have disabled the button forever with no recovery control.
The lock now carries pid + a 20s heartbeat: the boot command reclaims it if the recorded
pid is gone, and the panel treats a lock with no heartbeat in 90s as stale. Verified:
stale lock (dead pid) is reclaimed.

P2 partial skill checkouts were still accepted. Any non-empty plugin dir counted as
staged, so an interrupted clone containing only .git passed — including legacy dirs left
by the earlier race — and the arm ran without its skill. A completion marker is written
inside the temp dir only after the clone is validated and published by the atomic
rename; anything lacking it is re-cloned. Verified with a .git-only checkout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
robdmac and others added 5 commits July 21, 2026 21:21
…reclaim

P1 a successful run never released the lock. Cleanup relied on an EXIT trap, but the
command ends in `exec bash` — exec replaces the shell so the trap never fires, and the
heartbeat subshell keeps running under the same PID, refreshing the lock forever. Every
completed benchmark therefore left Run disabled until the terminal was killed. The
heartbeat is now killed and the lock removed explicitly before exec. Verified: lock is
gone after a normal completion and a later run acquires cleanly.

P1 `kill -0` cannot judge liveness across the PTY UID pool. Each PTY runs under its own
UID, so probing a live runner owned by another PTY returns EPERM — which the previous
check read as "pid is gone", deleted a LIVE lock, and re-opened the concurrent-run
corruption it was added to prevent. Reclamation is now based purely on heartbeat
staleness (>90s), which needs no cross-UID signal. Verified against a lock owned by an
unprobeable pid with a fresh heartbeat: refused rather than stolen; a stale lock is
still reclaimed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
…mis-config)

Live in the GUI, chat honoured "use codex subscription, baseline" by writing a config
with harness=opencode + model=openai/gpt-5.5-codex + codex_auth=subscription, then
reported "Launched for Codex". opencode can't use the codex CLI login (~/.codex/auth.json),
so every problem failed rc=1. The new honest exit code surfaced it correctly
([matrix done rc=1], "matrix finished with FAILURES: 0 ok, 1 failed") — that part worked.

Two fixes so the mis-config can't happen / is caught:
- scb-matrix: the subscription preflight now fires whenever codex_auth=subscription
  (not only when 'codex' is already a harness) and refuses any non-codex harness with a
  message pointing at harness=codex + openai/gpt-5.5.
- Guide: codex-subscription mode must set harnesses:[codex], models:[openai/gpt-5.5].
  The panel's Provider=Codex subscription already pins these; the guide now makes chat
  do the same instead of leaving harness at the opencode default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
…ct state

The results browser never appeared even though .scb-live.ready was written at 83s
(well inside the panel's 300s poll window) and was readable via the file API (200).
Root cause: the panel opened the browser from component state — set awaitingLive on
Run, poll .scb-live.ready, then create_browser. But launching the run adds the runner
terminal to the canvas, which re-renders/re-mounts the benchmark block and resets
awaitingLive to false, so the poll was dead before the marker appeared. The per-run
agent VIEWERS, created server-side by scb-visualize via MCP, appeared fine — same GUI
path, no fragile client state. So the fix is to open the results browser the same way:

- scb-visualize waits for scb-live's port to answer, then create_browser once
  (marker-guarded so re-launches don't stack). Robust to any panel re-render.
- Panel no longer polls/opens the browser (removed awaitingLive + effect). Kept the
  separate runActive poll that guards the Run/Stop button.
- Guide: browser auto-opens; chat must NOT create_browser (would duplicate).

This is the "eliminated the race too hard" fix: we'd swung from create-immediately
(connection-refused race) to a client-state poll that gets wiped. Server-side MCP
creation after readiness is both race-free and re-render-proof.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
Ships the SlopCodeBench Runner as a baked-in starter template (seeded on init via
seed_desktop_slopcodebench_v1) plus the full benchmark-runner surface: config panel
with provider selector + Stop, flock run-lock, server-side results-browser open,
codex-subscription auth, and the honest-exit-code / arm-isolation / race-free-staging
fixes from this branch. Version bumped in tauri.conf.json, Cargo.toml, Cargo.lock;
release cut separately via desktop/scripts/release.sh (signed + notarized).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
… run-lock

The SlopCodeBench run-lock uses flock(1) for atomic, crash-safe, UID-pool-safe
mutual exclusion. flock ships with util-linux; make it an explicit dependency rather
than relying on it being pulled in transitively, so a future base-image change can't
silently drop it and break the lock.
@robdmac
robdmac merged commit 9bb8ca5 into main Jul 21, 2026
1 check passed
@robdmac
robdmac deleted the fix/benchmark-broker-and-viz branch July 21, 2026 22:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant