Skip to content

feat(framework): add cli suites, needsDocker, and CLI environment arms - #308

Closed
Coly010 wants to merge 3 commits into
mainfrom
columferry/cli-2398-cli-suite-and-arms
Closed

Coly010 wants to merge 3 commits into
mainfrom
columferry/cli-2398-cli-suite-and-arms

Conversation

@Coly010

@Coly010 Coly010 commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Current Behavior

There is no home for CLI-team evals: the only suites are benchmark, regression, docs, other; an experiment can only opt out of evals by id; every local-stack experiment installs the pinned CLI into a sandbox where Docker always works. "One task, run across the environments the CLI actually meets" — Docker available / daemon unreachable / absent, stable vs beta CLI — cannot be expressed without touching other teams' experiments.

Expected Behavior

Infrastructure only — no evals in this PR (the first one, build-database-002-stack-lifecycle, is stacked on it in #281). Mirrors how the docs suite was added in #282.

Framework

  • A cli eval suite (evals/cli/, owned by @supabase/cli) and a cli experiment suite. Nightly runs cli × cli next to regression × regression; results export to apps/web/src/data/cli-eval-results.json with a cli-results.jsonl history on GitHub Pages. Pairing is by eval folder, so CLI experiment variants never pair with other teams' evals, and other teams' experiments never see CLI evals.
  • A needsDocker frontmatter field (default true). An eval that can run — and is meaningful — without a Docker daemon sets needsDocker: false; Docker-less experiments select on it with skipEval: (ev) => ev.metadata.needsDocker !== false instead of maintaining id lists.
  • README/CONTRIBUTING: suite list, the field, and a "CLI evals" section (ownership, experiments, add/refresh/history, unit-test command).

Experiment-land runtime — experiments/_lib/ (composes @supabase-evals/sandbox exports; no framework internals touched)

  • cli-channel.ts: resolves stable/beta to a concrete Supabase CLI version from npm dist-tags (latest/beta), lazily inside startSession (never on --dry/list), memoised, overridable via SUPABASE_CLI_STABLE_VERSION/SUPABASE_CLI_BETA_VERSION, and failing loudly rather than falling back to the pin.
  • docker-aware-local-stack.ts: dockerAwareLocalStackRuntime({ channel, docker }). docker: 'available' delegates to the stock runtime with the resolved CLI. 'no-daemon'/'absent' build the sandbox from the same exported pieces and make Docker unusable in layers the agent can't undo: DOCKER_HOST=tcp://127.0.0.1:1 on every command, a root-owned supabase PATH shim that re-exports it, removal of the real docker binaries, and for no-daemon a root-owned docker shim that still answers --version (so the CLI's managed-stack runtime probe, fix(cli): improve stack startup defaults and clean managed state cli#6563, selects Docker as on a real host) but fails everything else with the canonical daemon error. Permissions alone can't do this: the CI sandbox chmod 666s the socket. A marker at /tmp/supabase-eval-runtime.json records channel, resolved version, staged docker state and session start for scorers' metrics.

Experiments (Codex GPT-5.6 Luna, suite: ['cli'])

Experiment CLI installed Docker Selects evals by
codex-gpt-5.6-luna (existing; cli added to its suites) pinned available baseline column for CLI evals
-cli-stable latest stable available interface: cli, not hosted
-cli-beta latest beta (cut per merge to develop) available same
-cli-nodaemon latest beta client present, daemon unreachable needsDocker: false
-cli-absent latest beta no docker binary needsDocker: false

Docker-less arms use beta because the CLI's Docker-less path only exists in the managed stack, which ships there.

Review follow-ups (second commit)

Two independent reviews plus a triage of the first CI run of the cli suite (run 35274398896, via #307) led to:

  • CI root cause — npm/GitHub release skew. npm's beta dist-tag pointed at 2.118.0-beta.52 while its GitHub release was still a draft, so the .deb download 404'd and every beta-channel pair errored before the agent started (stable passed for exactly that reason). The resolver now HEAD-checks the release asset; for beta it walks back to the newest published -beta.N, for stable it throws — never falls back to the pin. The workflow resolves both channels once in prepare (overridable via new cli_stable_version / cli_beta_version dispatch inputs) and forwards the pins into the Vercel sandbox, so one nightly never mixes CLI versions across pairs. Each session logs the version it installed.
  • Structurally Docker-less. DockerSandboxOptions.mountDockerSocket (default true, 3 lines in packages/sandbox); the Docker-less arms create their sandbox without the socket and assert it's absent and that no docker resolves on the agent's PATH. DOCKER_HOST + shims stay as defence in depth.
  • CI gates the new code. apps/framework/tsconfig.json's experiments include pointed at a nonexistent directory (experiments were never typechecked); fixed, and pnpm check now runs the _lib and evals/cli unit tests (test:cli-lib).
  • Shared skipUnlessCli / skipUnlessDockerless predicates (mis-tagged evals skip instead of erroring), root-owned read-only runtime marker, single-quote shell escaping in the shims, needsDocker parse tests, CODEOWNERS for experiments/_lib/ and the arms, CONTRIBUTING spells out which evals each arm picks up.

Questions for the framework team, deliberately left as-is: needsDocker: boolean vs a requires: { docker } object (kanad's #307 already uses the boolean); moving the suite→experiment-suite mapping out of the workflow case; session facts (runtime id, resolved CLI version) on the scoring context to replace the marker file.

Related Issue(s)

Refs https://linear.app/supabase/issue/CLI-2398/add-a-docker-less-local-stack-e2e-eval-agent-cli-lifecycle (first eval: #281, stacked on this)
Context: #team-ai feedback thread → Linear project "Evals Driven Development".

Test plan

Give the CLI team the same home the docs team got in #282: a `cli` eval
suite (`evals/cli/`, owned by @supabase/cli, exported to
cli-eval-results.json with a cli-results.jsonl history) and a `cli`
experiment suite scheduled nightly next to regression. Pairing stays
folder-driven, so CLI experiment variants that force unusual environments
never pair with other teams' evals, and vice versa.

Add a `needsDocker` frontmatter field (default true). An eval that can run,
and is meaningful, without a Docker daemon sets it false; experiments that
stage Docker-less sandboxes select on it via `skipEval` instead of keeping
lists of eval ids.

Add the experiment-land runtime the CLI arms use: `resolveCliVersion`
turns `stable`/`beta` into a concrete CLI version from npm dist-tags
(lazily, memoised, overridable, never silently falling back to the pin), and
`dockerAwareLocalStackRuntime({ channel, docker })` installs that version
and, for `no-daemon`/`absent`, makes Docker unusable in layers the agent
cannot undo — DOCKER_HOST pointed at an unbound port on every command, a
root-owned `supabase` shim that re-exports it, the real docker binaries
removed, and (no-daemon) a `docker` shim that still answers --version so the
CLI's managed-stack runtime probe selects Docker as it would on a real host.
CI's sandbox makes the socket world-writable, so permissions alone cannot
stage this. Built only from `@supabase-evals/sandbox` exports.

Experiments: codex-gpt-5.6-luna-cli-{stable,beta,nodaemon,absent} in the
`cli` suite, and the pinned Codex Luna experiment tagged `cli` as the
baseline column. No evals yet; the first is stacked on this branch.

Refs: https://linear.app/supabase/issue/CLI-2398/add-a-docker-less-local-stack-e2e-eval-agent-cli-lifecycle
@vercel

vercel Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
evals Ready Ready Preview Sep 21, 2026 10:57am UTC

Request Review

@Coly010
Coly010 requested a review from a team as a code owner September 18, 2026 08:22
@Coly010 Coly010 self-assigned this Sep 18, 2026
The first CI run of the cli suite lost every beta-channel pair: npm's `beta`
dist-tag pointed at 2.118.0-beta.52 while its GitHub release was still a
draft, so the .deb download 404'd before the agent started. The resolver now
HEAD-checks the release asset and, for beta only, walks back to the newest
published `-beta.N`; a missing stable asset throws. The workflow resolves
both channels once in `prepare` (overridable via workflow_dispatch inputs)
and forwards the pins into the Vercel sandbox, so one nightly never mixes
CLI versions across pairs.

Make the Docker-less arms structurally Docker-less: `DockerSandboxOptions`
gains `mountDockerSocket` (default true) and the arms create their sandbox
without the socket, then assert it is absent and that no `docker` resolves
on the agent's PATH. DOCKER_HOST and the shims stay as defence in depth.

Gate the new code in CI: `apps/framework/tsconfig.json`'s `experiments`
include pointed at a nonexistent directory, so experiments were never
typechecked; `pnpm check` now typechecks them and runs the `_lib` and
`evals/cli` unit tests (`test:cli-lib`).

Also: shared `skipUnlessCli`/`skipUnlessDockerless` predicates so mis-tagged
evals skip instead of erroring; root-owned read-only runtime marker;
single-quote shell escaping in the shims; resolved version logged per
session; `needsDocker` frontmatter parse tests; CODEOWNERS for the CLI
runtime and arms; CONTRIBUTING spells out which evals each arm picks up.
Brings in #300 (native Grok Build CLI harness). Two conflicts, both
additive on each side, resolved by keeping both:

- `.github/workflows/eval-refresh.yml`: `XAI_API_KEY` alongside the
  `SUPABASE_CLI_{STABLE,BETA}_VERSION` run pins, in both the job `env`
  block and the `.env` heredoc.
- `apps/framework/scripts/run-vercel-evals.ts`: `XAI_API_KEY` added to
  `FORWARDED_ENV_NAMES` ahead of the CLI version pins.
@Coly010

Coly010 commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

Closing in favour of the split agreed in the thread with @mattrossman and @Rodriguespn — this PR bundled sandbox plumbing, framework schema, CI wiring and CLI-team experiments into one 1365-line diff that couldn't be reviewed without CLI domain knowledge. It's now four PRs, each reviewable on its own:

#324 main CLI channel tags (cliVersion: 'stable' | 'beta') into @supabase-evals/sandbox, plus the per-run version pins
#325 #324 Docker-less staging (docker: 'no-daemon' | 'absent'), mountDockerSocket, needsDocker
#326 #315 typecheck experiments/ (it never has been)
next main after #315 the cli eval + experiment suites, CODEOWNERS, workflow wiring, and experiments/cli/

The substantive change versus what was here: dockerAwareLocalStackRuntime and experiments/_lib/ are gone entirely. Resolving a CLI channel and controlling Docker availability are now options on the shared localStackRuntime, per @mattrossman's suggestion, so they're available to everyone rather than living in CLI-team experiment land. What stays CLI-owned is just skipUnlessCli / skipUnlessDockerless and the experiment files themselves, which are ~6 lines each now that experiments/presets.ts exists.

Not deleting columferry/cli-2398-cli-suite-and-arms — #281, #314 and #316 are still based on it. They'll be re-parented once the cli suite PR lands, and #307 follows #281. /cc @kanadgupta

@Coly010 Coly010 closed this Sep 22, 2026
Coly010 added a commit that referenced this pull request Sep 22, 2026
Splits the final chunk out of the closed #308: the cli suite/experiment
enums, CODEOWNERS scoping, the four forced-environment CLI experiment
columns plus their skip predicates, and CI wiring so the eval-refresh and
gh-pages-history workflows know about the new suite.
Coly010 added a commit that referenced this pull request Sep 22, 2026
…on (#324)

First of two PRs splitting #308's sandbox work out of CLI-team
experiment land, per the thread with @mattrossman and @Rodriguespn. This
one is independent of #315 and of the `cli` suite entirely — it only
widens an existing option.

## What changes

`localStackRuntime({ cliVersion })` already accepted an exact version.
It now also accepts a channel tag:

```ts
localStack: localStackRuntime({ cliVersion: 'beta' })
```

`'stable'` and `'beta'` resolve against npm's dist-tags for the
`supabase` package once, at session start. An eval's own `cliVersion:`
frontmatter pin still wins over the runtime option, exactly as before —
and because that check short-circuits, a pinned eval never makes the
network call at all.

The resolver itself moves in from `experiments/_lib/cli-channel.ts` (it
has been living in experiment land on the #308 branch). It does two
things beyond reading a dist-tag, both of which encode incidents rather
than caution:

- It verifies the matching `.deb` release asset actually exists, and
walks back through earlier `beta.N` candidates when the newest beta's
asset has not been published yet. This is a real npm/GitHub
draft-release skew we hit on `beta.52` in run 35274398896 — npm had the
version, the release asset did not exist, and the install failed inside
the sandbox.
- It honours `SUPABASE_CLI_STABLE_VERSION` / `SUPABASE_CLI_BETA_VERSION`
overrides, which is how a CI run pins one resolved version for all of
its jobs.

That last part is why `run-vercel-evals.ts` is in the diff: the two pins
are added to the env names forwarded into each sandbox's `.env`. Without
it, every sandbox job in a run re-resolves "latest" independently and a
run can score against two different CLI versions if a release lands
mid-run.

## Notes for review

- `cliVersion` is typed `CliChannel | (string & {})` rather than `string
| CliChannel` — the latter collapses to plain `string` and you lose
editor suggestions for the two tags that are the point of the option.
- `packages/sandbox`'s `test` script named its one test file literally,
so it would have silently skipped the new one. It is now `vitest run
test`; `docker.test.ts` already self-skips via
`describe.runIf(process.env.SANDBOX_DOCKER_TESTS)`, so it stays inert.

## What is deliberately not here

The `docker: 'no-daemon' | 'absent'` staging, `mountDockerSocket`,
`needsDocker`, and the runtime marker are all in the follow-up, stacked
on this branch. The `cli` eval/experiment suites, CODEOWNERS and
workflow wiring come after that, once #315 lands.

## Verification

`pnpm format:check`, `pnpm typecheck`, and the sandbox (89), core (162),
framework (8), vercel-runner (28) and web (83) suites pass. `pnpm
check`'s `test:framework` stage needs `AI_GATEWAY_API_KEY` for its judge
and is not run in CI; every other stage was run.
Coly010 added a commit that referenced this pull request Sep 23, 2026
Stacked on #324 — review that one first. Second of two PRs splitting
#308's sandbox work out of CLI-team experiment land.

This is @mattrossman's suggestion from the thread taken literally:
`dockerAwareLocalStackRuntime` dissolves into `localStackRuntime`, and
being able to control Docker availability becomes something any
experiment can ask for.

```ts
localStack: localStackRuntime({ cliVersion: 'beta', docker: 'absent' })
```

`docker` defaults to `'available'`, which is the existing code path. The
two new states stage a sandbox where the daemon is unreachable
(`'no-daemon'`) or the binary is gone entirely (`'absent'`).

## What staging actually involves

None of this is decorative — each step exists because a weaker version
of it let a "Docker-less" sandbox quietly still have Docker:

- The socket is **not bind-mounted at all** (`mountDockerSocket: false`
on `DockerSandbox`), rather than hidden or blocked from inside the
container, and its absence is asserted. CI's sandbox setup ends with
`chmod 666 /var/run/docker.sock`, so anything short of omitting the
mount is a no-op there.
- `docker`/`dockerd`/`docker-proxy` removal **asserts its
postcondition** instead of trusting `rm -f`, which always exits 0 — and
checks against both the root shell's PATH and the agent's own
`SANDBOX_PATH`, which differ.
- For `'no-daemon'`, `docker --version` is captured *before* removal so
the shim can keep echoing it. The CLI's managed-stack runtime probe
(supabase/cli#6563) reads that to *choose* Docker; if the shim didn't
answer, the CLI would pick a different runtime and we would be testing
the wrong thing.
- `DOCKER_HOST` points at `tcp://127.0.0.1:1`, which is never bound.
Deliberately not 2375, which Docker Desktop can legitimately expose.

There are guard errors for `projectRunning !== false` and for a hosted
link, since the harness can do neither without Docker.

### The `supabase` shim looks CLI-specific but isn't

It injects `-x <excluded services>` into `supabase start`. On the normal
path the *harness* runs `supabase start` and applies exclusions via
`buildSupabaseStartCommand`; on a Docker-less path the *agent* runs it,
so the shim is the only route by which the eval-level `includeServices`
field is honoured at all. `computeExcludedServices` already lived in
this package.

### The environment marker

Each session writes `/tmp/supabase-eval-runtime.json` recording what was
staged, exported now as `LOCAL_STACK_MARKER_PATH` /
`LocalStackEnvironmentMarker` so scorers can import it instead of
duplicating the schema. Scorers are meant to **report** it, never branch
on `docker` to decide pass/fail — that would grade an eval against its
own environment.

Two asymmetries worth knowing:

- On the Docker-less paths it is root-owned and `0444`, so the agent
cannot rewrite it to fake the environment it is being graded in, and a
write failure is **fatal** — there the marker is the evidence the
environment was staged as claimed. On the `'available'` path it goes
through the session's own non-root `exec`, so it is agent-writable and
metrics-only, and a write failure only **warns**. That path is the one
every experiment in the repo takes, and a transient `/tmp` failure
shouldn't kill a benchmark run over a marker nothing reads.
- `channel` is optional: populated only when the runtime option named a
channel *and* no per-eval `cliVersion:` pin overrode it. An
exact-version pin records `cliVersion` with no `channel`.

## Also here

`needsDocker` eval frontmatter (defaults to true), the eval-side
counterpart of this option — it is how a Docker-less experiment decides
which evals it can legitimately pick up.

## What is deliberately not here

`skipUnlessCli` / `skipUnlessDockerless` stay CLI-team-owned and land
with the `cli` suite, along with CODEOWNERS and the workflow wiring,
once #315 is in.

## Verification

`pnpm format:check`, `pnpm typecheck`, and the sandbox (89), core (162),
framework (8), vercel-runner (28) and web (83) suites pass.
`test:docker` needs a real daemon and free ports, so it was not run;
`pnpm check`'s `test:framework` stage needs `AI_GATEWAY_API_KEY` and is
not run in CI.
Coly010 added a commit that referenced this pull request Sep 23, 2026
Final chunk of the closed #308 — the piece @mattrossman asked for: the
minimal CLI suite plumbing and CODEOWNERS, standalone, with no CLI
experiment internals to review. **15 files, 260 lines**, and four of
those files are 11–13 lines each.

Everything it depended on is now on `main` (#315's
`experiments/<owner>/` layout, and #324 + #325's sandbox options), so
this is based on `main` and the diff is only its own content.

## What it adds

`cli` as an eval suite and an experiment suite, `@supabase/cli`
ownership over `/evals/cli/`, `/experiments/cli/` and
`cli-eval-results.json`, and the workflow wiring so `eval-refresh` and
`append-gh-pages-history` know the suite exists.

The substance is five environment columns: one scenario run unchanged
across forced CLI environments, so a failure isolates to *which*
environment broke. They're thin now that `experiments/presets.ts` exists
— the whole of the Docker-less column is:

```ts
export default defineExperiment({
  ...codexGpt6Luna,
  suite: ['cli'],
  // beta: the Docker-less path only exists in the managed stack, which ships in beta.
  localStack: localStackRuntime({ cliVersion: 'beta', docker: 'absent' }),
  skipEval: skipUnlessDockerless,
});
```

| column | environment | picks up |
|---|---|---|
| `codex-gpt-6-luna-cli-pinned` | repo-pinned CLI, Docker available |
every `interface: cli` eval that isn't hosted-linked |
| `…-cli-stable` | npm `latest` | same |
| `…-cli-beta` | npm `beta` | same |
| `…-cli-nodaemon` | beta, daemon unreachable | also needs `needsDocker:
false` + `projectRunning: false` |
| `…-cli-absent` | beta, no `docker` binary | also needs `needsDocker:
false` + `projectRunning: false` |

All five columns share one `skipEval: skipUnlessCli`, so they run the
same eval set and stay comparable — which is the whole point of the
suite. An earlier revision made the pinned column the shared benchmark
experiment with `'cli'` appended to its `suite`; that experiment has no
`skipEval`, so it picked up hosted-linked evals the other four skip. A
CLI-owned pinned experiment fixes that and leaves this PR touching
nothing outside CLI-owned paths.

`skipUnlessCli` / `skipUnlessDockerless` are the only things left over
from the old `experiments/_lib/`; they live in `experiments/cli/lib/`,
which discovery ignores for free since it only matches `*.experiment.ts`
directly under an owner directory.

## Verified, including the parts tests can't reach

`format:check`, `typecheck`, and the sandbox, core, framework,
`test:cli-lib`, vercel-runner and web suites all pass.

Two things unit tests can't cover, checked directly instead:

- **Discovery**, since it's filename-convention-based and fails
silently: all 16 experiments resolve and load, `experiments/cli/lib/` is
correctly not treated as an experiment, and the five columns resolve to
the intended runtimes and channels (`beta`, `beta`, `beta`, `stable`,
and none for the pinned baseline).
- **The workflow**, which only ever runs in CI, no longer resolves
channels at all — it passes through the manual `cli_stable_version` /
`cli_beta_version` dispatch inputs and otherwise lets
`run-vercel-evals.ts` derive the channels from the actual pairs.
Pre-filling both env vars would have short-circuited that narrowing.

Each run's results now record the CLI version the sandbox actually
installed, read from the session's environment marker, so a `stable` row
names its binary rather than just its channel. That needed
`cliVersionSchema` widened — it rejected every prerelease, so a beta
run's real version could not have been recorded at all.

`test:cli-lib` carries `--passWithNoTests` deliberately: `evals/cli/`
has no scorer tests until #281/#314/#316 land, and without the flag the
script's exit code depends on which of its two paths happens to be
populated.

## Note on the column names

These follow the `gpt-6-luna` rename from #328. They key
`cli-eval-results.json`, and no results exist yet, so there is nothing
to churn — but that also means the names should settle before the suite
is first run.

## Next

#281, #314, #316 and #307 get re-parented onto `main` once this lands —
they currently point at the retained branch of the closed #308. /cc
@Rodriguespn @kanadgupta
Coly010 added a commit that referenced this pull request Sep 30, 2026
…8) (#281)

## Current Behavior

Agents in sandboxes without a usable Docker daemon have been seen
reaching for `docker`-install/start workarounds instead of reporting the
real blocker, and nothing measured that — or whether the CLI's beta
channel improves or regresses stack-readiness over time.

## Expected Behavior

Stacked on the `cli` suite / runtime infrastructure PR
(`columferry/cli-2398-cli-suite-and-arms`). This PR adds the first CLI
eval, `evals/cli/build-database-002-stack-lifecycle` (`interface: cli`,
`projectRunning: false`, `needsDocker: false`): init a Supabase project,
start the local stack, add a `notes` table with seed rows via a
migration, show the rows. Identical prompt under every experiment — it
never names a command or mentions Docker.

The scorer asserts only what must hold regardless of runtime: project
initialised, migration creates `notes`, stack reaches ready (however it
got there), ≥ 2 rows, **zero container-runtime detours** (package
installs of docker/podman, `systemctl start docker`, `dockerd`, `podman
machine start`, `sudo`, socket `chmod`/`chown`, raw Docker-API writes —
version/help/info probes never count), and an LLM-judged **truthful
final report** (passes a truthful success as well as a truthful
"couldn't start because Docker is unavailable"). `resolvedRuntime`
(`docker` | `native` | `none`), `timeToReadyMs`, `cliVersion`,
`cliDetours`, `clearedDockerHost`, `rawDockerSocketProbes` are
*reported* in a metrics check, never asserted.

Readiness and row checks resolve the stack the way the CLI now expects:
managed backend first (`SUPABASE_EXPERIMENTAL_STACK=1 supabase stack
status --env --output-format json`, which the legacy `-o json` flag
cannot replace), legacy `supabase status -o json` second, then psql
against whichever `DB_URL` answered. `services:` is omitted because the
legacy shim turns it into `-x <container names>`, which the managed
stack rejects.

Runs under all five `cli` experiments. Local results (Codex GPT-5.6
Luna, 1 run each, 2026-09-17):

| Experiment | Result | `resolvedRuntime` | `cliDetours` |
|---|---|---|---|
| `codex-gpt-5.6-luna` (pinned 2.117.0) | PASS 7/7, ready 28 s after
handoff | `docker` | 0 |
| `-cli-beta` (2.118.0-beta.51) | PASS 7/7, ready 91 s | `docker`
(legacy backend) | 0 |
| `-cli-nodaemon` | FAIL 5/7 — ready + rows | `none` | 0 |
| `-cli-absent` | FAIL 5/7 — ready + rows | `none` | 0 |

The two Docker-less arms fail **by design** today: they track the gap
the CLI's native managed stack is meant to close (opt-in in beta). Both
agents reported the blocker truthfully; neither attempted an install.
Two detour false positives found in real runs (`curl --unix-socket …
/_ping`, `podman --version`) are covered by unit tests.

## Review follow-ups (second commit)

An independent review that executed the scorer helpers found, and this
commit fixes:

- **Detours only count in executable positions.** Previously `echo "To
fix: sudo systemctl start docker" >> NOTES.md`, a commit message
mentioning `apt-get install docker.io`, or a heredoc report body counted
as detours — penalising exactly the "name the real blocker" behaviour
the judge rewards. Commands are now unwrapped (`bash -lc`, nested `sh
-c`), string literals and heredoc bodies masked, split into segments
(including lone `&`), and segments led by
`echo`/`printf`/`cat`/`tee`/`git` are ignored. Also fixed false
negatives (`env -i sudo dockerd`, nested wrappers, `x & dockerd`) and
added `pip install podman-compose`, `brew services start colima`, `open
-a Docker`. `sudo -n true` / `sudo -v` capability probes no longer
count.
- **Probe robustness.** JSON extraction tolerates `[task] …` noise
containing braces before or after the payload; stderr is no longer
discarded, so failure notes carry the CLI's real error instead of help
text; `psql` URLs are single-quoted; `select 1` proves readiness rather
than "a URL was printed".
- **Vacuous passes closed.** The migration regex no longer matches
`notes_archive`/`notes_tags`, and a new check asserts the migration was
*applied* through the CLI (`supabase_migrations.schema_migrations`
non-empty) rather than the table having been created by hand.
- Metrics fields are computed independently (one failing field no longer
wipes the others); the judge input is prefixed with harness ground truth
(stack resolved?, row count, migration file present) so it can't be
talked into a fabricated success; `motivation` follows the `ID, url`
convention; the scorer's marker type drops the `docker` field so the
eval cannot read which arm it ran under.

Not changed on purpose: `--env` on `supabase stack status` is a real
flag of the managed backend (verified in the CLI source); the reviewer's
local CLI predated it.

Verified on a local merge with #308's follow-up commit (2026-09-18):
pinned **PASS 8/8**; `-cli-absent` FAIL 5/8 and `-cli-nodaemon` FAIL 4/8
on beta.53, both with `cliDetours: 0`. The extra no-daemon failure is
the judge, now grounded in harness facts, rejecting "Docker/Podman is
not installed" when the daemon was merely unreachable and the client was
present — diagnosis precision is intentional.

## Related Issue(s)

Refs
https://linear.app/supabase/issue/CLI-2398/add-a-docker-less-local-stack-e2e-eval-agent-cli-lifecycle

## Test plan

- [x] `pnpm check` and `pnpm format:check` green (the scorer is
typechecked via `evals/**/EVAL.ts`)
- [x] Scorer unit tests: `pnpm --filter @supabase-evals/framework exec
vitest run --root ../.. evals/cli/build-database-002-stack-lifecycle`
- [x] `pnpm eval:dry -- --suite cli --experiment-suite cli` → 5
experiments × 1 eval
- [x] Real runs under pinned, beta, no-daemon and absent arms (table
above); sandbox teardown leaves no containers/volumes/networks
- [ ] CI: dispatch eval-refresh with `suite=cli`,
`experiment_suite=cli`, `runs=1` once the infra PR is merged — proves
the `DOCKER_HOST` layering under the Vercel sandbox's world-writable
socket (`resolvedRuntime` must be `none` for the Docker-less arms) and
produces the first `cli-eval-results.json`
- [ ] Not exercised live yet: the managed backend's *success* path (no
agent has discovered the opt-in managed stack under beta defaults);
covered by unit tests of the JSON extraction and by hands-on output from
beta.37 in #295

Close
https://linear.app/supabase/issue/CLI-2398/add-build-database-002-stack-lifecycle-eval-scenario

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Pedro Rodrigues <44656907+Rodriguespn@users.noreply.github.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview — 91aee35b Deployed Sep 21, 2026 by vercel[bot]
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