Development Patterns
This page turns architecture boundaries into contributor decisions. Read Architecture Overview and relevant subsystem page first, then use this guide before editing architecture-facing code in Kilo-Org/kilocode or its cross-repository contracts.
Prefer Kilo-owned seams over broad changes to shared OpenCode files. Follow neighboring style when changing existing modules.
How to use this page
- Identify owning subsystem in architecture docs.
- Choose narrowest source boundary that can hold change.
- Update generated or cross-repository contracts when public surface changes.
- Run smallest relevant checks plus affected repository guards.
Where should change live?
| Change shape | Preferred location or action | Reason |
|---|---|---|
| Additive Kilo CLI behavior | packages/opencode/src/kilocode/ | Keeps Kilo-only behavior out of upstream-owned files |
| Kilo CLI test for additive behavior | packages/opencode/test/kilocode/ | Avoids shared tests that encode only Kilo behavior |
| Required shared OpenCode edit | Small import, route, or injection seam in shared file plus kilocode_change marker | Keeps upstream diff narrow and merge review obvious |
| VS Code, JetBrains, docs, indexing, UI, gateway, or telemetry change | Existing Kilo-owned package | These packages are Kilo-owned; do not add kilocode_change markers |
| CLI server endpoint change | Effect HttpApi route plus handler; then run root SDK generator | Keeps server contract and generated JavaScript SDK aligned |
| JetBrains API contract change | Shared CLI OpenAPI change; let Gradle regenerate build-local Kotlin client | Kotlin client is generated during JetBrains build |
| Kilo-only config-key change | Update CLI Effect Schema and cloud JSON Schema overlay | Runtime acceptance and editor validation are separate cross-repository paths |
| Docs page move or removal | Update nav and add permanent redirect | Preserves external links and bookmarks |
Kilo-owned boundaries
Kilo CLI forks upstream OpenCode. Prefer Kilo-owned directories and packages for additive behavior:
| Prefer | Avoid unless necessary |
|---|---|
packages/opencode/src/kilocode/ | Broad edits to shared packages/opencode/src/ files |
packages/opencode/test/kilocode/ | Shared tests that encode only Kilo behavior |
packages/kilo-vscode/, packages/kilo-jetbrains/, packages/kilo-docs/, packages/kilo-indexing/ | Moving Kilo-only behavior into upstream-owned modules |
| Narrow import or route seams in shared files | Refactors that enlarge upstream merge conflicts |
Shared OpenCode files
Use kilocode_change markers when Kilo-specific code must modify shared upstream files.
| Change shape | Marker |
|---|---|
| One line | Trailing // kilocode_change |
| Multi-line block | // kilocode_change start and // kilocode_change end |
| New file in shared path | Top-level // kilocode_change - new file |
| JSX or TSX | JSX comment equivalents |
Marker exemptions apply to paths already owned by Kilo, including paths whose names contain kilocode and Kilo packages such as packages/kilo-vscode/ or packages/kilo-ui/. Do not add markers there.
| Guard | When to run |
|---|---|
bun run script/check-opencode-annotations.ts | PR touches packages/opencode/; verifies shared OpenCode Kilo edits are annotated |
bun run script/check-opencode-promise-facades.ts | Service adapter changes; prevents new runtime-backed Promise facades in shared Effect services |
bun run check-kilocode-change from packages/kilo-vscode/ | VS Code or Kilo UI changes; markers must not appear in fully Kilo-owned packages |
bun run script/check-workflows.ts | Workflow add or remove changes; keeps workflow allowlist explicit |
CLI server API
CLI server uses Effect HttpApi and publishes OpenAPI-compatible HTTP + SSE surfaces consumed by JavaScript SDK and JetBrains build-local Kotlin client.
| Rule | Reason |
|---|---|
Define shared routes under packages/opencode/src/server/routes/instance/httpapi/ | Keeps route contract close to runtime handlers |
Normalize public spec in packages/opencode/src/server/routes/instance/httpapi/public.ts | Preserves legacy-compatible request and response shapes during Effect migration |
Put additive Kilo groups and handlers under packages/opencode/src/kilocode/server/httpapi/ | Reduces edits in shared upstream-owned files |
| Inject Kilo APIs through narrow shared seam | Keeps upstream diff small and marker placement obvious |
| Preserve route spans and stable attributes | Keeps diagnostics and telemetry understandable |
SDK generation
CLI Runtime SDK contract owns generation pipeline detail. Contributor rules are short:
| Change | Action |
|---|---|
| Add or change CLI server endpoint | Run root ./script/generate.ts after route and handler edits |
JavaScript SDK generated files under packages/sdk/js/src/v2/gen/ | Do not edit by hand |
| JavaScript SDK wrapper behavior | Edit handwritten packages/sdk/js/src/v2/client.ts |
| JetBrains generated Kotlin client | Let Gradle regenerate build-local client from normalized OpenAPI |
CLI config schema
Runtime config loading and editor validation are separate paths. New Kilo-only config key requires CLI Effect Schema change in Kilo-Org/kilocode and JSON Schema overlay change in Kilo-Org/cloud. Follow CLI Config Schema for exact workflow.
Module export pattern
For new public APIs, prefer flat ESM exports inside module, then namespace re-exports from index files when grouped access helps callers.
// packages/opencode/src/session/session.ts
export const create = fn(CreateSchema, async (input) => {
// ...
})
export const list = fn(ListSchema, async (input) => {
// ...
})
// packages/opencode/src/session/index.ts
export * as Session from "./session"
Import specific export when practical. Use namespace shape (Session.create) when preserving existing API or grouped module access improves clarity. Existing Kilo-owned namespaces remain valid; do not refactor them solely for style.
Tool implementation
Tools use Tool.define("id", Effect.gen(...)) with Effect Schema validation and typed execution.
export const ExampleTool = Tool.define(
"example",
Effect.gen(function* () {
return {
description: "Example tool",
parameters: Schema.Struct({
value: Schema.String,
}),
execute(args) {
return Effect.succeed({
title: args.value,
metadata: {},
output: args.value,
})
},
}
}),
)
Reuse tool helpers, permission gates, and telemetry conventions before adding abstractions. Tests should exercise implementation behavior rather than duplicating logic in mocks.
Build system
| Area | Tooling |
|---|---|
| Package manager | Bun workspaces |
| Task orchestration | Turborepo |
| CLI executable | Bun compile build in packages/opencode/script/build.ts |
| VS Code extension and webviews | esbuild |
| JetBrains plugin | Gradle, Kotlin JVM toolchain 21, build-local OpenAPI generation |
| Type checking | tsgo through bun turbo typecheck; Gradle compile checks for JetBrains |
| Tests | Package-level Bun test, Vitest, or Gradle test depending on package |
| Docs | Next.js, Markdoc, Mermaid, and custom Markdoc components |
Documentation changes
When adding or moving docs pages:
- Create page under
pages/. - Update matching navigation file in
lib/nav/. - Add redirects when removing or moving routes.
- Use compact markdown tables with unpadded cells.
- Use
/docsprefix for docs image paths.
Source map
Paths below are relative to Kilo-Org/kilocode.
| Concern | Source path |
|---|---|
| Tool definition API | packages/opencode/src/tool/tool.ts |
| Tool example | packages/opencode/src/tool/read.ts |
| Server APIs | packages/opencode/src/server/routes/instance/httpapi/ |
| Public OpenAPI normalization | packages/opencode/src/server/routes/instance/httpapi/public.ts |
| Kilo route seam | packages/opencode/src/kilocode/server/httpapi/ |
| JavaScript SDK generation | packages/sdk/js/script/build.tsscript/generate.ts |
| JetBrains client generation | packages/kilo-jetbrains/backend/build.gradle.kts |
| Upstream merge automation | script/upstream/ |
Upstream merge workflow
bun install runs script/setup-git.ts, which sets repo-local merge conflict style to zdiff3. Base-aware markers make manual resolution and syntax-aware tooling more useful. Upstream automation under script/upstream/ applies transforms before merge, forces zdiff3 for merge operation, and runs mergiraf against remaining textual conflicts. mergiraf is required by merge script.
From script/upstream/, use:
bun run analyze.ts --version <tag> bun run merge.ts --version <tag> --dry-run bun run merge.ts --version <tag>
Keep Kilo-specific logic extracted, shared seams narrow, markers accurate, and CI guards green before upstream merge work lands.
Related pages
- Architecture Overview - system layers and reading paths
- CLI Runtime - local runtime ownership and SDK contract
- CLI Config Schema - cross-repository config-key workflow