Skip to content

Sync#2

Open
metehanozdev wants to merge 985 commits into
emregucerr:mainfrom
browserbase:main
Open

Sync#2
metehanozdev wants to merge 985 commits into
emregucerr:mainfrom
browserbase:main

Conversation

@metehanozdev

Copy link
Copy Markdown
Collaborator

why

what changed

test plan

github-actions Bot and others added 29 commits March 18, 2026 14:38
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @browserbasehq/browse-cli@0.2.0

### Minor Changes

- [#1816](#1816)
[`687d54a`](687d54a)
Thanks [@shrey150](https://github.com/shrey150)! - Add `--context-id`
and `--persist` flags to `browse open` for loading and persisting
Browserbase Contexts across sessions

- [#1793](#1793)
[`e38c13b`](e38c13b)
Thanks [@shrey150](https://github.com/shrey150)! - Initial release of
browse CLI - browser automation for AI agents

### Patch Changes

- [#1806](#1806)
[`f8c7738`](f8c7738)
Thanks [@shrey150](https://github.com/shrey150)! - Fix `browse env`
showing stale mode after `browse env remote`

- Updated dependencies
\[[`505e8c6`](505e8c6),
[`2f43ffa`](2f43ffa),
[`63ee247`](63ee247),
[`7dc35f5`](7dc35f5),
[`335cf47`](335cf47),
[`6ba0a1d`](6ba0a1d),
[`4ff3bb8`](4ff3bb8),
[`c27054b`](c27054b),
[`2abf5b9`](2abf5b9),
[`7817fcc`](7817fcc),
[`7390508`](7390508),
[`611f43a`](611f43a),
[`521a10e`](521a10e),
[`2402a3c`](2402a3c)]:
    -   @browserbasehq/stagehand@3.2.0

## @browserbasehq/stagehand@3.2.0

### Minor Changes

- [#1779](#1779)
[`2f43ffa`](2f43ffa)
Thanks [@shrey150](https://github.com/shrey150)! - feat: add
`cdpHeaders` option to `localBrowserLaunchOptions` for passing custom
HTTP headers when connecting to an existing browser via CDP URL

- [#1834](#1834)
[`63ee247`](63ee247)
Thanks [@tkattkat](https://github.com/tkattkat)! - Update stagehand
agents search tool

- [#1774](#1774)
[`521a10e`](521a10e)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add new
page.setExtraHTTPHeaders() method

### Patch Changes

- [#1759](#1759)
[`505e8c6`](505e8c6)
Thanks [@shrey150](https://github.com/shrey150)! - Add bedrock to the
provider enum in model configuration schemas and regenerate OpenAPI
spec.

- [#1814](#1814)
[`7dc35f5`](7dc35f5)
Thanks [@tkattkat](https://github.com/tkattkat)! - Change usage of
openai provider in agent to default to store:false

- [#1846](#1846)
[`335cf47`](335cf47)
Thanks [@aq17](https://github.com/aq17)! - Fix streaming finished event
being silently dropped. The final SSE event containing the result
payload (success status, message, actions, usage, and messages) was
previously discarded instead of being yielded to the caller.

- [#1764](#1764)
[`6ba0a1d`](6ba0a1d)
Thanks [@shrey150](https://github.com/shrey150)! - Expose `headers` in
`GoogleVertexProviderSettings` so model configs can pass custom provider
headers (for example `X-Goog-Priority`) without TypeScript errors.

- [#1847](#1847)
[`4ff3bb8`](4ff3bb8)
Thanks [@miguelg719](https://github.com/miguelg719)! - Enable FlowLogger
on BROWSERBASE_FLOW_LOGS=1

- [#1752](#1752)
[`c27054b`](c27054b)
Thanks [@derekmeegan](https://github.com/derekmeegan)! - fix: pause
Browserbase agents while captcha solving is active and improve CUA
recovery after the solve completes

- [#1800](#1800)
[`2abf5b9`](2abf5b9)
Thanks [@shrey150](https://github.com/shrey150)! - Make projectId
optional for Browserbase sessions — only BROWSERBASE_API_KEY is required

- [#1766](#1766)
[`7817fcc`](7817fcc)
Thanks [@tkattkat](https://github.com/tkattkat)! - Add configurable
timeout to tools in agent

- [#1749](#1749)
[`7390508`](7390508)
Thanks [@pirate](https://github.com/pirate)! - When connecting to a
browser session that has zero open tabs, Stagehand now automatically
creates an initial `about:blank` tab so the connection can continue.

- [#1761](#1761)
[`611f43a`](611f43a)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - fix issue
where handlePossibleNavigation was producing unnecessary error logs on
clicks that trigger page close

- [#1817](#1817)
[`2402a3c`](2402a3c)
Thanks [@tkattkat](https://github.com/tkattkat)! - Add support for
passing custom headers in clientOptions

## @browserbasehq/stagehand-evals@1.1.9

### Patch Changes

- Updated dependencies
\[[`505e8c6`](505e8c6),
[`2f43ffa`](2f43ffa),
[`63ee247`](63ee247),
[`7dc35f5`](7dc35f5),
[`335cf47`](335cf47),
[`6ba0a1d`](6ba0a1d),
[`4ff3bb8`](4ff3bb8),
[`c27054b`](c27054b),
[`2abf5b9`](2abf5b9),
[`7817fcc`](7817fcc),
[`7390508`](7390508),
[`611f43a`](611f43a),
[`521a10e`](521a10e),
[`2402a3c`](2402a3c)]:
    -   @browserbasehq/stagehand@3.2.0

## @browserbasehq/stagehand-server-v3@3.6.1

### Patch Changes

- [#1759](#1759)
[`505e8c6`](505e8c6)
Thanks [@shrey150](https://github.com/shrey150)! - Add bedrock to the
provider enum in model configuration schemas and regenerate OpenAPI
spec.

- Updated dependencies
\[[`505e8c6`](505e8c6),
[`2f43ffa`](2f43ffa),
[`63ee247`](63ee247),
[`7dc35f5`](7dc35f5),
[`335cf47`](335cf47),
[`6ba0a1d`](6ba0a1d),
[`4ff3bb8`](4ff3bb8),
[`c27054b`](c27054b),
[`2abf5b9`](2abf5b9),
[`7817fcc`](7817fcc),
[`7390508`](7390508),
[`611f43a`](611f43a),
[`521a10e`](521a10e),
[`2402a3c`](2402a3c)]:
    -   @browserbasehq/stagehand@3.2.0

## @browserbasehq/stagehand-server-v4@3.6.1

### Patch Changes

- Updated dependencies
\[[`505e8c6`](505e8c6),
[`2f43ffa`](2f43ffa),
[`63ee247`](63ee247),
[`7dc35f5`](7dc35f5),
[`335cf47`](335cf47),
[`6ba0a1d`](6ba0a1d),
[`4ff3bb8`](4ff3bb8),
[`c27054b`](c27054b),
[`2abf5b9`](2abf5b9),
[`7817fcc`](7817fcc),
[`7390508`](7390508),
[`611f43a`](611f43a),
[`521a10e`](521a10e),
[`2402a3c`](2402a3c)]:
    -   @browserbasehq/stagehand@3.2.0

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
# why
- to add docs for the new `page.setExtraHTTPHeaders()` function
# what changed
### added docs to the page reference section:
<img width="748" height="525" alt="Screenshot 2026-03-17 at 1 49 33 PM"
src="https://github.com/user-attachments/assets/a97d8903-a17d-4af4-88e6-5d9770cb7227"
/>

### added an example in the code examples section:
<img width="724" height="766" alt="Screenshot 2026-03-17 at 1 50 26 PM"
src="https://github.com/user-attachments/assets/3e3da652-70db-4758-b388-c04316f99895"
/>





<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Added docs for `page.setExtraHTTPHeaders()` in the Page reference and
examples, covering page‑scoped behavior (applies to all requests and
child sessions) and how to replace or clear headers. Addresses Linear
STG-1413.

<sup>Written for commit be130f0.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1842">Review in
cubic</a></sup>

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

# what changed

# test plan

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Reverts an unintended test commit by deleting
`packages/core/tests/unit/rerender-missing-shadows.test.ts`; test suite
returns to its previous state. Also updates `turbo.json` to pass through
`BROWSERBASE_FLOW_LOGS`; no production code changes.

<sup>Written for commit 49fdf8c.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1850">Review in
cubic</a></sup>

<!-- End of auto-generated description by cubic. -->
# why
- `toolTimeout` was only being applied to the `act`, `extract`,
`fillForm`, & `ariaTree` tools
- we want the `toolTimeout` to take effect across all of the tools (with
the exception of `wait` & `think`)
- additionally, when a timeout was being hit, the duration of the
timeout was not being included in the error logs
# what changed
- wrapped all agent tool calls (other than `wait` & `think`) in
`withTimeout()`
- update error logs to include the timeout duration in the log message
### updated error logs with timeout duration:
<img width="887" height="98" alt="Screenshot 2026-03-17 at 3 53 50 PM"
src="https://github.com/user-attachments/assets/957d2e68-0078-47c2-98d7-32f28f76f105"
/>

### example of tool call respecting timeout (`screenshot()` had no
`toolTimeout` before)

<img width="698" height="108" alt="Screenshot 2026-03-17 at 4 13 02 PM"
src="https://github.com/user-attachments/assets/344df89d-f545-4380-92bb-562633e34ce9"
/>

# test plan
- extended tests in `timeout.spec.ts` to validate that all of the tools
correctly respect `toolTimeout`

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Applies a user-defined `toolTimeout` to all async agent tools (except
`wait` and `think`) via a centralized wrapper at tool init. Timeouts
return a consistent `TimeoutError` that includes the duration (ms) and
short hints. Addresses Linear STG-1510.

- New Features
- Centralized timeouts: wrap tool execute in `createAgentTools()` with
`withTimeout()`; covers `act`, `extract`, `fillForm`, `fillFormVision`,
`ariaTree`, `click`, `clickAndHold`, `dragAndDrop`, `type`, `keys`,
`scroll` (DOM/hybrid), `screenshot`, `goto`, `navback`, and `search`
(Brave/Browserbase).
- Consistent errors and logs: tools re-throw timeouts to the wrapper;
wrapper logs and returns `{ success: false, error }`; simplified
non-timeout messages in `search`, `click`, `clickAndHold`,
`dragAndDrop`, `type`, and `keys`.
- Tests: expanded `timeouts.spec.ts` to cover DOM and hybrid tools;
asserts the timeout duration (e.g., "1ms") is included.

- Migration
- No breaking changes. Set `toolTimeout` via `createAgentTools(v3, {
toolTimeout: <ms> })` or `v3.agent({ toolTimeout: <ms> })`.

<sup>Written for commit 9e81879.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1843">Review in
cubic</a></sup>

<!-- End of auto-generated description by cubic. -->
## Summary
- revert the Stainless streaming config change that turned the terminal
finished event from `done` into `yield`
- restore the pre-#1846 behavior while we replace it with a compatible
fix

## Testing
- `pnpm --dir /tmp/stagehand-revert-sse.mrKx4n exec prettier --check
stainless.yml`


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Reverts the Stainless streaming config so the finished SSE event is
handled as done instead of yield. Restores previous behavior and fixes
stream completion for clients expecting done.

<sup>Written for commit 687a11f.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1856">Review in
cubic</a></sup>

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

The `fillForm` tool (DOM mode) had a `value` field in its input schema
that the LLM would populate, but it was never actually used during
execution. The `execute` function only read `f.action` to build the
observe instruction — the `value` was completely ignored. This meant the
tool relied entirely on the action string (e.g. `"type foo into the
email field"`) to carry both the target and the value, while `value` was
dead weight.

This was also a problem for the `variables` feature: the schema told the
LLM to use `%variableName%` syntax in the `value` field, but since
`value` was never consumed, variable substitution through it could never
work.

# what changed

- **`fillform.ts`**: Removed the `value` field from the input schema.
The `action` field now explicitly guides the LLM to include the value to
type *within* the action description (e.g. `"type foo@bar.com into the
email field"`). When variables are available, the description shows
`%variableName%` syntax with an example. This aligns the schema with how
the tool actually works — `observe` parses the action string and returns
`Action` objects with the correct `method`, `selector`, and `arguments`,
which `act` then executes.
- **`agentSystemPrompt.ts`**: Removed the stale reference to `value` in
the variable usage instructions for DOM mode (`"value/action fields"` →
`"action fields"`).
- **`cache.ts`**: Updated `AgentReplayFillFormStep.fields` type to drop
`value`.
- **Tests**: Removed `value` from test fixtures in `timeouts.spec.ts`
and `agent-execution-model.test.ts`.

# test plan

- [ ] Run unit tests: `agent-execution-model.test.ts` passes with the
updated fillForm field shape
- [ ] Run integration tests: `timeouts.spec.ts` fillForm timeout test
passes
- [ ] Run agent eval with a form-filling task (e.g. `sign_in`) to verify
the LLM correctly embeds values in the action string and fields get
filled as expected
- [ ] Test with variables: confirm `%variableName%` in the action string
gets substituted correctly (e.g. `"type %password% into the password
field"`)

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Removed the unused `value` field from the DOM‑mode `fillForm` tool;
values must now be included in the `action` string using a strict "type
<value> into the <field>" pattern. Also removed the emoji from the tool
description and aligned variable guidance, types, and tests.

- **Migration**
- Update `fillForm` inputs to only use `action` that follows: "type
<exact value> into the <field name> <fieldType>", e.g. `{ fields: [{
action: "type john@example.com into the email input" }] }`.
- Use `%variableName%` inside the `action` string, e.g. `type %password%
into the password input`.
- If you reference replay types, `AgentReplayFillFormStep.fields` now
only includes `{ action: string }`.

<sup>Written for commit aaf6e67.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1811">Review in
cubic</a></sup>

<!-- End of auto-generated description by cubic. -->
# why
This splits the v4 route groups into real Fastify plugins so each group
owns its own registration and error boundary. It removes the
path-sniffing logic from `server.ts` and makes the v4 stub server a
cleaner base for future route work.

# what changed
`page` and `browsersession` now register through Fastify plugins instead
of array loops in `server.ts`, and each plugin has its own encapsulated
error handler. `server.ts` is reduced to composing the two v4 plugins
under `/v4`.

# test plan
- `pnpm --filter @browserbasehq/stagehand-server-v4 lint`
- `pnpm --filter @browserbasehq/stagehand-server-v4 build`



<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Grouped v4 `page` and `browsersession` routes into Fastify plugins under
`/v4`, each with its own error boundary and OpenAPI tag. Added shared
helpers for tagging and typed error normalization to simplify
`server.ts` and complete STG-1610.

- **Refactors**
- Introduced `pageRoutesPlugin` and `browserSessionRoutesPlugin`;
registered at `/v4`.
- Added shared `pluginUtils` (`withTag`, `normalizePluginError`) used by
both plugins.
- Plugins tag routes (`page`, `browserSession`) and return typed
validation/serialization errors via `buildErrorResponse` and
`buildBrowserSessionErrorResponse`; applied tags across
`openapi.v4.yaml`.
- Simplified the app-level error handler to a generic fallback; removed
path sniffing and route array loops from `server.ts`.

<sup>Written for commit e4a6876.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1851">Review in
cubic</a></sup>

<!-- End of auto-generated description by cubic. -->
# why
- so that the stainless generated SDKs can pass in the `useSearch` &
`toolTimeout` params
# what changed
- updated the `agentExecuteSchema` to include the above params

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Added `useSearch` and `toolTimeout` to Agent Execute options in public
types (`AgentExecuteOptionsSchema`) and the OpenAPI spec, and added V3
server tests to validate both fields. This lets Stainless SDKs enable
web search and set per-tool call timeouts (STG-1621).

<sup>Written for commit 71ce7e8.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1854">Review in
cubic</a></sup>

<!-- End of auto-generated description by cubic. -->
# why
- to unify `selector` into a single field across page request bodies
- this groups semantically similar fields into a shared & reusable type
# what changed
- replaced `PageSelectorSchema` (xpath-only object) with a discriminated
union of 4 selector types:
  - `PageXPathSelectorSchema`, 
  - `PageCssSelectorSchema`,
  - `PageTextSelectorSchema`, 
- `PageCoordinateSelectorSchema`
- added `PageElementSelectorSchema` (xpath/css/text, no coordinates) for
`waitForSelector`
- collapsed the dual `*SelectorParams` / `*CoordinatesParams` schema
pairs for click, hover, scroll, and dragAndDrop into single schemas with
a unified `selector` field

### example `/hover` before:
<img width="557" height="842" alt="Screenshot 2026-03-19 at 1 39 06 PM"
src="https://github.com/user-attachments/assets/efda7d84-bcd4-4a70-9bf9-7674ced59614"
/>

### example `/hover` after:
<img width="556" height="956" alt="Screenshot 2026-03-19 at 1 41 14 PM"
src="https://github.com/user-attachments/assets/f8a57ee7-c50f-4f06-81f0-965fb5a7d82b"
/>

# test plan
- added integration tests for css, text, and coordinate selector types

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Unifies the v4 page selector into a single `selector` union and adds
css, text, and coordinate options. This simplifies request bodies and
makes click, hover, scroll, and drag‑and‑drop consistent.

- **New Features**
- One `selector` union for all page actions: xpath, css, text, or
coordinates.
- Introduced `Selector` and `ElementSelector` unions; split former
`PageSelector` into `XPathSelector`, `CssSelector`, `TextSelector`, and
`CoordinateSelector`.
- Added `ElementSelector` (xpath/css/text) for element‑only cases like
`waitForSelector` and scroll‑by‑percentage; exported
`PageScrollElementParams` and `PageScrollCoordinateParams`.
- `dragAndDrop` supports mixing selector types (e.g., xpath →
coordinates).
- Added integration tests for css, text, coordinate selectors, and mixed
drag‑and‑drop.

- **Migration**
- click/hover: always send `{ selector: { xpath|css|text|x,y } }`.
Remove top‑level `x`/`y`.
- scroll: use element selector + `percentage`, or coordinate selector +
`deltaY`.
- dragAndDrop: `from`/`to` now use the unified selector (elements or
coordinates).
  - waitForSelector: use `ElementSelector` (no coordinates).

<sup>Written for commit b0b15f5.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1860">Review in
cubic</a></sup>

<!-- End of auto-generated description by cubic. -->
# why
- selectors can resolve to multiple nodes
- we need to give the user freedom to optionally provide an `index`,
indicating the desired element if multiple are found
# what changed
- added an optional `index` field to `XPathSelectorSchema`,
`CssSelectorSchema` & `TextSelectorSchema`
- note: `CoordinateSelectorSchema` remains unchanged, since coordinates
are inherently specific

### selector schema before:
<img width="555" height="944" alt="Screenshot 2026-03-19 at 4 31 50 PM"
src="https://github.com/user-attachments/assets/cd020162-02c2-44f9-a69e-d226d8501571"
/>

### selector schema after: 
<img width="603" height="980" alt="Screenshot 2026-03-19 at 4 33 32 PM"
src="https://github.com/user-attachments/assets/0e730297-a5db-4a82-a7ee-b828a8520275"
/>

# test plan
- added new test case in `packages/server-v4/src/schemas/v4/page.ts`

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds an optional non-negative idx to XPath, CSS, and text selectors so
callers can target a specific element when a selector matches multiple
nodes. Updates the v4 schema and OpenAPI spec without breaking existing
clients.

- **New Features**
- Added optional non-negative `idx` to `XPathSelectorSchema`,
`CssSelectorSchema`, and `TextSelectorSchema`.
  - Left `CoordinateSelectorSchema` unchanged.
  - Updated `openapi.v4.yaml` with the new `idx` field and examples.
- Expanded integration tests to cover `css`, `xpath`, and `text`
selectors with `idx`.

<sup>Written for commit 0c25f74.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1861">Review in
cubic</a></sup>

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

---------

Co-authored-by: Nick Sweeting <github@sweeting.me>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
# why
- to reduce bloat by removing routes that are (or will be) functionally
the same as other routes
# what changed
- removed:
  - `/mainFrame` since we already have `/mainFrameId`
  - `/asProtocolFrameTree` since we already have `/getFullFrameTree`
- `/getOrdinal` since it is unnecessary (should not really have been
exposed in the first place)
  - `/waitForMainLoadState` since we already have `/waitForLoadState`
# test plan
- removed associated tests

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Removed redundant v4 page routes and cleaned up the OpenAPI/spec and
schemas to shrink the API surface. Aligns with Linear STG-1635.

- **Refactors**
- Removed endpoints: /v4/page/mainFrame, /v4/page/asProtocolFrameTree,
/v4/page/getOrdinal, /v4/page/waitForMainLoadState
  - Updated OpenAPI, schemas, route registry, and integration tests

- **Migration**
  - Use /v4/page/mainFrameId instead of /v4/page/mainFrame
- Use /v4/page/getFullFrameTree instead of /v4/page/asProtocolFrameTree
- Use /v4/page/waitForLoadState instead of /v4/page/waitForMainLoadState
  - /v4/page/getOrdinal has no public replacement (was internal-only)

<sup>Written for commit b9cbb69.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1862">Review in
cubic</a></sup>

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

`act()` already supports variables, but `observe()` did not. That made
safety-sensitive flows like login automation harder to use correctly:
callers could inspect `observe()`
results before executing them, but then had to guess which returned
action should receive each secret value before calling `act()`.

This change brings variable support to `observe()` so it can return
placeholder-backed actions like `%username%` and `%password%`. That
preserves the existing safe pattern of:

  1. `observe()` candidate actions
  2. validate the returned actions
3. `act()` the validated actions with real variable values at execution
time

  # what changed

- Added `variables?: Variables` support to `observe()` in the public SDK
types and internal handler params.
- Threaded `observe` variables through the local SDK path, inference
layer, and prompt builder.
- Updated the observe prompt so the model sees available variable names
and returns `%variableName%` placeholders in action arguments instead of
literal sensitive values.
- Added observe variable support to the hosted/API path, including
schema updates and flattening rich variable values to the existing wire
format.
- Updated the internal `fillForm` tool to pass variables into
`observe()` as well as `act()`.
- Added docs for `observe({ variables })` and the validate-then-act
login flow.
- Added a dedicated example at
`packages/core/examples/observe_variables_login.ts` showing
placeholder-based login planning with `observe()`, explicit validation,
and execution via
  `act()`.

  # test plan

  - Ran targeted unit tests covering:
    - public `ObserveOptions` type support
    - observe variable forwarding into inference/prompting
    - placeholder preservation in returned observe actions
    - API client observe variable serialization
    - `fillForm` forwarding variables to `observe()`
  - Ran:
- `pnpm --filter @browserbasehq/stagehand exec vitest run --config
/tmp/stagehand-vitest-source.config.mjs
tests/unit/public-api/public-types.test.ts tests/unit/agent-execution-
model.test.ts tests/unit/timeout-handlers.test.ts
tests/unit/api-client-observe-variables.test.ts`
  - Ran formatting checks on changed files with Prettier.
- Added integration coverage for observe request schemas in both v3 and
v4 server tests.
- Full repo typecheck/build is still blocked by unrelated pre-existing
issues in `packages/core/lib/v3/launch/browserbase.ts` and existing
server test environment/type-resolution
  failures.
# why

the previous docs theme was sunset and changed 

# what changed

# test plan
## Why
The stainless sdks are dropping the final finished SSE event instead of
yielding it. This is due to the fact that we were not using `event`
fields (basically setting event types) as per the SSE spec.

The fix is to emit explicit SSE `event:` names and match them in
Stainless. But on the hosted API we cannot switch that on for everyone
at once, because older clients still expect the old `data:`-only SSE
framing. Thus, we will need to have branching logic in our hosted
server:

1. Legacy (old stainless sdks, stagehand-js): continue to not return
`event` field.
2. New Stainless SDKs on `>= 3.13.0`: use typed SSE framing with
`event:` + `data:`.

Once this and it's core counterpart PR are merged, then we will release
another version of all stainless sdks - `3.13`, which will be the first
typed-SSE release.

## What Changed
- emit explicit SSE `event:` names from the local v3 streaming helper
while keeping the JSON `data:` payload unchanged
- switch Stainless streaming matching to explicit `event_type` handlers
- update the documented stream shape, regenerated v3 OpenAPI, and a
focused integration assertion

## Testing
- `pnpm --dir /tmp/stagehand-local-sse.lSk5Av --filter
@browserbasehq/stagehand-server-v3 run gen:openapi`
- `pnpm --dir /tmp/stagehand-local-sse.lSk5Av exec prettier --check
stainless.yml`
- `pnpm --dir /tmp/stagehand-local-sse.lSk5Av --filter
@browserbasehq/stagehand lint`
- `pnpm --dir /tmp/stagehand-local-sse.lSk5Av --filter
@browserbasehq/stagehand-server-v3 lint`

A test run with the three different client types against the updated
server:
<img width="765" height="230" alt="Screenshot 2026-03-20 at 11 01 50 AM"
src="https://github.com/user-attachments/assets/af97c8ec-d7f9-4ae6-95a0-16a3c2906934"
/>

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Emit explicit SSE event names for v3 streaming (`starting`, `connected`,
`running`, `finished`, `error`) while keeping the JSON `data:` payload
unchanged. Updates the streaming helper, Stainless config, OpenAPI docs,
and tests to use and verify typed events.

- New Features
  - Server now sends `event: <status>` with `data: { data, type, id }`.
- Switched `stainless.yml` streaming matching to `event_type` (yields on
starting/connected/running/finished; handles `error`).
- Added integration test asserting event names match payload status;
updated OpenAPI description and core type docs.

- Dependencies
- Added changeset to publish patch updates for
`@browserbasehq/stagehand` and `@browserbasehq/stagehand-server-v3`.

<sup>Written for commit 96cd037.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1858">Review in
cubic</a></sup>

<!-- End of auto-generated description by cubic. -->
# why
Custom headers for LLM requests are supported in Stagehand, canonical
SDKs were not accounting for this until now.

# what changed
Added headers to ModelConfig

# test plan



<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds an optional `headers` field to `ModelConfig` to send custom headers
with every LLM request. Also includes a patch changeset for
`@browserbasehq/stagehand`.

- **New Features**
- Add `headers?: Record<string, string>` to `ModelConfigObjectSchema` in
`packages/core/lib/v3/types/public/api.ts`.
- Update `packages/server-v3/openapi.v3.yaml` to document `headers` in
`ModelConfig`.
- Update `packages/server-v4/openapi.v4.yaml` to use selector-based
result/output schemas for page actions (click, hover, scroll,
drag-and-drop) and add `returnSelector` support.

<sup>Written for commit f83bd8b.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1874">Review in
cubic</a></sup>

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

---------

Co-authored-by: Sean McGuire <seanmcguire1@outlook.com>
# why
JSON schema parsing for schemas generated from Pydantic
`.model_json_schema()` were dropping the nested references

# what changed
Before (from test script) with schema: 
```python
class RequiredInputType(StrEnum):
    USERNAME = "username"
    EMAIL = "email"
    PHONE = "phone"
    OTP = "otp"
    PASSWORD = "password"
    CARD_LAST_8 = "card_last_8"
    SOMETHING_ELSE = "something_else"

class PageState(StrEnum):
    LOGIN_FORM = "login_form"
    ERROR = "error"
    AUTHENTICATED = "authenticated"
    UNKNOWN = "unknown"

class PageAnalysis(BaseModel):
    page_state: PageState
    required_input: RequiredInputType | None = None
    contact_id_of_required_input: str | None = None
    error_message: str | None = None
    reasoning: str = ""
```
<img width="654" height="373" alt="Screenshot 2026-03-23 at 1 09 01 PM"
src="https://github.com/user-attachments/assets/29a24eea-cfda-40a8-8e43-10f959c71ad2"
/>

After: 
<img width="650" height="310" alt="Screenshot 2026-03-23 at 1 06 17 PM"
src="https://github.com/user-attachments/assets/9f62d1ad-04bb-41e4-9dbc-85d7b68c84aa"
/>

1. Added resolveRefs() function that inlines $ref/$defs before Zod
conversion (Pydantic
  v2's model_json_schema() generates these)
2. Changed z.string().refine() → z.enum() so enum constraints are
expressed in the
  schema sent to the LLM, not just validated post-hoc


# test plan

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes missing nested `$ref` when converting Pydantic v2
`.model_json_schema()` to Zod in `@browserbasehq/stagehand-server-v3`,
and emits proper `z.enum()` types so LLM-facing schemas are accurate.
Adds a `test:unit` script, moves tests to `tests/`, and updates the
runner to scan directories or single files.

- **Bug Fixes**
- Resolves `$ref` from `$defs` before conversion via `resolveRefs()`
with a cycle guard, handling refs in objects, arrays,
`anyOf`/`oneOf`/`allOf`, and root-level refs (unknown refs fall back
safely).
- Replaces `z.string().refine()` with `z.enum()` so enum values are
surfaced in generated schemas and enforced.

<sup>Written for commit ae9e2e5.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1873">Review in
cubic</a></sup>

<!-- End of auto-generated description by cubic. -->
## Summary
- Adds `userMetadata: { "browse-cli": "true" }` to every Browserbase
session created through the browse CLI
- Enables usage attribution to distinguish CLI-originated sessions from
SDK-originated ones

## Test plan
- [x] Run `browse open https://example.com` in Browserbase mode and
verify the session has `browse-cli: true` in its metadata
- [x] Verify local mode is unaffected

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

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds `userMetadata: { "browse-cli": "true" }` to
`browserbaseSessionCreateParams` for sessions started by
`@browserbasehq/browse-cli` in Browserbase mode, enabling usage
attribution and distinguishing CLI vs SDK sessions. Satisfies STG-1678;
local mode is unaffected.

<sup>Written for commit 58c10fd.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1890">Review in
cubic</a></sup>

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

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…1887)

## Summary
- When the browse CLI daemon outlives its Chrome browser (e.g., between
Claude Code sessions), `ensureBrowserInitialized()` returns the cached
`stagehand`/`context` without checking if the browser is still alive
- This causes `"No Page found for awaitActivePage: no page available"`
errors requiring a manual `browse stop` + retry
- Fix: register an `onTransportClosed` handler on the CDP connection
that clears the cached state, so the next command triggers a full
re-initialization

## Root cause
The daemon is spawned as a detached process (`detached: true`,
`child.unref()`) and persists across CLI sessions. When Chrome dies,
`_onCdpClosed` fires and calls `stagehand.close()`, but the daemon's
closure variables `stagehand` and `context` are never cleared. The next
command hits the early return at `if (stagehand && context) { return ...
}` and tries to use dead objects.

## Test plan

**Regression test added to PR is illustrative** 

- [x] Start a `browse` session, close Chrome manually, run another
`browse open` — should auto-recover instead of erroring
- [x] Normal `browse` workflow (open, snapshot, click, stop) still works
- [x] Daemon restart across mode switches (local ↔ remote) still works
- [x] Regression test added: kills Chrome under daemon, verifies
auto-recovery (fails on main, passes with fix)

## Verified test results

**On `main` (without fix):** Test fails — after killing Chrome, the
retry returns exit code 1 with `"Error: No Page found for
awaitActivePage: no page available"`. The daemon is alive but returns
stale cached Stagehand/context objects.

**On this branch (with fix):** Test passes — after killing Chrome, the
`onTransportClosed` handler nulls the cached state, and the retry
triggers a full browser re-initialization. All 37 CLI tests pass (full
suite, 67s).

> **Note:** When building locally, use `npx tsup src/index.ts --format
cjs --out-dir dist --no-splitting` inside `packages/cli/` rather than
`pnpm run build:cli`, since turbo may serve a stale cached build that
doesn't include the fix.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…essions (#1889)

## Summary

- Adds a global `--connect <session-id>` flag to the `browse` CLI that
connects to an existing Browserbase session instead of creating a new
one
- Uses Stagehand core's existing `browserbaseSessionID` support with
`keepAlive: true` so the session stays alive after the CLI disconnects
- Follows the same state-file pattern as `--context-id` (writes session
ID to `/tmp/browse-{session}.connect`, daemon reads it during
initialization)

## Test procedure

```
# 1. Sync cookies
node cookie-sync.mjs --domains github.com
# → Session ID: 3ee737d4-237f-4557-a1ca-09a3d5dcbaf2

# 2. Connect and browse (dev build)
browse --connect 3ee737d4-... open https://github.com/notifications  ✅
browse eval "..."  ✅
```

## Tests completed

- [x] `browse --connect <id> open ...` in local mode → errors with "only
supported in remote mode"
- [x] `browse --connect <id> open --context-id <ctx> ...` → errors with
mutual exclusion message
- [x] `--connect` writes session ID to connect file before daemon start
- [x] Commands without `--connect` clear stale connect file
- [x] `browse status` output includes `browserbaseSessionId` field
- [x] Manual: `browse --connect <valid-bb-session> open
https://example.com` connects and navigates
- [x] Manual: `browse stop` disconnects without killing the BB session

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

Fixes STG-1672


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds a `--connect <session-id>` flag to the `browse` CLI to attach to an
existing Browserbase session instead of creating a new one, leaving the
session running after the CLI exits. Implements STG-1672.

- **New Features**
- New global `--connect <session-id>` for `browse` to attach to an
existing Browserbase session
  - Remote mode only; errors in local mode
  - Mutually exclusive with `--context-id`
- Persists the session ID to `/tmp/browse-{session}.connect`; clears
when unused and restarts the daemon if the ID changes
- Uses `browserbaseSessionID` with `keepAlive: true` so the session
persists after disconnect
  - `browse status` now includes a `browserbaseSessionId` field
  - Minor release for `@browserbasehq/browse-cli`

<sup>Written for commit aff1bad.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1889">Review in
cubic</a></sup>

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary

- Adds `@browserbasehq/browse-cli` to the changeset `ignore` list so the
main "Version Packages" PR no longer includes CLI version bumps
- Creates a new `release-cli.yml` workflow (triggered via
`workflow_dispatch`) that independently versions and publishes
browse-cli to npm

### How it works

**Main release flow (`release.yml`)** — unchanged, but now skips
browse-cli. Core stagehand changesets are versioned and published as
before.

**CLI release flow (`release-cli.yml`)** — triggered manually via GitHub
Actions "Run workflow":
1. Detects pending browse-cli changesets (fails early if a changeset
mixes browse-cli with other packages)
2. Temporarily swaps the changeset ignore config to version only
browse-cli
3. Runs `changeset version` to consume CLI changesets and bump
`packages/cli/package.json`
4. Builds all packages, publishes browse-cli to npm via Trusted
Publishing
5. Commits the version bump and pushes a git tag

### Constraints
- Changesets that reference **both** browse-cli and another package
(e.g., stagehand core) must be split into separate changeset files — the
workflow enforces this with a pre-check
- browse-cli is excluded from canary (`alpha`) releases — add a canary
step to `release-cli.yml` if needed later

## Test plan
- [ ] Verify existing `release.yml` still creates "Version Packages" PR
for core changesets (browse-cli changesets should be excluded)
- [ ] Trigger `release-cli.yml` via workflow_dispatch with a pending
browse-cli changeset and verify it publishes correctly
- [ ] Verify mixed changeset detection fails the workflow with a clear
error

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

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Decouples the `@browserbasehq/browse-cli` release cycle from stagehand
core so we can ship CLI updates independently. Implements STG-1663.

- **New Features**
- Adds `@browserbasehq/browse-cli` to `.changeset` ignore so
`release.yml` skips CLI.
- Adds `release-cli.yml` (manual via `workflow_dispatch`, restricted to
`main`) to version and publish CLI with Trusted Publishing.
  - Pre-checks for CLI-only changesets; fails on mixed changesets.
- Temporarily swaps `.changeset` ignore to version only CLI and strips
the `@browserbasehq/stagehand` workspace dep before `changeset version`
to satisfy Changesets; restores both after.
- Builds and publishes `packages/cli` to npm, commits the bump, and
tags.

- **Migration**
  - Split changesets that touch CLI and core.
  - Use the “Release CLI” workflow to publish CLI releases.
  - Canary releases for CLI are not included.

<sup>Written for commit c298904.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1885">Review in
cubic</a></sup>

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e-cli workflow (#1893)

## Summary

- The `pnpm changeset version` step in `release-cli.yml` calls
`@changesets/changelog-github`, which fetches PR metadata from the
GitHub API and requires `GITHUB_TOKEN` to authenticate
- The token was only passed to the "Commit version bump and tag" step,
so the version step failed with a 403 when trying to generate changelogs
- Added `env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}` to the "Version
browse-cli" step

**Failed run:**
https://github.com/browserbase/stagehand/actions/runs/23624352586/job/68810210512

## Test plan

- [ ] Trigger the `Release CLI` workflow on main after merging and
verify the version step succeeds

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

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Pass `GITHUB_TOKEN` to the changeset version step in the Release CLI
workflow to authenticate `@changesets/changelog-github` and prevent 403s
during changelog generation. Addresses STG-xxx.

- **Bug Fixes**
- Added `env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}` to the "Version
browse-cli" step so `pnpm changeset version` can fetch PR metadata.

<sup>Written for commit 2f49dfb.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1893">Review in
cubic</a></sup>

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

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# why

- OpenAI now requires a screenshot on initial message for computer use
agents

# what changed

- added screenshot to initial message 
- imported types from openai sdk for stronger typing 

# test plan

- tested locally using operator-example script 

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Send an initial screenshot with the first message in `OpenAICUAClient`
to meet OpenAI’s computer-use requirement and prevent startup errors.
Also adopt `openai` SDK response types for stricter typing.

- **Bug Fixes**
- Capture and attach a high-detail screenshot on the first request when
available; gracefully skip on failure.
- Build the first message using `EasyInputMessage`, `ResponseInputText`,
and `ResponseInputImage`; include an optional system message from
`userProvidedInstructions`; make `createInitialInputItems` async and
update method signatures to accept a unified `OpenAIRequestInputItem`
type.

- **Dependencies**
  - Add changeset for a patch release of `@browserbasehq/stagehand`.

<sup>Written for commit f01d8a8.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1899">Review in
cubic</a></sup>

<!-- End of auto-generated description by cubic. -->
# why
- to group two types of clicking into a single route. ie, we don't need
to add a separate `/sendClickEvent` route
# what changed
- added an optional `synthetic` parameter to the `/click` schema, which
we will internally route to `.sendClickEvent()`

### screenshot:
<img width="436" height="693" alt="Screenshot 2026-03-24 at 3 58 50 PM"
src="https://github.com/user-attachments/assets/75a35a09-3f92-4b25-bfcb-8dc36a328088"
/>

# test plan
- added a test to verify passing `synthetic: true` passes validation

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Add a `method` param to the v4 `/click` route to support both
coordinate-based and JS event clicks in one endpoint, defaulting to `xy`
for backward compatibility. Caps `clickCount` at 3 and updates the
OpenAPI/action output. Addresses Linear STG-1659.

- **New Features**
- Adds `method: 'xy' | 'jsevent'` (default `xy`) to `/click` params;
action output now includes and requires `method` via
`PageClickParamsOutput`.
- Limits `clickCount` to 1–3 in schema and OpenAPI in
`packages/server-v4`.
- Adds integration tests for coordinate clicks and `method: "jsevent"`
selector clicks.

<sup>Written for commit c02c521.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1884">Review in
cubic</a></sup>

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

---------

Co-authored-by: Nick Sweeting <github@sweeting.me>
Adds the v4 `/llms` stubs, and the internal zod entities we will map to
drizzle DB objects later.

Note: An internal `LLMSession` is not the full accumulated history of an
entire browser session. It is the container for the LLM thread involved
in a single logical Stagehand operation, which today is usually very
small. For example, an `act()` call may create one `LLMSession` with one
`LLMCall`; `extract()` may create one `LLMSession` with two `LLMCall`
rows. It is **not** a giant transcript of every LLM call across the
whole browser session.

### Public:
- `/v4/llms` for reusable LLM configs
- `BrowserSession.llmId` as the primary/default LLM config reference,
plus `actLlmId`, `observeLlmId`, and `extractLlmId` as public
per-operation override references

### Internal only (for now):
- `LLMSession`
- `LLMCall`
- `StagehandStep`
- the browser-session-to-llm-session/call relationships shown in the
diagram below

We are intentionally not exposing `LLMSession` / `LLMCall` history in
this PR. That data can still be surfaced through logs/streaming work,
and we can decide later whether we want a first-class public API for it.

### Sequence Example
1. User creates a browser session.
2. If they do not pass `llmId`, the server materializes a real default
LLM config with `source = "system-default"` and attaches it to the
session.
3. If they want custom behavior up front, they can `POST /v4/llms` first
and then create the browser session with `llmId`.
4. If they want different defaults for specific operations, they can set
`actLlmId`, `observeLlmId`, and/or `extractLlmId` on the browser
session.
5. When an `act` / `observe` / `extract` call runs, Stagehand resolves
the template config from the per-operation override or falls back to
`llmId`, creates a dedicated `LLMSession` for that operation, and
records one or more `LLMCall` rows under it.
6. Updating an LLM config changes that reusable config resource;
updating a browser session changes which config ids future steps resolve
from.

```mermaid
classDiagram
  class BrowserSession {
    id: uuid
    projectId: uuid
    env: BrowserSessionEnv
    status: BrowserSessionStatus
    browserbaseSessionId: uuid?
    cdpUrl: string?
    llmId: uuid
    actLlmId: uuid?
    observeLlmId: uuid?
    extractLlmId: uuid?
    createdAt: datetime
    updatedAt: datetime
    endedAt: datetime?
  }

  class LLMSession {
    id: uuid
    copiedTemplateId: uuid?
    forkedSessionId: uuid?
    projectId: uuid
    browserSessionId: uuid
    createdAt: datetime
    updatedAt: datetime
    connectedAt: datetime?
    disconnectedAt: datetime?
    lastRequestAt: datetime?
    lastResponseAt: datetime?
    lastErrorAt: datetime?
    lastErrorMessage: string?
    status: LLMSessionStatus
    model: string
    baseUrl: string?
    options: json?
    extraHttpHeaders: json?
    systemPrompt: string?
    tokensInput: int
    tokensOutput: int
    tokensReasoning: int
    tokensCachedInput: int
    tokensTotal: int
  }

  class LLMCall {
    id: uuid
    llmSessionId: uuid
    sentAt: datetime
    receivedAt: datetime?
    prompt: string
    expectedResponseSchema: json?
    response: json?
    error: json?
    usage: json?
    model: string
  }

  class StagehandStep {
    id: uuid
    stagehandBrowserSessionId: uuid
    operation: StagehandStepOperation
    llmTemplateId: uuid
    llmSessionId: uuid?
    params: json
    result: json?
  }

  class LLMConfig {
    id: uuid
    projectId: uuid
    source: LLMSource
    displayName: string?
    modelName: string
    baseUrl: string?
    systemPrompt: string?
    providerOptions: LLMProviderOptions?
    createdAt: datetime
    updatedAt: datetime
  }

  BrowserSession "1" <-- "*" LLMSession : has many
  LLMSession "1" --> "*" LLMCall : has many
  BrowserSession "1" --> "*" StagehandStep : has many
  StagehandStep "1" --> "0..1" LLMSession : resolves to
  LLMConfig "1" <-- "*" BrowserSession : is referenced by many
```

Implementation notes
- Auto-created default configs are real `/llms` resources and appear in
`GET /v4/llms`.
- Defaults should be treated as versioned server presets in
implementation, not just implicit column defaults.
- Persisted public `LLM` configs remain non-secret; request auth still
stays out of the public config resource.
- Internally, `LLMSession` is the config-bearing per-operation thread
and `LLMCall` is the append-friendly log of provider exchanges under
that thread.
# why
- the existing /page/scroll endpoint had an awkward union of
`PageScrollElementParams` (element selector + percentage) and
`PageScrollCoordinateParams` (coordinate selector + deltaX/deltaY).
- these two modes were hard to distinguish and didn't cover common
scrolling patterns like "scroll N pages" or "scroll until this element
is visible"
# what changed
- replaced the two old scroll param schemas with a cleaner 3-way
discriminated union:
- `PageScrollByOffsetParams`: scroll by pixel delta (`{ cursorPosition?,
offset: {x, y} }`)
- `PageScrollByPagesParams`: scroll by viewport heights (`{
cursorPosition?, pages: number, delayBetweenMs? }`)
- `PageScrollToTargetParams`: scroll until a target is visible (`{
target: Selector }`)
- `cursorPosition` is an optional `Selector` that determines where the
mouse cursor is placed before scrolling. this controls which scrollable
container receives the event.
# test plan
- updated the route stub and integration test to match the new schema

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Refactors the v4 `/page/scroll` route into a simple 3‑mode API and
updates OpenAPI to return a typed `PageScrollActionOutput`. The stub now
returns `{x: 0, y: 0}`; adds `position` for target scrolls and
`delayBetweenMs` for page scrolling, aligning with Linear STG-1644.

- **Refactors**
- Replaced old params with 3 modes: by offset `{offset:{x,y}}`, by pages
`{pages, delayBetweenMs?}`, or to target `{target, position?}`; optional
`{cursorPosition}` selects the scroll container.
- OpenAPI now uses `PageScrollActionOutput` and
`PageScroll*ParamsOutput` (defaults resolved, stricter requireds) and
wires it into the `PageAction` union.
  - Route stub execution simplified to always return `{x: 0, y: 0}`.

- **Migration**
  - Stop using `selector + percentage` and `selector + deltaX/deltaY`.
- Send one of: `{offset:{x,y}}`, `{pages, delayBetweenMs?,
cursorPosition?}`, or `{target, position?}`.
- Expect `{x,y}` in the result; the action shape is now
`PageScrollActionOutput`.

<sup>Written for commit b8a85a3.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1875">Review in
cubic</a></sup>

<!-- End of auto-generated description by cubic. -->
# why
- to add a single `/elementInfo` route which encapsulates multiple
`locator` functions
# what changed
- added an `/elementInfo` route, which accepts a `Selector`, and returns
the following info:
  - `count`
  - `isVisible`
  - `isChecked`
  - `inputValue`
  - `textContent`
  - `innerHTML`
  - `innerText`
  - `centroid`

### screenshot:
<img width="538" height="693" alt="Screenshot 2026-03-23 at 5 10 55 PM"
src="https://github.com/user-attachments/assets/75d7cab4-5f47-40fc-bebb-8b0fb6a8b192"
/>



# test plan
- added a test which verifies that the `/elementInfo` route accepts a
request


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds the v4 `page.elementInfo` endpoint (`POST /v4/page/elementInfo`) to
fetch common element details in one call. The handler is a stub that
returns defaulted fields to unblock client work.

- **New Features**
- Implemented `page.elementInfo` route that accepts a `Selector` and
optional `fields` filter; returns: `count`, `selector`, `tagName`,
`backendNodeId`, `visibility`, `domRects`, `content`, and optional
`inputInfo`, `ariaInfo`, `attributes`, `styles`.
- Wired the route into v4 page routes, added
request/action/result/response schemas to OpenAPI, and added an
integration test posting an XPath selector to assert a successful
action.

<sup>Written for commit ebdb216.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1877">Review in
cubic</a></sup>

<!-- End of auto-generated description by cubic. -->
# why
- to add page routes for more of the locator functions 
# what changed
- added the following route stubs:
  - `/fill`
  - `/highlight`
  - `/selectOption`
  - `/setInputFiles`
- each of the above optionally returns a selector object if
`returnSelector: true`
- aligned existing `/click`, `/hover`, and `/dragAndDrop` routes to
default `returnSelector` to `false`

### screenshot:
<img width="606" height="925" alt="Screenshot 2026-03-24 at 1 25 11 PM"
src="https://github.com/user-attachments/assets/a7ec4602-2ad1-44e9-81be-54208021bc43"
/>

# test plan

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds v4 page route stubs for locator actions: /v4/page/fill,
/v4/page/highlight, /v4/page/selectOption, and /v4/page/setInputFiles.
Addresses STG-1646 with OpenAPI and schema updates; routes return
placeholder results with optional selector output.

- **New Features**
- Added POST endpoints: /v4/page/fill, /v4/page/highlight,
/v4/page/selectOption, /v4/page/setInputFiles (wired into router and
documented). Each supports returnSelector (default false) and returns
placeholder results.

- **Refactors**
- Default returnSelector is now false for /click, /hover, and
/dragAndDrop.
  - ResultSelector is now required in result schemas.

<sup>Written for commit 3637fdb.
Summary will update on new commits. <a
href="https://cubic.dev/pr/browserbase/stagehand/pull/1882">Review in
cubic</a></sup>

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

## Summary

- `browse env local` now auto-discovers an already-running Chrome with
remote debugging enabled (via `DevToolsActivePort` files and common port
probing), attaching to it instead of always launching an isolated
browser
- Falls back to isolated launch when no debuggable Chrome is found —
keeps the command reliable
- Added `--isolated` flag to force clean isolated browser (old behavior)
- Added positional CDP target argument: `browse env local 9222` or
`browse env local ws://...`
- `browse status` now reports `localStrategy`, `localSource`,
`resolvedCdpUrl`, and `fallbackReason`

**Sister PR:** browserbase/skills#54

## Test plan

<img width="698" height="373" alt="image"
src="https://github.com/user-attachments/assets/1612cf7e-61f4-46b8-9cc6-e739c7363a5a"
/>

- [x] All 44 existing tests pass
- [x] `browse env local` with Chrome running +
`--remote-debugging-port=9222` → attaches to existing Chrome
- [x] `browse env local` without debuggable Chrome → falls back to
isolated launch
- [x] `browse env local --isolated` → always launches clean browser
- [x] `browse env local 9222` → persists CDP strategy targeting
127.0.0.1:9222
- [x] `browse status` shows local strategy details
- [x] `browse env remote` → unchanged

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
github-actions Bot and others added 30 commits July 3, 2026 08:54
Prepare the next browse release by versioning the package on `main`.

What this PR does:
- bumps `packages/cli/package.json` to `0.9.3`
- updates the browse changelog
- consumes the pending browse changesets

After this PR merges, the `Release` workflow on `main` will publish
`browse@0.9.3` from that exact commit using `pnpm pack` + `npm publish
--provenance`.


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Prepare the `browse@0.9.3` release by bumping the package version,
updating the changelog, and consuming the pending changeset. On merge,
the Release workflow on `main` will publish via `pnpm pack` + `npm
publish --provenance`; this release also notes the new Docker image at
`ghcr.io/browserbase/browse`.

<sup>Written for commit 32f3a51.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2311?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: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
# why

`CdpSession.on(...)` stores session scoped listeners on the root
`CdpConnection` under keys like `${sessionId}:${event}`. when chrome
sends `Target.detachedFromTarget`, `CdpConnection` removes session
metadata, but the actual event handlers were not being removed

this was problematic because stale handler functions remained reachable
for the lifetime of the root connection. this could cause memory to
grow, particularly for long sessions with a lot of attach/detach churn

# what changed

added `CdpConnection.clearSessionEventHandlers(sessionId)`, which:
- clears session scoped handlers when `Target.detachedFromTarget` or
`Target.targetDestroyed` fires

# test plan

added tests in
`packages/core/dist/esm/tests/unit/cdp-connection-close.test.js`, which
verify:
- detached session handlers are removed
- multiple handlers for the same detached session are removed
- handlers for other live sessions are preserved
- root-level handlers are preserved
- stale session handlers are removed when `Target.targetDestroyed` fires

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes memory leaks and prevents hangs by cleaning up CDP session event
handlers and rejecting pending work when targets detach or are
destroyed. Aligns with Linear STG-2415.

- **Bug Fixes**
- Added `clearSessionEventHandlers(sessionId)` to remove
`${sessionId}:*` listeners.
- Added `rejectSessionPendingWork(sessionId, targetId)` to reject
in-flight `.send` calls and session dispatch waiters with
`PageNotFoundError`.
- On `Target.detachedFromTarget` and `Target.targetDestroyed`: call both
methods and delete affected sessions/mappings (for destroyed targets,
all sessions mapped to the target). Tests cover cleanup across
detach/destroy, multiple sessions per target, rejection of pending work,
and preservation of other sessions and root-level handlers.

<sup>Written for commit 5e7d04a.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2288?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. -->
…) (#2306)

thanks @yawbtng for the contribution here!

## why

Closes #2046. This is the reshaped version of #2159, following the
approach @seanmcguire12 outlined when closing that PR.

`setScreenshotProvider` returned a bare base64 string, so every CUA
client had to independently infer or hardcode the media type — all four
assumed `image/png`. A non-PNG screenshot (e.g. a JPEG from a custom
provider) was then mislabeled as PNG in the provider function-response
payload, which is the root of #2046. Clients also stripped a hardcoded
`data:image/png;base64,` prefix by regex, so any other prefix silently
broke.

## what changed

Move the media-type declaration to the capture boundary.
`setScreenshotProvider` now returns an explicit payload:

```ts
export interface ScreenshotProviderResult {
  base64: string;
  mediaType: "image/png" | "image/jpeg";
}
```

- **Default handler** (`v3CuaAgentHandler`) captures PNG explicitly
(`type: "png"`) and returns `{ base64, mediaType: "image/png" }`, so the
default is unchanged.
- **Anthropic**: `media_type: screenshot.mediaType`, `data:
screenshot.base64` (drops the `.replace(/^data:image\/png;base64,/,
"")`).
- **Google**: `mimeType: screenshot.mediaType` (drops the PNG-only
prefix strip).
- **OpenAI / Microsoft**: build
`data:${screenshot.mediaType};base64,${screenshot.base64}`.
- `options.base64Image` (caller-supplied) still defaults to `image/png`,
preserving existing behavior.

`ScreenshotProviderResult` is exported from the public entrypoint.

## testing

- New `cua-screenshot-mediatype.test.ts`: asserts a non-PNG
(`image/jpeg`) media type is honored by all four clients'
`captureScreenshot()`, and that the `options.base64Image` path still
defaults to png.
- Updated the public API type test for `setScreenshotProvider(...)` and
the Anthropic/Microsoft CUA client tests to the new provider shape.
- `pnpm --filter @browserbasehq/stagehand run typecheck` passes; the CUA
+ public-API unit suites are green (55 tests).



<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Declare the screenshot media type at the capture boundary and thread it
through all CUA clients to fix mislabeled images and remove PNG-only
prefix handling. Non‑PNG screenshots (e.g. JPEG) now work end-to-end.

- **Bug Fixes**
- `setScreenshotProvider` now returns `ScreenshotProviderResult` (`{
base64, mediaType }`) instead of a string.
- Default handler captures PNG (`type: "png"`) and returns `image/png`.
- Clients: Anthropic/Google pass `mediaType` through; OpenAI/Microsoft
build `data:${mediaType};base64,${base64}`; removed PNG-only prefix
stripping.
- `captureScreenshot({ base64Image })` accepts optional `mediaType`;
defaults to `image/png`.
- Added tests covering JPEG across clients and updated public API type
tests.

- **Migration**
- If you use a custom `setScreenshotProvider`, return `{ base64,
mediaType: "image/png" | "image/jpeg" }` instead of a base64 string. No
changes needed with the built-in handler.

<sup>Written for commit 42fb085.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2306?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. -->



# why

# what changed

# test plan

---------

Co-authored-by: yawbtng <154343001+yawbtng@users.noreply.github.com>
# why

# what changed

# test plan


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Removed an unnecessary comment from
`packages/core/tests/unit/agent-finalization-resilience.test.ts` to
reduce noise; no code or test behavior changes.

<sup>Written for commit 319fc33.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2320?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. -->
…n agent.execute() (#2305)

## What & why

Every hybrid/DOM `agent.execute()` call prints this AI SDK v5 warning to
the console:

> AI SDK Warning: System messages in the prompt or messages fields can
be a security risk because they may enable prompt injection attacks. Use
the system option instead when possible. Set allowSystemInMessages to
true to suppress this warning, or false to throw an error.

It's cosmetic (a warning, not an error), but it fires on essentially
every agent run, and the "prompt injection" wording reads alarmingly
enough to generate support tickets — it did (customer Christian /
kento9288).

### Root cause
The agent loop builds its system prompt as a **system-role message
inside the `messages` array** (`prependSystemMessage()` in
`v3AgentHandler.ts`) rather than the top-level `system` param. This is
deliberate: it lets the system prompt carry Anthropic **ephemeral
cache-control** via `providerOptions` (see the function's own comment).
AI SDK v5 warns whenever it sees a system message in `messages`, since
it can't distinguish a trusted, library-authored system prompt from
untrusted input.

The committed lockfile pinned `ai@5.0.133`, which predates this warning
— so the repo build never saw it, but the `^5.0.133` range means real
installs resolve to newer 5.0.x (e.g. `5.0.209`) that **do** warn.
That's why users hit it and CI didn't.

### Fix
- Pass `allowSystemInMessages: true` to the `generateText` /
`streamText` calls in the agent loop. This is the AI-SDK-sanctioned
opt-out for an intentional system-in-messages structure, and it **keeps
prompt caching intact** (vs. moving to top-level `system:`, which cannot
carry per-message `providerOptions` and would regress Anthropic
cache-control).
- Bump the `ai` floor `^5.0.133 → ^5.0.185`, where
`allowSystemInMessages` is a typed option.
- Add an explicit return type (`LanguageModelV2`) to
`getAISDKLanguageModel`. The `ai` bump pulls a newer `@ai-sdk/provider`,
and without the annotation tsc emits `TS2742` ("inferred type … not
portable").

## E2E Test Matrix

| Command / flow | Observed output | Confidence / sufficiency |
| --- | --- | --- |
| **BEFORE** — published `@browserbasehq/stagehand@3.6.0` (resolves
`ai@5.0.209`), hybrid `agent.execute()` on a real Browserbase session,
console captured | `AI SDK system-in-messages warning emitted: YES (1)`
— full "…security risk…prompt injection…" text | Reproduces the
customer's exact symptom on the version they run. |
| **AFTER** — local patched build (this branch, same `ai@5.0.209`),
identical hybrid `agent.execute()` flow, same model, real Browserbase
session | `AI SDK system-in-messages warning emitted: NO (0)`; run exits
cleanly | Proves the fix removes the warning without breaking the agent
run. Same harness as BEFORE → clean A/B. |
| `pnpm turbo run build --filter @browserbasehq/stagehand` | `Tasks: 3
successful, 3 total` (fails with `TS2742` before the `LLMProvider`
annotation) | Typecheck + emit green; confirms the `ai` bump +
annotation compile. |
| `prettier --check` + `eslint` on changed files | `All matched files
use Prettier code style!`, eslint clean | Style/lint gates pass. |

A/B model: `anthropic/claude-haiku-4-5-20251001`. Both runs used
identical `mode: "hybrid"` + `systemPrompt` config (the customer's
shape).

Closes STG-2463.

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

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Silences the AI SDK v5 “system message in messages” warning during
hybrid/DOM `agent.execute()` runs while preserving Anthropic
cache-control. Addresses STG-2463.

- **Bug Fixes**
- Pass `allowSystemInMessages: true` to `generateText`/`streamText` so
the agent’s system prompt in `messages` does not warn and caching stays
intact.

- **Dependencies**
  - Bump `ai` to `^5.0.185` (lock resolves to `5.0.209`).
- Add explicit `LanguageModelV2` return type in `getAISDKLanguageModel`
to avoid `TS2742` with newer `@ai-sdk/provider`.

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

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2305?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 Opus 4.8 (1M context) <noreply@anthropic.com>
… ref maps (#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)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ctions dev test (#2321)

## Summary

Fixes an intermittent **Windows-only** CI failure in
`packages/cli/tests/cli-functions-contract.test.ts`.

Failing test: `functions scaffolding and local dev > runs a local dev
server and invokes a function`

Observed error (Windows CI only):

```
Error: EBUSY: resource busy or locked, rmdir 'C:\...\Temp\functions-dev-XXXX'
```

## Root cause

The test spawns `browse functions dev`, which in turn spawns a **runtime
subprocess** (a grandchild of the test). The `afterEach` cleanup does
`child.kill("SIGTERM")` + `waitForExit(child)` — but on Windows `kill`
only terminates the **direct child**, not the grandchild runtime. That
grandchild can still hold open file handles inside the per-test temp dir
when cleanup runs.

The subsequent `rm(path, { recursive: true, force: true })` then hits a
directory with live handles, and Windows refuses to remove it → `EBUSY`.
Critically, `force: true` only **ignores missing paths**; it does
**not** retry on `EBUSY`. Node's `fs.rm` exposes
`maxRetries`/`retryDelay` precisely to retry transient Windows
`EBUSY`/`EPERM`/`ENOTEMPTY` errors during recursive removal.

## Fix

Add retry options to the temp-dir cleanup `rm(...)` call in `afterEach`:

```js
await rm(path, {
  recursive: true,
  force: true,
  maxRetries: 5,
  retryDelay: 100,
});
```

This is the only `rm`/`rmdir` call in the file that removes temp dirs.

## Scope / impact

- **Test-only change.** No production/source code touched.
- **No changeset** (non-release, no runtime behavior change).

## E2E Test Matrix

| Command / flow | Observed output | Confidence / sufficiency |
| --- | --- | --- |
| `pnpm vitest run tests/cli-functions-contract.test.ts` (in
`<worktree>/packages/cli`, after `pnpm install --frozen-lockfile` +
`pnpm turbo run build --filter=browse...`) | `Test Files 1 passed (1)` /
`Tests 13 passed (13)`; verbose shows `✓ ... runs a local dev server and
invokes a function 626ms` | Proves **no regression on POSIX (macOS)** —
all 13 tests including the previously-flaky one still pass with the
retry options. The Windows `EBUSY` path can't be reproduced on macOS
(POSIX unlinks directories with open handles), so this run cannot
exercise the retry itself; the `maxRetries`/`retryDelay` options are the
canonical, documented Node remedy for Windows
`EBUSY`/`EPERM`/`ENOTEMPTY` on recursive `rm`. |
| `npx prettier --check tests/cli-functions-contract.test.ts` | `All
matched files use Prettier code style!` | Formatting passes — no CI
format failure. |
| `npx eslint tests/cli-functions-contract.test.ts` | exit 0, no output
| Lint passes. |

## Linear


[STG-2477](https://linear.app/browserbase/issue/STG-2477/testcli-retry-temp-dir-cleanup-to-fix-windows-ebusy-flake-in-functions)

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


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes a Windows-only flaky test by retrying temp-dir cleanup in
`packages/cli/tests/cli-functions-contract.test.ts` to avoid `EBUSY`
during the `functions dev` test. Addresses STG-2477 and stabilizes CI
without touching production code.

- **Bug Fixes**
- Add `maxRetries: 5` and `retryDelay: 100` to `fs.rm` in the test
`afterEach` cleanup.
- Prevents transient `EBUSY`/`EPERM`/`ENOTEMPTY` errors when a
grandchild process holds open handles on Windows.

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

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2321?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 Opus 4.8 (1M context) <noreply@anthropic.com>
Prepare the next browse release by versioning the package on `main`.

What this PR does:
- bumps `packages/cli/package.json` to `0.9.4`
- updates the browse changelog
- consumes the pending browse changesets

After this PR merges, the `Release` workflow on `main` will publish
`browse@0.9.4` from that exact commit using `pnpm pack` + `npm publish
--provenance`.


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Release `browse@0.9.4`, which makes `browse snapshot` print only the
accessibility tree by default to reduce noisy output. Includes version
bump and changelog update for publishing.

- **Migration**
- To get the previous full output (including `xpathMap`/`urlMap`), run
`browse snapshot --full`. Ref-based element commands are unchanged.

<sup>Written for commit 72cbd19.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2322?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: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
# Why

The verifier should not become the default until it is benchmarked
against legacy and Approach A/B are compared on the same saved
trajectories. This PR adds the external-harness wiring and
instrumentation needed for that evidence gate.

# What Changed

- Wired the rubric verifier into the external `claude_code`/`codex`
bench harnesses: `benchHarness` builds a verifier-carrier V3 (default
model — harness models can be runner-only aliases; constructed before
the tool adapter and closed in the `finally` so nothing leaks) and a
TaskSpec that threads curated `precomputed_rubric`s from dataset params
through to the verifier.
- Consolidated the verifier integration into shared helpers in
`verifierAdapter.ts`:
- `resolveRubricTraced` — rubric resolution inside a `verifier.rubric`
span with accurate provenance (`precomputed` / `cached` / `generated`).
- `verifyTraced` — `verifier.verify` span with scores + evaluation
metadata.
- `gradeExternalTrajectory` — the full evaluator → rubric → verify →
persist → verdict-folding sequence, shared by both runners; failures
fold into `verifierError` on the self-reported result so ungraded runs
are distinguishable from graded ones.
- Braintrust spans around rubric resolution, agent execution, and
verifier judgment; `tracedSpan` passes a no-op logging span when
`BRAINTRUST_API_KEY` is absent.
- Preserved dataset row categories in bench metadata (`task_category`).
- The stagehand-harness default evaluator backend remains `legacy` until
benchmark evidence supports a flip; external-harness runs are
verifier-graded with the self-report + `verifierError` fallback.

# Tests

- `pnpm --filter @browserbasehq/stagehand-evals run typecheck`
- `pnpm --dir packages/evals exec vitest run tests/framework
tests/cli.test.ts` — 25 files, 195 tests
- New: `rubricResolution.test.ts` (precomputed / cached / generated /
drift / cache-disabled provenance) and a `claudeCodeRunner` test locking
the `verifierError` result shape.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…er map (#2327)

Mirrored from external contributor PR #2326 after approval by
@Kylejeong2.

Original author: @aq17
Original PR: #2326
Approved source head SHA: `f7085b51cf1a25af736387d251ed8fa863d15df4`

@aq17, please continue any follow-up discussion on this mirrored PR.
When the external PR gets new commits, this same internal PR will be
marked stale until the latest external commit is approved and refreshed
here.

## Original description
## Summary

The `All Supported CUA Models` table in
`packages/docs/v3/configuration/models.mdx` was missing 7 models that
are already registered in `packages/core/lib/v3/agent/AgentProvider.ts`
(`modelToAgentProviderMap`, lines 17–37). This PR brings the docs table
in sync with the code.

Added rows:

- `openai/gpt-5.4`
- `openai/gpt-5.4-mini`
- `openai/gpt-5.5`
- `anthropic/claude-sonnet-4-20250514`
- `anthropic/claude-haiku-4-5` (the short alias — only the dated
`claude-haiku-4-5-20251001` was listed)
- `anthropic/claude-fable-5`
- `google/gemini-3.5-flash`

Also lightly reordered the Anthropic and Google rows so variants of the
same model family sit next to each other (haiku → sonnet → opus → fable;
gemini-2.5 → gemini-3-flash → gemini-3.5-flash → gemini-3-pro).

## Test plan

Docs-only change. No code touched. Rendered table matches the
source-of-truth map at `packages/core/lib/v3/agent/AgentProvider.ts`.

Requested by: alexander@browserbase.com
Linear:
https://linear.app/browserbase/issue/STG-2497/docs-sync-cua-model-list-with-agentprovider-map
Slack thread:
https://browserbase.slack.com/archives/D0AHY8DEQPM/p1783452853684349

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

<!-- external-contributor-pr:owned source-pr=2326
source-sha=f7085b51cf1a25af736387d251ed8fa863d15df4 claimer=Kylejeong2
-->

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Synced the “All Supported CUA Models” table in
`packages/docs/v3/configuration/models.mdx` with
`modelToAgentProviderMap` in
`packages/core/lib/v3/agent/AgentProvider.ts`, adding seven missing
models and grouping families for clarity. Aligns the docs with code per
Linear STG-2497 so users see all available CUA models.

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

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2327?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: Alex Qiu <alexander@browserbase.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…act (#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 #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](#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 #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.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
# why

# what changed

# test plan


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Removed a stale changeset entry to prevent an unintended duplicate patch
release of `@browserbasehq/stagehand`. No code or behavior changes; only
release metadata cleanup.

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

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2332?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. -->
…idance (#2336)

## Summary

A 3-provider bare-loop smoke test (E2B/Modal/Vercel, real sandboxes,
local CLI build) surfaced two systematic gaps in the bundled `browse`
skill (`packages/cli/skills/browse/SKILL.md`):

1. **3/3 agents ignored the pre-wired `BROWSE_SESSION` env var** and
self-named sessions via `--session <name>` (one run orphaned a
Browserbase session as a result). The skill taught named `--session`
usage but never said "if `BROWSE_SESSION` is already set, every command
already targets that session — don't pass `--session` or invent a new
name." This defeats the sandbox-template env-steering pattern
(`BROWSERBASE_API_KEY` + `BROWSE_SESSION=<name>` → flagless remote
commands).
2. **2/3 agents tried `--local` first inside Chrome-less containers**,
hit "No Chrome or Chromium found", then recovered on retry. The skill
lacked explicit guidance that `--local` requires local Chrome/Chromium
and that containers/CI/sandboxes without Chrome should go straight to
`--remote`.

## What changed

Two minimal additions to `packages/cli/skills/browse/SKILL.md`, both in
the existing "Browser Target Selection" section (the section that
already teaches local/remote choice and named-session usage) — no
restructuring, no new sections:

```
`--local` requires Chrome or Chromium already installed on the machine. In containers, CI, and sandboxes with no browser installed, use `--remote` instead of `--local`. If `--local` fails with "No Chrome or Chromium found" and `BROWSERBASE_API_KEY` is set, switch to `--remote` — do not retry `--local`.
```

```
If `BROWSE_SESSION` is already set in the environment, every command already targets that session — do not pass `--session` or invent a new name. An explicit `--session <name>` always overrides `BROWSE_SESSION` for that command, so only pass it to deliberately target a different session.
```

Verified the precedence claim against the actual implementation before
writing the wording (not just trusting the smoke-test writeup):

- `packages/cli/src/lib/driver/flags.ts:73` — `export function
sessionName(value?: string): string { return value ??
process.env.BROWSE_SESSION ?? "default"; }`. An explicit `--session`
flag always wins over `BROWSE_SESSION`; when omitted, `BROWSE_SESSION`
becomes the effective session; with neither, it's `"default"`.
- `packages/cli/src/lib/driver/command-cli.ts:75` —
`runDriverCommandFromFlags` calls `sessionName(flags.session)` for every
driver command, so this precedence applies uniformly across `open`,
`snapshot`, `click`, `stop`, etc.
- `packages/cli/src/lib/driver/remote.ts:130` — exact no-Chrome error
string is `"No Chrome or Chromium found on this machine. Install one
(Linux: apt install chromium · macOS: brew install --cask google-chrome,
or Chromium with CHROME_PATH set), attach to a running browser with
--cdp <port>, or set BROWSERBASE_API_KEY to use a remote browser."`

## No changeset

This is a skill-text-only change (`SKILL.md`), same category as PR #2329
(skill-text-only fix, no changeset). The updated text ships with the
next `browse` release — a release is already queued by #2335's changeset
— and is consumed at runtime by the evals harness added in #2334.

## E2E Test Matrix

| Command / flow | Observed output | Confidence / sufficiency |
| --- | --- | --- |
| `pnpm install && pnpm turbo run build --filter=browse` (worktree,
local build) | `Tasks: 4 successful, 4 total` | Proves the local CLI
build succeeds with the change in place; no build regressions from a
docs-only edit. |
| Resolve `bundledCliSkillPath()` from the compiled
`dist/lib/skills/install.js` via `require.resolve` + manual path join
(no `skills show` command exists on `main` yet — it ships in PR #2335,
unmerged at time of this PR) | `resolved bundledCliSkillPath:
<worktree>/packages/cli/skills/browse` | Proves the exact file `browse
skills install` resolves to at runtime is the file this PR edits, not a
stale copy. |
| `grep -n "already set in the environment\|do not retry"
packages/cli/skills/browse/SKILL.md` | `76:If \`BROWSE_SESSION\` is
already set in the environment, every command already targets that
session...` `68:...If \`--local\` fails with "No Chrome or Chromium
found"...do not retry \`--local\`.` | Confirms the exact new lines are
present at the bundled path resolved above — the new guidance flows
through to the real command path. |
| `node bin/run.js skills install --help` | Prints usage for `browse
skills install` (no side effects triggered — did not run the real
install, which mutates the global `~/.agents/skills` pool via `npx
skills add --global`) | Confirms the command is wired and reachable;
deliberately did not execute the real install to avoid mutating local
global skill state outside this change's scope. |
| `pnpm test:cli` (vitest, worktree build) | `Test Files 23 passed (23)`
/ `Tests 344 passed (344)` | Full existing CLI test suite still green;
this change touches no runtime code. |
| `pnpm eslint .` | No output / exit 0 | Lint clean. |
| `pnpm format:check` (prettier) | `All matched files use Prettier code
style!` | Formatting clean, including the edited Markdown. |
| `tsc --noEmit -p tsconfig.json` | No output / exit 0 | Typecheck clean
(unaffected by a Markdown-only change, included for completeness). |

## Related

- Linear:
https://linear.app/browserbase/issue/STG-2513/browse-skill-document-browse-session-precedence-container-remote
- Evidence source: 3-provider (E2B/Modal/Vercel) bare-loop smoke test,
2026-07-09
- Text will be exercised by the evals harness landing in #2334
- Release vehicle: changeset already queued in #2335

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


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Clarifies the `browse` skill docs to prevent session misrouting and
container failures by documenting `BROWSE_SESSION` precedence and when
to use `--remote` instead of `--local`. Addresses Linear STG-2513.

- **Bug Fixes**
- Documented that if `BROWSE_SESSION` is set, commands target that
session; only pass `--session` to override (precedence: `--session` >
`BROWSE_SESSION` > `default`).
- Stated that `--local` requires Chrome/Chromium; in
containers/CI/sandboxes use `--remote`. If you see "No Chrome or
Chromium found," switch to `--remote` instead of retrying `--local`.

<sup>Written for commit 829ff7b.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2336?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>
#2335)

## Why

Agents run `browse` inside sandboxes/harnesses we don't control, so they
can't rely on a copied SKILL.md or a docs URL staying in sync with the
installed CLI version. Serving the skill straight from the binary means
the guidance is always version-matched to whatever `browse` the agent
actually has.

This converges with the shape competitors have already landed on:
- agent-browser: `agent-browser skills get core --full` + a
top-of-`--help` "Start here (for AI agents)" header
- Modal: `modal skills show`
- browser-use: `skill show`

## What changed

- Added `browse skills show`
(`packages/cli/src/commands/skills/show.ts`), following the existing
`skills` topic command style (`BrowseCommand` base, single-line
description, `examples`). It resolves and prints the bundled `SKILL.md`
via the same `bundledCliSkillPath()` helper `skills install` already
uses (exported from `packages/cli/src/lib/skills/install.ts`), so path
resolution is shared and works in both `src` and `dist` layouts. Reads
the file synchronously and calls `this.error(...)` with a clear message
+ non-zero exit if it's missing/unreadable.
- Added a custom oclif `Help` class (`packages/cli/src/help.ts`) that
prepends a short agent-facing block before oclif's auto-generated
VERSION/USAGE/DESCRIPTION/TOPICS/COMMANDS sections on **root help only**
(`browse --help` and bare `browse`). Wired via `"helpClass":
"./dist/help.js"` in `packages/cli/package.json`'s oclif config.
Per-command `--help` views (e.g. `browse open --help`) are unaffected.
- Added a patch changeset — `browse` is in the changesets `ignore` list
by default, but per team convention it still gets patch changesets for
release-impacting CLI changes.
- Tests: new `packages/cli/tests/skills-show.test.ts` (happy path +
resolution from a different cwd), plus updates to
`packages/cli/tests/cli-surface.test.ts` for the new `skills show`
command help, the agent header appearing before `USAGE` on root help,
and its absence on command help.

## E2E Test Matrix

| Command / flow | Observed output | Confidence / sufficiency |
| --- | --- | --- |
| `node packages/cli/bin/run.js skills show \| head -20` | Prints
bundled `SKILL.md` frontmatter: `name: browse`, `description: Use the
browse CLI for...`, `# Browse CLI` heading | Proves the command reads
and prints the real bundled skill from the local build, not a mock |
| `node packages/cli/bin/run.js skills show \| wc -lc` | `369 17221`
(369 lines, 17221 bytes) | Confirms full file is printed, not truncated
|
| `node packages/cli/bin/run.js --help \| head -25` | Output opens with
`Start here (for AI agents):` / `browse skills show` / rationale block,
then `Unified Browserbase CLI...`, `VERSION`, `USAGE`, `TOPICS` (incl.
`skills`) | Confirms header renders before the auto-generated sections
on `--help` |
| `node packages/cli/bin/run.js \| head -25` | Same header block, same
ordering, for bare `browse` with no args | Confirms bare invocation (not
just `--help`) also gets the header |
| `node packages/cli/bin/run.js open --help \| head -15` | Standard
command help (`Open a URL in a browse driver session.`, `USAGE`,
`ARGUMENTS`, `FLAGS`) with **no** agent header | Confirms per-command
help is unaffected |
| `cd /tmp && node <worktree>/packages/cli/bin/run.js skills show \|
head -3` | Same `SKILL.md` frontmatter, run from an unrelated cwd |
Confirms path resolution is relative to the module file, not
`process.cwd()` — required for the dist-layout published package |
| `pnpm test:cli` (vitest) | `Test Files 24 passed (24)`, `Tests 349
passed (349)` | Full existing CLI suite plus new/updated tests all green
— no regressions |
| `pnpm lint` (prettier + eslint + tsc) | All three steps pass with no
output/errors | Static checks clean |

## Linear


https://linear.app/browserbase/issue/STG-2511/add-browse-skills-show-agent-facing-help-header

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

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds `browse skills show` to print the bundled `SKILL.md` for
version-matched guidance, and adds an agent-facing header on root help
pointing to it. Addresses STG-2511.

- New Features
- `browse skills show` prints the bundled skill via
`bundledCliSkillPath()` and, on error, uses `fail()` (exit 1 with
`resultCode: "skill_show_missing"`).
- Root help (`browse --help` and bare `browse`) prepends a short “Start
here” header; per-command help is unchanged. Wired via `helpClass` in
`packages/cli/package.json`.
- Exported `bundledCliSkillPath()` so path resolution works in both
`src` and `dist`.

- Refactors
  - Use `fs/promises.readFile` for async skill file reads.

<sup>Written for commit 2a74053.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2335?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>
… 22 (#2344)

## Why

Every stagehand release since 2026-07-09 18:52 UTC is broken. The
Release workflow's "Update npm for Trusted Publishing" step runs `npm
install -g npm@latest`; npm@12.0.0 was published 2026-07-08 21:06 UTC
and became `latest`, and it requires node `^22.22.2 || ^24.15.0 || >=26`
— the release job runs node 20, so the step dies with `EBADENGINE`
before versioning even starts.

- Last green Release: 2026-07-08 17:50 UTC
- First failure: 2026-07-09 18:52 UTC
([run](https://github.com/browserbase/stagehand/actions/runs/29043380120)
— `npm error notsup Required: {"node":"^22.22.2 || ^24.15.0 ||
>=26.0.0"} / Actual: {"npm":"10.8.2","node":"v20.20.2"}`)
- This currently blocks the browse release carrying `browse skills show`
(#2335) and the browse-skill guidance fixes (#2336), whose changeset is
pending on main.

## What

Pin the step to `npm@11` (currently 11.18.0): supports node >= 20.17
**and** Trusted Publishing/OIDC (needs npm >= 11.5.1) — the reason this
step exists, since node 20 bundles npm 10.8.2 which predates it. One
line + a comment stating the constraint.

The alternative (bump the job to node 22/24 and keep `@latest`) changes
the entire release build environment (SEA binaries, prebuilds) and isn't
hotfix material; it can be considered separately.

## Validation

| Command / flow | Observed output | Confidence / sufficiency |
| --- | --- | --- |
| Failure-log inspection of both failed Release runs | `EBADENGINE ...
npm@12.0.0 ... Required node ^22.22.2 \|\| ^24.15.0 \|\| >=26 / Actual
node v20.20.2`, exit 1 at the pinned step | Proves the failure mechanism
exactly at this line |
| `curl registry.npmjs.org/npm` → dist-tags + publish times | `latest =
12.0.0`, published 2026-07-08T21:06Z — inside the 07-08 17:50 → 07-09
18:52 breakage window | Proves `@latest` drift is the trigger; nothing
repo-side changed |
| npm 11.x engine range vs job node | npm 11.18.0 supports node >=
20.17; job runs 20.20.2 | Proves the pin resolves the engine conflict
while keeping Trusted Publishing (>= 11.5.1) |
| Real release exercise | Not possible pre-merge — the Release workflow
only runs from main. Merging this PR pushes to main, which itself
triggers Release with the pending browse changeset — that run is the
live verification | The one path that cannot be tested from a branch |

Linear: STG-2520

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

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Pin `npm` in the Release workflow to `11.18.0` to avoid `npm@12`
requiring Node >=22. Restores releases on Node 20 and keeps Trusted
Publishing (Linear: STG-2520).

<sup>Written for commit 90a6d71.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2344?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>
Prepare the next browse release by versioning the package on `main`.

What this PR does:
- bumps `packages/cli/package.json` to `0.9.5`
- updates the browse changelog
- consumes the pending browse changesets

After this PR merges, the `Release` workflow on `main` will publish
`browse@0.9.5` from that exact commit using `pnpm pack` + `npm publish
--provenance`.


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Prepare the `browse@0.9.5` release on `main`. Bumps the package version,
updates the changelog, and consumes the pending changeset so the Release
workflow can publish.

- New Features
- Add `browse skills show` to print the bundled skill and reference it
from `--help`.

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

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2346?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: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
# why

BYOK/direct provider calls reject when page snapshot text we passed in
contained malformed UTF-16. I

# what changed

Normalize captured accessibility snapshot strings with `toWellFormed()`
at the snapshot boundary and add unit/integration coverage for
lone-surrogate page text.

# test plan

Red/greened the new unit and local SDK observe regressions, and the
focused `unicode-well-formed` integration test.


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes malformed Unicode in accessibility snapshots by normalizing text
with `String.prototype.toWellFormed()` at capture and during frame
merge, so both local SDK and provider prompts always get well-formed
UTF-16. Adds tests that confirm lone surrogates are repaired to U+FFFD
in prompts.

- **Bug Fixes**
- Normalize outline strings during capture and frame merge; per-frame
and combined trees are well-formed before prompting.
- Add integration test asserting U+FFFD appears in the model prompt and
a unit test verifying combined tree repair.

- **Dependencies**
  - Add changeset to publish a patch for `@browserbasehq/stagehand`.

<sup>Written for commit 143ac0e.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2345?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: samfinton <samfinton@samfintons-MacBook-Pro.local>
## Summary

Adds the GPT-5.6 Terra, Luna, and Sol OpenAI model variants to
Stagehand's CUA model registration.

## Changes

- Add `openai/gpt-5.6-terra`, `openai/gpt-5.6-luna`, and
`openai/gpt-5.6-sol` to `AVAILABLE_CUA_MODELS`
- Map the corresponding short model names to the OpenAI CUA provider
- Treat `gpt-5.6` models as hybrid-capable in core and eval model-mode
inference
- Add focused eval mode inference coverage for GPT-5.6 Terra, Luna, and
Sol defaulting to `hybrid`
- Update the public API type test model list

## Validation

- `node packages/evals/dist/cli/cli.js run agent/sign_in -t 1 -c 1 -p
openai -m openai/gpt-5.6-sol --agent-mode cua` passed with `1 passed, 0
failed`
- `node packages/evals/dist/cli/cli.js run agent/sign_in -t 1 -c 1 -p
openai -m openai/gpt-5.6-terra --agent-mode cua --preview` accepted the
run plan
- `node packages/evals/dist/cli/cli.js run agent/sign_in -t 1 -c 1 -p
openai -m openai/gpt-5.6-luna --agent-mode cua --preview` accepted the
run plan
- `vitest run packages/evals/tests/framework/agentModelModes.test.ts`
passed: 3 tests
- `vitest run
packages/core/tests/unit/public-api/llm-and-agents.test.ts` passed: 22
tests

Note: local commits were created with `HUSKY=0` because the pre-commit
hook invokes a pnpm version that hits a local non-TTY install guard; the
focused validation above passed.
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @browserbasehq/stagehand@3.7.0

### Minor Changes

- [#2283](#2283)
[`871ca7e`](871ca7e)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add
`context.setDomainPolicy({ allowedDomains: ["allowed.domain"] })` which
allows users to define a set of domains that are accessible to stagehand

- [#2274](#2274)
[`f31980f`](f31980f)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add
`context.setDomainPolicy({blockedDomains: ["some.domain"]})` which
allows users to define a list of domains that will be blocked by
stagehand

### Patch Changes

- [#2305](#2305)
[`cd1daad`](cd1daad)
Thanks [@shrey150](https://github.com/shrey150)! - Remove the noisy AI
SDK "system message in messages" warning logged on every hybrid/DOM
`agent.execute()` call.

- [#2328](#2328)
[`d287ff4`](d287ff4)
Thanks [@miguelg719](https://github.com/miguelg719)! - Allow modelName
"auto" in the constructor and per-primitive model overrides when running
through the Stagehand API

- [#2294](#2294)
[`3938590`](3938590)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! -
automatically close popups that violate user defined domain policy

- [#2298](#2298)
[`892701a`](892701a)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - Fix CUA
`keypress` actions to press key combinations as a single chord.

- [#2345](#2345)
[`21826c7`](21826c7)
Thanks [@monadoid](https://github.com/monadoid)! - Repair malformed
UTF-16 snapshot text before it reaches model prompts.

- [#2306](#2306)
[`8dcef1b`](8dcef1b)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - Use the
screenshot provider's declared media type when sending CUA image
payloads. The `setScreenshotProvider` callback now returns
`ScreenshotProviderResult` (`{ base64, mediaType }`) instead of a bare
base64 string.

- [#2273](#2273)
[`93a23d3`](93a23d3)
Thanks [@miguelg719](https://github.com/miguelg719)! - Add support for
the new `google/gemini-3.5-flash` computer-use tools model

- [#2278](#2278)
[`022d68f`](022d68f)
Thanks [@shrey150](https://github.com/shrey150)! - Fix `TypeError:
Converting circular structure to JSON` when creating an agent with MCP
`integrations` that include a `Client` instance (e.g. a local/stdio
server from `connectToMCPServer`). The agent-creation log serialized the
raw `integrations` array, and a live MCP `Client` is circular. It now
logs a safe descriptor (URL strings kept, client instances summarized)
so `agent({ integrations: [client] })` works.

- [#2288](#2288)
[`bb5ffa6`](bb5ffa6)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - clean up
cdp session event handlers on target detach

## @browserbasehq/stagehand-evals@2.0.4

### Patch Changes

- Updated dependencies
\[[`cd1daad`](cd1daad),
[`d287ff4`](d287ff4),
[`3938590`](3938590),
[`892701a`](892701a),
[`21826c7`](21826c7),
[`8dcef1b`](8dcef1b),
[`93a23d3`](93a23d3),
[`871ca7e`](871ca7e),
[`022d68f`](022d68f),
[`bb5ffa6`](bb5ffa6),
[`f31980f`](f31980f)]:
    -   @browserbasehq/stagehand@3.7.0

## @browserbasehq/stagehand-server-v3@3.7.2

### Patch Changes

- Updated dependencies
\[[`cd1daad`](cd1daad),
[`d287ff4`](d287ff4),
[`3938590`](3938590),
[`892701a`](892701a),
[`21826c7`](21826c7),
[`8dcef1b`](8dcef1b),
[`93a23d3`](93a23d3),
[`871ca7e`](871ca7e),
[`022d68f`](022d68f),
[`bb5ffa6`](bb5ffa6),
[`f31980f`](f31980f)]:
    -   @browserbasehq/stagehand@3.7.0

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
# why
- adds documentation for `setDomainPolicy()`
# what changed

<img width="1288" height="955" alt="Screenshot 2026-07-13 at 11 07
35 AM"
src="https://github.com/user-attachments/assets/3d02d0b2-f7e2-4c33-af88-9469d97d041f"
/>


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds docs for `context.setDomainPolicy()` and
`context.getDomainPolicy()` to control HTTP(S) requests by domain across
a context, with examples and error details. Addresses Linear STG-2426.

- **New Features**
- Documented `setDomainPolicy(policy: DomainPolicy | null)` and
`getDomainPolicy()` with TypeScript signatures, behavior, scope (pages,
iframes, subresources), and how to clear.
- Clarified domain-only patterns: exact vs wildcard, subdomain rules
(`*.example.com` doesn’t match `example.com`), case-insensitive
matching, duplicate patterns ignored, and no schemes/paths/ports;
`blockedDomains` overrides `allowedDomains`; non-HTTP(S) unaffected;
blocked requests show Chrome “BlockedByClient”.
- Added a “Domain Policy” example tab using `@browserbasehq/stagehand`,
and updated API reference with new errors
(`StagehandInvalidArgumentError`, `StagehandSetDomainPolicyError`) plus
`V3Context` and `DomainPolicy` interface snippets.

<sup>Written for commit 644f32e.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2289?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. -->
…ct/observe (#2359)

## What & why

[#2305](#2305) silenced the
AI SDK v5 "system message in messages" warning, but only for
`agent.execute()`'s outer loop (`v3AgentHandler.ts`). `act()`,
`extract()`, and `observe()` build their system prompt the same way — a
`{ role: "system", ... }` message inside the `messages` array — and
route through a separate call path that was never patched:
`AISdkClient.createChatCompletion()` in
`packages/core/lib/v3/llm/aisdk.ts` (the default client for any
`"provider/model"` string), plus the identical pattern in the public
BYOC client `packages/core/lib/v3/external_clients/aisdk.ts`.

Since the hybrid/DOM agent's own `act`/`extract`/`observe` tools call
these primitives internally on every step, an `agent.execute()` run that
clicks/types repeatedly still spammed the warning even after upgrading
past the first fix — a customer reported exactly this.

### Fix
Add `allowSystemInMessages: true` to the 4 remaining
`generateObject`/`generateText` call sites (2 in `llm/aisdk.ts`, 2 in
`external_clients/aisdk.ts`), same pattern as the original fix.

## E2E Test Matrix

| Command / flow | Observed output | Confidence / sufficiency |
| --- | --- | --- |
| **BEFORE** — published `@browserbasehq/stagehand@alpha` (already
includes #2305's fix), `act()` → `extract()` → `observe()` chain on a
real Browserbase session | `AI SDK system-in-messages warnings emitted:
YES (4)` | Reproduces the customer's exact symptom on the current alpha
— confirms #2305 left this path unpatched. |
| **AFTER** — local patched build (this branch), identical `act()` →
`extract()` → `observe()` chain, same model, real Browserbase session |
`AI SDK system-in-messages warnings emitted: NO (0)` | Proves the fix
removes the warning from all three primitives. |
| **AFTER** — local patched build, full chain: `act()` + `extract()` +
`observe()` + hybrid `agent.execute()` (multi-step form fill, real
Browserbase session) | `AI SDK system-in-messages warnings emitted: NO
(0 total)` | Confirms no regression on the already-fixed
`agent.execute()` outer loop, and that the agent's internal
act/extract/observe tool calls are also silenced end-to-end. |
| `pnpm turbo run build --filter @browserbasehq/stagehand` | `Tasks: 3
successful, 3 total` | Typecheck + emit green. |
| `eslint` + `prettier --check` on changed files | Both clean |
Style/lint gates pass. |

A/B model: `anthropic/claude-haiku-4-5-20251001`, `experimental: true`,
hybrid agent mode — same harness shape as #2305's own E2E matrix.

Closes STG-2573.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Silences the noisy "system message in messages" warning in `act()`,
`extract()`, and `observe()`, including internal tool calls. Sets
`allowSystemInMessages: true` at 4 `generateObject`/`generateText` sites
and adds unit tests for both AISDK clients; closes STG-2573.

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

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2359?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. -->
…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>
…I publisher (#2318)

## Why

Sonnet 5, GPT-5.6, and Grok eval runs published to the Braintrust UI had
no pricing entry, so their cost metrics fell through as unknown.

## What changed

Added to the publisher's `MODEL_PRICING_USD_PER_1M_TOKENS` map (prefixed
and unprefixed forms per the map's convention):

| Model | Input | Cached input | Output |
|---|---|---|---|
| `claude-sonnet-5` | $3 | $0.30 | $15 |
| `gpt-5.6-sol` | $5 | $0.50 | $30 |
| `gpt-5.6-terra` | $2.50 | $0.25 | $15 |
| `gpt-5.6-luna` | $1 | $0.10 | $6 |
| `grok-4.5` | $2 | $0.50 | $6 |
| `grok-4.3` | $1.25 | $0.20 | $2.50 |

GPT-5.6 keys match the CUA model ids added in #2343
(`openai/gpt-5.6-{sol,terra,luna}`). Grok rates per
docs.x.ai/developers/models (2026-07).

Deliberate trade-off (noted in the code comment): Sonnet 5 introductory
pricing ($2 / $0.20 / $10) applies through **2026-08-31**, so costs
published before then are overstated ~1.5x; standard rates keep the
dashboard stable across the cutover instead of requiring a September
flip.

## Tests

Internal publish script; no behavior beyond the map entries. Typecheck
green.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary

`browse` has auto-loaded a `.env` file from the current directory on
startup since it moved into this monorepo, with no way to turn it off. A
user reported `.env` values in one project being silently overridden by
a stale shell-level key from another project -- the shell-level
`BROWSERBASE_API_KEY` from an earlier project took precedence over the
correct key in a different project's `.env`, so session replays went to
the wrong Browserbase account.

The agreed direction (per internal eng review) is that CLI tools used
across many unrelated projects/directories shouldn't auto-load `.env` at
all -- only `process.env` should matter, and it's on the user to
`export` vars or source `.env` themselves. Ripping that out in one PR
would break the existing install base that relies on the implicit
behavior, so this PR is the non-breaking first step:

- Default behavior is unchanged: `browse` still loads `.env` by default.
- New `BROWSE_LOAD_DOTENV` env var makes that behavior explicitly
toggleable: `0`/`false`/`no` (case-insensitive) skips `.env` loading
entirely today; any other explicit value (e.g. `1`) keeps loading with
no nagging.
- When the toggle is left unset (the implicit default) and loading a
`.env` file actually applies a variable not already in `process.env`, we
print a one-time deprecation warning to stderr naming the variable(s),
so people relying on the implicit default get advance notice.

A future PR (not this one) will flip the default of `BROWSE_LOAD_DOTENV`
to off, once the warning has had time to reach users.

This complements #2360, which adds a `browse doctor` warning when
`process.env` and `.env`/`.env.local` values *disagree* -- a useful
on-demand diagnostic, but it doesn't touch the auto-load behavior
itself. This PR does not depend on #2360.

## E2E Test Matrix

Built the CLI locally (`pnpm turbo run build --filter=browse`) and ran
the built `bin/run.js` directly in a scratch temp dir containing a
`.env` with a fake `BROWSERBASE_API_KEY`.

| Command / flow | Observed output | Confidence / sufficiency |
| --- | --- | --- |
| `node bin/run.js doctor --json` in scratch dir with `.env` containing
`BROWSERBASE_API_KEY=<fake>`, `BROWSE_LOAD_DOTENV` unset | stderr:
`[browse] Loaded BROWSERBASE_API_KEY from .env. Auto-loading .env is
deprecated and will be disabled by default in a future release -- export
these variables in your shell instead, or set BROWSE_LOAD_DOTENV=1 to
keep this behavior explicitly once that happens. Set
BROWSE_LOAD_DOTENV=0 to opt out today. Run \`browse doctor\` to check
for conflicts with your shell environment.` / stdout `verdict: "ok"` /
exit 0 | Proves default (implicit) path still loads `.env` and now warns
exactly once, naming the applied var. |
| `node bin/run.js doctor --remote` in same dir with
`BROWSE_LOAD_DOTENV=0` | stdout: `[fail] browserbase BROWSERBASE_API_KEY
is not set` / no stderr / exit 1 | Proves opting out today skips `.env`
entirely -- the key never reaches `process.env`, no warning printed
(explicit choice). |
| `node bin/run.js doctor --remote` in same dir with
`BROWSE_LOAD_DOTENV=1` | stdout: `[ok] browserbase BROWSERBASE_API_KEY
is set` / no stderr / exit 0 | Proves explicit opt-in keeps loading
`.env` with zero nagging. |
| `pnpm exec vitest run tests/cli-cloud-contract.test.ts
tests/doctor.test.ts` | `Test Files 2 passed (2)` / `Tests 69 passed
(69)` | Covers the existing `.env`-loads-config contract (unchanged
default) plus: warns when unset+applied, silent when
`BROWSE_LOAD_DOTENV=1`, and fails auth (key never loaded, no warning)
for each of `0`/`false`/`no`/`FALSE`/`NO` as `BROWSE_LOAD_DOTENV` --
added the extra opt-out aliases per cubic review feedback below. |
| `pnpm exec eslint bin/run.js tests/cli-cloud-contract.test.ts` + `pnpm
exec prettier --check bin/run.js README.md
tests/cli-cloud-contract.test.ts` + `pnpm exec tsc --noEmit -p
tsconfig.json` | All clean, no output/errors | Static checks on touched
files; supporting evidence, not a substitute for the live runs above. |

## Linear

STG-2579 (linked via branch name).

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
## Summary

- replace the redundant “LLM model” wording in the Models page
description
- keep the existing “Models” page title unchanged

## Why

“LLM model” repeats “model,” since LLM already stands for large language
model. The updated description reads “Use any LLM with Stagehand.”

## Impact

This improves the wording shown in the Models page header and search
previews. There is no runtime or API impact.

## Validation

- `git diff --check`
- confirmed the Models page no longer contains “LLM model”

Linear:
[GRO-1928](https://linear.app/browserbase/issue/GRO-1928/replace-llm-model-wording-in-stagehand-docs)

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Replace “LLM model” with “LLM” in the Models page description to remove
redundancy and improve clarity. Title remains “Models”; docs-only change
with no runtime or API impact. Addresses Linear GRO-1928.

<sup>Written for commit 63ba4a7.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2366?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. -->
# Why

[OdysseysBench](https://odysseysbench.com) is a 200-task web-agent
benchmark (45 easy / 46 medium / 109 hard) where every task ships a
**weighted rubric** (weights sum to 1.0). It slots naturally into the
rubric-based verifier path — like WebTailBench — so we can score process
*and* outcome against the published criteria instead of generating
rubrics.

# What Changed

- **Dataset** (`packages/evals/datasets/odysseysbench/`): committed
source snapshot (`source/tasks.json`, mirrored from
`https://odysseysbench.com/assets/data/tasks.json`) plus the generated
`OdysseysBench_data.jsonl` (200 rows).
- **Converter** (`scripts/build-odysseysbench-dataset.ts`):
deterministic transform of each task's `rubrics` map → the verifier's
`precomputed_rubric` (`{ items: [{ criterion, description, max_points }]
}`). Rubric weights scale to integer points (sum ≈ 100; scale is
immaterial since the process score is a ratio). Run with `--fetch` to
refresh the snapshot.
- **Suite** (`suites/odysseysbench.ts`): `buildOdysseysBenchTestcases`,
mirroring the WebTailBench suite. Env knobs: `EVAL_ODYSSEYSBENCH_LIMIT`
(default 25), `EVAL_ODYSSEYSBENCH_SAMPLE`, `EVAL_ODYSSEYSBENCH_LEVEL`
(easy/medium/hard filter), `EVAL_ODYSSEYSBENCH_IDS`.
- **Bench task** (`tasks/bench/agent/odysseysbench.ts`): runs the agent
through `TrajectoryRecorder` + `V3Evaluator.verify()` with the
precomputed rubric.
- **Wiring**: dataset fan-out in `index.eval.ts` (respects
`EVAL_DATASET=odysseysbench`); `external_agent_benchmarks` category
override in `taskConfig.ts` and `cli-legacy.ts`.

# How to run

```
pnpm evals --eval-name agent/odysseysbench
EVAL_ODYSSEYSBENCH_LEVEL=hard EVAL_ODYSSEYSBENCH_LIMIT=10 pnpm evals --eval-name agent/odysseysbench
```

# Tests

- `pnpm --filter @browserbasehq/stagehand-evals run typecheck` — clean
- `prettier --check` on all changed files — clean
- Dataset fidelity: 200/200 rows; instructions, websites, levels match
source; rubric counts + order preserved; all `max_points ≥ 1`; task_ids
unique.
- Discovery smoke: `agent/odysseysbench` registers under
`external_agent_benchmarks`; suite builds testcases with rubric
attached; `EVAL_ODYSSEYSBENCH_LEVEL=hard` returns exactly 109 tasks.


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds OdysseysBench as a built-in agent benchmark with precomputed
rubrics for outcome and process scoring across 200 web tasks. Tightens
env parsing and rubric validation to preserve scoring fidelity and avoid
sampling bypasses; fully wires modern CLI and external harness support
under `external_agent_benchmarks`.

- **New Features**
- Dataset: committed source snapshot and generated
`OdysseysBench_data.jsonl`; task rubrics converted to verifier
`precomputed_rubric`.
- Converter: `packages/evals/scripts/build-odysseysbench-dataset.ts`
(deterministic; `--fetch` refreshes upstream).
- Suite: `packages/evals/suites/odysseysbench.ts` with
limit/sample/level/ids knobs.
- Bench task: `packages/evals/tasks/bench/agent/odysseysbench.ts` via
TrajectoryRecorder + `V3Evaluator.verify()`; success mode via
`EVAL_SUCCESS_MODE` (outcome|process|both).
- Wiring: dataset fan-out in `packages/evals/index.eval.ts`; category
override to `external_agent_benchmarks`; run with `pnpm evals
--eval-name agent/odysseysbench`.

- **Bug Fixes**
- Legacy CLI: register in `packages/evals/evals.config.json` and
`packages/evals/cli-legacy.ts` so `b:odysseysbench` resolves.
- Modern CLI: register in `packages/evals/tui/commands/parse.ts` and
`packages/evals/framework/benchPlanner.ts`, and add
`packages/evals/framework/externalHarnessPlan.ts` support so
`b:odysseysbench` runs and external harnesses get instruction/startUrl;
ensure discovery lists under `external_agent_benchmarks`.
- Suite: sanitize
`EVAL_MAX_K`/`EVAL_ODYSSEYSBENCH_LIMIT`/`EVAL_ODYSSEYSBENCH_SAMPLE` to
prevent NaN from bypassing caps.
  - Bench task: hard-fail if a task is missing `precomputed_rubric`.
- Rubric points: scale weights x1000 to avoid rounding distortion of
small criteria.
- Converter: validate `task_id`/`confirmed_task`; validate each rubric
item (non-empty fields; weight in (0,1]); ensure weights sum to ~1.0;
assert row count.

<sup>Written for commit 29ccd1a.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2275?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>
## Why

The `stagehand-server-v3` SEA binary release workflow
(`.github/workflows/stagehand-server-v3-release.yml`) only cuts a new
tag/binary build when a changeset added since the last
`stagehand-server-v3/v*` tag explicitly lists the
`@browserbasehq/stagehand-server-v3` package. It doesn't look at
`package.json` versions.

The last such changeset landed on 2026-06-09 (v3.7.2, #2217). Since
then, 39 changesets have merged that touch
`packages/core`/`packages/server-v3` — including Gemini 3.5 Flash CUA
support (#2273) and an AI SDK warning fix affecting act/extract/observe
(#2359) — and none of them included the `stagehand-server-v3` package
line. So the release-detect job has returned `release=false` on every
push for over a month, even though `updateInternalDependencies: patch`
cosmetically bumps server-v3's `package.json`/CHANGELOG on every Version
Packages PR, making it look like a release happened when it didn't.

This changeset is a one-time catch-up: it doesn't change any code, it
just gives the release workflow a qualifying trigger so it cuts a binary
build containing everything already merged to `packages/core` since
v3.7.2.

Related: a user asked about this gap on Discord, referencing #2333
(Gemini 3.5 Flash support request).

## What changed

- Added `.changeset/stagehand-server-v3-catchup-release.md` bumping
`@browserbasehq/stagehand-server-v3` (patch, 3.7.2 → 3.7.3).

## E2E Test Matrix

| Command / flow | Observed output | Confidence / sufficiency |
| --- | --- | --- |
| Ran the exact front-matter regex from
`stagehand-server-v3-release.yml`'s `detect` job against the new
changeset file | `Parsed: @browserbasehq/stagehand-server-v3 patch
matches target package: true` | Proves this changeset satisfies the
workflow's own detection logic and will set `release=true` on merge to
main, advancing the tag from `v3.7.2` to `v3.7.3`. |
| Verified all 13 historical `stagehand-server-v3` tags against their
triggering commit's changeset | 13/13 correlate exactly with a changeset
explicitly bumping `@browserbasehq/stagehand-server-v3` | Confirms the
detection mechanism is real and consistent — this isn't a guess about
how the pipeline works. |
| Scanned all 39 changesets added to main since the `v3.7.2` tag commit
| 0/39 include a `stagehand-server-v3` line | Confirms the gap is total
(not partial) and this PR is the correct/only trigger needed. |

Linear:
[STG-2587](https://linear.app/browserbase/issue/STG-2587/cut-a-stagehand-server-v3-release-to-catch-up-on-binary-drift-since)

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Triggers a catch-up SEA binary release for
`@browserbasehq/stagehand-server-v3` by adding a changeset, so all core
changes since v3.7.2 ship (including Gemini 3.5 Flash computer-use
support).
Addresses Linear STG-2587 by closing the drift between version bumps and
actual binary releases.

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

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2367?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. -->
…riments (#2177)

## What

Stacked on #2138.

- Persist trajectories under
`<root>/<experiment>__<model>/<task.id>/<runId>/` instead of scattered
per-task timestamp dirs, so a run's trajectories live in one folder and
concurrent multi-model runs of the same suite never interleave on disk.
- Write a `metadata.json` into every trajectory dir (experiment, model,
provider, environment, task, runId, status) so a trajectory's run never
has to be reverse-engineered after the fact.
- Write an `experiment.json` at the group root cross-linking the local
trajectories to the resolved Braintrust experiment (hashed name, id,
project, URLs) once `Eval()` resolves.
- New `framework/trajectoryGroup.ts` owns the slugging/layout/metadata
helpers; `TrajectoryRecorder`, the external-harness persister, and both
eval entrypoints (`index.eval.ts`, `framework/runner.ts`) consume it.

## Why

Mapping on-disk trajectories back to their Braintrust experiment
previously required guessing by timestamp, and concurrent runs of
different models wrote into the same root with no on-disk marker —
making post-hoc analysis error-prone.

## Notes

- Local persistence only; Braintrust experiment naming/metadata is
unchanged.
- Tooling that globs `.trajectories/*` keeps working (one extra path
level); the trajectoryRecorder unit test asserts the new layout +
`metadata.json`.



<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Groups eval trajectories by experiment, unambiguous model, and a
run-unique token, and cross-links each run to its Braintrust experiment.
Adds atomic dir reservation, single-flight caching, and env hygiene so
runs never overwrite, mislabel, or leak; the run token uses 64‑bit
entropy and result persistence is order-free (also trims internal
comments; no logic changes).

- **New Features**
- Store trajectories at
`<root>/<experiment>[__<model>]__<runToken>/<task.id>/<runId>/`; write
`metadata.json` per trajectory and an `experiment.json` at the group
root linking to the resolved Braintrust experiment; implemented in
`framework/trajectoryGroup.ts` and used by the recorder, adapter
persister, runner, and `index.eval.ts`.
- Generate one run token per run (64‑bit entropy); derive the
group/metadata model from the actual testcase matrix when unambiguous
(`EVAL_TRAJECTORY_MODEL`), not the requested override; stamp
`EVAL_EXPERIMENT_NAME`/`EVAL_TRAJECTORY_GROUP`; `writeExperimentLink()`
takes an explicit group, respects `EVAL_TRAJECTORY_ROOT`, and only
writes if persistence is on and the group dir exists.
- Bench hardening: add `taskDates.ts` for safe rolling dates (WebMD);
update Healthline to stop before final subscribe; clarify Google Maps
rubric; KFC requires pickup scheduling; SFPL uses the live application
URL; default `maxSteps`: `agent/sf_library_card` 1→10,
`agent/sf_library_card_multiple` 20→25. TUI `verify` help and docs
reflect the new layout.

- **Bug Fixes**
- Prevent clobbering by atomically reserving trajectory dirs
(`reserveTrajectoryDir` adds `-2/-3` on collision) with single-flight
caching in the recorder; `finish()` is idempotent; result persistence is
order-free (now covered by tests); `metadata.json` records `runDir` and
`attempt`.
- Hardened slugging: reject pure-dot values (e.g., `"."`, `".."`) to
avoid path escapes; fall back to the `"default"` group.
- REPL env hygiene: `withEnvOverrides()` snapshots/restores run-stamped
env (`EVAL_TRAJECTORY_GROUP`, `EVAL_EXPERIMENT_NAME`,
`EVAL_TRAJECTORY_MODEL`) so runs don’t leak between commands; recorder
and adapter both honor `EVAL_TRAJECTORY_ROOT`.
- Trimmed internal comments and moved a misplaced docstring; no logic
changes.

<sup>Written for commit 2d17642.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2177?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 Opus 4.8 <noreply@anthropic.com>
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.