- To regenerate the JavaScript SDK, run
./packages/sdk/js/script/build.ts. - The default branch in this repo is
dev. - Local
mainref may not exist; usedevororigin/devfor diffs.
- This repo is a Bun workspace monorepo rooted at
package.json. - Most product code lives under
packages/. - The core runtime and session engine live in
packages/lfcode. - The web UI lives in
packages/app, and the Electron host lives inpackages/desktop. - Shared packages such as
packages/ui,packages/plugin,packages/sdk, andpackages/sharedsupport the app/runtime layers. - Cross-repo specs live in
specs/.
- Use Bun workspace commands from the repo root unless a section below says otherwise.
- Main dev entrypoints are
bun run dev,bun run dev:web, andbun run dev:desktop. - Root validation commands are
bun run lintandbun run typecheck. - The root
bun run testcommand is a guard that intentionally fails; run tests from package directories instead. - After completing changes in this repo, always rerun the Windows desktop package build with
bun run package:winfrompackages/desktopand verify the refreshed output underpackages/desktop/dist/win-unpacked. - For desktop/runtime changes that the user will test in the local installed-use copy, do the development checks, rebuild the Windows desktop package, then sync the refreshed app into
C:\算法\小应用\Lfcode; do not stop at dev-server validation. Report a short manual checklist for the user to test in the use copy. - Packaging must preserve persistent runtime data across rebuilds by backing it up before
bun run package:winand restoring it afterward;data,state,cache, database files, snapshots, and other user data may live underpackages/desktop/dist/win-unpacked, but the rebuild flow must round-trip them intact. - Packaged Windows root config is preserved through
packages/desktop/local-config/lfcode.jsonc;packages/desktop/scripts/sync-local-config.tsrefreshes that template from the currentdist/win-unpacked/lfcode.jsonc, andpackages/desktop/electron-builder.config.tscopies it back into the packaged app root viaextraFiles.
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.
- 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 "@lfcode-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/lfcode.
- Always run
bun typecheckfrom package directories (e.g.,packages/lfcode), nevertscdirectly.
- 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.