HttpApi, run bun run generate from packages/client. Do not edit generated client files directly.sdk-next composes Client, Core, and Server.packages/core, packages/cli, packages/server, packages/protocol, packages/schema, and related generated client surfaces when required.v2.v2, or origin/v2 when the local v2 ref is unavailable. Do not base them on dev.main ref may not exist; use v2 or origin/v2 for diffs.bun run dev:live from a development worktree to test its TUI against the currently elected opencode2 background server and live sessions.bun run dev:live /path/to/project.opencode2 service status, injects its private local credential from opencode2 service get password, and uses the next TUI storage channel so tabs and other client-local state match the installed client.dev:live over plain bun run dev for this workflow. An implicit managed-service connection may replace the live server when the worktree client version differs; explicit --server warns and continues without replacing it.packages/tui/src/feature-plugins/system/storybook and register it in index.tsx.StoryFooter; include a reset command when combinations can leave the fixture in a confusing state.OPENCODE_STORY=<story-id> bun run dev:live from the development worktree, and exercise narrow and wide terminal sizes when layout is relevant.theme.hue values or borrow an unrelated semantic token to achieve a preferred appearance.text.feedback and background.feedback only for outcome or status feedback such as errors, warnings, success messages, and informational messages. Use formfield states for form-control text, ordinals, and selection markers, and action states for actions.Use a short branch name of at most three words, separated by hyphens. Do not use slashes or type prefixes such as feat/ or fix/.
Examples: session-recovery, fix-scroll-state, regenerate-sdk.
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.
try/catch where possibleany typeBun.file()src/config, follow the existing self-export pattern at the top of the file (for example export * as ConfigAgent from "./agent") when adding a new config module.yield* (yield* Foo.Service).bar().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
import { foo as bar } from "..." or renamed imports like resolve as pathResolve.import("...") references such as Schema.declare<import("@opencode-ai/plugin/effect/plugin").Plugin["effect"]>. Only when two imports genuinely collide on a name and no other option exists, an aliased type import (import type { Plugin as PluginDefinition } from "...") is permitted as a last resort — still strongly preferred not to.import * as Foo from "..." or import type * as Foo from "...".import { Project } from "@opencode-ai/core/project", then reference Project.ID.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 = 2
Avoid 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) {
...
}
requireConfig or readMetadata.Effect from helpers unless they actually perform effectful work. Synchronous parsing, validation, and option building should stay synchronous.Schema.UnknownFromJsonString and Schema.decodeUnknownOption over manual JSON.parse wrapped in Effect.try when parsing untrusted JSON strings.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(),
})
do-not-run-tests-from-root); run from package directories such as packages/core.bun typecheck from package directories (for example, packages/core), never tsc directly.Session.prompt(...) publishes session.inbox.enqueued, whose projection inserts one durable session_inbox row, before scheduling advisory SessionExecution.wake(sessionID) unless resume: false requests admit-only behavior. Delivery publishes session.inbox.delivered; its projection consumes the inbox row and inserts the visible message in the same transaction. session_inbox stores only unconsumed work.SessionExecution process-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement through SessionStore plus LocationServiceMap.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; interruption of a known but idle or locally unowned Session is a no-op, while the public API rejects an unknown Session.SessionRunner, model resolution, tool registry, permissions, and filesystem Location-scoped. Omitted Location.workspaceID means implicit-local placement; explicit workspace identity remains reserved for future placement semantics.llm.stream(request) call per Physical Attempt and reload projected history before durable continuation. A logical Step may use generic pre-output retries, one full-context retry after continuation rejection, incomplete-stream continuation, or one overflow-compaction rebuild. Generic retries retain the logical step number and do not consume another agent-step allowance. Do not delegate orchestration to an in-memory tool loop.SessionRunCoordinator joins explicit same-Session resumes, coalesces prompt wakeups, and allows different Sessions to run concurrently. A write-ahead execution claim marks a process-local busy period for restart recovery: terminal completion, failure, or user interruption releases it, while shutdown interruption and process death preserve it. Startup recovery resumes claimed top-level Sessions with durable per-execution attempt accounting. The claim is a recovery marker, not clustered ownership, fencing, or an exactly-once guarantee.src/instructions; keep instruction producers with their observed domains, and keep Session History selection plus InstructionState and InstructionEntry persistence Session-owned. InstructionDiscovery observes ambient global and upward-project instructions. The runner composes built-ins, discovery, guidance, and entries explicitly in loadInstructions; there is no instruction registry.session.instructions.updated stores changed source keys and content hashes and may freeze rendered chronological update text. Blob values live once in instruction_blob; the projected instruction_state row is the normal boundary-processing source of current and initial values. Request assembly renders the epoch baseline from stored values, while later frozen updates enter history as durable System messages. Completed compaction moves the instruction epoch; Session movement retains it so destination instruction changes are chronological, while committed revert clears it. Forks adopt the parent's newest instruction values even when copied message history ends at an earlier boundary. Unavailable sources retain the last value and block only the initial complete delta.