RuleStack

Configs

Stacks

Compare

Diff

RuleStack

Configs

Stacks

Compare

Diff

Read API

RuleStack

Configs

Stacks

Compare

Diff

Read API

Diff/anomalyco-opencode-agents ↔ anomalyco-opencode-packages-opencode-test-agents

Comparison

A · AGENTS.md · anomalyco/opencodeB · AGENTS.md · anomalyco/opencode
What each file covers, counted
DimensionSharedOnly in AOnly in BOverlap
Sections013180%
Commands0310%
Section tags34233%

What each file covers

Sections

0 shared · 13 only in A · 18 only in B
  • − Branch Names
  • − Commits and PR Titles
  • − Style Guide
  • − General Principles
  • − Destructuring
  • − Imports
  • − Variables
  • − Control Flow
  • − Complex Logic
  • − Schema Definitions (Drizzle)
  • − Testing
  • − Type Checking
  • − V2 Session Core
  • + Test Fixtures Guide
  • + Temporary Directory Fixture
  • + Basic Usage
  • + Options
  • + Examples
  • + Returned Object
  • + Notes
  • + Testing With Effects
  • + Core Pattern
  • + `it.effect` vs `it.live`
  • + Effect Fixtures
  • + Style
  • + Partial Service Stubs
  • + Synchronizing With Concurrent Work
  • + The Anti-Pattern
  • + The Fix
  • + Example
  • + When Fixed Sleeps Are OK

Commands

0 shared · 3 only in A · 1 only in B
  • − bun run generate
  • − bun typecheck
  • − tsc
  • + git?: boolean

Section tags

3 shared · 4 only in A · 2 only in B
  • − lint-format
  • − types
  • − git-pr
  • − database
  • + architecture
  • + testing-strategy
  •   test
  •   code-style
  •   do-not

Line diff

+157 added−114 removed48 unchanged23.4% identical
anomalyco/opencode · AGENTS.md
@@ −1 @@
1- To regenerate the legacy JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
2- After changing the public Protocol or Server `HttpApi`, run `bun run generate` from `packages/client`. Do not edit `src/generated` or `src/generated-effect` directly.
3- Keep runtime dependencies directed from Schema to Core and Protocol, then from Core and Protocol to Server. Client runtime code may depend on Schema and Protocol but never Core or Server; `sdk-next` composes Client, Core, and Server.
4- The default branch in this repo is `dev`.
5- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
6 
7## Branch Names
8 
9Use a short branch name of at most three words, separated by hyphens. Do not use slashes or type prefixes such as `feat/` or `fix/`.
10 
11Examples: `session-recovery`, `fix-scroll-state`, `regenerate-sdk`.
12 
13## Commits and PR Titles
 
14 
15Use conventional commit-style messages and PR titles: `type(scope): summary`.
 
 
 
 
 
16 
17Valid 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`.
18 
19Examples: `fix(tui): simplify thinking toggle styling`, `docs: update contributing guide`, `chore(sdk): regenerate types`.
 
 
 
20 
21## Style Guide
22 
23### General Principles
24 
25- Keep things in one function unless composable or reusable
26- 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.
27- Avoid `try`/`catch` where possible
28- Avoid using the `any` type
29- Use Bun APIs when possible, like `Bun.file()`
30- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
31- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
32- 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.
33- In Effect generators, bind services to named variables before calling methods. Do not use nested service yields such as `yield* (yield* Foo.Service).bar()`.
34 
35Reduce total variable count by inlining when a value is only used once.
36 
37```ts
38// Good
39const journal = await Bun.file(path.join(dir, "journal.json")).json()
40 
41// Bad
42const journalPath = path.join(dir, "journal.json")
43const journal = await Bun.file(journalPath).json()
44```
45 
46### Destructuring
47 
48Avoid unnecessary destructuring. Use dot notation to preserve context.
 
 
 
 
 
 
 
 
 
49 
50```ts
51// Good
52obj.a
53obj.b
54 
55// Bad
56const { a, b } = obj
 
 
 
 
 
 
 
 
 
 
57```
58 
59### Imports
60 
61- Never alias imports. Do not use `import { foo as bar } from "..."` or renamed imports like `resolve as pathResolve`.
62- Never use star imports. Do not use `import * as Foo from "..."` or `import type * as Foo from "..."`.
63- 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`.
64- 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.
65 
66### Variables
67 
68Prefer `const` over `let`. Use ternaries or early returns instead of reassignment.
 
 
69 
70```ts
71// Good
72const foo = condition ? 1 : 2
73 
74// Bad
75let foo
76if (condition) foo = 1
77else foo = 2
78```
79 
80### Control Flow
81 
82Avoid `else` statements. Prefer early returns.
 
 
 
