- Install with
pnpm install --frozen-lockfile;pnpm-lock.yamlis canonical. Bun is still required because build, setup, and dashboard scripts run through it—do not substitutebun install. - Full verification:
pnpm build && pnpm typecheck && pnpm lint && pnpm test. Build must precede tests becausetest/dashboard/app-bundle.test.tsrejects a stale source hash. - Focus a Node test with
pnpm test --project node test/path.test.ts; add-t "test name"for one case. Run dashboard DOM tests withpnpm test --project dom test/dashboard/app-dom.test.ts. Thedomproject is the only happy-dom suite; all other tests use thenodeproject and abun:sqliteshim. pnpm typecheckcoverssrc/, not tests or scripts. ESLint also ignores tests and generated dashboard files.
- Server plugin
src/index.ts— published as root (.) and./serveralias. TUI pluginsrc/tui.tsx. Installer CLIsrc/install/cli.ts(bin). Standalone dashboard viapnpm dashboard(scripts/dashboard.ts); not a package export. src/index.tsis the server composition root. Core boundaries:src/loop/(runtime/state machine),src/storage/(database and repositories),src/tools/(OpenCode tools),src/agents/(agent definitions).
pnpm buildrewritessrc/version.ts,src/dashboard/marked-source.ts, andsrc/dashboard/app-bundle.ts. Editpackage.json,src/dashboard/marked.min.js, orsrc/dashboard/app/respectively, never the generated files.pnpm buildremoves the output directory before compiling (scripts/build.ts), so deleting or renaming source modules cannot leave stale output indist/.scripts/build.tsrunstscfirst, thenBun.buildoverwritesdist/index.js(server) anddist/tui.js(TUI) with self-contained bundles. Both must stay bundled so the plugin loads without resolvingnode_modules; the vendored installer mode (bunx opencode-forge --vendor) depends on it.@opentui/*,@opencode-ai/plugin/tui,solid-js, andbun:sqlitestay external because opencode's runtime provides them.resolveShippedRootinsrc/utils/shipped-paths.tsis the only way to locate shipped files on disk. Never derive paths fromimport.meta.urldirectly: bundling collapses it, which would silently break the migration SQL loader, prompt loading, and bundled-asset sync.- Bundled prompts (
src/prompts/) and skills (skills/) sync on every plugin load, preserving user edits. The standalone installer handles conflicts and orphan pruning. - The section-summary block template is the single
SECTION_SUMMARY_TEMPLATEinsrc/loop/prompts.ts, built from the marker constants insrc/utils/section-summary.ts; do not hand-write the marker strings into prompt markdown files or other prompt builders.
MAX_TOTAL_SECTIONSinsrc/constants/loop.tsis the single section cap. Update botharchitect.mdandarchitect-auto.mdprose when it changes.PLAN_AUTHORING_TOOL_NAMESandFORGE_MANAGED_PERMISSIONS/FORGE_REQUIRED_PERMISSIONSinsrc/constants/loop.tsare the single sources for tool-exclude lists, permission rulesets, and the audit ruleset. Do not re-list tool names by hand.- Watchdog has exactly two progress signals (
recordActivity,recordSessionContent);busyis not one of them. Content signal is fed fromloop.tickfirst branch, must stay O(1). Busy ceiling (busyStallTimeoutMs) is measured from the newest of both across loop and subagent sessions. - Multiple plugin instances share one process. Ownership gate:
ownsLoopWorktreeinsrc/loop/runtime.tscomparesdirectorytoworktreeDirviacanonicalizePath.startWatchdogandsession.errorhandler gate on it — fail-safe, never a hang. Do not extend the gate to the idle handler. LoopService.resolveActiveLoopForSessionis the only correct "is this session inside a running loop" check.resolveLoopNamematches terminated loops too.
src/sandbox/msb.tsis the sole TypeScript runtime/lifecycle facade andmsbCLI argument owner; route runtime operations through itsSandboxRuntimefacade. The one required exception is the generated shell shim (src/sandbox/shell-shim.ts), which invokesmsb execdirectly when an agent shell command must run inside a sandbox.src/sandbox/process.tsis the only child-process spawner; all TypeScript shell execution goes throughrunCommand.buildSandboxWorkspacescanonicalizes the host side of every mount and leavescontainerDiras the original path. msb refuses a host path that traverses a symlink and fails the whole sandbox withENOTDIR, which on macOS breaks everyos.tmpdir()mount because/varis a symlink toprivate/var. Never canonicalize the container side: absolute paths handed to the agent must resolve identically inside the sandbox.getSandboxStateis the only liveness primitive; five states:running,stopped(reusable, never create/evict),transient(real but not directly executable:Created/Starting/Draining/Pausedmap here),unknown(query failed),missing(may create or evict).Stopped/Crashedmap to the reusablestoppedstate becausemsb execstarts them in place.registerActiveSandboxis the only place a usable sandbox is recorded.container/Dockerfilemust derive from a plain OCI base and keep the finalUSER agent;ENTRYPOINT/CMDare ignored because msb runsagentdas PID 1.
- Dashboard uses
solid-js/html, not JSX. No<${Show}>or<${For}>; use reactive thunks/memos and.map(). Root component returns one wrapper element.test/dashboard/app-dom.test.tsenforces these constraints. src/dashboard/render.tsowns the stylesheet;test/dashboard/render.test.tsenforces CSS token usage, sticky stack--z-subnav<--z-app-bar<--z-popover, app-bar height variables at relevant breakpoints, and matching loop-tabledata-colattributes.- Storage migrations are registered explicitly, in execution order, in the lowercase
migrationsarray insrc/storage/migrations/index.ts; they are not discovered from filenames. resolveDashboardConfiginsrc/dashboard/config.tsis the only dashboard bind host/port resolver. Every launch surface must pass overrides intostartDashboardServerand render warnings. Dashboard has no auth;DASHBOARD_EXPOSED_WARNINGis the single warning.resolveForgeDbPathinsrc/utils/opencode-paths.tsis the only place<dataDir>/forge.dbis built.