Skip to content

[STG-2457] fix(evals): update browse eval harness to CLI v0.9.1 contract - #2299

Merged
shrey150 merged 2 commits into
mainfrom
shrey/evals-claude-code-verifier
Jul 8, 2026
Merged

[STG-2457] fix(evals): update browse eval harness to CLI v0.9.1 contract#2299
shrey150 merged 2 commits into
mainfrom
shrey/evals-claude-code-verifier

Conversation

@shrey150

@shrey150 shrey150 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Scope note (2026-07-06): this PR has been slimmed down to only the browse CLI contract fix. The V3Evaluator verifier wiring that previously lived here is superseded by #2138, which consolidates verifier grading for both the claude_code and codex harnesses (shared verifierAdapter helpers + Braintrust spans). The two PRs are independent and merge-order agnostic: this one fixes whether the agent can drive the browser at all; #2138 fixes how finished runs are graded.

Scope note (2026-07-07): per Miguel's review, this PR is now scoped to the bench path only (claudeCodeToolAdapter.ts, shared by the claude_code/codex external harnesses). The core/tools/browse_cli.ts change is reverted — that file is a separate, independently-hardcoded consumer of the browse CLI used only by the deterministic core tool-vs-tool comparison suite, and its contract drift runs deeper than this PR's scope (newpage vs tab new, click_xy vs mouse click, -t/-s vs --timeout/--state, etc.). That gets a full sweep in a separate follow-up PR rather than a partial fix here.

Contract fix — browse CLI v0.9.1 (bench harnesses)

The harness drove browse with a stale contract (browse --json … env local). browse CLI v0.9.1 dropped the env subcommand and the global --json flag, so every browse invocation in the harness was broken. Fixed to:

  • Per-command mode selection: --local / --remote instead of the removed env subcommand.
  • --session <name> on every command to pin the eval's daemon.
  • Rely on the CLI's JSON-by-default output (no --json).
  • The mode flag is only appended to commands that accept it (skipped for stop / status) and is explicit so a set BROWSERBASE_API_KEY cannot silently auto-select remote when we asked for local.
  • runBrowseSetup (which ran browse env … at adapter-prepare time) is deleted — the daemon now starts lazily on the first driver command, so adapter preparation no longer launches a browser at all.

Because codexToolAdapter delegates to the same prepareBrowseCliHarnessAdapter, the fix propagates to both external harnesses.

Files: packages/evals/framework/claudeCodeToolAdapter.ts.

No changeset: @browserbasehq/stagehand-evals is private: true (never published), so it does not trigger a release.

E2E Test Matrix

Command / flow Observed output Confidence / sufficiency
pnpm --dir packages/evals build + typecheck build Done (esm + cli), tsc --noEmit exit 0 Proves the changed file compiles and typechecks against current core.
pnpm --dir packages/evals test:unit (vitest run) 345 passed across 46 test files, 0 failed Full unit suite green on this branch, including after the core/tools/browse_cli.ts revert.
Adapter E2E smoke — real prepareBrowseCliHarnessAdapter (LOCAL) against <local build> of packages/cli, driving its generated wrapper script open https://example.com → JSON {"mode":"managed-local","pages":[…"url":"https://example.com/"…]}; snapshot → parsed JSON tree; status (modeless) → JSON OK; cleanup(){"stopped":true} on the pinned session Proves the exact wrapper the agent receives works end-to-end on the current CLI contract: explicit --local, pinned --session, JSON-by-default, and the modeless skip for stop/status.
Negative control — browse status --local invoked directly against <local build> Error: Nonexistent flag: --local (non-zero exit) Proves the wrapper's modeless skip is load-bearing: passing the mode flag to status/stop would hard-fail.
Compatibility with #2138 — throwaway local merge of miguelgonzalez/verifier-10-benchmark-instrumentation into this branch Merge clean (0 conflicts — the PRs share no files), typecheck exit 0, vitest 355 passed across 48 files (including #2138's new suites) Proves the two PRs land independently, in either order, with no interaction.

Linear

STG-2457 — https://linear.app/browserbase/issue/STG-2457/repair-claude-code-browse-eval-harness-rubric-score-via-v3evaluator

Follow-up (full core/tools/browse_cli.ts contract sweep) to be filed as a separate ticket/PR per Miguel's review.

@changeset-bot

changeset-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: e63c335

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 5 files

Confidence score: 3/5

  • In packages/evals/framework/claudeCodeRunner.ts, V3Evaluator can silently fall back to legacy self-report when judgeModel is overridden without matching judgeClientOptions, which can invalidate verifier-based results while appearing successful — require explicit client-option/model validation (or fail fast) before merging.
  • In packages/evals/framework/benchHarness.ts, deriving verifier task IDs from raw instruction text can create unstable or unsafe trajectory paths when instructions include path-like characters, risking misplaced outputs or run failures — sanitize and normalize the fallback ID segment before assigning taskSpec.id.
  • In packages/evals/framework/claudeCodeToolAdapter.ts, the new browse wrapper has untested command-specific --local/--remote flag assembly, so a regression here could silently break harness startup/dispatch for affected subcommands — add targeted tests for each browse subcommand/flag combination before merging.
