phase-h: TS harness port (v0.1.0 cut)#41
Conversation
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).
There was a problem hiding this comment.
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
| try: () => session.connect({ wsUrl: cdpUrl }), | ||
| catch: (err) => (err instanceof Error ? err : new Error(String(err))), | ||
| }) | ||
| return { id, liveUrl } as const | ||
| }) |
There was a problem hiding this comment.
P1: Stop the provisioned cloud browser when session.connect fails; otherwise failed attach attempts can leak running cloud browser instances.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/bcode-browser/src/cloud-browser.ts, line 103:
<comment>Stop the provisioned cloud browser when `session.connect` fails; otherwise failed attach attempts can leak running cloud browser instances.</comment>
<file context>
@@ -0,0 +1,109 @@
+ Effect.runPromise(stop(id).pipe(Effect.ignore)),
+ )
+ yield* Effect.tryPromise({
+ try: () => session.connect({ wsUrl: cdpUrl }),
+ catch: (err) => (err instanceof Error ? err : new Error(String(err))),
+ })
</file context>
| try: () => session.connect({ wsUrl: cdpUrl }), | |
| catch: (err) => (err instanceof Error ? err : new Error(String(err))), | |
| }) | |
| return { id, liveUrl } as const | |
| }) | |
| try { | |
| yield* Effect.tryPromise({ | |
| try: () => session.connect({ wsUrl: cdpUrl }), | |
| catch: (err) => (err instanceof Error ? err : new Error(String(err))), | |
| }) | |
| } catch (err) { | |
| yield* stop(id).pipe(Effect.ignore) | |
| throw err | |
| } |
| output += a.map((x) => (typeof x === "string" ? x : serialize(x))).join(" ") + "\n" | ||
| if (ctx.onChunk) Effect.runFork(ctx.onChunk(output)) | ||
| } | ||
| console.log = tee |
There was a problem hiding this comment.
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.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/bcode-browser/src/browser-execute.ts, line 131:
<comment>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.</comment>
<file context>
@@ -59,96 +54,100 @@ export const parameters = Schema.Struct({
+ output += a.map((x) => (typeof x === "string" ? x : serialize(x))).join(" ") + "\n"
+ if (ctx.onChunk) Effect.runFork(ctx.onChunk(output))
+ }
+ console.log = tee
+ console.error = tee
+ console.warn = tee
</file context>
|
|
||
| Options worth knowing: | ||
| - `landscape: true` — flip orientation | ||
| - `displayHeaderFooter: true` + `headerTemplate` / `footerTemplate` — printed HTML (mustache-style variables: `{{pageNumber}}`, `{{totalPages}}`, `{{title}}`, `{{url}}`) |
There was a problem hiding this comment.
P2: headerTemplate/footerTemplate placeholders are documented incorrectly; CDP expects class-based spans, not {{...}} variables.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/bcode-browser/skills/interaction-skills/print-as-pdf.md, line 27:
<comment>`headerTemplate`/`footerTemplate` placeholders are documented incorrectly; CDP expects class-based spans, not `{{...}}` variables.</comment>
<file context>
@@ -0,0 +1,69 @@
+
+Options worth knowing:
+- `landscape: true` — flip orientation
+- `displayHeaderFooter: true` + `headerTemplate` / `footerTemplate` — printed HTML (mustache-style variables: `{{pageNumber}}`, `{{totalPages}}`, `{{title}}`, `{{url}}`)
+- `scale: 0.8` — shrink to fit
+- `pageRanges: '1-3,7'` — subset of pages
</file context>
| - `displayHeaderFooter: true` + `headerTemplate` / `footerTemplate` — printed HTML (mustache-style variables: `{{pageNumber}}`, `{{totalPages}}`, `{{title}}`, `{{url}}`) | |
| - `displayHeaderFooter: true` + `headerTemplate` / `footerTemplate` — printed HTML (inject values with classes like `<span class="pageNumber"></span>`, `<span class="totalPages"></span>`, `<span class="title"></span>`, `<span class="url"></span>`) |
|
|
||
| - A frame that was same-origin can become cross-origin after navigation inside it (e.g. OAuth redirect). Re-check with `contentDocument` truthiness. | ||
| - `iframe.contentDocument === null` right after insertion — wait for `load` on the iframe before reading. | ||
| - CSP `frame-ancestors`/`sandbox="allow-same-origin"` can block `contentDocument` access even when origins match. |
There was a problem hiding this comment.
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.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/bcode-browser/skills/interaction-skills/iframes.md, line 69:
<comment>`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.</comment>
<file context>
@@ -0,0 +1,69 @@
+
+- A frame that was same-origin can become cross-origin after navigation inside it (e.g. OAuth redirect). Re-check with `contentDocument` truthiness.
+- `iframe.contentDocument === null` right after insertion — wait for `load` on the iframe before reading.
+- CSP `frame-ancestors`/`sandbox="allow-same-origin"` can block `contentDocument` access even when origins match.
</file context>
| - CSP `frame-ancestors`/`sandbox="allow-same-origin"` can block `contentDocument` access even when origins match. | |
| - CSP `frame-ancestors` can block the iframe from loading, and `sandbox` **without** `allow-same-origin` can block `contentDocument` access even when URLs look same-origin. |
|
|
||
| 1. `await session.connect()` — auto-detect the running browser. | ||
| 2. `const tabs = await listPageTargets()` — see what real pages exist. | ||
| 3. `await session.use(tabs[0].targetId)` — route Page/DOM/Runtime/Network calls to that target. |
There was a problem hiding this comment.
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.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/bcode-browser/skills/interaction-skills/connection.md, line 72:
<comment>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.</comment>
<file context>
@@ -0,0 +1,98 @@
+
+1. `await session.connect()` — auto-detect the running browser.
+2. `const tabs = await listPageTargets()` — see what real pages exist.
+3. `await session.use(tabs[0].targetId)` — route Page/DOM/Runtime/Network calls to that target.
+4. `await session.Target.activateTarget({ targetId: tabs[0].targetId })` — bring the tab visually to front.
+5. Enable the domains you need: `await session.Page.enable()`, `await session.Network.enable({})`, etc.
</file context>
| const iframe = targetInfos.find(t => t.type === 'iframe' && t.url.includes('stripe.com')) | ||
|
|
||
| // Route subsequent calls to the iframe target | ||
| await session.use(iframe.targetId) |
There was a problem hiding this comment.
P2: Guard the find(...) result before using iframe.targetId to avoid a runtime crash when no matching OOPIF target exists yet.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/bcode-browser/skills/interaction-skills/cross-origin-iframes.md, line 26:
<comment>Guard the `find(...)` result before using `iframe.targetId` to avoid a runtime crash when no matching OOPIF target exists yet.</comment>
<file context>
@@ -0,0 +1,67 @@
+const iframe = targetInfos.find(t => t.type === 'iframe' && t.url.includes('stripe.com'))
+
+// Route subsequent calls to the iframe target
+await session.use(iframe.targetId)
+
+await session.Runtime.enable()
</file context>
| await session.use(iframe.targetId) | |
| if (!iframe) throw new Error('iframe target not found; interact first and re-query Target.getTargets') | |
| await session.use(iframe.targetId) |
|
|
||
| ## CDP path: `pierceShadow` | ||
|
|
||
| `DOM.querySelector` / `DOM.querySelectorAll` accept `pierceShadow: true` — one call crosses every open shadow boundary: |
There was a problem hiding this comment.
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.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/bcode-browser/skills/interaction-skills/shadow-dom.md, line 11:
<comment>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.</comment>
<file context>
@@ -0,0 +1,80 @@
+
+## CDP path: `pierceShadow`
+
+`DOM.querySelector` / `DOM.querySelectorAll` accept `pierceShadow: true` — one call crosses every open shadow boundary:
+
+```js
</file context>
| `DOM.querySelector` / `DOM.querySelectorAll` accept `pierceShadow: true` — one call crosses every open shadow boundary: | |
| `DOM.querySelector` / `DOM.querySelectorAll` do **not** support a `pierceShadow` option; use `DOM.getDocument({ depth: -1, pierce: true })` (or `DOM.describeNode(..., pierce: true)`) to traverse shadow roots first, then query within discovered shadow-root node IDs. |
| @@ -0,0 +1,12 @@ | |||
| Provision a Browser Use cloud browser and bind it to this session. | |||
|
|
|||
| After this tool returns, every subsequent `browser_execute` snippet drives the cloud browser via the same `session` object — there is no per-snippet attach step. The cloud browser is automatically stopped when the bcode session ends. | |||
There was a problem hiding this comment.
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.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/tool/browser-open-cloud.txt, line 3:
<comment>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.</comment>
<file context>
@@ -0,0 +1,12 @@
+Provision a Browser Use cloud browser and bind it to this session.
+
+After this tool returns, every subsequent `browser_execute` snippet drives the cloud browser via the same `session` object — there is no per-snippet attach step. The cloud browser is automatically stopped when the bcode session ends.
+
+Use this when:
</file context>
| After this tool returns, every subsequent `browser_execute` snippet drives the cloud browser via the same `session` object — there is no per-snippet attach step. The cloud browser is automatically stopped when the bcode session ends. | |
| After this tool returns, every subsequent `browser_execute` snippet drives the cloud browser via the same `session` object — there is no per-snippet attach step. The cloud browser is stopped when the session is evicted (currently this is typically at process exit). |
| // Option A: dismiss after navigating (CDP, safe, undetectable) | ||
| await session.Page.navigate({ url: 'https://new-url.com' }) | ||
| try { | ||
| await session.Page.handleJavaScriptDialog({ accept: true }) // "Leave" |
There was a problem hiding this comment.
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.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/bcode-browser/skills/interaction-skills/dialogs.md, line 69:
<comment>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.</comment>
<file context>
@@ -0,0 +1,75 @@
+// Option A: dismiss after navigating (CDP, safe, undetectable)
+await session.Page.navigate({ url: 'https://new-url.com' })
+try {
+ await session.Page.handleJavaScriptDialog({ accept: true }) // "Leave"
+} catch { /* no dialog — normal */ }
+
</file context>
|
|
||
| ## Reusing code: write to the workspace, import from snippet | ||
|
|
||
| The agent-workspace is per-project: `./.bcode/agent-workspace/`. It's a flat directory of `.ts` files you own and edit with the standard `write`/`edit` tools. Saved scripts travel with the project (`.bcode/agent-workspace/` is committed by default), so `git clone && cd && bcode` shares them. |
There was a problem hiding this comment.
P3: The docs contradict themselves about workspace layout (flat directory vs any depth/subdir). Clarify this to avoid users organizing scripts incorrectly.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/bcode-browser/skills/BROWSER.md, line 72:
<comment>The docs contradict themselves about workspace layout (`flat directory` vs `any depth/subdir`). Clarify this to avoid users organizing scripts incorrectly.</comment>
<file context>
@@ -0,0 +1,113 @@
+
+## Reusing code: write to the workspace, import from snippet
+
+The agent-workspace is per-project: `./.bcode/agent-workspace/`. It's a flat directory of `.ts` files you own and edit with the standard `write`/`edit` tools. Saved scripts travel with the project (`.bcode/agent-workspace/` is committed by default), so `git clone && cd && bcode` shares them.
+
+Write once, import many:
</file context>
Phase H — TS harness port (v0.1.0 cut)
Drops
uvfrom the install path and replaces the Pythonbrowser-harnesssubprocess + 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)
(Excludes
generated.ts~15k and the two protocol JSONs ~1.5 MB, which are committed-data, not maintenance surface —bun src/cdp/gen.tsregeneratesgenerated.tsfrom 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.tsto regenerate),generated.ts(~15k LOC machine-generated, committed),browser_protocol.json+js_protocol.jsonfromchromedevtools/devtools-protocol. Initial copy frombrowser-use/browser-harness-js@95b7a22a; ours from here on. Provenance recorded inpackages/bcode-browser/src/cdp/PROVENANCE.md. Theexport namespacecarve-out ingenerated.tsis documented inEXCEPTIONS.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 withnew AsyncFunction("session", code)against a per-opencode-sessionSessionfrom a process-scopeSessionStore. Snippet scope binds onlysessionplus 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/infomonkey-patched around each snippet, restored in afinallyeven on throw/timeout.packages/bcode-browser/src/session-store.ts— per-opencode-session CDPSessionmap, shared betweenbrowser_executeandbrowser_open_cloud. OptionalonEvictcleanup callbacks (cloud-browser stops register here).packages/opencode/src/tool/browser-execute.ts— Level-2 wrapper resolves<projectDir>/.bcode/agent-workspace/per-call fromInstanceState.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 fromprocess.cwd()inside a snippet.agent.tspermission glob: harness/archive entries gone; replaced with**/.bcode/agent-workspace/**/*(read+edit) plus the runtime skills tree at<dataDir>/skills/*.>>> /...Python-REPL prompts →> /JS prompts;input.python→input.code.harness.ts+uv-locate.ts.Step 3 — workspace dynamic-import smoke (
packages/bcode-browser/test/workspace-import.test.ts). Four cases, allbun testagainst realfs: 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 documentedawait 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.onEvictregisters the PATCH stop. Fails fast ifBROWSER_USE_API_KEYis absent.packages/opencode/src/tool/browser-open-cloud.{ts,txt}+ registry hook (one line).browser_open_cloud, every subsequentbrowser_executedrives the cloud browser via the sameSession(no per-snippet re-attach).Step 5 — skills, BROWSER.md, embed.
packages/bcode-browser/skills/BROWSER.md— browsercode-owned prompt for the agent's firstbrowser_executecall. 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-levelimport, no CPU-bound loops withoutawait, JSON-serializable returns).packages/bcode-browser/skills/interaction-skills/*.md— 16 UI-mechanic recipes (dialogs, dropdowns, iframes, shadow DOM, uploads, screenshots, …) initial-copied frombrowser-harness-js@95b7a22a. Read-only docs; the agent reads them withread, neverimports.packages/bcode-browser/script/embed-skills.ts(new) replacesembed-harness.tsinpackages/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.packages/bcode-browser/harness/(entire vendored Python tree),embed-harness.ts,harness-sync.md,script/check-harness-diff.sh. Trimmedscript/check-upstream.shto drop the harness branch.UPSTREAM.mdretitled to track onlyanomalyco/opencode; the old harness sync log is preserved as historical archaeology in §3.memory/browsercode/harness_watchlist.mdrecords 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.
bun build --compile→bcode-linux-x64(148 MB) boots cleanly withoutuv. Three test files, 9 cases: workspace-import (4) + cdp-smoke (1, attaches to real Chrome, navigates, readsdocument.title) + browser-execute (4, end-to-end against headless Chrome incl. workspace dynamic-import inside a snippet). All pass.memory/browsercode/phase_h_smoke_log.mdon the agent side.BROWSER_USE_API_KEYand outbound toapi.browser-use.com. Defer to admin smoke or Phase D bring-up.Hard rules honored
rm -rf ~/.local/share/bcode/ ~/.cache/bcode/before upgrading. No migration code.browser-harness-js@95b7a22ais the last "verbatim" event. No sync cadence. Python-harness behaviors of interest get watch-listed and ported individually.<projectDir>/.bcode/agent-workspace/is a flat dir of.tsfiles the agent owns and edits with the standardread/write/edittools. Snippet scope binds onlysession. Reuse =await import("/abs/path?t=" + Date.now()). Same mechanism for a 5-line wrapper and a 500-line scrape script.git clone && bcodeshares the agent's accumulated scripts (.bcode/agent-workspace/is tracked-by-default).browser_executeis unchanged in mental model (write a snippet, drive the browser, save reusable code as.tsfiles). Only the snippet language (Python → JS) and process model (subprocess → in-process) change.browser_open_cloudlands in the same PR asbrowser_execute.export namespacecarve-out forgenerated.tsdocumented inEXCEPTIONS.md.Verification
bun typecheckclean across all browsercode packages (@browser-use/browsercode-core,@browser-use/bcode-browser,@browser-use/bcode-laminar,@opencode-ai/{core,plugin,sdk}).bun testfrompackages/bcode-browser/: 9/9 pass (4 workspace-import always-on; 5 chrome-gated underBCODE_SMOKE_CHROME=1).bun run --cwd packages/opencode build -- --single): producesbcode-linux-x64, smoke-test--versionpasses.Release-notes draft (for the v0.1.0 tag)
Review notes
git rebase -ito break out the four commits already on the branch (9811ba170step 1+2,13d028a94step 3+4,8875a7c33step 5,d5ddc7880step 6).SessionStore.evictis 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'suv runsubprocess shape (a stuck Python interpreter also outlives the bcode session). Not a regression; documented incloud-browser.ts.