Development Guide: Environment, Workflow, Testing, Packaging, and CI
July 27, 2026 · View on GitHub
Related: architecture.md (module map) · conventions.md (coding conventions)
1. Environment Setup
- Node >= 22.22.2 (matches the minimum of the pinned OpenCode dependency chain; PR CI pins
Node 24, release CI pins Node 22 in all four jobs that set up Node — compute-version,
release-mac, release-win, create-release; the fifth job, refresh-cdn-cache, installs no Node at
all). pnpm
9.14.4through Corepack +pnpm-lock.yaml. - All dependency sources are public:
@oomol/connection/@oomol/connection-electron-adapterare published to the public registry (registry.npmjs.org);corepack pnpm installneeds no token or.npmrc— since the repo went open source, a fresh clone and an external fork's CI both install directly (historically this went through the GitHub Packages private registry + aread:packagesPAT; that auth chain was removed once the repo turned public and the packages shipped to public npm). If your machine's global~/.npmrcstill points the@oomolscope atnpm.pkg.github.com, it overrides the default public registry — delete that line. Note: the postinstall binary/skill download scripts are best-effort (warn only), but dev cannot start without@oomol/connection— an install failure must not be ignored. pnpm install's postinstall chains the binary/skill download scripts and finally builds the custom tool runtime:scripts/download-electron.ts→ downloads the dev-only Electron copy into.electron-dist/and rewrites the macOS Info.plist tocom.oomol.wanta-local/ thewanta-localscheme (for dev deep-links).ELECTRON_SKIP_BINARY_DOWNLOAD=1skips it.scripts/download-oo.ts→ downloads the oo binary into.oo-bin/(version pinned byOO_CLI_VERSIONinscripts/oo-cli.ts; includes sha512 integrity verification andchmod 0o755). oo / ripgrep downloads share one policy: 30-second per-request timeout and at most 3 request attempts (at most 2 retries); deterministic 4xx is not retried.OO_SKIP_BINARY_DOWNLOAD=1skips it.scripts/download-skills.ts→ exports the 4 bundled oo skills (oo,oo-find-skills,oo-create-skill,oo-publish-skill) toresources/skills/viaoo skills install --out-dir, using isolatedOO_CONFIG/DATA/LOGdirs (scripts/skills.ts). Best-effort;OO_SKIP_BINARY_DOWNLOAD=1skips it.scripts/download-ripgrep.ts→ downloads ripgrep into.oo-bin/(version pinned byRIPGREP_VERSION = "14.1.1"inscripts/ripgrep.ts). It exists because OpenCode's grep tool needsrgon PATH, and a GUI-launched process cannot assume the system PATH. Best-effort;OO_SKIP_BINARY_DOWNLOAD=1orWANTA_SKIP_RIPGREP_DOWNLOAD=1skips it.scripts/build-agent-tool-runtime.ts→ uses Rolldown to merge@opencode-ai/plugin/tooland Zod intoresources/agent-tool-runtime/tool.js; both dev and the packaged artifact sync it into the private workspace, so tool loading does not depend on a package install succeeding at first launch.
2. .env Configuration
cp .env.example .env.local # .env.local is gitignored
WANTA_ENDPOINT: the endpoint apex domain, defaultoomol.com; switch tooomol.devto target the dev environment. Read rules (resolveOoEndpointinvite.config.ts): dev and vitest read.env(.local)vialoadEnv; build deliberately reads no file (keeps dev domains out of release artifacts); both modes honor an explicit environment variable — internal test builds useWANTA_ENDPOINT=oomol.dev corepack pnpm run build. Known trap: theoomol.devLLM gateway once returned 403 "Model disabled" for Auto/oopilot(a backend restriction, not a code bug); the dev endpoint is mainly for connector integration — on a chat 403, suspect the gateway side first.WANTA_OO_BIN(optional; a process environment variable, not read from .env files): overrides the oo binary path,WANTA_OO_BIN=/abs/path/to/oo corepack pnpm run dev. When set, it only skips the oo-binary existence/version check in the predev guard; the guard still fails on a missing ripgrep binary and still auto-exports the bundled skills.WANTA_PACKAGE_ASSETS_BASE_URL(optional): overrides the package-assets base URL, injected as the__PACKAGE_ASSETS_BASE_URL__define, defaulthttps://package-assets.<endpoint>.vite.config.tsreads it fromprocess.envonly;vitest.config.tsreads it vialoadEnv.
3. Day-to-Day Development
corepack pnpm run dev # predev runs scripts/check-oo.ts first (three checks, see below)
# vite dev server on port 5273; vite-plugin-electron starts the main process too
# Electron userData is the repo-local ./wanta directory
corepack pnpm run dev:no-electron
# only starts vite + the electron bundle watch, without auto-launching Electron;
# for code-side debugging that needs no UI window
- The predev guard (
scripts/check-oo.ts) checks three things: the oo binary is present AND its version marker matchesOO_CLI_VERSION, ripgrep is present in.oo-bin/(both fatal — the guard exits with an error), and the bundled skills inresources/skills/are complete (auto re-export, non-fatal). - vite dev server is fixed at port
5273withstrictPort=true: if an existingpnpm run devholds the port, the new dev process fails outright instead of silently moving to5274+and launching a second Electron. To temporarily disable Electron auto-start, you can also useWANTA_ELECTRON_AUTO_START=0 corepack pnpm run dev. - When
.electron-distexists, vite automatically setsELECTRON_OVERRIDE_DIST_PATH, so dev uses the Electron copy with thewanta-localscheme (the menu bar shows the dev identity) — required for the browser-login round trip to hit the dev instance. - Dev userData lives at
./wantain the current checkout; agent data under itsagent/(workspace / isolation / oo-store). Link runtime selection and encrypted OpenConnector token metadata live inlink-runtime.jsonat the userData root. Packaged production builds keep Electron's default platform userData path such as~/Library/Application Support/wantaon macOS. corepack pnpm run dev:worktreekeeps the same./wantauserData rule for each worktree while adding per-worktree port and protocol-registration isolation. When the target./wantais missing or empty, it copies the canonical main repo's./wantaonce as an initialization source; it never shares or overwrites an existing worktree userData directory.WANTA_DEV_AUTH_SOURCE_DIR=/abs/pathcan override that initialization source.- Code changes must go through a temporary branch + PR: first align local
mainwithorigin/main, then cut a one-off branch frommain(e.g.codex/<task>,ci/<task>,fix/<task>). Once the change is done and passes the quality gate, push the temporary branch and open a PR tooomol-lab/wanta:main; land it by merging the PR. Never commit or push directly onmain. After the PR merges, sync the latestmain, delete the local temporary branch, and delete the same-named temporary branch on the fork/remote. All human-readable text in Git operations must be English — commit messages, branch names, PR titles, PR descriptions, PR reviews/comments, tags/release notes. - Quality gate after any change, all four green:
corepack pnpm run ts-check && corepack pnpm run lint && corepack pnpm run format && corepack pnpm test.
Local OpenConnector development
Wanta consumes an already-running OpenConnector; it does not supervise the sibling repository. Start the two projects separately:
cd /path/to/connect
npm run dev
cd /path/to/wanta
corepack pnpm run dev
The source development defaults are API http://localhost:3000 and Console
http://localhost:5173. Verify both GET /health and the standard-envelope GET /v1/health, then
open Settings → Link Runtime, enter the two origins and any runtime token, test, save, and select
OpenConnector. A token is optional when OpenConnector authentication is disabled. Use the
Connections route for the sanitized inventory and the external Console for provider credentials.
Do not set OO_CONNECTOR_URL or run oo connector login inside Wanta; the app owns an isolated,
non-persisting sidecar environment.
4. Testing
corepack pnpm test=vitest run;vitest.config.tsincludeselectron/**/*.test.ts,src/**/*.test.ts,scripts/**/*.test.ts, environment node, and injects__OO_ENDPOINT__and__PACKAGE_ASSETS_BASE_URL__via the same loadEnv mechanism as vite (test assertions derive fromooEndpoint; never hardcode a domain, so local and CI both pass deterministically).- Tests are colocated with the source (
*.test.tsnext to the module it covers, ~240 files acrosselectron/,src/, andscripts/— see the source tree). Most are pure-function unit tests, with noted exceptions — e.g.scripts/renderer-boundary.test.tsscans thesrc/tree on disk to enforce the renderer→electron import allowlist. - Real-run verification uses the manual smoke scripts under
.wanta-dev/(gitignored, not packaged, not covered by lint/format/tsc):agent-smoke.ts(headless golden path),chat-stream-smoke.ts,connections-smoke.ts,r4-smoke.ts,system-probe.ts(verifies body.system is append-not-replace),spike.mjs. Run:OO_API_KEY=... node --experimental-strip-types .wanta-dev/xxx.ts(smoke scripts constructAgentManagerdirectly, no browser login; theAgentManageroption is nowauthTokenand takes the session token — the env var name staysOO_API_KEYpurely as oo-cli's external contract; the gateway authenticates uniformly). A fresh clone has none of these scripts (they exist only on the original dev machine): when missing, write your own by constructingAgentManagerdirectly per architecture.md §2 (electron/agent/is electron-free). - UI real-machine verification bypasses (dev-only env vars, harmless in production):
VITE_WANTA_SMOKE(auto-sends one message once AppShell is ready,AppShell.tsx),VITE_WANTA_ROUTE=settings(also supportsknowledgeand other AppShell pages),VITE_WANTA_LOCALE(forces the locale,src/i18n/i18n.ts); pair with macOSscreencapturefor screenshot evidence. Electron 42's macOS native notifications require a valid app signature; the ad-hoc dev signature of.electron-dist/Electron.appcannot be used for notification acceptance — the settings page marks this and disables test notifications there. Use a CI-signed artifact or a locally packaged app with a valid Apple Development / Developer ID signature to cover: allow/deny notification; whether a test result's unique ID can be confirmed inNotification.getHistory(); the copy for the case where the notification center received it but the banner is suppressed by Focus/Do Not Disturb or a screen-sharing policy; sound; foreground / non-foreground task conditions; the click-back-to-task path. Note that "delivered to the notification center" still does not mean the user necessarily saw a banner. - Known verification gaps (per session records these were never exercised on a real machine — do not assume them verified while debugging): real bash/edit/external_directory calls under the permission prompt UI, the real visual result of the ai-elements migration (no display was available at the time), and the browser-login round trip with a real account (needs a human login).
5. Lint / Format / Type Check
pnpm run lint=oxlint .(.oxlintrc.json: correctness=error;react/only-export-componentserror, but overridden off for the two vendored dirssrc/components/ui/**andsrc/components/ai-elements/**; ignorePatterns includes.wanta-dev).pnpm run format=oxfmt --check .(.oxfmtrc.json: printWidth 120, no semicolons, double quotes, trailingComma all, sortImports with type imports first, sortTailwindcss recognizing cn/clsx/cva).pnpm run ts-check=tsgo -p tsconfig.json --incremental false(TypeScript native preview,@typescript/native-preview; the flag overrides tsconfig'sincremental: true). tsconfig: strict, verbatimModuleSyntax, module Preserve, allowImportingTsExtensions, noEmit; include is src / electron / scripts / vite.config.ts / electron-builder.ts.
6. Packaging / Signing / Notarization / Auto-Update
corepack pnpm run build:mac # = build:app + prepare:binaries + electron-builder --mac (also build:win / build:linux / build:electron)
scripts/prepare-binaries.ts: copies three binaries intoresources/bin/and chmods 755 — opencode (node_modules/opencode-ai/bin/opencode.exe, this exact filename on all platforms), oo (from.oo-bin/, downloaded on the spot if missing), and ripgrep (rg, placed in the sameresources/bin/dir, whichAgentManagerprepends to PATH so OpenCode's grep tool finds it). It also exports the bundled skills and rebuilds the custom tool runtime.electron-builder.ts: appId / productName / protocols derive fromelectron/branding.ts; asar; outputrelease/${version}; files only dist + dist-electron (excluding map/d.ts, no electron/ sources or tests); extraResources includeresources/bin → bin,resources/skills → skills, andresources/agent-tool-runtime → agent-tool-runtime; thesqlite3native module is unpacked from asar (used by the exactly pinnedwiki-graph@0.4.0/wiki-graph-core@0.4.0runtime); afterPackscripts/electron-builder-after-pack.cjs(deletes the ~20MB LICENSES.chromium.html; the hook is .cjs because electron-builder's require hook does not support .ts). mac dmg+zip arm64; win nsis x64 (signtool certificate fingerprint); linux AppImage.- Signing/notarization can only happen in CI; local builds produce unsigned artifacts only
(mac certificates, Apple ID, and the win USB certificate all live in CI secrets). macOS
notarization requires every executable inside the app to be signed with Hardened Runtime —
the oo, opencode, and rg under
Resources/binare all in scope; any newly bundled binary (or a changed extraResources layout) must be added to the signing/notarization scope, or notarization fails. - Auto-update (
electron/update/: common.ts contract + node.ts implementation + channel.ts / policy.ts pure functions): electron-updater generic provider, feed =https://static.<ep>/release/apps/wanta/<platform>/<arch>; packaged builds only. First check after startup at a random 5–15 s delay; stable checks every 2 hours, beta every 1 hour (±12.5% jitter); on wake from sleep (Windows/macOS/Linux), or when the window returns to the foreground with more than 30 minutes since the last successful check, a catch-up check runs after a random 10–30 s / 3–10 s delay respectively. Once an update is found it downloads in the background; when the download completes with the window in the background, a native notification is sent and Windows gets a "Restart and update" tray item; with the window in the foreground, a single non-blocking toast appears once the Agent is idle. The persistent title-bar action remains available until restart. A normal quit still installs a downloaded update.autoDownload=false— the Wanta state machine controls the download explicitly, andautoInstallOnAppQuit=trueis armed only after a download starts. Dual channel: stable pullslatest*.yml, beta pullsbeta*.yml; the channel is passed via thechannelfield ofsetFeedURL(never theautoUpdater.channelsetter — it silently flipsallowDowngradeto true), with an explicitallowDowngrade=false(switching beta back to stable converges at the next stable release by default; never auto-downgrade). Channel merge rule:user setting ?? (own version contains -beta ? beta : stable)(channel.ts), persisted in settings.json under theupdateChannelkey. Update scheduling, check results, native notifications, and tray install failures all go to the diagnostics log. generateUpdatesFilesForAllChannels: trueinelectron-builder.ts: a stable build also emitsbeta*.yml(pointing at that stable), so beta users converge immediately after a stable release; the generic provider derives the channel from the-beta.Nversion suffix automatically (detectUpdateChannel is on by default).electron-builderandelectron-updaterare exactly pinned (channel behavior is version-sensitive; before upgrading, verify the GenericProvider/PublishManager channel logic is unchanged).
7. CI (.github/workflows/)
- pr.yml (PR → main, ubuntu, Node 24):
pnpm install --frozen-lockfile(no registry auth — @oomol packages come from public npm; setsELECTRON_SKIP_BINARY_DOWNLOAD=1+OO_SKIP_BINARY_DOWNLOAD=1to skip binary downloads) → lint → format → ts-check → test → build. The test step setsELECTRON_OVERRIDE_DIST_PATHas a defensive guard against the electron stub downloading a binary. - release.yml (workflow_dispatch, inputs channel stable/beta + expected_version +
version_bump):
compute-version(version math inscripts/release-version.ts, vitest-covered — stable auto-bump filters out all beta tags; beta baseline = max(latest stable's patch+1, highest existing beta baseline), N increments) →release-mac(macos-latest: import certificates, sign + notarize,pnpm versionrewrite, build:mac, channel-yml validation, rclone upload to Aliyun OSSoomol-static-cn-prod/release/apps/wanta, OIDC) +release-win(self-hosted Windows x64 runner + USB certificate; do not rely on system tools like tar being present) →create-release(tag + GitHub release, stable--latest/ beta--prerelease) →refresh-cdn-cache(refreshes pointers per channel: stable refreshes all 4 latest*+beta*, beta refreshes only the 2 beta*). No linux release job. Secret names are copied from oo-desktop (MACOS_CERTIFICATE/MACOS_CERTIFICATE_PWD/APPLEID/APPLEID_PASS/APPLE_TEAM_IDetc.); do not invent your own. Release size metadata (scripts/release-size.ts, vitest-covered): release-mac and release-win each collect installer/app-bundle sizes into arelease-size-<platform>-<arch>.jsonartifact, and create-release renders them into a Downloads table in the release notes — the release hard-fails if any platform's metadata is missing or its version mismatches. - Dual-channel release discipline: the rclone upload is an include allowlist — a beta release
never touches
latest*.yml(that is the guard rail for the stable pointer); a stable release also uploads + refreshesbeta*.yml(converging beta users), unless compute-version yieldsrefresh_beta=false(this stable is below the highest existing beta baseline; skip the beta pointer to prevent regression); mac/win each have a hard channel-yml validation step (a missing file / wrong version fails loudly before upload). The generic provider hard-errors on a missing channel yml (ERR_UPDATER_CHANNEL_FILE_NOT_FOUND, no fallback), so once the beta channel opens,beta*.ymlmust always exist in both platform directories. Before the first beta, a stable with the channel-aware updater must already have shipped. The whole workflow serializes in thereleaseconcurrency group (concurrent dispatches would compute the same version and race-write OSS).
8. Special Directory Quick Reference (all gitignored, except resources/ itself)
| Directory | Role | Producer |
|---|---|---|
.oo-bin/ | oo + ripgrep (rg) binaries shared by dev and packaging | postinstall scripts/download-oo.ts + scripts/download-ripgrep.ts |
.electron-dist/ | dev-only Electron copy (wanta-local scheme) | postinstall scripts/download-electron.ts |
resources/bin/ | pre-packaging binary staging (→ extraResources) | scripts/prepare-binaries.ts |
resources/skills/ | bundled oo skills export (→ extraResources; re-ensured by predev and prepare-binaries) | postinstall scripts/download-skills.ts (scripts/skills.ts) |
resources/agent-tool-runtime/ | self-contained runtime for custom tools (→ extraResources) | scripts/build-agent-tool-runtime.ts |
.wanta-dev/ | manual smoke / experiment scripts, outside every toolchain | handwritten |
wanta/ | dev Electron userData for this checkout | pnpm run dev / pnpm run dev:worktree |
dist/ dist-electron/ | vite build output (renderer / main+preload) | pnpm run build |
release/ | electron-builder output | pnpm run build:* |
9. Upgrade Notes
- Upgrading oo: change only
OO_CLI_VERSIONinscripts/oo-cli.ts(the.versionmarker triggers a re-download). The binary inside oo's upstream tarball has no +x bit; any path that uses that binary from node_modules directly must chmod it itself — do not fall back to the package-dependency approach. - Upgrading ripgrep: change only
RIPGREP_VERSIONinscripts/ripgrep.ts(pinned, currently14.1.1); the download scripts pick it up in postinstall/predev. Rememberrgships inresources/binand is inside the macOS signing/notarization scope. - Upgrading OpenCode: bump
opencode-ai/@opencode-ai/sdk/@opencode-ai/plugintogether at the same version, and run the.wanta-dev/smoke scripts first (upstream makes no API stability promise). opencode-aimust stay in devDependencies (build-time only, so prepare-binaries can take the binary; runtime uses the extraResources copy); putting it in dependencies would duplicate ~100MB of platform binaries into app.asar — do not "fix" this when tidying dependencies.- Upgrading vendored ai-elements: compare against
.claude/skills/ai-elements/references/andskills-lock.json; note this repo carries a trimmed version (see key-decisions.md §8).