Skip to content

phase-h: TS harness port (v0.1.0 cut)#41

Merged
Alezander9 merged 7 commits intomainfrom
feat/phase-h-ts-harness
May 8, 2026
Merged

phase-h: TS harness port (v0.1.0 cut)#41
Alezander9 merged 7 commits intomainfrom
feat/phase-h-ts-harness

Conversation

@Alezander9
Copy link
Copy Markdown
Member

Phase H — TS harness port (v0.1.0 cut)

Drops uv from the install path and replaces the Python browser-harness subprocess + daemon with an in-process TypeScript CDP layer. Implements the migration plan end-to-end (steps 0–6). Step 7 is "tag from main"; that's an admin call after merge.

Net LOC (hand-written maintenance surface)

Lines
Hand-written code added (TS/TSX/MD/TXT/SH) +2,752
Hand-written code deleted (incl. ~3,400 LOC vendored Python harness) −5,254
Net −2,499

(Excludes generated.ts ~15k and the two protocol JSONs ~1.5 MB, which are committed-data, not maintenance surface — bun src/cdp/gen.ts regenerates generated.ts from the JSONs.)

The plan's target was −1,800 LOC; we beat it because the rewrite was tighter than budgeted (browser-execute.ts: 163 → ~125 LOC; harness.ts + uv-locate.ts: 217 LOC removed entirely).

What's in the box

