Skip to content

docs: add CONTRIBUTING.md for external contributors - #2561

Closed
felipeofdev-ai wants to merge 1406 commits into
browserbase:mainfrom
felipeofdev-ai:add-contributing-guide
Closed

docs: add CONTRIBUTING.md for external contributors#2561
felipeofdev-ai wants to merge 1406 commits into
browserbase:mainfrom
felipeofdev-ai:add-contributing-guide

Conversation

@felipeofdev-ai

@felipeofdev-ai felipeofdev-ai commented Aug 3, 2026

Copy link
Copy Markdown

Summary Adds a root CONTRIBUTING.md so external contributors have a clear path beyond the short README note. ## Changes - Development setup (pnpm install/build/example) - Priority guidance (reliability  extensibility  speed  cost) - PR expectations + note about fork CI approval handoff - Bug report pointers - Links README Contributing section to the new guide Fixes #163 ## Test plan - [ ] Skim CONTRIBUTING.md for accuracy vs current scripts in root package.json - [ ] Confirm Discord / priority wording still matches maintainer intent


Summary by cubic

Adds a root CONTRIBUTING.md and overhauls the contributor workflow: clear guidelines, issue/PR templates, external‑contributor CI handoff, and formatting/linting hooks. Converts the repo to a pnpm monorepo and introduces a new browse CLI package with release workflows; fixes #163.

  • New Features
    • Guide covers contribution priorities, dev setup with pnpm, test scripts, PR expectations (incl. fork CI approval handoff), and bug reporting; README links to it.
    • Automation: external PR mirror/approval workflows, unified CI, release pipelines (core and servers v3/v4), eval runners, SDK build, CODEOWNERS, and a PR template.
    • Tooling: Prettier + Husky pre-commit, ESLint, docs prose guide, expanded .env.example, updated .gitignore; Node.js ^20.19.0 or >=22.12.0 enforced.
    • Monorepo layout with Turbo and a new packages/cli (browse) package (commands, docs, Dockerfile) plus Changesets config, CHANGELOG, and LICENSE.

Written for commit d1364a2. Summary will update on new commits.

Review in cubic


— Felipe Fernandes · Systems & Agentic AI Engineer
https://github.com/felipeofdev-ai · https://felipeofdev-ai.github.io/

seanmcguire12 and others added 30 commits March 20, 2026 17:17
# 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.
…#1879)

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

---------
…se#1873)

# 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. -->
…se#1890)

## 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. -->
…rowserbase#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)

