[STG-2457] fix(evals): update browse eval harness to CLI v0.9.1 contract - #2299
Conversation
|
There was a problem hiding this comment.
3 issues found across 5 files
Confidence score: 3/5
- In
packages/evals/framework/claudeCodeRunner.ts,V3Evaluatorcan silently fall back to legacy self-report whenjudgeModelis overridden without matchingjudgeClientOptions, 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 assigningtaskSpec.id. - In
packages/evals/framework/claudeCodeToolAdapter.ts, the new browse wrapper has untested command-specific--local/--remoteflag 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
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…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>
There was a problem hiding this comment.
2 issues found across 1 file (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
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
|
No changeset needed: this change is scoped entirely to |
… 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>
821d497 to
9fbc081
Compare
|
@miguelg719 slimmed this down per our thread — the branch is now a single commit on latest A few things verified while slimming (details in the updated PR body):
|
…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.
…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>
…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. ---------
…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. --> ---------
Summary
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 theenvsubcommand and the global--jsonflag, so every browse invocation in the harness was broken. Fixed to:--local/--remoteinstead of the removedenvsubcommand.--session <name>on every command to pin the eval's daemon.--json).stop/status) and is explicit so a setBROWSERBASE_API_KEYcannot silently auto-select remote when we asked for local.runBrowseSetup(which ranbrowse 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
codexToolAdapterdelegates to the sameprepareBrowseCliHarnessAdapter, the fix propagates to both external harnesses.Files:
packages/evals/framework/claudeCodeToolAdapter.ts.No changeset:
@browserbasehq/stagehand-evalsisprivate: true(never published), so it does not trigger a release.E2E Test Matrix
pnpm --dir packages/evals build+typecheckDone(esm + cli),tsc --noEmitexit 0pnpm --dir packages/evals test:unit(vitest run)core/tools/browse_cli.tsrevert.prepareBrowseCliHarnessAdapter(LOCAL) against<local build>ofpackages/cli, driving its generated wrapper scriptopen https://example.com→ JSON{"mode":"managed-local","pages":[…"url":"https://example.com/"…]};snapshot→ parsed JSONtree;status(modeless) → JSON OK;cleanup()→{"stopped":true}on the pinned session--local, pinned--session, JSON-by-default, and the modeless skip forstop/status.browse status --localinvoked directly against<local build>Error: Nonexistent flag: --local(non-zero exit)status/stopwould hard-fail.miguelgonzalez/verifier-10-benchmark-instrumentationinto this branchtypecheckexit 0,vitest355 passed across 48 files (including #2138's new suites)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.tscontract sweep) to be filed as a separate ticket/PR per Miguel's review.