83 
84```ts
85// Good
86function foo() {
87 if (condition) return 1
88 return 2
89}
90 
91// Bad
92function foo() {
93 if (condition) return 1
94 else return 2
95}
 
 
 
 
96```
97 
98### Complex Logic
99 
100When 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.
 
 
 
101 
102```ts
103// Good
104export function loadThing(input: unknown) {
105 const config = requireConfig(input)
106 const metadata = readMetadata(input)
107 return createThing({ config, metadata })
108}
109 
110function requireConfig(input: unknown) {
111 ...
112}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113```
114 
115- Keep helpers close to the code they support, below the main export when that improves readability.
116- Do not over-abstract simple expressions into many single-use helpers; extract only when it names a real concept like `requireConfig` or `readMetadata`.
117- Do not return `Effect` from helpers unless they actually perform effectful work. Synchronous parsing, validation, and option building should stay synchronous.
118- Prefer Effect schema helpers such as `Schema.UnknownFromJsonString` and `Schema.decodeUnknownOption` over manual `JSON.parse` wrapped in `Effect.try` when parsing untrusted JSON strings.
119- Add comments for non-obvious constraints and surprising behavior, not for obvious assignments or control flow.
120 
121### Schema Definitions (Drizzle)
122 
123Use snake_case for field names so column names don't need to be redefined as strings.
 
 
 
 
124 
125```ts
126// Good
127const table = sqliteTable("session", {
128 id: text().primaryKey(),
129 project_id: text().notNull(),
130 created_at: integer().notNull(),
131})
132 
133// Bad
134const table = sqliteTable("session", {
135 id: text("id").primaryKey(),
136 projectID: text("project_id").notNull(),
137 createdAt: integer("created_at").notNull(),
 
 
 
138})
139```
140 
141## Testing
142 
143- Avoid mocks as much as possible, you shouldn't be using globalThis.\* at all unless it's the only option.
144- Test actual implementation, do not duplicate logic into tests
145- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package dirs like `packages/opencode`.
146 
147## Type Checking
148 
149- Always run `bun typecheck` from package directories (e.g., `packages/opencode`), never `tsc` directly.
150 
151## V2 Session Core
152 
153- 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.
154- 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.
155- 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.
156- 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.
157- 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.
158- 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 continuation recovery requires a separate explicit design before it may retry provider work. A drain has no durable identity or transcript boundary.
159- Keep delivery vocabulary explicit. Prompts steer by default and promote at the next safe provider-turn boundary while the current drain requires continuation. An explicit `queue` input remains pending until the Session would otherwise become idle; promote one queued input at that boundary, then reevaluate continuation before promoting another. Promoting any new user input resets the selected agent's provider-turn allowance; a batch of steers resets it once.
160- Keep EventV2 replay owner claims separate from clustered Session execution ownership.
161- 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.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162 
anomalyco/opencode · packages/opencode/test/AGENTS.md
@@ +1 @@
1# Test Fixtures Guide
 
 
 
 
2 
3## Temporary Directory Fixture
4 
5The `tmpdir` function in `fixture/fixture.ts` creates temporary directories for tests with automatic cleanup.
6 
7### Basic Usage
8 
9```typescript
10import { tmpdir } from "./fixture/fixture"
11 
12test("example", async () => {
13 await using tmp = await tmpdir()
14 // tmp.path is the temp directory path
15 // automatically cleaned up when test ends
16})
17```
18 
19### Options
20 
21- `git?: boolean` - Initialize a git repo with a root commit
22- `config?: Partial<Config.Info>` - Write an `opencode.json` config file
23- `init?: (dir: string) => Promise<T>` - Custom setup function, returns value accessible as `tmp.extra`
24- `dispose?: (dir: string) => Promise<T>` - Custom cleanup function
25 
26### Examples
27 
28**Git repository:**
29 
30```typescript
31await using tmp = await tmpdir({ git: true })
32```
 
 
 
 
 
 
33 
34**With config file:**
35 
36```typescript
37await using tmp = await tmpdir({
38 config: { model: "test/model", username: "testuser" },
39})
 
 
 
40```
41 
42**Custom initialization (returns extra data):**
43 
44```typescript
45await using tmp = await tmpdir<string>({
46 init: async (dir) => {
47 await Bun.write(path.join(dir, "file.txt"), "content")
48 return "extra data"
49 },
50})
51// Access extra data via tmp.extra
52console.log(tmp.extra) // "extra data"
53```
54 
55**With cleanup:**
 
 
 