Step 1 — vendor the CDP layer. packages/bcode-browser/src/cdp/: session.ts (~380 LOC), gen.ts (~270 LOC, bun src/cdp/gen.ts to regenerate), generated.ts (~15k LOC machine-generated, committed), browser_protocol.json + js_protocol.json from chromedevtools/devtools-protocol. Initial copy from browser-use/browser-harness-js@95b7a22a; ours from here on. Provenance recorded in packages/bcode-browser/src/cdp/PROVENANCE.md. The export namespace carve-out in generated.ts is documented in EXCEPTIONS.md (it's regenerated machine output, not hand-authored).

Step 2 — in-process browser_execute.

  • packages/bcode-browser/src/browser-execute.ts (full rewrite). Wraps the snippet with new AsyncFunction("session", code) against a per-opencode-session Session from a process-scope SessionStore. Snippet scope binds only session plus standard JS globals — nothing auto-loaded (Phase H hard rule sync: upstream v1.14.22 (eb7555d3c on dev) #3, "workspace as plain code, no privileged files"). console.log/error/warn/info monkey-patched around each snippet, restored in a finally even on throw/timeout.
  • packages/bcode-browser/src/session-store.ts — per-opencode-session CDP Session map, shared between browser_execute and browser_open_cloud. Optional onEvict cleanup callbacks (cloud-browser stops register here).
  • packages/opencode/src/tool/browser-execute.ts — Level-2 wrapper resolves <projectDir>/.bcode/agent-workspace/ per-call from InstanceState.context (opencode's existing project-detection — same source that finds .bcode/plans, .bcode/db, etc.).
  • packages/opencode/src/tool/browser-execute.txt — new prompt centered on the no-magic / workspace-as-plain-code model. Substitutes {{SKILLS_DIR}} at make-time; workspace path is project-relative (./.bcode/agent-workspace/) and constructible from process.cwd() inside a snippet.
  • agent.ts permission glob: harness/archive entries gone; replaced with **/.bcode/agent-workspace/**/* (read+edit) plus the runtime skills tree at <dataDir>/skills/*.
  • TUI: >>> /... Python-REPL prompts → > / JS prompts; input.pythoninput.code.
  • Deleted harness.ts + uv-locate.ts.

Step 3 — workspace dynamic-import smoke (packages/bcode-browser/test/workspace-import.test.ts). Four cases, all bun test against real fs: import abs path, edit-then-cache-bust, syntax-error rejection, nested workspace paths. The point isn't to build a framework — it's to verify the documented await import("/abs?t=" + Date.now()) pattern is honest about what it claims.

Step 4 — cloud-browser tool (browser_open_cloud).

  • packages/bcode-browser/src/cloud-browser.ts — POST /api/v3/browsers{id, cdpUrl, liveUrl}Session.connect({ wsUrl: cdpUrl })SessionStore.onEvict registers the PATCH stop. Fails fast if BROWSER_USE_API_KEY is absent.
  • packages/opencode/src/tool/browser-open-cloud.{ts,txt} + registry hook (one line).
  • After browser_open_cloud, every subsequent browser_execute drives the cloud browser via the same Session (no per-snippet re-attach).

Step 5 — skills, BROWSER.md, embed.

  • packages/bcode-browser/skills/BROWSER.md — browsercode-owned prompt for the agent's first browser_execute call. Covers connect, target attach, common moves (Page.navigate, mouse, Input.insertText, screenshot), the workspace write-once-import-many pattern, and a guardrails block (no top-level import, no CPU-bound loops without await, JSON-serializable returns).
  • packages/bcode-browser/skills/interaction-skills/*.md — 16 UI-mechanic recipes (dialogs, dropdowns, iframes, shadow DOM, uploads, screenshots, …) initial-copied from browser-harness-js@95b7a22a. Read-only docs; the agent reads them with read, never imports.
  • packages/bcode-browser/script/embed-skills.ts (new) replaces embed-harness.ts in packages/opencode/script/build.ts. Glob-driven, content-hash buildHash sentinel. Compiled binary extracts to <dataDir>/skills/ once per buildHash; baseline-overwrite on every launch (no agent-editable surface here — the agent's editable surface is <projectDir>/.bcode/agent-workspace/).
  • packages/bcode-browser/src/skills.ts (new) — runtime resolver. Dev: in-tree path. Compiled: extract on first call, stat-and-skip on warm launch.
  • Deleted packages/bcode-browser/harness/ (entire vendored Python tree), embed-harness.ts, harness-sync.md, script/check-harness-diff.sh. Trimmed script/check-upstream.sh to drop the harness branch. UPSTREAM.md retitled to track only anomalyco/opencode; the old harness sync log is preserved as historical archaeology in §3.
  • New memory file (agent-side): memory/browsercode/harness_watchlist.md records Python-harness behaviors the agent might want to port to TS later, individually and on cadence-of-need (not a sync schedule). Hard rule f4: scope turbo typecheck to browsercode-packages filter #2 mechanism.

Step 6 — cross-platform smoke.

  • Linux x64 (this Sprite, headless Chrome 147): bun build --compilebcode-linux-x64 (148 MB) boots cleanly without uv. Three test files, 9 cases: workspace-import (4) + cdp-smoke (1, attaches to real Chrome, navigates, reads document.title) + browser-execute (4, end-to-end against headless Chrome incl. workspace dynamic-import inside a snippet). All pass.
  • macOS arm64 + Windows x64: pending admin-laptop runs. Recorded in memory/browsercode/phase_h_smoke_log.md on the agent side.
  • Cloud browser: not exercised here. Requires BROWSER_USE_API_KEY and outbound to api.browser-use.com. Defer to admin smoke or Phase D bring-up.

Hard rules honored

  1. No backwards compatibility. v0.1.0 reads its own filesystem only. Teammates rm -rf ~/.local/share/bcode/ ~/.cache/bcode/ before upgrading. No migration code.
  2. We own the harness now. Initial copy from browser-harness-js@95b7a22a is the last "verbatim" event. No sync cadence. Python-harness behaviors of interest get watch-listed and ported individually.
  3. Workspace as plain code, per-project. <projectDir>/.bcode/agent-workspace/ is a flat dir of .ts files the agent owns and edits with the standard read/write/edit tools. Snippet scope binds only session. Reuse = await import("/abs/path?t=" + Date.now()). Same mechanism for a 5-line wrapper and a 500-line scrape script. git clone && bcode shares the agent's accumulated scripts (.bcode/agent-workspace/ is tracked-by-default).
  4. Net code deletion as success criterion. -2,499 LOC hand-written (-1,800 LOC was the target).
  5. Single tool surface stays. browser_execute is unchanged in mental model (write a snippet, drive the browser, save reusable code as .ts files). Only the snippet language (Python → JS) and process model (subprocess → in-process) change.
  6. Cloud is a parallel surface from day one (decisions §1d). browser_open_cloud lands in the same PR as browser_execute.
  7. export namespace carve-out for generated.ts documented in EXCEPTIONS.md.

Verification

  • bun typecheck clean across all browsercode packages (@browser-use/browsercode-core, @browser-use/bcode-browser, @browser-use/bcode-laminar, @opencode-ai/{core,plugin,sdk}).
  • bun test from packages/bcode-browser/: 9/9 pass (4 workspace-import always-on; 5 chrome-gated under BCODE_SMOKE_CHROME=1).
  • Compiled-binary build (bun run --cwd packages/opencode build -- --single): produces bcode-linux-x64, smoke-test --version passes.

Release-notes draft (for the v0.1.0 tag)

Breaking: TS harness port. v0.1.0 has no uv requirement and is fully incompatible with v0.0.x state.

The browser harness moved from Python-via-uv-run subprocess to in-process TypeScript. browser_execute now takes JS, not Python. Reusable code lives per-project at <projectDir>/.bcode/agent-workspace/ as plain .ts files; the agent imports them with await import("/abs/path?t=" + Date.now()) (no auto-loaded helpers file). git clone && cd && bcode shares the agent's scripts with the project. Cloud-browser attach is a new tool gated on BROWSER_USE_API_KEY.

Before upgrading: rm -rf ~/.local/share/bcode/ ~/.cache/bcode/. No migration path.

Gains: no uv install, single-process, faster cold starts, instant cancellation on yield-point, cleaner TUI streaming, native Windows path support (no AF_UNIX sun_path budget hacks).

Review notes

  • The migration plan suggested two natural review seams (after step 1, after step 4). I landed it as one PR because the seams cross-cut a lot of files and review is easier with the whole shape visible. Happy to split into stacked PRs if the diff size is unwelcome — git rebase -i to break out the four commits already on the branch (9811ba170 step 1+2, 13d028a94 step 3+4, 8875a7c33 step 5, d5ddc7880 step 6).
  • One judgement call worth flagging: opencode has no clean "session-end" hook in the tool layer today, so SessionStore.evict is wired but not called from outside. Cloud browsers stay running until the bcode process exits (which then closes the WS, which the BU side handles). This matches today's uv run subprocess shape (a stuck Python interpreter also outlives the bcode session). Not a regression; documented in cloud-browser.ts.
  • The smoke matrix's macOS / Windows cells are pending admin-laptop runs — the Sprite can't reach those targets. Linux x64 is green end-to-end.

bcode added 4 commits May 7, 2026 23:31
Step 1 — vendor browser-harness-js@95b7a22a CDP layer into
packages/bcode-browser/src/cdp/ (session.ts, gen.ts, generated.ts,
browser_protocol.json, js_protocol.json) + PROVENANCE.md. Initial copy
only; subsequent edits diverge by design (Phase H hard rule #2 — no
sync cadence, we own this from here on).

Step 2 — rewrite packages/bcode-browser/src/browser-execute.ts to evaluate
JS in-process via new AsyncFunction("session", code) against a per-
opencode-session CDP Session singleton (closed via Effect.addFinalizer).
console.log/error/warn/info monkey-patched around each snippet; restored
in finally even on throw/timeout. Snippet scope binds only `session`
plus standard JS globals — nothing auto-loaded (Phase H hard rule #3:
workspace as plain code, no privileged files).

Level-2 wrapper resolves the per-project workspace dir
<projectDir>/.bcode/agent-workspace/ from InstanceState.context at
execute-time; the impl mkdir's it on first call. Prompt rewritten around
the no-magic model with the write-once-import-many pattern as the first
example.

Permission glob in agent.ts: harnessGlob/harnessArchiveGlob/
harnessArchiveEditDeny removed; replaced with project-relative
**/.bcode/agent-workspace/**/* edit-allow.

TUI rendering: switched from Python REPL prompts (">>> " / "... ") to
JS prompts ("> " / "  "); input.python -> input.code.

Deleted: harness.ts, uv-locate.ts (no subprocess/uv path anymore).

Typecheck clean across all browsercode packages.
Step 3 — packages/bcode-browser/test/workspace-import.test.ts. Four
scenarios verifying the BROWSER.md-documented pattern: import abs path,
edit-then-cache-bust, syntax-error rejection, nested workspace paths.
All four pass with bun test against real fs.

Step 4 — packages/bcode-browser/src/cloud-browser.ts (provisions BU cloud
browser via /api/v3/browsers, connects the per-opencode-session Session
to the cdpUrl, registers stop on session evict). Level-2 wrapper at
packages/opencode/src/tool/browser-open-cloud.{ts,txt} + registry
registration.

Restructured the Session lifecycle as a process-scope SessionStore keyed
on opencode sessionID. Both browser_execute and browser_open_cloud share
the same Session per session — a snippet that follows browser_open_cloud
drives the cloud browser, not a freshly-auto-detected local one. The
store also collects per-session cleanup callbacks (cloud-browser stop
via PATCH /api/v3/browsers/<id>); these run when SessionStore.evict is
called. opencode has no clean session-end hook today so evict is wired
but not yet invoked from outside — known gap matching today's subprocess
shape.

BROWSER_USE_API_KEY required; fail-fast with a one-line error pointing
at https://browser-use.com when absent.
- packages/bcode-browser/skills/ — browsercode-owned. BROWSER.md (the
  agent's prompt for browser_execute, centered on the no-magic /
  workspace-as-plain-code model) + interaction-skills/*.md (verbatim
  initial copy from browser-harness-js@95b7a22a; ours after).
- packages/bcode-browser/script/embed-skills.ts — smaller cousin of the
  retired embed-harness.ts. Walks skills/, emits bcode-skills.gen.ts
  with file map + content-hash buildHash sentinel.
- packages/bcode-browser/src/skills.ts — runtime resolver. Dev: in-tree
  path. Compiled: extracts to <dataDir>/skills/ once per build hash;
  baseline-overwrite (no agent-editable surface; the agent's editable
  surface is per-project <projectDir>/.bcode/agent-workspace/).

- packages/opencode/script/build.ts — swap createEmbeddedHarnessBundle
  for createEmbeddedSkillsBundle; rename bcode-harness.gen.ts ->
  bcode-skills.gen.ts in the Bun.build files map and entrypoints.
- packages/opencode/src/agent/agent.ts — drop harnessGlob/harnessArchive*
  (already dropped in step 2); add browserSkillsGlob pointing at
  Skills.skillsDir(Global.Path.data).
- packages/opencode/src/tool/browser-execute.{ts,txt} — substitute
  {{SKILLS_DIR}} at make-time; prompt points at <skillsDir>/BROWSER.md
  and <skillsDir>/interaction-skills/.

- packages/bcode-browser/{README.md,src/index.ts} — refreshed to reflect
  the new contents; harness column gone.

- DELETE packages/bcode-browser/harness/ entirely (~3400 LOC; the
  largest single deletion of the port). Net hand-written code drop from
  this step alone is ~-2000 LOC even after adding ~600 LOC of TS for
  cdp/, browser-execute.ts, cloud-browser.ts, skills.ts, session-store.ts,
  embed-skills.ts.
- DELETE harness-sync.md (retired with the harness vendoring).
- DELETE script/check-harness-diff.sh; trim harness branch from
  script/check-upstream.sh.
- UPSTREAM.md retitled to track only anomalyco/opencode; old harness
  sync log preserved as historical archaeology.
- memory side: created memory/browsercode/harness_watchlist.md (Phase H
  hard rule #2 mechanism — patterns to track for possible porting later,
  no sync cadence).

Typecheck clean across all browsercode packages. workspace-import
smoke tests still pass.
Two new env-gated test files exercise the in-process CDP stack against
headless Chrome (Chrome 147, via google-chrome --headless=new):

- packages/bcode-browser/test/cdp-smoke.test.ts — connect via
  profileDir, attach a target, Page.enable + navigate +
  waitFor("Page.loadEventFired") + Runtime.evaluate("document.title").
  Verifies the vendored CDP layer (session.ts + generated.ts) drives a
  real browser end-to-end.

- packages/bcode-browser/test/browser-execute.test.ts — exercises the
  full Level-1 execute path: AsyncFunction wrapping, console.log
  capture, return-value serialization, multi-call Session reuse via
  SessionStore (a follow-up snippet on the same sessionID sees the
  Session previously connected), and workspace dynamic-import inside
  a snippet (await import(absPath + "?t=" + Date.now())).

Both gated on BCODE_SMOKE_CHROME=1 + BCODE_SMOKE_PROFILE_DIR. Skipped
otherwise — CI does not yet drive a real browser. All 9 tests
(4 workspace-import + 1 cdp-smoke + 4 browser-execute) pass on
Linux x64.

Compiled binary smoke verified separately: bun build --compile
produces bcode-linux-x64 (148 MB) that boots cleanly, prints --help,
and runs without uv on PATH (the only dependency was the deleted
Python harness).

Cloud-browser tool not exercised here — requires BROWSER_USE_API_KEY
and outbound to api.browser-use.com. Defer to admin smoke or Phase D
bring-up.

Smoke matrix: Linux x64 ✓; macOS arm64 + Windows x64 pending
admin-laptop runs (recorded in memory/browsercode/phase_h_smoke_log.md
on the agent side).
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

12 issues found across 96 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/bcode-browser/skills/interaction-skills/print-as-pdf.md">

<violation number="1" location="packages/bcode-browser/skills/interaction-skills/print-as-pdf.md:27">
P2: `headerTemplate`/`footerTemplate` placeholders are documented incorrectly; CDP expects class-based spans, not `{{...}}` variables.</violation>
</file>

<file name="packages/bcode-browser/skills/interaction-skills/iframes.md">

<violation number="1" location="packages/bcode-browser/skills/interaction-skills/iframes.md:69">
P2: `sandbox="allow-same-origin"` is described as a blocker, but it actually enables same-origin DOM access. This guidance can mislead users to debug the wrong cause of `contentDocument` failures.</violation>
</file>

<file name="packages/bcode-browser/skills/interaction-skills/connection.md">

<violation number="1" location="packages/bcode-browser/skills/interaction-skills/connection.md:72">
P2: Handle the empty `tabs` case in the startup sequence before accessing `tabs[0].targetId`, otherwise this snippet can throw on fresh browser starts with no real page targets.</violation>
</file>

<file name="packages/bcode-browser/skills/BROWSER.md">

<violation number="1" location="packages/bcode-browser/skills/BROWSER.md:72">
P3: The docs contradict themselves about workspace layout (`flat directory` vs `any depth/subdir`). Clarify this to avoid users organizing scripts incorrectly.</violation>
</file>

<file name="packages/bcode-browser/skills/interaction-skills/dropdowns.md">

<violation number="1" location="packages/bcode-browser/skills/interaction-skills/dropdowns.md:38">
P2: Guard `textContent` before calling `.trim()` to avoid a runtime TypeError in the dropdown option lookup example.</violation>
</file>

<file name="packages/bcode-browser/skills/interaction-skills/cross-origin-iframes.md">

<violation number="1" location="packages/bcode-browser/skills/interaction-skills/cross-origin-iframes.md:26">
P2: Guard the `find(...)` result before using `iframe.targetId` to avoid a runtime crash when no matching OOPIF target exists yet.</violation>

<violation number="2" location="packages/bcode-browser/skills/interaction-skills/cross-origin-iframes.md:35">
P2: Define how to resolve the parent target before calling `session.use(parentTargetId)`; the current example references an undefined variable.</violation>
</file>

<file name="packages/bcode-browser/skills/interaction-skills/shadow-dom.md">

<violation number="1" location="packages/bcode-browser/skills/interaction-skills/shadow-dom.md:11">
P2: The Shadow DOM guidance documents a non-existent CDP option (`pierceShadow` on `DOM.querySelector`/`DOM.querySelectorAll`). Use `DOM.getDocument(..., pierce: true)` / shadow-root traversal before querying.</violation>
</file>

<file name="packages/opencode/src/tool/browser-open-cloud.txt">

<violation number="1" location="packages/opencode/src/tool/browser-open-cloud.txt:3">
P2: The tool description claims cloud browsers are stopped at bcode session end, but current implementation notes they usually persist until process exit because session eviction is not currently called.</violation>
</file>

<file name="packages/bcode-browser/skills/interaction-skills/dialogs.md">

<violation number="1" location="packages/bcode-browser/skills/interaction-skills/dialogs.md:69">
P2: The `beforeunload` “Option A” snippet has a race: it tries to handle the dialog immediately after navigation without waiting for the dialog-open event, so it can miss late-opening dialogs.</violation>
</file>

<file name="packages/bcode-browser/src/cloud-browser.ts">

<violation number="1" location="packages/bcode-browser/src/cloud-browser.ts:103">
P1: Stop the provisioned cloud browser when `session.connect` fails; otherwise failed attach attempts can leak running cloud browser instances.</violation>
</file>

<file name="packages/bcode-browser/src/browser-execute.ts">

<violation number="1" location="packages/bcode-browser/src/browser-execute.ts:131">
P1: Global `console` monkey-patching is not concurrency-safe. If two `execute` calls overlap (different sessions), they clobber each other's patches — one call's output capture silently breaks. Consider a per-call capture approach, e.g. passing a custom logger object into the snippet as an argument (`log`) instead of mutating the global.</violation>
</file>

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
On a pro plan you can use ultrareview for larger PRs.
Fix all with cubic

Comment thread packages/bcode-browser/src/cloud-browser.ts Outdated
Comment thread packages/bcode-browser/src/browser-execute.ts Outdated
Comment thread packages/bcode-browser/skills/interaction-skills/print-as-pdf.md Outdated
Comment thread packages/bcode-browser/skills/interaction-skills/iframes.md Outdated
Comment thread packages/bcode-browser/skills/interaction-skills/connection.md Outdated
Comment thread packages/bcode-browser/skills/interaction-skills/shadow-dom.md Outdated
Comment thread packages/opencode/src/tool/browser-open-cloud.txt Outdated
Comment thread packages/bcode-browser/skills/interaction-skills/dialogs.md Outdated
Comment thread packages/bcode-browser/skills/BROWSER.md Outdated
The local-browser connect path is already snippet-side (the agent calls
`await session.connect(...)` itself in a snippet, picks Way 1 / Way 2).
`browser_open_cloud` was the only opaque path — it wrapped one HTTP call
and hid the Browser Use API surface from the agent. That asymmetry was
the root of the cubic P2 lifecycle finding (auto-stop wired but never
invoked) and the broader "two patterns mixed" review feedback on PR #41.

This commit makes cloud match local. The agent provisions, connects,
stops, and swaps cloud browsers from inside `browser_execute` snippets
using `fetch` against `https://api.browser-use.com/api/v3/browsers`.

Removed:
- packages/bcode-browser/src/cloud-browser.ts (-109)
- packages/opencode/src/tool/browser-open-cloud.{ts,txt} (-47)
- registry hookup for BrowserOpenCloudTool
- SessionStore.onEvict + cleanup-callback machinery (~ -25 LOC,
  including the Entry wrapper type). evict() now just closes + deletes;
  kept because the test file uses it for cleanup between cases.

Added:
- packages/bcode-browser/skills/cloud-browser.md (~150 LOC). Way 3
  documentation: provision/connect/stop/swap, plus a recommended
  reusable workspace helper (.bcode/agent-workspace/cloud.ts) for
  projects that use cloud browsers more than once. BU API auth via
  process.env.BROWSER_USE_API_KEY (stays as an env var; opencode's
  auth.json was considered and rejected — see decisions.md §3.10).

Updated:
- BROWSER.md restructured around Way 1 / Way 2 / Way 3 (real Chrome
  popup-gated / isolated debug-port / cloud). New "Switching browsers
  mid-session" section. Listability hint added (`read
  {{SKILLS_DIR}}/interaction-skills/` to enumerate without committing).
  Removed the misleading "first call connects automatically" line —
  agent always calls connect() explicitly, just sometimes with no args.
- browser-execute.txt mentions cloud-browser.md and notes process.env
  is in snippet scope.
- bcode-browser README + package.json + index.ts: cloud-browser.ts
  references removed; brief note that cloud is intentionally not its
  own Level-1 surface.
- comments in browser-execute.ts: no more `browser_open_cloud`
  cross-references.

Net LOC: -200 deletions vs +150 markdown additions = ~-50 net hand-
written. Bigger win is one fewer tool surface (back to the original
Phase H §3.2 single-tool target) and zero wrapper-PR treadmill as the
BU API surface grows (profile sync, custom proxies, regional pools,
recording, etc. all reachable from a snippet without bcode changes).

Verification:
- bun typecheck clean across all browsercode packages.
- bun test from packages/bcode-browser/: 4 pass + 5 skip (chrome-gated).
- embed-skills.ts now embeds 18 files (was 17, +1 for cloud-browser.md).

Cubic P2 finding closed: no tool claims cloud auto-stop, so there is
no false claim. Stop is the agent's responsibility, documented in
cloud-browser.md "Stop".

Stacked on feat/phase-h-ts-harness; PR-A (#41) lands first, then this
rebases onto main and lands separately. v0.1.0 tag waits for both.
@Alezander9
Copy link
Copy Markdown
Member Author

Added a 5th commit folding Option A (was previously stacked as PR #42, now closed): c5ea60cc7 Option A: cloud browser via snippet, drop browser_open_cloud tool.

This addresses the "two patterns mixed" review note and closes cubic's P2 lifecycle finding by deletion (no tool can claim auto-stop if there's no tool).

What changed in this commit (12 files, +239 / -217, net hand-written ~-50 LOC):

Removed:

  • packages/bcode-browser/src/cloud-browser.ts (-109)
  • packages/opencode/src/tool/browser-open-cloud.{ts,txt} (-47)
  • tool/registry.ts browser_open_cloud hookup (-4)
  • SessionStore.onEvict + cleanup callbacks + Entry wrapper (~-25). evict() kept (just close + delete now) because the test file uses it.

Added:

  • packages/bcode-browser/skills/cloud-browser.md (~150 LOC). The Way-3 deep dive: provision (POST), connect, stop (PATCH), swap (stop + close + provision again), reusable workspace-helper template (./.bcode/agent-workspace/cloud.ts). Pointer to BU's broader API for everything else (profile sync, custom proxies, regional pools, recording, …).

Updated:

  • skills/BROWSER.md restructured around Way 1 / Way 2 / Way 3 (real Chrome popup-gated / debug-port / cloud). New "Switching browsers mid-session" section. Listability hint (read {{SKILLS_DIR}}/interaction-skills/). Replaced misleading "first call connects automatically" line with explicit session.connect() examples per Way.
  • tool/browser-execute.txt mentions cloud-browser.md and notes process.env is in snippet scope.
  • bcode-browser/{README.md, package.json, src/index.ts} — cloud-browser.ts references removed; brief note that cloud is intentionally not its own Level-1 surface.

Auth shape: BROWSER_USE_API_KEY stays as an environment variable (matches the AWS Bedrock pattern in opencode). opencode's auth.json provider system was considered and rejected — it's plugin-driven and tied to the LLM-provider abstraction; treating "browser-use" as a provider would force the plugin/provider system on us for one API key, drag a Yellow-zone modification into packages/opencode/, and confuse users about whether bcode is selecting BU as a model. Documented in agent-side memory/browsercode/decisions.md §3.10 (Rev 7).

Verification:

  • bun typecheck clean across all browsercode packages (turbo-filtered root run, full-cache after cherry-pick).
  • bun test from packages/bcode-browser/: 4 pass + 5 chrome-gated skip — same shape as the rest of the PR.
  • embed-skills.ts now embeds 18 files (was 17, +cloud-browser.md). Verified locally.

Mental model after this commit:

  • One tool surface: browser_execute. Back to migration-plan §3.2's single-tool target.
  • Agent owns the connection. session.connect(...) for local (Way 1 / Way 2) or cloud-via-fetch-then-connect (Way 3). Same mechanism, no special path.
  • BU API growth (profile sync, custom proxies, recording, etc.) reachable from snippets without bcode-side wrapper PRs.

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

1 issue found across 12 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/bcode-browser/skills/BROWSER.md">

<violation number="1" location="packages/bcode-browser/skills/BROWSER.md:50">
P2: The Way 3 quick-start snippet only reads `cdp_url`/`live_url`; it should accept both snake_case and camelCase response keys to avoid failed cloud connects in some regions.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Comment thread packages/bcode-browser/skills/BROWSER.md Outdated
Fixes cubic P2 finding on PR #41. Two overlapping execute() calls
clobbered each other's global console.log/error/warn/info patches
and corrupted the originals process-wide via a stale 'finally'
restore. Cases this hits: server-mode multi-session, sub-agent
sessions, parallel browser_execute tool calls in one assistant turn
(AI SDK runs those via Promise.all), TUI tabs sharing a daemon.

Fix: bind a per-call { log, error, warn, info } object as the second
AsyncFunction argument. JS scope chain resolves console.log() in the
snippet to the function parameter before reaching the global, so
existing snippets keep working byte-identically and the global
console is never mutated. Also concurrency-safe by construction --
no shared state between calls.

Test 'overlapping execute calls do not clobber each other's console
capture' is a regression guard: verified to fail on the old impl
(empty output captures + 'bye from B' leaking to stderr) and pass
on the new one.
@Alezander9
Copy link
Copy Markdown
Member Author

Cubic's concurrency-safety finding on browser-execute.ts:131 is valid — fix landed in 8199718.

Bug: Two overlapping execute() calls didn't just clobber the live console.* patches; the finally restore was also wrong because each call saved whatever console.log happened to be pointing at when it started — i.e. the previous call's tee, not the real console.log. Sequence:

  1. Call A: saves realLog = console.log (original), sets console.log = teeA.
  2. Call B starts before A finishes: saves realLog = teeA (!), sets console.log = teeB.
  3. A finishes, restores console.log = teeA. B's output now flows into A's already-returned buffer.
  4. B finishes, restores console.log = teeA. The original console.log is permanently lost until process exit.

Net: B captures partial/nothing, A's output gets foreign log lines, the global console stays corrupted process-wide. Realistic concurrency modes that hit this: server-mode multi-session, sub-agent (Task tool) sessions, parallel browser_execute tool calls in one assistant turn (AI SDK runs them via Promise.all), TUI tabs sharing a daemon.

Fix: Bind a per-call { log, error, warn, info } object as the second AsyncFunction argument. JS scope chain resolves console.log(...) in the snippet to the function parameter before reaching the global, so existing snippets keep working byte-identically and the global console is never mutated. Concurrency-safe by construction — no shared state between calls. Chose this over cubic's suggested log argument to avoid breaking the existing BROWSER.md contract ("console.log/.error/.warn/.info are captured") and the agent's JS-native muscle memory.

Regression test: overlapping execute calls do not clobber each other's console capture runs without Chrome (no smoke gate), launches two execute() calls concurrently with a 50ms yield, asserts each gets exactly its own output and the global console.log is unchanged. Verified to fail on the old impl (empty captures, log leaks to stderr) and pass on the new one.

Diff: 2 files, +68 / −29.

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/bcode-browser/src/browser-execute.ts">

<violation number="1" location="packages/bcode-browser/src/browser-execute.ts:136">
P2: The injected `console` only exposes four methods, which breaks snippets that use other standard console APIs (e.g. `console.debug`).</violation>
</file>

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

Comment thread packages/bcode-browser/src/browser-execute.ts Outdated
Skills (factual corrections from cubic):
- print-as-pdf.md: headerTemplate/footerTemplate use class-tagged spans
  (<span class="pageNumber"></span>), not mustache variables.
- iframes.md: sandbox=allow-same-origin ENABLES same-origin DOM access;
  the blocker is sandbox WITHOUT it. Was documented backwards.
- connection.md: Startup sequence now handles tabs[]==[] (fresh window
  with omnibox-only) by createTarget('about:blank') before session.use.
- dropdowns.md: guard textContent with ?? '' before .trim().
- cross-origin-iframes.md: define parentTargetId by querying Target.getTargets
  before session.use(iframe.targetId); guard the find() result.
- shadow-dom.md: rewrite CDP path -- DOM.querySelector does NOT accept
  pierceShadow. Use DOM.getDocument({pierce: true}) to fetch the flat
  tree, walk shadowRoots[], then querySelector against the shadow-root
  nodeId. The '>>>'  combinator note removed (non-standard).
- dialogs.md: beforeunload Option A now subscribes via session.onEvent
  before navigating, eliminating the race where the dialog opens before
  handleJavaScriptDialog is called.
- BROWSER.md: Way 3 example now accepts both snake_case and camelCase
  response keys (cdp_url/cdpUrl, live_url/liveUrl) -- BU returns either
  depending on region. Workspace contradiction resolved: 'flat directory'
  → 'flat for small projects, subdirs when accumulation helps'.

browser-execute.ts (cubic console.debug finding):
- Snippet console now prototype-chains to the real console via
  Object.create(console). console.debug is tee'd into capture; uncommon
  methods (console.table, .dir, .trace, .group, …) fall through to the
  real console without throwing TypeError. Added regression test.

All cubic comments on PR #41 now resolved.
@Alezander9
Copy link
Copy Markdown
Member Author

Addressed remaining cubic findings in 7e72a0c. Summary:

Factual corrections in skill docs (cubic was right on all of them):

  • print-as-pdf.mdheaderTemplate/footerTemplate use class-tagged spans (<span class="pageNumber"></span> etc.), not mustache {{...}}. Verified against CDP reference.
  • iframes.mdsandbox="allow-same-origin" enables same-origin DOM access; the blocker is sandbox without allow-same-origin. Was documented backwards.
  • connection.md — Startup sequence now handles tabs == [] (fresh window with omnibox-only) by createTarget('about:blank') before session.use.
  • dropdowns.md — guard textContent with ?? '' before .trim().
  • cross-origin-iframes.md — define parentTargetId by querying Target.getTargets before session.use(iframe.targetId); guard the find() result with a clear OOPIF-not-yet-mounted error.
  • shadow-dom.md — substantive rewrite. CDP DOM.querySelector does not accept pierceShadow. Correct path: DOM.getDocument({ depth: -1, pierce: true }) to get the flat tree, walk shadowRoots[], then query against the shadow-root nodeId. The non-standard >>> combinator hint removed.
  • dialogs.mdbeforeunload Option A now subscribes via session.onEvent before Page.navigate, eliminating the race where the dialog opens before handleJavaScriptDialog can be called.
  • BROWSER.md Way 3 — example accepts both cdp_url/cdpUrl and live_url/liveUrl (BU returns either depending on region; previously only the trap-note section mentioned this). Workspace-layout contradiction (flat vs any depth) resolved.

browser-execute.ts console-API completeness:

  • Snippet console now prototype-chains to the real console via Object.create(console). console.debug joins log/error/warn/info in the captured tee; uncommon methods (table, dir, trace, group, …) fall through to the real console without throwing. Test console.debug is captured; uncommon methods fall through without throwing covers both behaviors.

Verification: bun typecheck clean (turbo full-cache), bun test from packages/bcode-browser/: 6 pass + 5 chrome-gated skip (was 5 + 5 — adds the console-debug test).

Followup logged in roadmap. All these inherited skill drafts are not yet battle-tested; cubic surfaced ~10 factual errors in one read. Added Phase G7 to memory/browsercode/ROADMAP.md: an eval-driven prompt + tool-description + skill tuning pass post-v0.1.0, with a concrete plan (skills-coverage harness exercising every snippet end-to-end against real Chrome + cloud, agent-quality eval scoring whether the right skills get read, length audit targeting ~70 LOC per skill, and a final proofread of all 18 skill files). Defer until Phase E so we have actual usage data to tune against.

@Alezander9 Alezander9 merged commit 9a7a71d into main May 8, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant