[STG-2450] feat(cli): browse snapshot lean by default, add --full for ref maps - #2296
Conversation
🦋 Changeset detectedLatest commit: 8754565 The changes in this PR will be included in the next version bump. This PR includes changesets to release 0 packagesWhen changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
2 issues found across 4 files
Confidence score: 3/5
- In
packages/cli/src/commands/snapshot.ts, the new default path still applies the oldcompacttree filter, so lean mode may remove accessibility-tree lines (not just ref maps), which can change snapshot content unexpectedly for users — split “omit maps” logic from “compact tree” before merging the default switch. - In
packages/cli/src/commands/snapshot.ts, the default flip plus new--fullflag and deprecated flag behavior are untested, so regressions in typical CLI usage could ship unnoticed — add unit tests for default lean output,--full, and deprecated-flag compatibility before merging.
Architecture diagram
sequenceDiagram
participant Agent as Agent (LLM/Client)
participant CLI as CLI (browse commands)
participant Session as Session Manager
participant Daemon as Driver Daemon
participant Cache as Refs Cache (in-memory)
Note over Agent,Cache: NEW: Lean snapshot flow
Agent->>CLI: browse snapshot
CLI->>CLI: Parse flags (--full default=false)
alt --full flag absent (default)
CLI->>CLI: Set compact=true (lean output)
CLI->>Daemon: snapshot(compact=true)
Daemon->>Daemon: Capture accessibility tree only
Daemon-->>CLI: { tree: formattedTree }
CLI-->>Agent: JSON with tree only (hasMaps=false)
else --full flag present
CLI->>CLI: Set compact=false
CLI->>Daemon: snapshot(compact=false)
Daemon->>Daemon: Capture tree + xpathMap + urlMap
Daemon-->>CLI: { tree, xpathMap, urlMap }
CLI-->>Agent: JSON with tree + ref maps
end
Note over CLI,Daemon: --compact deprecation path (TTY only)
opt --compact flag AND stderr is TTY
CLI->>CLI: Print deprecation warning to stderr
end
Note over Agent,Cache: CHANGED: Refs still cached server-side
Agent->>CLI: browse click @0-1588
CLI->>Daemon: click(ref="0-1588")
Daemon->>Cache: Resolve ref to XPath
Cache-->>Daemon: XPath
Daemon->>Daemon: Execute click on element
Daemon-->>CLI: { clicked: true }
CLI-->>Agent: Result
Note over Agent,Cache: CHANGED: browse refs still works
Agent->>CLI: browse refs
CLI->>Daemon: getRefs()
Daemon->>Cache: Retrieve cached maps
Cache-->>Daemon: { xpathMap, urlMap }
Daemon-->>CLI: Maps
CLI-->>Agent: Formatted ref maps
Note over Agent,Cache: Eval represent() with BROWSE_SNAPSHOT_FULL toggle
alt BROWSE_SNAPSHOT_FULL=1
Agent->>CLI: represent() → snapshot --full
CLI-->>Agent: { tree, xpathMap, urlMap }
Agent->>Agent: refCount = Object.keys(xpathMap).length
else default (lean)
Agent->>CLI: represent() → snapshot
CLI-->>Agent: { tree }
Agent->>Agent: refCount = count refs in tree text
end
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
1 issue found across 5 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/cli/tests/driver-commands.test.ts">
<violation number="1" location="packages/cli/tests/driver-commands.test.ts:363">
P3: `toHaveBeenCalledWith` doesn't verify caching happens on the lean call specifically. If the handler is refactored to only cache maps in the `--full` path the test would still pass.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
… maps browse snapshot previously emitted the formatted tree plus xpathMap+urlMap (~217KB / ~60K tokens on content-heavy pages) on every call. Refs resolve from a server-side cache, so the printed maps were dead weight in agent context and ~18x larger than Playwright MCP / agent-browser / our own managed Agents output. - Default output is now the formatted tree only (no ref maps). - Add --full to restore tree + xpathMap + urlMap (the previous default). - --compact is now a deprecated no-op alias of the default (stderr + TTY-gated notice). - browse refs still prints the cached maps on demand; ref-based commands unaffected. - evals(browse_cli): derive refCount from the tree when maps are absent; add BROWSE_SNAPSHOT_FULL toggle for the core-tier represent() path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses PR review: - Default snapshot now emits the full tree (no line pruning) and only omits the ref maps; --full adds xpathMap/urlMap. Verified default tree === --full tree. - Driver snapshot handler takes `full` instead of overloading `compact`. - Simplify the command description and drop verbose/historical comments. - Add a driver test: default omits maps, --full includes them, maps cached either way. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verify setRefMaps runs on the default (lean) call specifically via call-count + last-called-with, so caching moving into the --full path would fail the test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ab29d17 to
c05d2a4
Compare
The `browse refs` command was removed on main; the snapshot command description still pointed users to it. Remove the stale clause. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop implementation details (byte/token counts, server-side caching, `--compact` no-op mechanics) per changeset-brevity convention. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@shrey150 dont forget to update the readme. it still says |
`browse snapshot` is lean by default now; replace `--compact` (deprecated no-op) with `--full` for including the ref maps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Good catch — fixed in 8754565: README quick-start + snapshot examples now use |
…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>
… ref maps (browserbase#2296) ## Summary `browse snapshot` now prints the formatted accessibility tree only by default, omitting the `xpathMap`/`urlMap` ref maps that were previously included on **every** call. - **`browse snapshot`** (default): formatted tree only (~14KB on a content-heavy page). - **`browse snapshot --full`**: tree + `xpathMap` + `urlMap`. - **`browse snapshot --compact`**: deprecated no-op alias of the default (prints a stderr-only, TTY-gated deprecation notice). - **`browse refs`**: prints the cached maps on demand. Ref-based element commands (`click`, `fill`, `select`, …) are **unaffected** — the maps are still captured and cached server-side, so refs resolve exactly as before. ## Why The default snapshot emitted **~241KB (~60K tokens)** on a content-heavy page, of which **~217KB (90%) is the `xpathMap`** (a ref→XPath entry for every node) + `urlMap`. The tree an agent actually reasons over is only ~13.5KB. Because refs resolve from a **server-side cache** (not from stdout), the printed maps were dead weight in the context of any agent consuming `browse snapshot` output. Making the tree lean by default cuts the default payload ~17× with no loss to element interaction; `--full` and `browse refs` recover the maps when needed. ## E2E Test Matrix ### Behavior + size (local build, live session on capeair.com) Verified against `packages/cli/bin/run.js` (the local build), not the published `browse`. | Command / flow | Observed output | Confidence / sufficiency | | --- | --- | --- | | `browse snapshot` (default) | `13,845` bytes, keys = `tree` only, `hasMaps=false` | Lean default; ~17× smaller than before | | `browse snapshot --full` | `241,104` bytes, keys = `tree,urlMap,xpathMap` (1,640 entries) | `--full` returns the full tree + maps | | `browse snapshot --compact` (piped) | `13,868` bytes, `hasMaps=false`, **0 bytes stderr** | Deprecated alias == default; no noise on non-TTY | | `browse refs` | `count=1640` | Maps still captured + retrievable on demand | | `browse click @0-1588` after a **lean** snapshot | `{"clicked": true}` → navigated to `/about_us/` | Ref interaction unaffected by dropping maps from stdout | | default tree vs `--full` tree | identical (341 lines both) | Lean mode omits **only** the maps; it does not prune tree content | | `pnpm --dir packages/cli build` (`tsc`) + evals `tsc --noEmit` | success / clean | Typechecks | | `driver-commands` unit test | 15/15 pass | Locks in: default omits maps, `--full` includes them, maps cached in both modes | ### Agent task-success A/B (lean vs full) Controlled A/B — same task / model (Sonnet) / session per pair; **the only variable is snapshot mode** (`--full` = maps vs default = lean), against the local build. | Task | Arm | Outcome | Snapshots | Agent tokens | | --- | --- | --- | --- | --- | | capeair (regional booking) | full (maps) | ✅ SUCCESS | 9 | 109.8K | | capeair | **lean** | ✅ SUCCESS | 8 | 93.5K | | Google Flights | full (maps) | ✅ SUCCESS | 15 | 85.3K | | Google Flights | **lean** | ✅ SUCCESS | 15 | 86.4K | **4/4 success — lean-by-default held task success with zero capability loss.** (A standard-benchmark A/B via the evals package is in progress and will be added here.) ## What's in the diff - `packages/cli/src/commands/snapshot.ts` — lean default (`compact` decoupled from map omission); add `--full`; deprecate `--compact`. - `packages/cli/src/lib/driver/commands/snapshot.ts` — driver handler emits the full tree and includes ref maps only when `full` is requested. - `packages/cli/skills/browse/SKILL.md` — document lean default + `--full`. - `packages/cli/tests/driver-commands.test.ts` — test the default/`--full`/caching behavior. - `packages/evals/core/tools/browse_cli.ts` — derive `refCount` from the tree when maps are absent. - `.changeset/lean-browse-snapshot.md` — `browse` patch. Linear: [STG-2450](https://linear.app/browserbase/issue/STG-2450) ---------
…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
browse snapshotnow prints the formatted accessibility tree only by default, omitting thexpathMap/urlMapref maps that were previously included on every call.browse snapshot(default): formatted tree only (~14KB on a content-heavy page).browse snapshot --full: tree +xpathMap+urlMap.browse snapshot --compact: deprecated no-op alias of the default (prints a stderr-only, TTY-gated deprecation notice).browse refs: prints the cached maps on demand.Ref-based element commands (
click,fill,select, …) are unaffected — the maps are still captured and cached server-side, so refs resolve exactly as before.Why
The default snapshot emitted ~241KB (~60K tokens) on a content-heavy page, of which ~217KB (90%) is the
xpathMap(a ref→XPath entry for every node) +urlMap. The tree an agent actually reasons over is only ~13.5KB.Because refs resolve from a server-side cache (not from stdout), the printed maps were dead weight in the context of any agent consuming
browse snapshotoutput. Making the tree lean by default cuts the default payload ~17× with no loss to element interaction;--fullandbrowse refsrecover the maps when needed.E2E Test Matrix
Behavior + size (local build, live session on capeair.com)
Verified against
packages/cli/bin/run.js(the local build), not the publishedbrowse.browse snapshot(default)13,845bytes, keys =treeonly,hasMaps=falsebrowse snapshot --full241,104bytes, keys =tree,urlMap,xpathMap(1,640 entries)--fullreturns the full tree + mapsbrowse snapshot --compact(piped)13,868bytes,hasMaps=false, 0 bytes stderrbrowse refscount=1640browse click @0-1588after a lean snapshot{"clicked": true}→ navigated to/about_us/--fulltreepnpm --dir packages/cli build(tsc) + evalstsc --noEmitdriver-commandsunit test--fullincludes them, maps cached in both modesAgent task-success A/B (lean vs full)
Controlled A/B — same task / model (Sonnet) / session per pair; the only variable is snapshot mode (
--full= maps vs default = lean), against the local build.4/4 success — lean-by-default held task success with zero capability loss. (A standard-benchmark A/B via the evals package is in progress and will be added here.)
What's in the diff
packages/cli/src/commands/snapshot.ts— lean default (compactdecoupled from map omission); add--full; deprecate--compact.packages/cli/src/lib/driver/commands/snapshot.ts— driver handler emits the full tree and includes ref maps only whenfullis requested.packages/cli/skills/browse/SKILL.md— document lean default +--full.packages/cli/tests/driver-commands.test.ts— test the default/--full/caching behavior.packages/evals/core/tools/browse_cli.ts— deriverefCountfrom the tree when maps are absent..changeset/lean-browse-snapshot.md—browsepatch.Linear: STG-2450