56 
57```typescript
58await using tmp = await tmpdir({
59 init: async (dir) => {
60 const specialDir = path.join(dir, "special")
61 await fs.mkdir(specialDir)
62 return specialDir
63 },
64 dispose: async (dir) => {
65 // Custom cleanup logic
66 await fs.rm(path.join(dir, "special"), { recursive: true })
67 },
68})
69```
70 
71### Returned Object
72 
73- `path: string` - Absolute path to the temp directory (realpath resolved)
74- `extra: T` - Value returned by the `init` function
75- `[Symbol.asyncDispose]` - Enables automatic cleanup via `await using`
 
76 
77### Notes
78 
79- Directories are created in the system temp folder with prefix `opencode-test-`
80- Use `await using` for automatic cleanup when the variable goes out of scope
81- Paths are sanitized to strip null bytes (defensive fix for CI environments)
82 
83## Testing With Effects
 
 
84 
85Use `testEffect(...)` from `test/lib/effect.ts` for tests that exercise Effect services or Effect-based workflows.
 
 
 
 
86 
87### Core Pattern
88 
89```typescript
90import { describe, expect } from "bun:test"
91import { Effect, Layer } from "effect"
92import { testEffect } from "../lib/effect"
93 
94const it = testEffect(Layer.mergeAll(MyService.defaultLayer))
 
 
 
 
 
95 
96describe("my service", () => {
97 it.instance("does the thing", () =>
98 Effect.gen(function* () {
99 const svc = yield* MyService.Service
100 const out = yield* svc.run()
101 expect(out).toEqual("ok")
102 }),
103 )
104})
105```
106 
107### `it.effect` vs `it.live`
108 
109- Use `it.effect(...)` when the test should run with `TestClock` and `TestConsole`.
110- Use `it.live(...)` when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
111- Use `it.instance(...)` for live Effect tests that need a scoped temporary directory and instance context.
112- Most integration-style tests in this package use `it.live(...)`.
113 
114### Effect Fixtures
 
 
 
 
 
 
115 
116Prefer the Effect-aware helpers from `fixture/fixture.ts` instead of building a manual runtime in each test.
117 
118- `tmpdirScoped(options?)` creates a scoped temp directory and cleans it up when the Effect scope closes.
119- `provideInstance(dir)(effect)` is the low-level helper. It does not create a directory; it runs an Effect with `InstanceRef` provided for `dir`.
120- `provideTmpdirInstance((dir) => effect, options?)` is the convenience helper. It creates a temp directory, binds it as the active instance, and disposes the instance on cleanup.
121- `provideTmpdirServer((input) => effect, options?)` does the same, but also provides the test LLM server.
122 
123Use `it.instance(...)` by default when a test only needs one temp instance. Yield `TestInstance` from `fixture/fixture.ts` when the test needs the temp directory path:
124 
125```typescript
126import { TestInstance } from "../fixture/fixture"
127 
128it.instance("uses the temp directory", () =>
129 Effect.gen(function* () {
130 const test = yield* TestInstance
131 expect(test.directory).toContain("opencode-test-")
132 }),
133)
134```
135 
136Use `provideTmpdirInstance(...)` or `tmpdirScoped()` plus `provideInstance(...)` when a test needs multiple directories, custom setup before binding, needs to switch instance context within one test, or explicitly tests instance disposal/reload lifetime.
 
 
 
 
137 
138### Style
139 
140- Define `const it = testEffect(...)` near the top of the file.
141- Keep the test body inside `Effect.gen(function* () { ... })`.
142- Yield services directly with `yield* MyService.Service` or `yield* MyTool`.
143- Avoid custom `ManagedRuntime`, `attach(...)`, or ad hoc `run(...)` wrappers when `testEffect(...)` already provides the runtime.
144- When a test needs instance-local state, prefer `it.instance(...)` over manual `Instance.provide(...)` inside Promise-style tests.
145 
146### Partial Service Stubs
 
 
 
 
 
 
147 
148When a test only needs to override one or two methods of a service, prefer `Layer.mock` over a hand-rolled `Layer.succeed(Service, Service.of({ ... }))`. `Layer.mock` lets you supply just the methods that matter — anything else throws an `UnimplementedError` defect if the test accidentally calls it, which is exactly the signal you want.
149 
150```typescript
151import { Effect, Layer } from "effect"
152import { Account } from "@/account/account"
153 
154const failingAccountLayer = Layer.mock(Account.Service, {
155 orgsByAccount: () => Effect.fail(new Account.AccountServiceError({ message: "simulated upstream failure" })),
156})
157```
158 
159This is much shorter than stubbing every method with `Effect.void` / `Effect.succeed(...)` placeholders, and it keeps the test focused on the behaviour under test.
160 
161## Synchronizing With Concurrent Work
 
 
162 
163### The Anti-Pattern
164 
165Using `Effect.sleep(N)` or `setTimeout` as a "wait for the forked fiber to be ready" hack races the scheduler. The forked fiber may not have reached the synchronization point within `N` ms on a slow CI host, and the test fails intermittently. See PR #27622 for a concrete flake that fell out of this exact pattern.
166 
167### The Fix
168 
169Wait on a **published readiness signal**, not wall-clock time. Available affordances:
170 
171- `pollWithTimeout(effect, message, duration?)` from `test/lib/effect.ts` — repeatedly run a predicate effect until it returns a non-`undefined` value, with a timeout.
172- `awaitWithTimeout(effect, message, duration?)` from `test/lib/effect.ts` — wrap any effect with `Effect.timeoutOrElse` and a custom error message.
173- `llm.wait(n)` from `test/lib/llm-server.ts` — wait until the mock LLM has received `n` HTTP calls.
174- `SessionStatus.Service` `.get(sessionID)` — observable per-session state (`{ type: "busy" | "idle" | ... }`).
175- `BackgroundJob.wait({ id, timeout })` from `src/background/job.ts` — wait for a background job to complete.
176- Bus subscriptions — fork `Stream.runForEach(bus.subscribe(Event), ...)` and open a `Latch` inside the callback to signal first-event readiness.
177- `Deferred.await(deferred).pipe(Effect.timeoutOrElse(...))` for one-shot signals.
178 
179### Example
180 
181```ts
182// Antipattern — race
183yield * prompt.shell({ command: "sleep 30" }).pipe(Effect.forkChild)
184yield * Effect.sleep(50)
185yield * prompt.cancel(chat.id)
186 
187// Fix — wait for a published readiness signal
188yield * prompt.shell({ command: "sleep 30" }).pipe(Effect.forkChild)
189yield *
190 pollWithTimeout(
191 Effect.gen(function* () {
192 const s = yield* (yield* SessionStatus.Service).get(chat.id)
193 return s.type === "busy" ? (true as const) : undefined
194 }),
195 "session never became busy",
196 )
197yield * prompt.cancel(chat.id)
198```
199 
200### When Fixed Sleeps Are OK
201 
202- Testing debounce or throttle behavior, where the sleep **is** the test.
203- Letting real wall-clock advance past a genuine timestamp resolution boundary (e.g. mtime granularity).
204- Simulating network latency in race-regression tests that intentionally exercise ordering.
205 
@@ −1 +1 @@
1−- To regenerate the legacy JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
2−- After changing the public Protocol or Server `HttpApi`, run `bun run generate` from `packages/client`. Do not edit `src/generated` or `src/generated-effect` directly.
3−- Keep runtime dependencies directed from Schema to Core and Protocol, then from Core and Protocol to Server. Client runtime code may depend on Schema and Protocol but never Core or Server; `sdk-next` composes Client, Core, and Server.
4−- The default branch in this repo is `dev`.
5−- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
1+# Test Fixtures Guide
62  
7−## Branch Names
3+## Temporary Directory Fixture
84  
9−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/`.
5+The `tmpdir` function in `fixture/fixture.ts` creates temporary directories for tests with automatic cleanup.
106  
11−Examples: `session-recovery`, `fix-scroll-state`, `regenerate-sdk`.
7+### Basic Usage
128  
13−## Commits and PR Titles
9+```typescript
10+import { tmpdir } from "./fixture/fixture"
1411  
15−Use conventional commit-style messages and PR titles: `type(scope): summary`.
12+test("example", async () => {
13+ await using tmp = await tmpdir()
14+ // tmp.path is the temp directory path
15+ // automatically cleaned up when test ends
16+})
17+```
1618  
17−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`.
19+### Options
1820  
19−Examples: `fix(tui): simplify thinking toggle styling`, `docs: update contributing guide`, `chore(sdk): regenerate types`.
21+- `git?: boolean` - Initialize a git repo with a root commit
22+- `config?: Partial<Config.Info>` - Write an `opencode.json` config file
23+- `init?: (dir: string) => Promise<T>` - Custom setup function, returns value accessible as `tmp.extra`
24+- `dispose?: (dir: string) => Promise<T>` - Custom cleanup function
2025  
21−## Style Guide
26+### Examples
2227  
23−### General Principles
28+**Git repository:**
2429  
25−- Keep things in one function unless composable or reusable
26−- 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.
27−- Avoid `try`/`catch` where possible
28−- Avoid using the `any` type
29−- Use Bun APIs when possible, like `Bun.file()`
30−- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
31−- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
32−- 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.
33−- In Effect generators, bind services to named variables before calling methods. Do not use nested service yields such as `yield* (yield* Foo.Service).bar()`.
30+```typescript
31+await using tmp = await tmpdir({ git: true })
32+```
3433  
35−Reduce total variable count by inlining when a value is only used once.
34+**With config file:**
3635  
37−```ts
38−// Good
39−const journal = await Bun.file(path.join(dir, "journal.json")).json()
40− 
41−// Bad
42−const journalPath = path.join(dir, "journal.json")
43−const journal = await Bun.file(journalPath).json()
36+```typescript
37+await using tmp = await tmpdir({
38+ config: { model: "test/model", username: "testuser" },
39+})
4440 ```
4541  
46−### Destructuring
42+**Custom initialization (returns extra data):**
4743  
48−Avoid unnecessary destructuring. Use dot notation to preserve context.
44+```typescript
45+await using tmp = await tmpdir<string>({
46+ init: async (dir) => {
47+ await Bun.write(path.join(dir, "file.txt"), "content")
48+ return "extra data"
49+ },
50+})
51+// Access extra data via tmp.extra
52+console.log(tmp.extra) // "extra data"
53+```
4954  
50−```ts
51−// Good
52−obj.a
53−obj.b
55+**With cleanup:**
5456  
55−// Bad
56−const { a, b } = obj
57+```typescript
58+await using tmp = await tmpdir({
59+ init: async (dir) => {
60+ const specialDir = path.join(dir, "special")
61+ await fs.mkdir(specialDir)
62+ return specialDir
63+ },
64+ dispose: async (dir) => {
65+ // Custom cleanup logic
66+ await fs.rm(path.join(dir, "special"), { recursive: true })
67+ },
68+})
5769 ```
5870  
59−### Imports
71+### Returned Object
6072  
61−- Never alias imports. Do not use `import { foo as bar } from "..."` or renamed imports like `resolve as pathResolve`.
62−- Never use star imports. Do not use `import * as Foo from "..."` or `import type * as Foo from "..."`.
63−- 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`.
64−- 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.
73+- `path: string` - Absolute path to the temp directory (realpath resolved)
74+- `extra: T` - Value returned by the `init` function
75+- `[Symbol.asyncDispose]` - Enables automatic cleanup via `await using`
6576  
66−### Variables
77+### Notes
6778  
68−Prefer `const` over `let`. Use ternaries or early returns instead of reassignment.
79+- Directories are created in the system temp folder with prefix `opencode-test-`
80+- Use `await using` for automatic cleanup when the variable goes out of scope
81+- Paths are sanitized to strip null bytes (defensive fix for CI environments)
6982  
70−```ts
71−// Good
72−const foo = condition ? 1 : 2
83+## Testing With Effects
7384  
74−// Bad
75−let foo
76−if (condition) foo = 1
77−else foo = 2
78−```
85+Use `testEffect(...)` from `test/lib/effect.ts` for tests that exercise Effect services or Effect-based workflows.
7986  
80−### Control Flow
87+### Core Pattern
8188  
82−Avoid `else` statements. Prefer early returns.
89+```typescript
90+import { describe, expect } from "bun:test"
91+import { Effect, Layer } from "effect"
92+import { testEffect } from "../lib/effect"
8393  
84−```ts
85−// Good
86−function foo() {
87− if (condition) return 1
88− return 2
89−}
94+const it = testEffect(Layer.mergeAll(MyService.defaultLayer))
9095  
91−// Bad
92−function foo() {
93− if (condition) return 1
94− else return 2
95−}
96+describe("my service", () => {
97+ it.instance("does the thing", () =>
98+ Effect.gen(function* () {
99+ const svc = yield* MyService.Service
100+ const out = yield* svc.run()
101+ expect(out).toEqual("ok")
102+ }),
103+ )
104+})
96105 ```
97106  
98−### Complex Logic
107+### `it.effect` vs `it.live`
99108  
100−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.
109+- Use `it.effect(...)` when the test should run with `TestClock` and `TestConsole`.
110+- Use `it.live(...)` when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
111+- Use `it.instance(...)` for live Effect tests that need a scoped temporary directory and instance context.
112+- Most integration-style tests in this package use `it.live(...)`.
101113  
102−```ts
103−// Good
104−export function loadThing(input: unknown) {
105− const config = requireConfig(input)
106− const metadata = readMetadata(input)
107− return createThing({ config, metadata })
108−}
114+### Effect Fixtures
109115  
110−function requireConfig(input: unknown) {
111− ...
112−}
116+Prefer the Effect-aware helpers from `fixture/fixture.ts` instead of building a manual runtime in each test.
117+ 
118+- `tmpdirScoped(options?)` creates a scoped temp directory and cleans it up when the Effect scope closes.
119+- `provideInstance(dir)(effect)` is the low-level helper. It does not create a directory; it runs an Effect with `InstanceRef` provided for `dir`.
120+- `provideTmpdirInstance((dir) => effect, options?)` is the convenience helper. It creates a temp directory, binds it as the active instance, and disposes the instance on cleanup.
121+- `provideTmpdirServer((input) => effect, options?)` does the same, but also provides the test LLM server.
122+ 
123+Use `it.instance(...)` by default when a test only needs one temp instance. Yield `TestInstance` from `fixture/fixture.ts` when the test needs the temp directory path:
124+ 
125+```typescript
126+import { TestInstance } from "../fixture/fixture"
127+ 
128+it.instance("uses the temp directory", () =>
129+ Effect.gen(function* () {
130+ const test = yield* TestInstance
131+ expect(test.directory).toContain("opencode-test-")
132+ }),
133+)
113134 ```
114135  
115−- Keep helpers close to the code they support, below the main export when that improves readability.
116−- Do not over-abstract simple expressions into many single-use helpers; extract only when it names a real concept like `requireConfig` or `readMetadata`.
117−- Do not return `Effect` from helpers unless they actually perform effectful work. Synchronous parsing, validation, and option building should stay synchronous.
118−- Prefer Effect schema helpers such as `Schema.UnknownFromJsonString` and `Schema.decodeUnknownOption` over manual `JSON.parse` wrapped in `Effect.try` when parsing untrusted JSON strings.
119−- Add comments for non-obvious constraints and surprising behavior, not for obvious assignments or control flow.
136+Use `provideTmpdirInstance(...)` or `tmpdirScoped()` plus `provideInstance(...)` when a test needs multiple directories, custom setup before binding, needs to switch instance context within one test, or explicitly tests instance disposal/reload lifetime.
120137  
121−### Schema Definitions (Drizzle)
138+### Style
122139  
123−Use snake_case for field names so column names don't need to be redefined as strings.
140+- Define `const it = testEffect(...)` near the top of the file.
141+- Keep the test body inside `Effect.gen(function* () { ... })`.
142+- Yield services directly with `yield* MyService.Service` or `yield* MyTool`.
143+- Avoid custom `ManagedRuntime`, `attach(...)`, or ad hoc `run(...)` wrappers when `testEffect(...)` already provides the runtime.
144+- When a test needs instance-local state, prefer `it.instance(...)` over manual `Instance.provide(...)` inside Promise-style tests.
124145  
125−```ts
126−// Good
127−const table = sqliteTable("session", {
128− id: text().primaryKey(),
129− project_id: text().notNull(),
130− created_at: integer().notNull(),
131−})
146+### Partial Service Stubs
132147  
133−// Bad
134−const table = sqliteTable("session", {
135− id: text("id").primaryKey(),
136− projectID: text("project_id").notNull(),
137− createdAt: integer("created_at").notNull(),
148+When a test only needs to override one or two methods of a service, prefer `Layer.mock` over a hand-rolled `Layer.succeed(Service, Service.of({ ... }))`. `Layer.mock` lets you supply just the methods that matter — anything else throws an `UnimplementedError` defect if the test accidentally calls it, which is exactly the signal you want.
149+ 
150+```typescript
151+import { Effect, Layer } from "effect"
152+import { Account } from "@/account/account"
153+ 
154+const failingAccountLayer = Layer.mock(Account.Service, {
155+ orgsByAccount: () => Effect.fail(new Account.AccountServiceError({ message: "simulated upstream failure" })),
138156 })
139157 ```
140158  
141−## Testing
159+This is much shorter than stubbing every method with `Effect.void` / `Effect.succeed(...)` placeholders, and it keeps the test focused on the behaviour under test.
142160  
143−- Avoid mocks as much as possible, you shouldn't be using globalThis.\* at all unless it's the only option.
144−- Test actual implementation, do not duplicate logic into tests
145−- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package dirs like `packages/opencode`.
161+## Synchronizing With Concurrent Work
146162  
147−## Type Checking
163+### The Anti-Pattern
148164  
149−- Always run `bun typecheck` from package directories (e.g., `packages/opencode`), never `tsc` directly.
165+Using `Effect.sleep(N)` or `setTimeout` as a "wait for the forked fiber to be ready" hack races the scheduler. The forked fiber may not have reached the synchronization point within `N` ms on a slow CI host, and the test fails intermittently. See PR #27622 for a concrete flake that fell out of this exact pattern.
150166  
151−## V2 Session Core
167+### The Fix
152168  
153−- 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.
154−- 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.
155−- 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.
156−- 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.
157−- 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.
158−- 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 continuation recovery requires a separate explicit design before it may retry provider work. A drain has no durable identity or transcript boundary.
159−- Keep delivery vocabulary explicit. Prompts steer by default and promote at the next safe provider-turn boundary while the current drain requires continuation. An explicit `queue` input remains pending until the Session would otherwise become idle; promote one queued input at that boundary, then reevaluate continuation before promoting another. Promoting any new user input resets the selected agent's provider-turn allowance; a batch of steers resets it once.
160−- Keep EventV2 replay owner claims separate from clustered Session execution ownership.
161−- 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.
169+Wait on a **published readiness signal**, not wall-clock time. Available affordances:
170+ 
171+- `pollWithTimeout(effect, message, duration?)` from `test/lib/effect.ts` — repeatedly run a predicate effect until it returns a non-`undefined` value, with a timeout.
172+- `awaitWithTimeout(effect, message, duration?)` from `test/lib/effect.ts` — wrap any effect with `Effect.timeoutOrElse` and a custom error message.
173+- `llm.wait(n)` from `test/lib/llm-server.ts` — wait until the mock LLM has received `n` HTTP calls.
174+- `SessionStatus.Service` `.get(sessionID)` — observable per-session state (`{ type: "busy" | "idle" | ... }`).
175+- `BackgroundJob.wait({ id, timeout })` from `src/background/job.ts` — wait for a background job to complete.
176+- Bus subscriptions — fork `Stream.runForEach(bus.subscribe(Event), ...)` and open a `Latch` inside the callback to signal first-event readiness.
177+- `Deferred.await(deferred).pipe(Effect.timeoutOrElse(...))` for one-shot signals.
178+ 
179+### Example
180+ 
181+```ts
182+// Antipattern — race
183+yield * prompt.shell({ command: "sleep 30" }).pipe(Effect.forkChild)
184+yield * Effect.sleep(50)
185+yield * prompt.cancel(chat.id)
186+ 
187+// Fix — wait for a published readiness signal
188+yield * prompt.shell({ command: "sleep 30" }).pipe(Effect.forkChild)
189+yield *
190+ pollWithTimeout(
191+ Effect.gen(function* () {
192+ const s = yield* (yield* SessionStatus.Service).get(chat.id)
193+ return s.type === "busy" ? (true as const) : undefined
194+ }),
195+ "session never became busy",
196+ )
197+yield * prompt.cancel(chat.id)
198+```
199+ 
200+### When Fixed Sleeps Are OK
201+ 
202+- Testing debounce or throttle behavior, where the sleep **is** the test.
203+- Letting real wall-clock advance past a genuine timestamp resolution boundary (e.g. mtime granularity).
204+- Simulating network latency in race-regression tests that intentionally exercise ordering.
162205  
RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack

RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack

RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack