Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/bcode-browser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ See `decisions.md §1c` (three-level model) and `§1d` (this package) in the Bro
| `src/browser-execute.ts` | In-process JS-eval `browser_execute` body. |
| `src/session-store.ts` | Per-opencode-session CDP `Session` map. The agent calls `session.connect(...)` from a snippet; subsequent snippets find the same Session. |
| `src/skills.ts` | Runtime resolver for embedded skills (extract on first call in compiled mode; in-tree path in dev). |
| `skills/` | `browser-execute-guide.md` (the agent's prompt for `browser_execute`, covering all three connection Ways including Browser Use cloud provisioning via raw HTTP from inside a snippet). Embedded into the binary by `script/embed-skills.ts`. The interaction-skills set inherited from the Python harness was archived 2026-05-09 — we'll reintroduce only what evals show is needed, one skill at a time. |
| `skills/` | `browser-execute/SKILL.md` (the agent-facing reference for `browser_execute`, covering all three connection Ways including Browser Use cloud provisioning via raw HTTP from inside a snippet). Registered as an opencode skill via frontmatter; surfaced in `<available_skills>` and loaded with the `skill` tool. Embedded into the binary by `script/embed-skills.ts`. The interaction-skills set inherited from the Python harness was archived 2026-05-09 — we'll reintroduce only what evals show is needed, one skill at a time. |
| `script/embed-skills.ts` | Build-time embed; emits `bcode-skills.gen.ts` consumed by the compiled binary. |
| `test/` | `bun test` smoke coverage for the workspace dynamic-import pattern. |

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
name: browser-execute
description: Use ONLY when calling the `browser_execute` tool or driving a real browser via the Chrome DevTools Protocol. Required reading before the first `browser_execute` call in a session. Covers the three connection methods (local Chrome with remote debugging, isolated debug-port profile, Browser Use cloud), the in-process `session` / `console` snippet model, attaching to a page target, common CDP commands, the per-project `.bcode/agent-workspace/` for reusable scripts, and screenshot auto-attachment.
---

The `browser_execute` tool evaluates JavaScript against a connected browser `session` via the Chrome DevTools Protocol.
The snippet runs in-process; `session` is bound to a long-lived CDP `Session` that persists. Connect once, then drive many snippets.
There is no helper namespace, just `session`, `console`, and standard JS globals.
Expand Down
4 changes: 2 additions & 2 deletions packages/bcode-browser/src/browser-execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const MAX_TIMEOUT_MS = 10 * 60 * 1000
export const parameters = Schema.Struct({
code: Schema.String.annotate({
description:
"The JavaScript snippet to execute. `session` (CDP Session) and `console` are in scope; see browser-execute-guide.md for the snippet model.",
"The JavaScript snippet to execute. `session` (CDP Session) and `console` are in scope; see the `browser-execute` skill for the snippet model.",
}),
timeout: Schema.optional(Schema.Number).annotate({
description: `Optional timeout in milliseconds (default ${DEFAULT_TIMEOUT_MS}, max ${MAX_TIMEOUT_MS})`,
Expand Down Expand Up @@ -145,7 +145,7 @@ const serialize = (v: unknown): string => {

// Snippet executor. The CDP Session is resolved per-call from `SessionStore`
// keyed on `ctx.sessionID`. The agent connects with `await session.connect(...)`
// in one snippet (Way 1 / Way 2 / Way 3 in browser-execute-guide.md); the Session persists
// in one snippet (Way 1 / Way 2 / Way 3 in skills/browser-execute/SKILL.md); the Session persists
// for follow-up snippets in the same opencode session.
//
// `dataDir` is opencode's XDG_DATA_HOME for bcode (~/.local/share/bcode/ on
Expand Down
4 changes: 2 additions & 2 deletions packages/bcode-browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
// src/browser-execute.ts — in-process JS-eval browser_execute body
// src/session-store.ts — per-opencode-session CDP Session map
// src/skills.ts — runtime resolver for embedded skills
// skills/ — browser-execute-guide.md (embedded into binary)
// skills/ — browser-execute/SKILL.md (embedded into binary)
//
// Cloud browser provisioning is intentionally NOT a separate Level-1
// surface. The agent reads Way 3 of `skills/browser-execute-guide.md` and
// surface. The agent reads Way 3 of `skills/browser-execute/SKILL.md` and
// writes the fetch+connect snippet itself, matching how local-browser
// connect works (snippet-side, not tool-side). Decisions trail in
// `memory/browsercode/decisions.md` §3.4.
Expand Down
6 changes: 3 additions & 3 deletions packages/bcode-browser/test/skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ test("resolveSkillsDir materializes skills with {{SKILLS_DIR}} substituted", asy
try {
const dir = await Skills.resolveSkillsDir(dataDir)
expect(dir).toBe(path.join(dataDir, "skills"))
const browser = (await fs.readFile(path.join(dir, "browser-execute-guide.md"), "utf8")).replaceAll("\\", "/")
const browser = (await fs.readFile(path.join(dir, "browser-execute", "SKILL.md"), "utf8")).replaceAll("\\", "/")
expect(browser).not.toContain("{{SKILLS_DIR}}")
expect(browser).toContain(`${dir.replaceAll("\\", "/")}/`)
} finally {
Expand All @@ -29,8 +29,8 @@ test("different dataDirs get their own substituted paths", async () => {
const dirA = await Skills.resolveSkillsDir(a)
const dirB = await Skills.resolveSkillsDir(b)
const [browserA, browserB] = (await Promise.all([
fs.readFile(path.join(dirA, "browser-execute-guide.md"), "utf8"),
fs.readFile(path.join(dirB, "browser-execute-guide.md"), "utf8"),
fs.readFile(path.join(dirA, "browser-execute", "SKILL.md"), "utf8"),
fs.readFile(path.join(dirB, "browser-execute", "SKILL.md"), "utf8"),
])).map((s) => s.replaceAll("\\", "/"))
const [a2, b2] = [dirA.replaceAll("\\", "/"), dirB.replaceAll("\\", "/")]
expect(browserA).toContain(a2)
Expand Down
2 changes: 1 addition & 1 deletion packages/bcode-browser/test/workspace-import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// at runtime from a `browser_execute` snippet via
// `await import("/abs/path?t=" + Date.now())`. We don't run a real
// `browser_execute` here — the point is to verify the dynamic-import
// mechanism behaves as the browser-execute-guide.md prompt claims.
// mechanism behaves as the browser-execute skill prompt claims.
//
// All four scenarios run against a real tmp dir, real .ts files, and
// the real Bun module loader. No mocks.
Expand Down
35 changes: 17 additions & 18 deletions packages/opencode/src/skill/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { ConfigMarkdown } from "@/config/markdown"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Glob } from "@opencode-ai/core/util/glob"
import * as Log from "@opencode-ai/core/util/log"
import { Skills as BcodeSkills } from "@browser-use/bcode-browser/skills"
import { Discovery } from "./discovery"
import CUSTOMIZE_OPENCODE_SKILL_BODY from "./prompt/customize-opencode.md" with { type: "text" }
import { isRecord } from "@/util/record"

const log = Log.create({ service: "skill" })
Expand All @@ -25,15 +25,6 @@ const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md"
const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md"
const SKILL_PATTERN = "**/SKILL.md"

// Built-in skill that ships with opencode. The model's intuition for what an
// opencode.json should look like is often wrong, and opencode hard-fails on
// invalid config, so users hit cryptic startup errors. Loading this skill
// when the model is asked to touch opencode's own config files gives it the
// actual schemas instead of guesses.
const CUSTOMIZE_OPENCODE_SKILL_NAME = "customize-opencode"
const CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION =
"Use ONLY when the user is editing or creating opencode's own configuration: opencode.json, opencode.jsonc, files under .opencode/, or files under ~/.config/opencode/. Also use when creating or fixing opencode agents, subagents, skills, plugins, MCP servers, or permission rules. Do not use for the user's own application code, or for any project that is not configuring opencode itself."

export const Info = Schema.Struct({
name: Schema.String,
description: Schema.optional(Schema.String),
Expand Down Expand Up @@ -216,6 +207,16 @@ const discoverSkills = Effect.fnUntraced(function* (
}
}

// BrowserCode-shipped skills (browser-execute and any future first-party
// reference docs) live at <dataDir>/skills/<name>/SKILL.md after the
// bcode-browser materialization step. Scan unconditionally — the dir may
// not exist yet on the very first launch before BrowserExecute.make has
// run, and that's fine (Glob returns empty).
const bcodeSkillsDir = BcodeSkills.skillsDir(global.data)
if (yield* fsys.isDir(bcodeSkillsDir)) {
yield* scan(state, bcodeSkillsDir, SKILL_PATTERN, { scope: "bcode" })
}

return {
matches: Array.from(state.matches),
dirs: Array.from(state.dirs),
Expand Down Expand Up @@ -258,14 +259,12 @@ export const layer = Layer.effect(
const state = yield* InstanceState.make(
Effect.fn("Skill.state")(function* () {
const s: State = { skills: {}, dirs: new Set() }
// Register the built-in skill BEFORE disk discovery so a user-disk
// skill with the same name can override it.
s.skills[CUSTOMIZE_OPENCODE_SKILL_NAME] = {
name: CUSTOMIZE_OPENCODE_SKILL_NAME,
description: CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION,
location: "<built-in>",
content: CUSTOMIZE_OPENCODE_SKILL_BODY,
}
// BrowserCode-specific: the upstream `customize-opencode` built-in
// registration was removed here. The skill teaches the model
// opencode.json / opencode plugin authoring and is irrelevant to
// browser-driving workflows; eval traces showed it correlated with
// a score regression. A user-disk skill of the same name still
// loads normally through the regular discovery path.
yield* loadSkills(s, yield* InstanceState.get(discovered), bus)
return s
}),
Expand Down
Loading
Loading