Architecture diagram
sequenceDiagram
    participant H as benchHarness
    participant P as externalHarnessPlan
    participant TA as claudeCodeToolAdapter
    participant BCLI as browse CLI v0.9.1
    participant CR as claudeCodeRunner
    participant VE as V3Evaluator
    participant LLM as Judge Model (Gemini)
    participant RC as RubricCache

    Note over H,RC: Claude Code Eval Flow with Verifier

    H->>P: buildExternalHarnessTaskPlan(input)
    P-->>H: plan (includes precomputedRubric, expectedAnswer)

    H->>H: buildClaudeCodeVerifierConfig(plan, logger)
    Note over H: Check EVAL_CLAUDE_CODE_VERIFIER env
    alt Verifier enabled (default)
        H->>H: Resolve judge model & API key
        H->>H: Create V3 carrier (disableAPI: true)
        H->>H: Build taskSpec with rubric/answer
        H-->>CR: verifier config (v3, taskSpec, judgeModel, judgeClientOptions)
    else Verifier disabled
        H-->>CR: verifier undefined
    end

    H->>CR: runClaudeCodeAgent(plan, model, verifier, ...)

    CR->>TA: prepareBrowseCliHarnessAdapter(input)
    Note over TA: Build wrapper script with new CLI contract
    TA->>TA: Write wrapper bash script
    Note over TA: Uses --local/--remote, --session, no --json
    TA-->>CR: tool surface, adapter cleanup

    CR->>CR: Spawn agent process (Claude Code)
    Note over CR: Agent uses browse via wrapper script

    CR->>BCLI: browse (via wrapper) with new args
    Note over BCLI: e.g., browse --local --session eval-sess-123 goto url

    alt Verifier config present
        CR->>VE: V3Evaluator(v3, { backend:"verifier", modelName, modelClientOptions })
        VE->>VE: Determine rubric
        alt precomputedRubric provided
            VE->>VE: Use taskSpec.precomputedRubric
        else generate rubric
            VE->>RC: getOrGenerateRubric(dataset, instruction, judgeModel)
            RC->>LLM: Generate rubric
            LLM-->>RC: rubric
            RC-->>VE: rubric
        end
        VE->>LLM: Judge trajectory using rubric
        LLM-->>VE: EvaluationResult (outcome, process, evidence)
        VE-->>CR: result
        CR-->>H: TaskResult with verifier score
    else No verifier (legacy)
        CR->>CR: Parse agent self-reported EVAL_RESULT
        CR-->>H: TaskResult with self-report score
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/evals/framework/claudeCodeToolAdapter.ts
Comment thread packages/evals/framework/claudeCodeRunner.ts Outdated
Comment thread packages/evals/framework/benchHarness.ts Outdated
shrey150 added a commit that referenced this pull request Jul 1, 2026
…ved judge key

Address cubic review on #2299:
- Sanitize the instruction-derived fallback segment of the verifier TaskSpec id
  (replace non [A-Za-z0-9_-] with _) so it can't inject `/` or `..` into the
  persisted trajectory directory path.
