- To regenerate the JavaScript SDK, run
./packages/sdk/js/script/build.ts. - ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
- Prefer automation: execute requested actions without confirmation unless blocked by missing info or safety/irreversibility.
- The default branch in this fork is
main; upstream's default branch isdev.
Use conventional commit-style messages and PR titles: type(scope): summary.
Valid types are feat, fix, docs, chore, refactor, and test. Scopes are optional; use the affected package or area when helpful, e.g. core, opencode, tui, app, desktop, sdk, or plugin.
Examples: fix(tui): simplify thinking toggle styling, docs: update contributing guide, chore(sdk): regenerate types.
- Keep things in one function unless composable or reusable
- Do not extract single-use helpers preemptively. Inline the logic at the call site unless the helper is reused, hides a genuinely complex boundary, or has a clear independent name that improves the caller.
- Avoid
try/catchwhere possible - Avoid using the
anytype - Use Bun APIs when possible, like
Bun.file() - Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
- In
src/config, follow the existing self-export pattern at the top of the file (for exampleexport * as ConfigAgent from "./agent") when adding a new config module.
Reduce total variable count by inlining when a value is only used once.
// Good
const journal = await Bun.file(path.join(dir, "journal.json")).json()
// Bad
const journalPath = path.join(dir, "journal.json")
const journal = await Bun.file(journalPath).json()Avoid unnecessary destructuring. Use dot notation to preserve context.
// Good
obj.a
obj.b
// Bad
const { a, b } = obj- Never alias imports. Do not use
import { foo as bar } from "..."or renamed imports likeresolve as pathResolve. - Never use star imports. Do not use
import * as Foo from "..."orimport type * as Foo from "...". - If a namespace-style value is needed, import the module's own exported namespace by name, for example
import { Project } from "@opencode-ai/core/project", then referenceProject.ID. - Prefer dynamic imports for heavy modules that are only needed in selected code paths, especially in startup-sensitive entrypoints. Destructure dynamic import bindings near the top of the narrowest scope that needs them so they read like normal imports. Avoid inline chains such as
await import("./module").then((mod) => mod.value())or(await import("./module")).value(). Keep branch-specific imports inside the branch that needs them to preserve lazy loading.
Prefer const over let. Use ternaries or early returns instead of reassignment.
// Good
const foo = condition ? 1 : 2
// Bad
let foo
if (condition) foo = 1
else foo = 2Avoid else statements. Prefer early returns.
// Good
function foo() {
if (condition) return 1
return 2
}
// Bad
function foo() {
if (condition) return 1
else return 2
}When a function has several validation branches or supporting details, make the main function read as the happy path and move supporting details into small helpers below it.
// Good
export function loadThing(input: unknown) {
const config = requireConfig(input)
const metadata = readMetadata(input)
return createThing({ config, metadata })
}
function requireConfig(input: unknown) {
...
}- Keep helpers close to the code they support, below the main export when that improves readability.
- Do not over-abstract simple expressions into many single-use helpers; extract only when it names a real concept like
requireConfigorreadMetadata. - Do not return
Effectfrom helpers unless they actually perform effectful work. Synchronous parsing, validation, and option building should stay synchronous. - Prefer Effect schema helpers such as
Schema.UnknownFromJsonStringandSchema.decodeUnknownOptionover manualJSON.parsewrapped inEffect.trywhen parsing untrusted JSON strings. - Add comments for non-obvious constraints and surprising behavior, not for obvious assignments or control flow.
Use snake_case for field names so column names don't need to be redefined as strings.
// Good
const table = sqliteTable("session", {
id: text().primaryKey(),
project_id: text().notNull(),
created_at: integer().notNull(),
})
// Bad
const table = sqliteTable("session", {
id: text("id").primaryKey(),
projectID: text("project_id").notNull(),
createdAt: integer("created_at").notNull(),
})- Avoid mocks as much as possible
- Test actual implementation, do not duplicate logic into tests
- Tests cannot run from repo root (guard:
do-not-run-tests-from-root); run from package dirs likepackages/opencode.
- Always run
bun typecheckfrom package directories (e.g.,packages/opencode), nevertscdirectly.
This repo is a fork of anomalyco/opencode that adds browser-use integration.
The opencode style above still applies to all TS code. A few extras:
See UPSTREAM.md for the canonical version. Short form:
- Green (add freely): new files, new packages.
packages/bcode-browser/is the home for BrowserCode-specific code (decisions §1d). - Yellow (touch + document): edits to
packages/opencode/source go in the maintainer-sidememory/browsercode/EXCEPTIONS.md(kept outside this repo with the agent's roadmap/decisions docs) with justification. Every Yellow edit is a future merge-conflict candidate, so keep them surgical. - Red (never touch):
@opencode-ai/*package names,@opencode/...Effect service IDs,x-opencode-*wire headers,OPENCODE_*env vars, third-party provider User-Agents.
- Level 1 — pure additions in
packages/bcode-browser/. No upstream diff. Always preferred. - Level 2 — thin adapters in
packages/opencode/src/tool/that wrap Level-1 implementations. Small, mostly schema/context translation. - Level 3 — modifications to upstream source. Last resort. Document in
the maintainer-side
memory/browsercode/EXCEPTIONS.md. Always evaluate whether the change could be upstreamed as an extension point first.
packages/bcode-browser/harness/ is vendored from
browser-use/browser-harness. Path-allowlist policy (post upstream PR #229
src-layout reorg):
agent-workspace/agent_helpers.py— editable. Primary BrowserCode extension surface.src/browser_harness/*.py(daemon.py,admin.py,helpers.py,run.py,_ipc.py) — protected. Pull verbatim. If behavior change is needed, upstream a PR tobrowser-use/browser-harness.interaction-skills/— verbatim. Never edit.(agent-workspace/)?domain-skills/— excluded from vendored tree. Sync agents skip these paths; see UPSTREAM.md §3 "Excluded paths".
Sync workflow lives in harness-sync.md.
Pull from anomalyco/opencode: see opencode-sync.md. Pull from
browser-use/browser-harness: see harness-sync.md. Both append a row
to UPSTREAM.md's sync log.
When a sync PR is open, do not land feature work on main — it
creates conflicts the sync agent has to redo. Wait for the sync to merge
first.
Use the REST endpoint via curl, not gh pr create (the project's PAT
allows the REST mutation but not the GraphQL one used by gh). Templates
live in opencode-sync.md and harness-sync.md.
Root bun run typecheck uses a turbo filter limiting to the packages we
ship (@browser-use/browsercode-core, @browser-use/bcode-browser,
@opencode-ai/{shared,plugin,sdk}). This avoids upstream packages we
don't build (e.g. enterprise, web, console). The pre-push hook
runs this filtered command.
- Keep durable prompt admission separate from model execution.
SessionV2.prompt(...)admits one durablesession_inputrow before scheduling advisorySessionExecution.wake(sessionID)unlessresume: falserequests admit-only behavior. The serialized runner promotes admitted inputs into visible user messages at safe boundaries. - Reusing a Session ID adopts the existing Session. Reusing a prompt message ID reconciles an exact retry only when Session, prompt, and delivery mode match; conflicting reuse fails. Historical projected prompts lazily synthesize promoted inbox records during exact retry.
- Keep
SessionExecutionprocess-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement throughSessionStoreplusLocationServiceMap.get(session.location)only when a drain starts; no layer should take a Session ID. V2 interruption targets the active process-local ownership chain for that Session; idle or missing interruption is a no-op. - Keep
SessionRunner, model resolution, tool registry, permissions, and filesystem Location-scoped. OmittedLocation.workspaceIDmeans implicit-local placement; explicit workspace identity remains reserved for future placement semantics. - Preserve one explicit
llm.stream(request)call per provider turn and reload projected history before durable continuation. Do not bridge through legacySessionPrompt.loop(...)or delegate orchestration to an in-memory tool loop. - Keep local Session drains process-local until clustering is implemented.
SessionRunCoordinatorjoins explicit same-Session resumes, coalesces prompt wakeups, and allows different Sessions to run concurrently. Advisory wakes drain eligible durable inbox rows only; post-crash activity recovery requires a separate explicit design before it may retry provider work. - Keep delivery vocabulary explicit. Prompts steer by default and coalesce into the active activity at the next safe provider-turn boundary. Explicit
queueinputs open FIFO future activities one at a time after the active activity settles. - Keep EventV2 replay owner claims separate from clustered Session execution ownership.
- Keep the System Context algebra, registry, and built-ins in
src/system-context; keep Context Source producers with their observed domains, and keep Session History selection plus Context Epoch persistence Session-owned.