Status: implementation plan for specs/simulation/simulation.md.
The full simulation architecture is intentionally broad. This document breaks it into phases that can be implemented and reviewed incrementally.
Goal: start the normal app in simulation mode and inspect/drive the TUI through an external WebSocket driver.
This phase proves the core shape without swapping every foundational layer yet.
Implementation checklist:
OPENCODE_DRIVE=<name> activation in V1/full-TUI startup.ui.state, ui.action, ui.render.trace.list, trace.clear, trace.export.Scope:
OPENCODE_DRIVE=<name> activation.ui.state, ui.action, ui.render.trace.list, trace.clear, trace.export.Done when:
OPENCODE_DRIVE=<name> bun run dev starts the normal app and UI drive server.Out of scope:
Goal: make the app safe and controlled by swapping the lowest layers, not app logic.
Implementation checklist:
packages/simulation/src/backend as the home for backend simulation layer replacements, exported from backend/index.ts as simulationReplacements; @opencode-ai/simulation is private/non-published and depends on logic/framework packages (core, llm, effect, OpenTUI), while server and tui consume it.makeRoutes via Layer.unwrap + dynamic import("@opencode-ai/simulation/backend") gated on OPENCODE_SIMULATE, so the simulation module is never loaded eagerly and makeRoutes stays synchronous.FileSystem.FileSystem (simulation/filesystem.ts) replacing the NodeFileSystem platform node. Backed by a flat path map; implements the operations the app uses (stat, access, chmod, realPath, read/write file, make/read directory, remove, rename, copy, copyFile, temp dirs, read-only open handles); unused operations die with a clear defect; watch fails as unsupported.process.cwd() at layer-build time. The anchor is a real, empty host directory the runner creates and cds into.PermissionDenied simulation errors. Probe operations (stat/access/exists) report NotFound outside the root so walk-up loops (project discovery, findUp, globUp) terminate naturally.SimulationFSUtil replacement (simulation/fs-util.ts): wraps the real FSUtil layer and reroutes readDirectoryEntries, glob, and globUp — which bypass the injected FileSystem via node fs/promises and the glob package — through the simulated filesystem.LayerNode.hoist conflict detection to compare node implementations instead of object identity; replacement rewriting produces dependency-rewritten copies of the same node, which previously false-positived as "conflicting implementations".OPENCODE_SIMULATE_STATE: files/ contents of the snapshot directory are read from the host once at layer-build time and seeded into the in-memory tree joined onto the anchor root.opencode serve boots with OPENCODE_SIMULATE=1 + OPENCODE_SIMULATE_STATE + path/DB env seams (OPENCODE_CONFIG_DIR, OPENCODE_TEST_HOME, OPENCODE_DB=:memory:); fs.list/fs.read observe only seeded in-memory files; the anchor directory on the host remains empty after the run.chdir + env seam setup automatically in CLI startup when simulation mode is enabled (currently set manually by the runner; a full run needs OPENCODE_SIMULATE_STATE, OPENCODE_CONFIG_DIR, OPENCODE_TEST_HOME, OPENCODE_DB=:memory:, and XDG_*_HOME pointed into the anchor, plus Bun's --preload=@opentui/solid/preload when launched outside packages/cli).packages/simulation/src/backend/network.ts): replaces the httpClient platform node, resolves all outbound HTTP against an in-memory route table, denies unknown destinations loudly, and keeps a bounded request log (design: simulated-network-llm.md).openai.ts + llm-exchange.ts): provider requests open exchanges; the driver streams chunks back which are encoded as real OpenAI Chat SSE (schema-checked against OpenAIChatEvent) and consumed by the real protocol pipeline. No enqueue store — the driver is the model.control.ts): JSON-RPC at the named manifest's backend endpoint, started when OPENCODE_DRIVE is set. Drivers connect directly (standalone topology — no frontend proxy): llm.attach (replays pending exchanges), llm.chunk, llm.finish, llm.pending, network.log; llm.request notifications push opened exchanges. This is also the headless-simulation interface. Drivers manage the manifest's UI endpoint for UI control and backend endpoint for LLM/network control.https://models.dev/api.json with an empty catalog in the simulated network; providers come from seeded config (opencode.json in the snapshot defines an openai-compatible provider with a dummy apiKey, which passes the catalog availability gate and resolves onto the real openai-chat route).buildLocationServiceMap to apply replacements when compiling hoisted global nodes; platform-node replacements (filesystem, httpClient) were silently ignored inside hoisted globals.llm.request -> driver chunks -> assistant message contains driver text; script: packages/server/script/e2e-sim.ts) and through the TUI (fake renderer, both sockets: type + submit via TUI WS, answer llm.request via backend WS, assistant reply rendered on screen; script: packages/tui/script/sim-llm-driver.ts).just-bash, minimal fake git, deny unsupported spawns).Scope:
AppNodeBuilder.build(...) and AppNodeBuilderV1.build(...).mkdtemp) and process.chdir into it before any command resolves its working directory; skip creation when the runner already spawned the app inside an anchor.process.cwd() (the anchor). No cwd monkey-patching: cwd, $PWD, and path.resolve() stay truthful.OPENCODE_SIMULATE_STATE: read the snapshot directory once at startup and seed the in-memory filesystem (snapshot files/ paths joined onto the anchor root), config, env, and optional LLM/network state from it.OPENCODE_CONFIG_DIR, OPENCODE_TEST_HOME, OPENCODE_DB=:memory:), set before packages/core/src/global.ts import-time path setup runs.just-bash against the simulated filesystem.git support for discovery/status paths.Done when:
OPENCODE_SIMULATE_STATE and observes the seeded project files, config, and env through normal app paths.just-bash; unsupported process spawns fail.Out of scope:
Goal: explore different app states using generated commands and plugin-provided config state.
Scope:
fast-check yet.ui.state.actions.Done when:
Out of scope:
Goal: turn exploratory simulation into durable tests and prepare for larger campaigns.
Scope:
Done when:
Out of scope:
fast-check integration if the custom runner becomes too limited.