- Move the judge-key check ahead of the try/catch and throw a clear config
  error when EVAL_CLAUDE_CODE_VERIFIER_MODEL is set but its provider key can't
  be resolved, instead of silently downgrading the run to legacy self-report.
  The built-in gemini default stays graceful.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 1 file (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/evals/framework/benchHarness.ts Outdated
Comment thread packages/evals/framework/benchHarness.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/evals/framework/benchHarness.ts Outdated
Comment thread packages/evals/framework/benchHarness.ts Outdated
@shrey150

shrey150 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

No changeset needed: this change is scoped entirely to @browserbasehq/stagehand-evals, which is a private package ("private": true) and not published, so there is no release/version bump to trigger. Both cubic nits addressed in 821d497 (gateway judge credential resolution + focused verifier-config regression tests; unit suite 345 → 349).

… contract

browse CLI v0.9.1 dropped the env subcommand and the global --json flag,
so every browse invocation from the external-harness wrapper and the
browse_cli core tool was broken. Fix both call sites:

- Per-command mode selection: explicit --local/--remote instead of the
  removed env subcommand, so a set BROWSERBASE_API_KEY cannot silently
  auto-select remote.
- --session <name> on every command to pin the eval's daemon.
- Rely on JSON-by-default output (no --json).
- Skip the mode flag for subcommands that reject it (stop, status).

The verifier wiring that previously shipped in this PR is superseded by
#2138, which consolidates it for both claude_code and codex harnesses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@shrey150
shrey150 force-pushed the shrey/evals-claude-code-verifier branch from 821d497 to 9fbc081 Compare July 7, 2026 00:39
@shrey150 shrey150 changed the title [STG-2457] fix(evals): repair claude_code browse harness + rubric-score via V3Evaluator [STG-2457] fix(evals): update browse eval harness to CLI v0.9.1 contract Jul 7, 2026
@shrey150

shrey150 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

@miguelg719 slimmed this down per our thread — the branch is now a single commit on latest main containing only the browse CLI v0.9.1 contract fix (claudeCodeToolAdapter.ts + core/tools/browse_cli.ts). All the V3Evaluator wiring, judge plumbing, and verifierConfig.test.ts are dropped in favor of #2138.

A few things verified while slimming (details in the updated PR body):

@shrey150
shrey150 requested a review from miguelg719 July 7, 2026 17:58
Comment thread packages/evals/core/tools/browse_cli.ts
…ollow-up

Per Miguel's #2299 review: this file's browse CLI drift (newpage vs
tab new, click_xy vs mouse click, -t/-s vs --timeout/--state, etc.)
runs deeper than what this PR fixed, and it's a separate consumer
from the bench/claudeCodeToolAdapter.ts path this PR is actually
scoped to. Scoping this PR to bench only; core gets a full contract
sweep in a separate PR.
@shrey150
shrey150 merged commit 04bb7ea into main Jul 8, 2026
37 checks passed
shrey150 added a commit that referenced this pull request Jul 14, 2026
…ILL.md (#2334)

## Why

The Claude Code eval harness installs a browser skill for the agent it
spawns from `packages/evals/skills/browser/SKILL.md` — a 68-line
eval-local copy created once in "Evals v2" (#2011) and never touched
again. The real, maintained browse skill lives at
`packages/cli/skills/browse/SKILL.md` (355 lines, ships with the CLI).
Checking git history:

- `packages/evals/skills/browser/SKILL.md`: **1 commit ever**
(2026-05-01, #2011), never edited since.
- `packages/cli/skills/browse/SKILL.md`: **10 commits since
2026-06-05**, most recently 2 days ago — roughly one edit every 3 days.

That gap already produced a concrete factual error: the most recent
CLI-skill commit (#2296, STG-2450) made `browse snapshot`
lean-by-default with `--full` needed for ref maps, but the frozen eval
skill still documented the old always-full behavior. It also never
mentioned `--verified`/`--proxies`/`--auto-connect`, `browse doctor`,
retry discipline, tab/network/cdp/mouse/viewport commands, or Browse.sh
skill discovery. The harness was testing an outdated mental model of
`browse`.

## Design

Single source of truth with an install-time eval addendum:

1. `BROWSER_SKILL_SOURCE` → `BROWSE_SKILL_SOURCE`, now pointing at
`packages/cli/skills/browse/SKILL.md` (built from `getRepoRootDir()`,
matching the existing `BROWSE_CLI_ENTRYPOINT`-style constants).
2. `installBrowserSkill` → `installBrowseSkill` now reads the CLI skill
and inserts a code-level `EVAL_HARNESS_ADDENDUM` template literal
**immediately after the YAML frontmatter** (not appended at the end)
before writing the combined file. The addendum:
- States `browse` is preinstalled/pinned by the harness (no `npm
install`, no `--local`/`--remote`/`--session` — the wrapper injects
those).
- Requires exactly one `browse ...` command per Bash call (shell
operators rejected by the harness).
- Tells the model to ignore the CLI skill's
install/Browse.sh-discovery/cloud/Functions/templates sections — out of
scope during evals.
- Reiterates no repo edits, no non-`browse` network tools, and the
`EVAL_RESULT` reporting format.

Why prepend and not append: `isAllowedBrowseCommand` only checks that a
Bash command starts with `browse ` and has no shell metacharacters — it
does **not** restrict which `browse` subcommand runs. So the addendum's
"ignore cloud/functions/skills" instruction is the actual
scope-enforcement mechanism, not just a courtesy note, and it needs to
be read before the model encounters the CLI skill's concrete (and
tempting) examples of those commands, not after. A live smoke run (see
below) shows the model reaching for `browse cloud fetch` and `browse
skills find` once it got stuck on a bot-protected page — evidence this
ordering concern is real, not theoretical.

3. Skill name consistency: installed skill dir renamed
`.claude/skills/browser/` → `.claude/skills/browse/` to match the CLI
skill's own `name: browse` frontmatter; all harness prompt/log
references to "a project skill named browser" updated to "browse". The
`stagehand_browser` MCP server name (used by the unrelated
`playwright_code`/`cdp_code` tool surfaces) is untouched.
4. Deleted `packages/evals/skills/browser/SKILL.md` and the now-empty
`packages/evals/skills/` directory.
5. Updated
`packages/evals/tests/framework/claudeCodeToolAdapter.test.ts` for the
renamed export/path/skill-name, plus new assertions that the installed
file contains both the CLI-skill content and the addendum, with the
addendum's string index before `## Cloud APIs`'s index (regression guard
against the addendum silently drifting back to append-at-end).

**Follow-up (review comment from
[ajmcquilkin](#2334 (comment)
the hand-rolled regex in `insertAfterFrontmatter` had already needed a
CRLF patch and still failed silently on BOM-prefixed files or a `---`
line embedded in a YAML multiline string. Swapped it for
[`gray-matter`](https://www.npmjs.com/package/gray-matter) (new
`packages/evals` devDependency, private package, no changeset), but only
for *boundary detection* — `matter(markdown)` locates where the
frontmatter block ends; reassembly still uses the original raw string
(`markdown.slice(0, markdown.length - parsed.content.length)` for the
frontmatter, `parsed.content` for the body) rather than
`matter.stringify()`, since that would re-serialize the YAML through
js-yaml and reformat the shipped skill's frontmatter (e.g. its folded
`description: >` block). `insertAfterFrontmatter` is now exported and
directly unit-tested.

No changeset — `packages/evals` is private, eval-infra only.

**Overlap note:** this touches `claudeCodeToolAdapter.ts`; open PR #2299
also touches that file but in a different region (contract fix, not the
skill-install path). Trivial rebase for whichever lands second.

Linear:
[STG-2510](https://linear.app/browserbase/issue/STG-2510/evals-source-browse-skill-from-packagescli-skillmd-instead-of-stale)

## E2E Test Matrix

| Command / flow | Observed output | Confidence / sufficiency |
| --- | --- | --- |
| `pnpm turbo run build --filter=@browserbasehq/stagehand
--filter=browse` then `pnpm --dir packages/evals build` | All 4 turbo
tasks + evals `build:esm`/`build:cli` completed successfully in
`<worktree>` | Confirms the changed adapter compiles against the real
CLI/core build artifacts it now depends on
(`packages/cli/skills/browse/SKILL.md`, `packages/cli/dist/...`). |
| `pnpm --dir packages/evals exec vitest run
tests/framework/claudeCodeToolAdapter.test.ts` | `Test Files 1 passed
(1)`, `Tests 17 passed (17)` | Covers the renamed export, new install
path/skill name, the addendum-ordering assertion, and (as of the
gray-matter follow-up below) the frontmatter boundary-detection cases.
Narrow to this file. |
| `pnpm --dir packages/evals run test:unit` (full evals suite) | `Test
Files 48 passed (48)`, `Tests 362 passed (362)` | Confirms no other test
in the package depends on the old `browser` skill name/path,
`installBrowserSkill` export, or the removed regex helper. |
| `node -e` script importing `installBrowseSkill` from built
`packages/evals/dist/esm/framework/claudeCodeToolAdapter.js`, run
against a temp dir | Installed at `<temp
dir>/.claude/skills/browse/SKILL.md`; head matches CLI skill frontmatter
(`name: browse`, full description); tail/body contains `## Eval Harness
Addendum` positioned before `## Cloud APIs` (indices 1054 vs 9018);
frontmatter YAML block intact as the first bytes of the file | Direct
artifact proof of the exact shape described above — installed path,
single-source content, and addendum placement — independent of the eval
harness runtime. |
| Live harness run: `EVAL_CLAUDE_CODE_ALLOW_UNSANDBOXED_LOCAL=true
EVAL_CLAUDE_CODE_MAX_TURNS=45 node packages/evals/dist/cli/cli.js run
b:webtailbench -l 1 -t 1 -c 1 --harness claude_code -e local -m
anthropic/claude-haiku-4-5-20251001` (verbose logging on) | Log line
`Installed browse skill at <temp dir>/.claude/skills/browse/SKILL.md`;
agent then issued `browse open`, `browse snapshot`, `browse doctor`,
`browse status`, `browse stop --force`, `browse cloud fetch`, `browse
skills find` — every one of them accepted by the harness with no "Only
browse commands are allowed" / "Only Skill and Bash are allowed"
contract denial anywhere in the log. Task itself ended `error_max_turns`
fighting a bot-protected United.com page (unrelated to this change) |
Proves the real install → load → drive pipeline works end-to-end with
zero contract errors. Task pass/fail is expected to be noisy on this
benchmark target and is not the bar here; the pipeline mechanics are
what this row proves. |
| **Follow-up (review comment):** `pnpm --dir packages/evals build` (esm
+ cli) after adding `gray-matter` as a devDependency and rewriting
`insertAfterFrontmatter` to use it for boundary detection only | Both
`build:esm`/`build:cli` completed;
`dist/esm/framework/claudeCodeToolAdapter.js` shows `import matter from
"gray-matter"` and the new exported `insertAfterFrontmatter` | Confirms
the new dependency resolves and the adapter still builds against
`packages/cli/skills/browse/SKILL.md`. |
| `node` script against the **built**
`installBrowseSkill`/`insertAfterFrontmatter` (not source), run in a
temp dir | `frontmatter byte-identical to source? true`; text
immediately after the frontmatter is only whitespace before `## Eval
Harness Addendum` (no leftover body content); `## Cloud APIs` still
present after it; direct `insertAfterFrontmatter` calls for the
no-frontmatter fallback and an embedded `---` inside a YAML multiline
string both produced correctly-bounded output | Real artifact proof (not
unit-test mocks) that swapping in gray-matter didn't change the
installed file's byte layout — the specific regression the reviewer's
suggested library could introduce via `matter.stringify()`, which this
implementation deliberately avoids. |
| New unit tests in `claudeCodeToolAdapter.test.ts`:
LF/CRLF/BOM-prefixed frontmatter, a `---` line inside an indented YAML
`>` block, no-frontmatter fallback, unterminated/invalid-YAML fallback
(gray-matter throws; now caught), and a byte-identical-to-source
frontmatter assertion via `installBrowseSkill` | All pass as part of the
17/17 and 362/362 runs above | Locks in the exact boundary-detection
contract the reviewer flagged as fragile; the byte-identical assertion
is a standing regression guard against ever switching to
`matter.stringify()`. |
| `pnpm --dir packages/evals run lint` (prettier + eslint + tsc) | `All
matched files use Prettier code style!`; eslint clean; `tsc --noEmit`
clean | Confirms the gray-matter typings
(`matter.GrayMatterFile<string>`) satisfy the package's strict TS config
and formatting rules. |

## Future work

A separate in-flight PR (`shrey/cli-skills-show`, not yet on main) adds
`browse skills show` (prints the bundled skill to stdout) plus a "Start
here (for AI agents)" pointer in `browse --help`, so a real sandboxed
agent with no eval scaffolding could self-discover the skill instead of
having it handed to it.

This PR's shape — inject the real skill + eval addendum into
`.claude/skills/browse/` at prepare time — stays the right *default*:
it's the conventional eval pattern (benchmarks inject tool docs
deterministically, the agent can't skip it), and it mirrors the actual
supported CLI workflow (a user who already ran `browse skills install`).
It also can't reference `browse skills show` today since that command
doesn't exist on main yet.

Once `browse skills show` ships, "agent discovers the skill itself via
`browse --help`" becomes a good **second, more sandbox-realistic eval
arm** (skill-injected vs. self-discovered A/B), not a replacement —
worth noting for whoever builds it: `isAllowedBrowseCommand` in this
file only checks for a `browse ` prefix and absence of shell
metacharacters, so `browse skills show` already passes that gate today
with zero adapter changes needed for permissions. The one thing that arm
would still need is its own way to deliver the eval-specific overrides
(session/environment pinning, one-command-per-call, out-of-scope
sections) — `browse skills show` would print the bundled skill verbatim,
so that arm likely wants the same `EVAL_HARNESS_ADDENDUM` content
delivered via the top-level task prompt instead of a pre-installed skill
file.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Switches the eval harness to install the real `browse` skill from
`packages/cli/skills/browse/SKILL.md`, injecting a small eval-only
addendum right after the frontmatter. Fixes stale guidance and keeps
eval behavior aligned with the CLI (Linear STG-2510).

- **Bug Fixes**
- Source `browse` skill from `packages/cli/skills/browse/SKILL.md` to
stop drift and correct outdated docs/behavior.
- Insert `EVAL_HARNESS_ADDENDUM` after frontmatter to pin env/session,
require one `browse` command per Bash call, and de-scope
cloud/functions/templates/skills install.
- Parse frontmatter with `gray-matter` to handle BOM/CRLF/embedded
`---`, ensuring correct insertion across platforms.

- **Refactors**
- Rename `installBrowserSkill` → `installBrowseSkill`; install to
`.claude/skills/browse/`; update prompts/logs to "browse". Keep
`stagehand_browser` MCP name unchanged.
  - Remove `packages/evals/skills/browser/SKILL.md`.
- Export `insertAfterFrontmatter`, add unit tests for boundary cases and
addendum-before-"Cloud APIs"; add `gray-matter` as a devDependency.

<sup>Written for commit 486cab8.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2334?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
felipeofdev-ai pushed a commit to felipeofdev-ai/stagehand that referenced this pull request Aug 4, 2026
…act (browserbase#2299)

## Summary

> **Scope note (2026-07-06):** this PR has been slimmed down to **only
the browse CLI contract fix**. The V3Evaluator verifier wiring that
previously lived here is superseded by browserbase#2138, which consolidates
verifier grading for both the `claude_code` and `codex` harnesses
(shared `verifierAdapter` helpers + Braintrust spans). The two PRs are
independent and merge-order agnostic: this one fixes whether the agent
can drive the browser at all; browserbase#2138 fixes how finished runs are graded.

> **Scope note (2026-07-07):** per [Miguel's
review](browserbase#2299 (comment)),
this PR is now scoped to the `bench` path only
(`claudeCodeToolAdapter.ts`, shared by the `claude_code`/`codex`
external harnesses). The `core/tools/browse_cli.ts` change is reverted —
that file is a separate, independently-hardcoded consumer of the browse
CLI used only by the deterministic `core` tool-vs-tool comparison suite,
and its contract drift runs deeper than this PR's scope (`newpage` vs
`tab new`, `click_xy` vs `mouse click`, `-t`/`-s` vs
`--timeout`/`--state`, etc.). That gets a full sweep in a separate
follow-up PR rather than a partial fix here.

### Contract fix — browse CLI v0.9.1 (bench harnesses)

The harness drove browse with a stale contract (`browse --json … env
local`). browse CLI **v0.9.1 dropped the `env` subcommand and the global
`--json` flag**, so every browse invocation in the harness was broken.
Fixed to:

- Per-command mode selection: `--local` / `--remote` instead of the
removed `env` subcommand.
- `--session <name>` on every command to pin the eval's daemon.
- Rely on the CLI's JSON-by-default output (no `--json`).
- The mode flag is only appended to commands that accept it (skipped for
`stop` / `status`) and is **explicit** so a set `BROWSERBASE_API_KEY`
cannot silently auto-select remote when we asked for local.
- `runBrowseSetup` (which ran `browse env …` at adapter-prepare time) is
deleted — the daemon now starts lazily on the first driver command, so
adapter preparation no longer launches a browser at all.

Because `codexToolAdapter` delegates to the same
`prepareBrowseCliHarnessAdapter`, the fix propagates to **both**
external harnesses.

Files: `packages/evals/framework/claudeCodeToolAdapter.ts`.

No changeset: `@browserbasehq/stagehand-evals` is `private: true` (never
published), so it does not trigger a release.

## E2E Test Matrix

| Command / flow | Observed output | Confidence / sufficiency |
| --- | --- | --- |
| `pnpm --dir packages/evals build` + `typecheck` | build `Done` (esm +
cli), `tsc --noEmit` exit 0 | Proves the changed file compiles and
typechecks against current core. |
| `pnpm --dir packages/evals test:unit` (`vitest run`) | **345 passed**
across 46 test files, 0 failed | Full unit suite green on this branch,
including after the `core/tools/browse_cli.ts` revert. |
| Adapter E2E smoke — real `prepareBrowseCliHarnessAdapter` (LOCAL)
against `<local build>` of `packages/cli`, driving its generated wrapper
script | `open https://example.com` → JSON
`{"mode":"managed-local","pages":[…"url":"https://example.com/"…]}`;
`snapshot` → parsed JSON `tree`; `status` (modeless) → JSON OK;
`cleanup()` → `{"stopped":true}` on the pinned session | Proves the
exact wrapper the agent receives works end-to-end on the current CLI
contract: explicit `--local`, pinned `--session`, JSON-by-default, and
the modeless skip for `stop`/`status`. |
| Negative control — `browse status --local` invoked directly against
`<local build>` | `Error: Nonexistent flag: --local` (non-zero exit) |
Proves the wrapper's modeless skip is load-bearing: passing the mode
flag to `status`/`stop` would hard-fail. |
| Compatibility with browserbase#2138 — throwaway local merge of
`miguelgonzalez/verifier-10-benchmark-instrumentation` into this branch
| Merge clean (**0 conflicts** — the PRs share no files), `typecheck`
exit 0, `vitest` **355 passed** across 48 files (including browserbase#2138's new
suites) | Proves the two PRs land independently, in either order, with
no interaction. |

## Linear

STG-2457 —
https://linear.app/browserbase/issue/STG-2457/repair-claude-code-browse-eval-harness-rubric-score-via-v3evaluator

Follow-up (full `core/tools/browse_cli.ts` contract sweep) to be filed
as a separate ticket/PR per Miguel's review.

---------
felipeofdev-ai pushed a commit to felipeofdev-ai/stagehand that referenced this pull request Aug 4, 2026
…ILL.md (browserbase#2334)

## Why

The Claude Code eval harness installs a browser skill for the agent it
spawns from `packages/evals/skills/browser/SKILL.md` — a 68-line
eval-local copy created once in "Evals v2" (browserbase#2011) and never touched
again. The real, maintained browse skill lives at
`packages/cli/skills/browse/SKILL.md` (355 lines, ships with the CLI).
Checking git history:

- `packages/evals/skills/browser/SKILL.md`: **1 commit ever**
(2026-05-01, browserbase#2011), never edited since.
- `packages/cli/skills/browse/SKILL.md`: **10 commits since
2026-06-05**, most recently 2 days ago — roughly one edit every 3 days.

That gap already produced a concrete factual error: the most recent
CLI-skill commit (browserbase#2296, STG-2450) made `browse snapshot`
lean-by-default with `--full` needed for ref maps, but the frozen eval
skill still documented the old always-full behavior. It also never
mentioned `--verified`/`--proxies`/`--auto-connect`, `browse doctor`,
retry discipline, tab/network/cdp/mouse/viewport commands, or Browse.sh
skill discovery. The harness was testing an outdated mental model of
`browse`.

## Design

Single source of truth with an install-time eval addendum:

1. `BROWSER_SKILL_SOURCE` → `BROWSE_SKILL_SOURCE`, now pointing at
`packages/cli/skills/browse/SKILL.md` (built from `getRepoRootDir()`,
matching the existing `BROWSE_CLI_ENTRYPOINT`-style constants).
2. `installBrowserSkill` → `installBrowseSkill` now reads the CLI skill
and inserts a code-level `EVAL_HARNESS_ADDENDUM` template literal
**immediately after the YAML frontmatter** (not appended at the end)
before writing the combined file. The addendum:
- States `browse` is preinstalled/pinned by the harness (no `npm
install`, no `--local`/`--remote`/`--session` — the wrapper injects
those).
- Requires exactly one `browse ...` command per Bash call (shell
operators rejected by the harness).
- Tells the model to ignore the CLI skill's
install/Browse.sh-discovery/cloud/Functions/templates sections — out of
scope during evals.
- Reiterates no repo edits, no non-`browse` network tools, and the
`EVAL_RESULT` reporting format.

Why prepend and not append: `isAllowedBrowseCommand` only checks that a
Bash command starts with `browse ` and has no shell metacharacters — it
does **not** restrict which `browse` subcommand runs. So the addendum's
"ignore cloud/functions/skills" instruction is the actual
scope-enforcement mechanism, not just a courtesy note, and it needs to
be read before the model encounters the CLI skill's concrete (and
tempting) examples of those commands, not after. A live smoke run (see
below) shows the model reaching for `browse cloud fetch` and `browse
skills find` once it got stuck on a bot-protected page — evidence this
ordering concern is real, not theoretical.

3. Skill name consistency: installed skill dir renamed
`.claude/skills/browser/` → `.claude/skills/browse/` to match the CLI
skill's own `name: browse` frontmatter; all harness prompt/log
references to "a project skill named browser" updated to "browse". The
`stagehand_browser` MCP server name (used by the unrelated
`playwright_code`/`cdp_code` tool surfaces) is untouched.
4. Deleted `packages/evals/skills/browser/SKILL.md` and the now-empty
`packages/evals/skills/` directory.
5. Updated
`packages/evals/tests/framework/claudeCodeToolAdapter.test.ts` for the
renamed export/path/skill-name, plus new assertions that the installed
file contains both the CLI-skill content and the addendum, with the
addendum's string index before `## Cloud APIs`'s index (regression guard
against the addendum silently drifting back to append-at-end).

**Follow-up (review comment from
[ajmcquilkin](browserbase#2334 (comment)
the hand-rolled regex in `insertAfterFrontmatter` had already needed a
CRLF patch and still failed silently on BOM-prefixed files or a `---`
line embedded in a YAML multiline string. Swapped it for
[`gray-matter`](https://www.npmjs.com/package/gray-matter) (new
`packages/evals` devDependency, private package, no changeset), but only
for *boundary detection* — `matter(markdown)` locates where the
frontmatter block ends; reassembly still uses the original raw string
(`markdown.slice(0, markdown.length - parsed.content.length)` for the
frontmatter, `parsed.content` for the body) rather than
`matter.stringify()`, since that would re-serialize the YAML through
js-yaml and reformat the shipped skill's frontmatter (e.g. its folded
`description: >` block). `insertAfterFrontmatter` is now exported and
directly unit-tested.

No changeset — `packages/evals` is private, eval-infra only.

**Overlap note:** this touches `claudeCodeToolAdapter.ts`; open PR browserbase#2299
also touches that file but in a different region (contract fix, not the
skill-install path). Trivial rebase for whichever lands second.

Linear:
[STG-2510](https://linear.app/browserbase/issue/STG-2510/evals-source-browse-skill-from-packagescli-skillmd-instead-of-stale)

## E2E Test Matrix

| Command / flow | Observed output | Confidence / sufficiency |
| --- | --- | --- |
| `pnpm turbo run build --filter=@browserbasehq/stagehand
--filter=browse` then `pnpm --dir packages/evals build` | All 4 turbo
tasks + evals `build:esm`/`build:cli` completed successfully in
`<worktree>` | Confirms the changed adapter compiles against the real
CLI/core build artifacts it now depends on
(`packages/cli/skills/browse/SKILL.md`, `packages/cli/dist/...`). |
| `pnpm --dir packages/evals exec vitest run
tests/framework/claudeCodeToolAdapter.test.ts` | `Test Files 1 passed
(1)`, `Tests 17 passed (17)` | Covers the renamed export, new install
path/skill name, the addendum-ordering assertion, and (as of the
gray-matter follow-up below) the frontmatter boundary-detection cases.
Narrow to this file. |
| `pnpm --dir packages/evals run test:unit` (full evals suite) | `Test
Files 48 passed (48)`, `Tests 362 passed (362)` | Confirms no other test
in the package depends on the old `browser` skill name/path,
`installBrowserSkill` export, or the removed regex helper. |
| `node -e` script importing `installBrowseSkill` from built
`packages/evals/dist/esm/framework/claudeCodeToolAdapter.js`, run
against a temp dir | Installed at `<temp
dir>/.claude/skills/browse/SKILL.md`; head matches CLI skill frontmatter
(`name: browse`, full description); tail/body contains `## Eval Harness
Addendum` positioned before `## Cloud APIs` (indices 1054 vs 9018);
frontmatter YAML block intact as the first bytes of the file | Direct
artifact proof of the exact shape described above — installed path,
single-source content, and addendum placement — independent of the eval
harness runtime. |
| Live harness run: `EVAL_CLAUDE_CODE_ALLOW_UNSANDBOXED_LOCAL=true
EVAL_CLAUDE_CODE_MAX_TURNS=45 node packages/evals/dist/cli/cli.js run
b:webtailbench -l 1 -t 1 -c 1 --harness claude_code -e local -m
anthropic/claude-haiku-4-5-20251001` (verbose logging on) | Log line
`Installed browse skill at <temp dir>/.claude/skills/browse/SKILL.md`;
agent then issued `browse open`, `browse snapshot`, `browse doctor`,
`browse status`, `browse stop --force`, `browse cloud fetch`, `browse
skills find` — every one of them accepted by the harness with no "Only
browse commands are allowed" / "Only Skill and Bash are allowed"
contract denial anywhere in the log. Task itself ended `error_max_turns`
fighting a bot-protected United.com page (unrelated to this change) |
Proves the real install → load → drive pipeline works end-to-end with
zero contract errors. Task pass/fail is expected to be noisy on this
benchmark target and is not the bar here; the pipeline mechanics are
what this row proves. |
| **Follow-up (review comment):** `pnpm --dir packages/evals build` (esm
+ cli) after adding `gray-matter` as a devDependency and rewriting
`insertAfterFrontmatter` to use it for boundary detection only | Both
`build:esm`/`build:cli` completed;
`dist/esm/framework/claudeCodeToolAdapter.js` shows `import matter from
"gray-matter"` and the new exported `insertAfterFrontmatter` | Confirms
the new dependency resolves and the adapter still builds against
`packages/cli/skills/browse/SKILL.md`. |
| `node` script against the **built**
`installBrowseSkill`/`insertAfterFrontmatter` (not source), run in a
temp dir | `frontmatter byte-identical to source? true`; text
immediately after the frontmatter is only whitespace before `## Eval
Harness Addendum` (no leftover body content); `## Cloud APIs` still
present after it; direct `insertAfterFrontmatter` calls for the
no-frontmatter fallback and an embedded `---` inside a YAML multiline
string both produced correctly-bounded output | Real artifact proof (not
unit-test mocks) that swapping in gray-matter didn't change the
installed file's byte layout — the specific regression the reviewer's
suggested library could introduce via `matter.stringify()`, which this
implementation deliberately avoids. |
| New unit tests in `claudeCodeToolAdapter.test.ts`:
LF/CRLF/BOM-prefixed frontmatter, a `---` line inside an indented YAML
`>` block, no-frontmatter fallback, unterminated/invalid-YAML fallback
(gray-matter throws; now caught), and a byte-identical-to-source
frontmatter assertion via `installBrowseSkill` | All pass as part of the
17/17 and 362/362 runs above | Locks in the exact boundary-detection
contract the reviewer flagged as fragile; the byte-identical assertion
is a standing regression guard against ever switching to
`matter.stringify()`. |
| `pnpm --dir packages/evals run lint` (prettier + eslint + tsc) | `All
matched files use Prettier code style!`; eslint clean; `tsc --noEmit`
clean | Confirms the gray-matter typings
(`matter.GrayMatterFile<string>`) satisfy the package's strict TS config
and formatting rules. |

## Future work

A separate in-flight PR (`shrey/cli-skills-show`, not yet on main) adds
`browse skills show` (prints the bundled skill to stdout) plus a "Start
here (for AI agents)" pointer in `browse --help`, so a real sandboxed
agent with no eval scaffolding could self-discover the skill instead of
having it handed to it.

This PR's shape — inject the real skill + eval addendum into
`.claude/skills/browse/` at prepare time — stays the right *default*:
it's the conventional eval pattern (benchmarks inject tool docs
deterministically, the agent can't skip it), and it mirrors the actual
supported CLI workflow (a user who already ran `browse skills install`).
It also can't reference `browse skills show` today since that command
doesn't exist on main yet.

Once `browse skills show` ships, "agent discovers the skill itself via
`browse --help`" becomes a good **second, more sandbox-realistic eval
arm** (skill-injected vs. self-discovered A/B), not a replacement —
worth noting for whoever builds it: `isAllowedBrowseCommand` in this
file only checks for a `browse ` prefix and absence of shell
metacharacters, so `browse skills show` already passes that gate today
with zero adapter changes needed for permissions. The one thing that arm
would still need is its own way to deliver the eval-specific overrides
(session/environment pinning, one-command-per-call, out-of-scope
sections) — `browse skills show` would print the bundled skill verbatim,
so that arm likely wants the same `EVAL_HARNESS_ADDENDUM` content
delivered via the top-level task prompt instead of a pre-installed skill
file.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Switches the eval harness to install the real `browse` skill from
`packages/cli/skills/browse/SKILL.md`, injecting a small eval-only
addendum right after the frontmatter. Fixes stale guidance and keeps
eval behavior aligned with the CLI (Linear STG-2510).

- **Bug Fixes**
- Source `browse` skill from `packages/cli/skills/browse/SKILL.md` to
stop drift and correct outdated docs/behavior.
- Insert `EVAL_HARNESS_ADDENDUM` after frontmatter to pin env/session,
require one `browse` command per Bash call, and de-scope
cloud/functions/templates/skills install.
- Parse frontmatter with `gray-matter` to handle BOM/CRLF/embedded
`---`, ensuring correct insertion across platforms.

- **Refactors**
- Rename `installBrowserSkill` → `installBrowseSkill`; install to
`.claude/skills/browse/`; update prompts/logs to "browse". Keep
`stagehand_browser` MCP name unchanged.
  - Remove `packages/evals/skills/browser/SKILL.md`.
- Export `insertAfterFrontmatter`, add unit tests for boundary cases and
addendum-before-"Cloud APIs"; add `gray-matter` as a devDependency.

<sup>Written for commit 486cab8.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2334?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

---------
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.

2 participants