Skip to content

Latest commit

 

History

History
220 lines (157 loc) · 9.96 KB

File metadata and controls

220 lines (157 loc) · 9.96 KB

@ECOSYSTEM.md

Private infrastructure docs

Host IPs, deploy instructions, and homelab topology are in the private companion repo:

~/dev/docs-skein/

If not present: git clone git@github.com:androidand/docs-skein.git ~/dev/docs-skein

Ecosystem position

This is a fork of opencode. It adds local provider discovery (mDNS + LAN scan) and the --agent flag for role-based agent sessions. It is the agent runner in the Skein ecosystem — skein supervisor calls it; it calls llama-skein for inference.

skein supervisor → opencode (this repo) → llama-skein (inference proxy)

Multi-repo rules — read before touching provider or agent code

llama-skein client (TypeScript)

  • The llama-skein API is design-first: ~/dev/llama-skein/contracts/llama-skein.openapi.json is the source of truth.
  • Read ~/dev/llama-skein/docs/openapi-contract.md before changing src/local/llama-skein/.
  • Generated types live in packages/opencode/src/local/llama-skein/gen/ — never edit these by hand.
  • To regenerate: bun run build:llama-skein-client from packages/opencode
  • If the OpenAPI spec changed in llama-skein, regenerate before writing callers.

--agent flag and session run API

  • The --agent flag in src/cli/cmd/run.ts is a fork-specific addition. Never remove it.
  • skein's supervisor dispatches agents via opencode run --agent <role> "/skein-<cmd> <slug>".
  • If the run API shape changes, update RunAgent() in ~/dev/skein/internal/supervisor/.

mDNS + LAN discovery (src/local/)

  • src/local/ is the biggest fork-specific addition. Protect it during upstream syncs.
  • llama-skein registers itself via mDNS; opencode's src/local/mdns.ts discovers it.
  • Changes to llama-skein mDNS service names or ports must be reflected here.

Upstream sync

  • Remote upstreamanomalyco/opencode, branch dev. Gap is 500-700 commits — do not rebase.
  • Use bun run sync-upstream (dry run) / bun run sync-upstream:apply for merges.
  • See ~/dev/skein/docs/ECOSYSTEM.md for the full sync policy.

Code generation

# Regenerate llama-skein TypeScript client (run from packages/opencode)
bun run build:llama-skein-client
# Output: packages/opencode/src/local/llama-skein/gen/

# Regenerate JavaScript SDK
./packages/sdk/js/script/build.ts

Branch and commit conventions

  • Default branch is dev. Local main ref may not exist; use dev or origin/dev for diffs.
  • Do not manually mirror llama-skein OpenAPI schemas in handwritten TypeScript when generated types exist.

Branch Names

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.

Commits and PR Titles

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.

Style Guide

General Principles

  • 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/catch where possible
  • Avoid using the any type
  • 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 example export * 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()

Destructuring

Avoid unnecessary destructuring. Use dot notation to preserve context.

// Good
obj.a
obj.b

// Bad
const { a, b } = obj

Imports

  • Never alias imports. Do not use import { foo as bar } from "..." or renamed imports like resolve as pathResolve.
  • Never use star imports. Do not use import * as Foo from "..." or import 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 reference Project.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.

Variables

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

Control Flow

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
}

Complex Logic

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 requireConfig or readMetadata.
  • Do not return Effect from helpers unless they actually perform effectful work. Synchronous parsing, validation, and option building should stay synchronous.
  • Prefer Effect schema helpers such as Schema.UnknownFromJsonString and Schema.decodeUnknownOption over manual JSON.parse wrapped in Effect.try when parsing untrusted JSON strings.
  • Add comments for non-obvious constraints and surprising behavior, not for obvious assignments or control flow.

Schema Definitions (Drizzle)

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(),
})

Testing

  • 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 like packages/opencode.

Type Checking

  • Always run bun typecheck from package directories (e.g., packages/opencode), never tsc directly.

V2 Session Core

  • Keep durable prompt admission separate from model execution. SessionV2.prompt(...) admits one durable session_input row before scheduling advisory SessionExecution.wake(sessionID) unless resume: false requests 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 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; idle or missing interruption is a no-op.
  • Keep 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.
  • Preserve one explicit llm.stream(request) call per provider turn and reload projected history before durable continuation. Do not bridge through legacy SessionPrompt.loop(...) or delegate orchestration to an in-memory tool loop.
  • Keep local Session drains process-local until clustering is implemented. SessionRunCoordinator joins 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 queue inputs 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.