---------
…essions (browserbase#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. -->

---------
…base#1885)

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

---------
…e-cli workflow (browserbase#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. -->
# 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. -->

---------
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. -->
…owserbase#1886)

## 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)

---------
# why
- the `timeoutMs` param in `goto` was not being respected when using the
stagehand api
- the `/navigate` route on the server expects `timeout`, while the
actual `goto()` implementation in node expects `timeoutMs`
# what changed
- added logic to correctly send the `timeoutMs` param to the server as
`timeout`
- added server side logic to map the `timeout` back to `timoeutMs`
(which is what `goto()` expects)
### screenshot: (navigate correctly adheres to defined timeout)
<img width="691" height="462" alt="Screenshot 2026-03-27 at 4 13 04 PM"
src="https://github.com/user-attachments/assets/946de567-70d5-4fa9-be8e-646bb996e894"
/>


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes timeout handling for navigation by mapping `timeoutMs` on the
client to `timeout` over the API, then back to `timeoutMs` for
`page.goto()`. Navigations now respect the configured timeout when using
the Stagehand API.

- **Bug Fixes**
  - Client sends `timeout` to `/navigate` from `options.timeoutMs`.
  - Server maps `timeout` to `timeoutMs` before calling `page.goto()`.

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

<!-- End of auto-generated description by cubic. -->
…browserbase#1888)

## Summary
- Adds a pre-commit hook that runs prettier on staged files to catch
formatting issues before CI
- Uses `husky` for git hooks and `lint-staged` to scope prettier to only
staged files
- Preserves the existing `prepare` script behavior (`node
packages/core/scripts/prepare.js`)

## Changes
- Added `husky` and `lint-staged` as root devDependencies
- Created `.husky/pre-commit` hook that runs `pnpm exec lint-staged`
- Configured lint-staged: `{ "*": "prettier --write --ignore-unknown" }`
(respects `.prettierignore`)

## Test plan
- [x] Hook runs on commit — verified: badly formatted file auto-fixed
during commit
- [x] `pnpm install` sets up hooks correctly via `prepare` script

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

---------
## Why
The original server-v4 stacked PRs merged into each other after
`STG-1614` had already merged into `main`, so the env/test-harness work,
Drizzle foundation, and Drizzle schema/controller flow never actually
reached `main`.

This PR recovers that full stack onto current `main` in one branch.

## What Changed
- cherry-picked the three stacked squash-merge commits from `browserbase#1896`,
`browserbase#1897`, and `browserbase#1898` onto fresh `main`
- preserved the full `server-v4` env/test harness cleanup, Drizzle
foundation, DB smoke tests, schema/migration work, and `/v4/llms`
controller/service/repo flow
- added one small follow-up commit to normalize `pnpm-lock.yaml` against
current `main` after applying the recovered stack

## Test Plan
- `pnpm install`
- `pnpm --filter @browserbasehq/stagehand-server-v4 run lint`
- `pnpm --filter @browserbasehq/stagehand-server-v4 run test:unit`
- `pnpm --filter @browserbasehq/stagehand-server-v4 run build`
- `pnpm --filter @browserbasehq/stagehand-server-v4 run gen:openapi`

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Recovers the server‑v4 Drizzle stack onto `main`, restoring DB‑backed
`/v4/llms` and switching the public `LLMId` to UUID. Adds a typed env,
clean app bootstrap, and a local `pglite`/Postgres runtime with
migrations, tests, and a simpler source‑based test harness.

- **New Features**
- Drizzle foundation with `pglite` default and Postgres option; new
`db:*` scripts via `drizzle-kit`.
- Fastify `databasePlugin` and unified DB client; `app.ts` bootstrap
with health/readiness and Swagger/OpenAPI; env via `@t3-oss/env-core`.
- Tables/relations for LLMs and Stagehand; DB‑derived Zod schemas; first
migration; OpenAPI updated for UUID `LLMId` with strict format and
pattern.
- LLM module (repo/service/controller) backing `/v4/llms`; routes now
delegate to the controller; browser session create uses the service and
auto‑creates a system default LLM when `llmId` is missing.
- Tests run from source via `tsx`; test server script simplified;
`turbo` tasks cleaned up; schema cleanup (`z.url()`).

- **Migration**
  - Run: `pnpm --filter @browserbasehq/stagehand-server-v4 db:migrate`.
- Local dev uses `pglite` at `~/.stagehand/db/stagehand-v4`. For
Postgres set `STAGEHAND_DB_MODE=postgres` and `DATABASE_URL`.
- Update clients to treat `LLMId` as a UUID and regenerate from
`openapi.v4.yaml` if needed.

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

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

---------
## Summary
Move browse-cli to a merge-first release flow so `main` becomes the
source of truth before npm is mutated.

## New release flow
1. Merge normal feature PRs into `main`, with browse-cli changesets as
usual.
2. Trigger `Prepare CLI Release` when browse-cli is ready to ship.
3. That workflow versions only `@browserbasehq/browse-cli` and opens or
updates a release PR.
4. Merge the release PR.
5. The existing `Release` workflow on `main` detects the browse-cli
version bump, publishes a packed tarball to npm with provenance, and
tags the merge commit.

## What changed
- rewired `release-cli.yml` into a prepare-only workflow that creates or
updates a release PR instead of publishing directly
- moved browse-cli publishing into `release.yml`, gated on an actual
version bump landing on `main`
- kept the `pnpm pack` -> `npm publish --provenance` path so workspace
dependencies are resolved in the published tarball
- made the main-branch publish path idempotent when that browse-cli
version is already on npm
- kept browse-cli tagging on the merge commit instead of trying to tag
from the manual workflow
- synced `packages/cli/package.json` to `0.3.0` and documented npm
`0.3.0` as a broken publish in the CLI changelog

## Why this is better
The previous standalone release design could succeed at publishing to
npm and then fail while trying to sync `main`, which left npm ahead of
git. That is the hard failure mode because npm versions are immutable.

This PR flips the order:
- git changes land first through a normal PR
- npm publish happens afterward from the exact merged commit

That removes the whole class of "published version is not reflected on
`main`" failures and makes reruns much safer.

## Validation
- `pnpm exec prettier --check .github/workflows/release.yml
.github/workflows/release-cli.yml packages/cli/package.json
packages/cli/CHANGELOG.md`
- packed `packages/cli` locally and verified the tarball manifest
resolves `@browserbasehq/stagehand` to `3.2.0`
- confirmed the currently published npm `0.3.0` manifest still contains
the broken `workspace:*` dependency
- simulated the prepare flow in a throwaway worktree and confirmed it
would cut `0.4.0` from the current 2 minor + 2 patch browse-cli
changesets
Prepare the next browse-cli release by versioning the package on `main`.

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

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


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Release `@browserbasehq/browse-cli` 0.4.0 with new session attach and
smarter local Chrome discovery, plus stability fixes. Bumps the package
version and updates the changelog; the Release workflow will publish
from this commit.

- **New Features**
  - Add `--connect` to attach to an existing Browserbase session by ID.
- `browse env local` auto-discovers debuggable Chrome and attaches when
found (falls back to isolated). Adds `--isolated`, positional CDP
target, and `--ws` accepts bare port numbers.

- **Bug Fixes**
  - Add CLI metadata to Browserbase sessions created via the CLI.
- Clear cached browser state when CDP connection drops to prevent
“awaitActivePage: no page available” errors.

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

<!-- End of auto-generated description by cubic. -->
# why
- models would sometimes respond with the variable name without wrapping
it in percentage signs.
- when this happens, the regex would fail to replace the variable name
with the actual variable value
# what changed
- adjusted the prompting for the condition when `variables` are provided
by the user
- the updated prompting is more specific around what the variables are
used for, and the required return shape.
# test plan
- existing act evals

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Hardened the act variables prompt so models always return argument
placeholders as %variableName%, preventing failed variable substitution.
Also clarified the element ID format with concrete examples.

- **Bug Fixes**
- Added explicit, repeated guidance to use %variableName% in the
`arguments` array when variables are provided.
- Improved element ID schema description with examples like '0-76' and
'16-21'.

- **Refactors**
- Introduced `buildActVariablesPrompt()` and reused it in both act and
step-two prompts to keep messaging consistent and avoid duplication.

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

<!-- End of auto-generated description by cubic. -->
# why
- when a user created a new tab by clicking the "+" icon in a headful
chromium browser, stagehand would not attach to it, because it filtered
out the `"chrome://newtab"` url pattern
# what changed
- extended the conditional in `isNonWebTarget()` to allow for
`"chrome://newtab"` URLs to pass through, so that stagehand can attach
to them
<img width="491" height="95" alt="Screenshot 2026-03-30 at 4 00 48 PM"
src="https://github.com/user-attachments/assets/64638fc4-7334-4438-8df5-5a91344b6afb"
/>
# test plan
- added a test which creates a newtab target, & confirms that a new page
object gets created

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes `@browserbasehq/stagehand` not attaching to tabs opened via the
"+" button in Chromium by tracking `chrome://newtab/` page targets so a
page is created and becomes usable after navigation.

- **Bug Fixes**
- Updated `isNonWebTarget()` to always track top-level `page` targets,
even with non-web schemes like `chrome://newtab/`.
- Added Playwright tests to verify `chrome://newtab/` tabs appear in
`pages()` and work after navigating to a web URL.

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

<!-- End of auto-generated description by cubic. -->
…erbase#1911)

## Summary
- The `browse-cli` metadata key (with hyphen) is rejected by the
Browserbase API as an invalid metadata key, causing `env remote` to fail
with a `400 Key is not a valid metadata key: browse-cli` error.
- Changed the key to `browse_cli` (underscore) which is a valid metadata
key format.

Linear:
https://linear.app/browserbase/issue/STG-1733/fix-use-valid-metadata-key-for-browse-cli-sessions

## Test plan
- [x] Verified old key (`browse-cli`) returns 400 from Browserbase API
- [x] Verified new key (`browse_cli`) returns 201 and session creates
successfully

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

---------
Prepare the next browse-cli release by versioning the package on `main`.

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

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


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Release `@browserbasehq/browse-cli@0.4.1`, a patch that fixes an invalid
session metadata key. This bumps the package version and updates the
changelog for publishing from `main`.

- **Bug Fixes**
- Use underscore in `browse_cli` session metadata keys instead of a
hyphen.

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

<!-- End of auto-generated description by cubic. -->
…test (browserbase#1934)

## Summary

- **Root cause:** The `changesets/action` step corrupts git state by
checking out `changeset-release/main` and creating a new commit. After
it returns, `HEAD` points to the wrong branch/commit. The browse-cli
check step uses `HEAD`/`HEAD^` which are now wrong, so it always says
"browse-cli version did not change" and skips the publish. Then the
stagehand canary step (`changeset publish --tag alpha`) finds the
unpublished version and publishes it under `alpha` instead of `latest`.
- **Fix:** Replace all `HEAD`/`HEAD^` references with `${{ github.sha
}}`/`${{ github.sha }}^` (the actual merge commit SHA, immune to branch
switches). Add `git checkout ${{ github.sha }}` to restore the working
tree. Add explicit `--tag latest` to the publish command.
- **New:** Add a browse-cli canary step that publishes
`X.Y.Z-alpha-{sha}` versions under the `alpha` tag on feature branch
merges (matching stagehand's existing canary pattern).

## What was happening

1. PR merged to main with browse-cli version bump (e.g. 0.3.0 -> 0.4.0)
2. `changesets/action` runs, switches to `changeset-release/main`,
creates commit
3. `HEAD` now points to the changeset PR commit, not the merge commit
4. browse-cli check compares wrong commits, finds no version change,
sets `should_publish=false`
5. Stagehand canary step runs `changeset publish --tag alpha`, which
picks up the unpublished browse-cli and publishes it under `alpha`
6. Result: `npm info @browserbasehq/browse-cli` shows `latest: 0.3.0`,
`alpha: 0.4.0`

## What this PR does

1. Uses `${{ github.sha }}` (the actual merge commit) instead of `HEAD`
for all git comparisons
2. Adds `git checkout ${{ github.sha }}` to restore the working tree
after changesets/action
3. Adds `--tag latest` to the `npm publish` command for clean releases
4. Adds a new "Publish browse-cli canary" step for feature branch merges
(publishes `X.Y.Z-alpha-{sha}` under `alpha` tag)

## Validation

- Traced CI logs from recent releases to confirm `HEAD` was pointing to
`changeset-release/main` after the changesets step
- Confirmed `npm view @browserbasehq/browse-cli` shows `latest: 0.3.0`
while `alpha: 0.4.1` (the bug in action)
- `${{ github.sha }}` is set by GitHub Actions to the merge commit SHA
and is immutable throughout the run

## Linear


https://linear.app/browserbase/issue/STG-1739/fix-browse-cli-release-publishes-under-alpha-instead-of-latest

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

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes the release workflow so `@browserbasehq/browse-cli` clean releases
publish to `latest`, and canaries publish to `alpha`. Addresses Linear
STG-1739 by using the immutable merge SHA to avoid `changesets/action`
HEAD switches.

- **Bug Fixes**
- Use `${{ github.sha }}`/`${{ github.sha }}^` for diffs and version
reads to avoid wrong `HEAD`.
- Restore the working tree before checks/publish and reset
`packages/cli/package.json` after canary publish.
  - Publish clean releases with `npm publish ... --tag latest`.

- **New Features**
- Add canary step that publishes `X.Y.Z-alpha-{shortSha}` to `alpha`
when CLI files changed but no clean release occurred.
- Skip canary if the version already exists or no CLI changes are
detected.

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

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

---------
# why

- In some scenarios, users need more granular control / access to the
LLM at the provider leve

# what changed

- Added support for passing custom "middleware" in the model options 

# test plan

- wrote tests 
- tested locally with own middleware 

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds first-class LLM middleware to intercept and wrap model calls for
logging, usage tracking, or request transforms without changing call
sites. Per-call middleware is used only for that call and is not cached.

- **New Features**
- Accepts `middleware: LanguageModelV2Middleware` in `V3` model config;
`LLMProvider` and `getAISDKLanguageModel` wrap models via
`wrapLanguageModel` from `ai`.
- Supports per-call overrides via `model` options; override middleware
is passed through `LLMProvider.getClient`, applies only to that call,
and is never cached.
- Works across providers and preserves `modelId`, `doGenerate`, and
`doStream`. Tests cover usage capture, streaming, errors, chaining, and
edge cases.

- **Migration**
- To use: pass `middleware` in `model` config or a per-call override,
e.g. `{ modelName: 'openai/gpt-4o', apiKey: '...', middleware }`.
- Middleware is only applied in direct/local execution and is not
serialized over HTTP.

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

<!-- End of auto-generated description by cubic. -->
… family (browserbase#1852)

Mirrored from external contributor PR browserbase#1844 after approval by
@miguelg719.

Original author: @praveentcom
Original PR: browserbase#1844
Approved source head SHA: `a637dc329bfc5426bb71c8551c812191ed631527`

@praveentcom, 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
All GPT-5.x series models don't support `minimal` as the
`reasoningEffort`. Currently, it is enabled only for GPT-5.1 and GPT-5.2
models to set the reasoningEffort as `low`. This would start throwing
errors like these.

`Unsupported value: 'minimal' is not supported with the 'gpt-5.4' model.
Supported values are: 'none', 'low', 'medium', 'high', and 'xhigh'.`

This PR fixes the behavior to set the reasoning effort as low for all
GPT-5.x series models so that we don't need to manually patch it every
time when a new SOTA model is released.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Default `reasoningEffort` to "none" for all `gpt-5.*` models (excluding
`codex`) to avoid unsupported "minimal" errors, with an override via
`ClientOptions.reasoningEffort`, and default GPT-5 `textVerbosity` set
to "low".
Also plumbs `clientOptions` into both `AISdkClient` and
`AISdkClientWrapped` and applies the same reasoning/text-verbosity
behavior (`codex` stays "medium").

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

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

<!-- external-contributor-pr:owned source-pr=1844
source-sha=a637dc329bfc5426bb71c8551c812191ed631527 claimer=miguelg719
-->

---------
seanmcguire12 and others added 19 commits July 13, 2026 11:46
# 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 (browserbase#2359)

## What & why

[browserbase#2305](browserbase#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 browserbase#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 browserbase#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 browserbase#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 (browserbase#2334)

## Why

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

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

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

## Design

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

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

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

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

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

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

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

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

## E2E Test Matrix

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

## Future work

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

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

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

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

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

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

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

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

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

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

---------
…I publisher (browserbase#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 browserbase#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.

---------
…wserbase#2361)

## 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 browserbase#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 browserbase#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).

---------
…se#2366)

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

---------
## 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, browserbase#2217). Since
then, 39 changesets have merged that touch
`packages/core`/`packages/server-v3` — including Gemini 3.5 Flash CUA
support (browserbase#2273) and an AI SDK warning fix affecting act/extract/observe
(browserbase#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 browserbase#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 (browserbase#2177)

## What

Stacked on browserbase#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. -->

---------
## Summary

- Add Kyle Jeong as CODEOWNER for docs and Markdown changes.
- Add the docs styling guide to the repo's agent instruction files.
- Configure Cubic with a file-backed docs style custom rule so PR
reviews actively flag violations.

## Validation

- `git diff --check`
- Parsed `cubic.yaml` locally with Ruby YAML
- Confirmed `.cubic/docs-style-guide.md` is under Cubic's
10,000-character custom-agent limit
…rowserbase#2347)

## Summary
- add `openaiEndpointFormat` to public model configuration
- select OpenAI Chat Completions when `openaiEndpointFormat` is `chat`
- preserve Responses API as the default
- omit the Responses-only `store` option for Chat providers, including
finalization calls
- add a patch changeset

## Usage

```ts
const stagehand = new Stagehand({
  model: {
    modelName: "openai/databricks-claude-opus-4-6",
    apiKey: process.env.DATABRICKS_TOKEN,
    baseURL: "https://example.databricks.com/serving-endpoints",
    openaiEndpointFormat: "chat",
  },
});
```

The option names the *wire format* used against the OpenAI-compatible
endpoint at `baseURL` — deliberately not `apiMode` (too generic) or
`openaiEndpoint` (reads like it holds a URL, and sits right next to
`baseURL`).

## Databricks verification
- basic Chat Completions request: 200
- request with `store: false` reproduced the 400: extra inputs are not
permitted
- two-turn tool call and tool-result replay without `store`: 200 on both
turns

## Testing
- focused llm-provider and agent suites: 19 passed
- TypeScript typecheck
- ESLint on touched TypeScript files
- Prettier check on touched files
Propagates openaiEndpointFormat through the server-v3 request schema and
generated OpenAPI spec so Stainless can expose it in the Python SDK.
Adds a regression test covering request and session initialization
parsing

Validation:
- core build
- server-v3 OpenAPI generation
- server-v3 typecheck
- requestModelConfig unit tests (9 passing)


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Expose the OpenAI endpoint wire format in v3 via `openaiEndpointFormat`
on model config and OpenAPI, so SDKs can target `responses` or `chat`
(defaults to Responses). Adds a unit test to ensure the format is
preserved through request parsing and session initialization, and a
changeset to release in `@browserbasehq/stagehand` and
`@browserbasehq/stagehand-server-v3`.

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

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

## Why

`e2e/local/chrome-newtab-page-tracking` fails intermittently on
unrelated PRs (e.g. [this run on
browserbase#2347](https://github.com/browserbase/stagehand/actions/runs/29538363318/job/87758303923?pr=2347)).
This removes the cause.

## What changed

Updated `chrome://new-tab` to go to `chrome://version` from a static
injected site

## Test plan
- [x] get CI fully green again
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.1

### Patch Changes

- [browserbase#2359](browserbase#2359)
[`2cd1edf`](browserbase@2cd1edf)
Thanks [@shrey150](https://github.com/shrey150)! - Remove the noisy AI
SDK "system message in messages" warning from `act()`, `extract()`, and
`observe()` (including when the agent's own tools call them internally).

- [browserbase#2347](browserbase#2347)
[`84197d8`](browserbase@84197d8)
Thanks [@miguelg719](https://github.com/miguelg719)! - Allow
OpenAI-compatible models to select the Chat Completions API with
`openaiEndpointFormat: "chat"`

## @browserbasehq/stagehand-evals@2.1.0

### Minor Changes

- [browserbase#2275](browserbase#2275)
[`cdae405`](browserbase@cdae405)
Thanks [@miguelg719](https://github.com/miguelg719)! - Add OdysseysBench
as a supported agent benchmark in the evals CLI. OdysseysBench is a
200-task web-agent benchmark (45 easy / 46 medium / 109 hard); each task
ships a weighted rubric that is baked into the verifier's
`precomputed_rubric` format so process + outcome are scored against the
published criteria. Run with `--eval-name agent/odysseysbench` (or the
`external_agent_benchmarks` category); supports
`EVAL_ODYSSEYSBENCH_LIMIT`, `EVAL_ODYSSEYSBENCH_SAMPLE`,
`EVAL_ODYSSEYSBENCH_LEVEL`, and `EVAL_ODYSSEYSBENCH_IDS`.

### Patch Changes

- Updated dependencies
\[[`2cd1edf`](browserbase@2cd1edf),
[`84197d8`](browserbase@84197d8)]:
    -   @browserbasehq/stagehand@3.7.1

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

### Patch Changes

- [browserbase#2347](browserbase#2347)
[`84197d8`](browserbase@84197d8)
Thanks [@miguelg719](https://github.com/miguelg719)! - Allow
OpenAI-compatible models to select the Chat Completions API with
`openaiEndpointFormat: "chat"`

- [browserbase#2367](browserbase#2367)
[`a985943`](browserbase@a985943)
Thanks [@shrey150](https://github.com/shrey150)! - Cut a new
stagehand-server-v3 SEA binary release to catch up with recent core
changes, including Gemini 3.5 Flash computer-use support.

- Updated dependencies
\[[`2cd1edf`](browserbase@2cd1edf),
[`84197d8`](browserbase@84197d8)]:
    -   @browserbasehq/stagehand@3.7.1
## What
Adds `--only-errors` (and `--failed-requests`) to `browse cloud sessions
logs`.

By default the command returns the full CDP firehose (~hundreds of
events, unchanged). `--only-errors` runs a deterministic reducer that
returns just the high-signal error records:
- console errors / warnings / asserts
- uncaught exceptions (with app-frame-trimmed stacks)
- HTTP 4xx/5xx responses
- net-level load failures (CORS / DNS / connection)

deduped, no LLM.

```
browse cloud sessions logs <id> --only-errors
browse cloud sessions logs <id> --only-errors --failed-requests
```

## Why
Agents debugging Browserbase sessions (build/verification agents for AI
app builders) want the runtime errors, not the raw firehose. Today they
pull ~hundreds of CDP events and grep. `--only-errors` returns the
handful that matter in one call — far fewer tokens/tool-calls in the
agent loop, and language-agnostic (shell out from any agent).

## Scope / notes
- **Default behavior unchanged** (raw firehose) — opt-in only, so no
breaking change.
- Reducer lives in `packages/cli/src/lib/cloud/reduce-logs.ts` (pure,
unit-testable).
- Catches console / exception / 4xx-5xx / net-failure classes. Does
**not** catch an HTTP 200 response carrying an error *body* (that needs
response-body capture at ingest — follow-up).

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

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Add --only-errors to cloud sessions logs to return only high-signal
errors, with an optional --failed-requests to narrow to failed network
calls. Default output is unchanged.

- **New Features**
- `--only-errors`: returns console errors/warnings/asserts, uncaught
exceptions (trimmed stacks), HTTP 4xx/5xx, and network load failures;
deduped.
- `--failed-requests`: with `--only-errors`, returns only
failed/error-status network requests.
- Deterministic reducer added in
`packages/cli/src/lib/cloud/reduce-logs.ts` (pure and unit-testable).

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

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

`model: "auto"` shipped in browserbase#2328 but is undocumented. This adds a
**Model Router** section to `/v3/configuration/models`, nested under
**Model Gateway** right after **Setup**, plus a launch callout at the
top of the page.

# what changed

`packages/docs/v3/configuration/models.mdx` only — no code, so no
changeset.

- **`(NEW) Model Router` callout** at the top of the page, above the
Model Gateway one, linking to `#model-router`. Dropped `(NEW)` from the
Model Gateway callout so only one feature is flagged as new.
- **`### Model Router`** subsection: what it does, basic usage, Model
Gateway billing (market-price tokens on your Browserbase key), and the
30–40% cost reduction vs. pinning one frontier model.
- **`#### Requirements`** table covering the four API-only constraints
the constructor enforces (`env: "BROWSERBASE"`, `disableAPI: false`,
`experimental: false`, no `llmClient`), plus a warning that `"auto"` has
no local fallback if the API is unavailable at init.
- **`#### Per-call routing`**: `{ model: "auto" }` as a per-primitive
override on top of a concrete default, including the object form (`{
modelName: "auto", temperature: 0.5 }`) and the note that overrides
don't inherit the session provider/API key.
- Agent support: works in default `dom`/`hybrid`, **not** with `mode:
"cua"` (`"auto"` isn't in `AVAILABLE_CUA_MODELS`, so it throws
`CuaModelRequiredError`).
- Caching: local replay is skipped for `"auto"` sessions; the API's
server-side cache handles it.
- Troubleshooting accordion for both auto-specific errors, and an
env-var table row.

Behavior claims were taken from `modelUtils.ts`, `v3.ts`, `api.ts`,
`AgentCache.ts`, and `tests/unit/auto-model.test.ts`. The routing
heuristic itself is server-side and not in this repo, so the copy
deliberately stays general about how a model gets picked — nothing here
goes stale when the router changes.

# open question

Should omitting `model` entirely also route through Model Router?

Today it doesn't — `resolveModelConfiguration()` falls back to
`DEFAULT_MODEL_NAME = "openai/gpt-4.1-mini"`
(`packages/core/lib/v3/v3.ts:101,115`), so an omitted `model` pins
gpt-4.1-mini (run through Model Gateway if only a Browserbase key is
set). That's a fixed model, not per-call routing. I left the omit path
out of the docs for now. If the default should become `"auto"`, that's a
core change and the docs can follow alongside it.

# test plan

- `mintlify broken-links` — no new broken links (the 5 reported are
pre-existing and in other files)
- JSX components balanced, code fences even, heading nesting verified,
`#model-router` anchor resolves

---------
## Summary

- translate daemon socket `ECONNREFUSED` and `ENOENT` failures into a
human-readable error
- print the exact `browse open` command that restarts the requested
session
- document that API keys are forwarded to an already-running daemon and
that `browse stop` is idempotent
- preserve regression coverage for late environment variables and
exit-code-zero cleanup

## Root cause

The daemon client passed raw Unix socket errors through to users when
the daemon disappeared between the readiness check and the request.
Agents received `ECONNREFUSED` without a recovery command.

## Impact

Agents now get an actionable `daemon_not_running` failure with the exact
command needed to restart the session. Recovery command arguments are
shell-quoted, and a daemon disappearing between status and stop is
treated as an already-stopped session. Cleanup synchronizes with daemon
startup so it preserves a replacement daemon that starts during the stop
race. The bundled SKILL.md also makes the already-fixed env timing and
stop behavior explicit.

## E2E Test Matrix

| Command / flow | Observed output | Confidence / sufficiency |
| --- | --- | --- |
| `pnpm --filter browse build` | TypeScript compilation and oclif
manifest generation completed successfully. | Proves the exact local CLI
code under review builds; does not exercise a live browser. |
| `pnpm --filter browse test:cli` | 25 test files passed; 366/366 tests
passed. This includes deterministic daemon disappearance/restart races,
adversarial recovery-command shell quoting, malformed CDP log payloads,
and the complete CLI contract suite. | Proves the full CLI suite passes
against the completed local build on macOS; GitHub CI provides the
Ubuntu/Windows matrix. |
| `BROWSE_DAEMON_DIR=<temp dir> node packages/cli/bin/run.js stop
--session no-daemon-smoke` | Exited `0` and printed `{ "stopped": false,
"session": "no-daemon-smoke" }`. | Exercises the built CLI's real
absent-daemon stop path and confirms its documented successful no-op
behavior. |
| `pnpm --filter browse lint` | Full-package Prettier, ESLint, and
TypeScript checks completed successfully. | Reproduces the CI lint
pipeline locally and verifies the daemon changes, log reducer, tests,
and inherited CLI lint baseline. |

Linear:
[GRO-1908](https://linear.app/browserbase/issue/GRO-1908/cli-namespace-2027-dev-suggested-docs-updates)

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Replaces raw socket errors with a clear `daemon_not_running` message and
prints the exact, shell-quoted `browse open` recovery command. `browse
stop` now treats a missing daemon as already stopped, validates
ownership before cleanup, and avoids tearing down a daemon that restarts
mid-race. Addresses Linear:
https://linear.app/browserbase/issue/GRO-1908/cli-namespace-2027-dev-suggested-docs-updates.

- **Bug Fixes**
- Map `ECONNREFUSED`/`ENOENT` to `daemon_not_running` and show the
precise, session-aware recovery command.
- Make `browse stop` idempotent and safe: if the daemon vanished (no
`--force`), return `{ stopped: false }`; acquire a lock and verify
daemon ownership before removing pid/socket/lock; preserve a replacement
daemon started during the stop race (handles PID reuse).
- Harden CDP log reduction: stricter parsing, ignore malformed payloads,
and require numeric response statuses.
- Docs: clarify `BROWSERBASE_API_KEY` is forwarded on every command and
`browse stop` is idempotent; add troubleshooting for the new error.
- Tests: cover the error message and shell-quoted recovery command
(round-tripped through `/bin/sh`), stop race cleanup and restart
preservation, PID reuse, and reduce-logs edge cases.

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

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

---------
Prepare the next browse release by versioning the package on `main`.

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

After this PR merges, the `Release` workflow on `main` will publish
`browse@0.9.6` from that exact commit using `pnpm pack` + `npm publish
--provenance`.
Addresses browserbase#163 with setup, PR expectations, and fork CI notes.
@changeset-bot

changeset-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: d1364a2

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

This PR includes no changesets

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

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

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

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

This PR is from an external contributor and must be approved by a stagehand team member with write access before CI can run.
Approving the latest commit mirrors it into an internal PR owned by the approver.
If new commits are pushed later, the internal PR stays open but is marked stale until someone approves the latest external commit and refreshes it.

@github-actions github-actions Bot added external-contributor Tracks PRs mirrored from external contributor forks. external-contributor:awaiting-approval Waiting for a stagehand team member to approve the latest external commit. labels Aug 3, 2026

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 2 files

Architecture diagram
sequenceDiagram
    participant Dev as External Contributor
    participant Repo as GitHub Repository
    participant CI as CI Pipeline
    participant Maintainer as Maintainer

    Note over Dev,Maintainer: NEW: External Contribution Flow

    Dev->>Repo: Fork & clone repo

    Note over Dev: Local Development Setup
    Dev->>Dev: Run pnpm install
    Dev->>Dev: Run pnpm run build
    Dev->>Dev: Run pnpm run example
    Dev->>Dev: Run lint/format checks
    alt Local tests
        Dev->>Dev: Run pnpm run test:core/local
        Dev->>Dev: Run pnpm run test:e2e/local
        Dev->>Dev: Copy .env.example for credentials
    end

    Dev->>Repo: Create focused branch
    Dev->>Repo: Submit Pull Request

    alt External Contributor PR
        Repo->>CI: Trigger fork CI approval handoff
        CI->>Maintainer: Request CI approval
        Maintainer->>CI: Approve CI run
        CI->>Repo: Run full CI pipeline
        CI-->>Dev: Build/lint/test results
    else Internal PR
        Repo->>CI: Run full CI automatically
        CI-->>Dev: Build/lint/test results
    end

    Maintainer->>Repo: Review PR
    alt PR Approved
        Repo->>Repo: Merge PR
        Repo-->>Dev: Contribution accepted (MIT License)
    else Changes Requested
        Repo-->>Dev: Feedback
        Dev->>Repo: Push updates
    end

    Note over Dev: Bug Reporting Path
    Dev->>Repo: Use bug report template
    Repo-->>Dev: Issue created with version/browser/env
Loading

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

Re-trigger cubic

Comment thread CONTRIBUTING.md Outdated
Comment thread CONTRIBUTING.md Outdated
Match package.json engines (^20.19.0 || >=22.12.0) and point the
example script at packages/core/examples/example.ts.

Signed-off-by: Felipe Fernandes <felipe.of.dev@gmail.com>
@felipeofdev-ai

Copy link
Copy Markdown
Author

Addressed cubic review

  • Prerequisites now match package.json engines: Node.js ^20.19.0 or >=22.12.0.
  • Example path points to packages/core/examples/example.ts.

— Felipe Fernandes · Systems & Agentic AI Engineer
https://github.com/felipeofdev-ai · https://felipeofdev-ai.github.io/

@felipeofdev-ai
felipeofdev-ai force-pushed the add-contributing-guide branch 2 times, most recently from 1a2f5b7 to d1364a2 Compare August 4, 2026 03:26
@felipeofdev-ai
felipeofdev-ai requested a review from a team as a code owner August 4, 2026 03:26
@miguelg719 miguelg719 closed this Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contributor:awaiting-approval Waiting for a stagehand team member to approve the latest external commit. external-contributor Tracks PRs mirrored from external contributor forks.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Creating CONTRIBUTING.md doc