Contributing and Quality Gates
September 6, 2026 · View on GitHub
This guide explains how to install, develop, test, and run the local quality gates before opening a PR. The goal is to help maintainers and contributors answer one question before review: did this change break the core Coding Agent workflow?
Setup
Install root dependencies with Bun:
bun install
If your change touches desktop/, also install desktop dependencies:
cd desktop
bun install
If your change touches adapters/, or if you run check:adapters / check:native, install adapter dependencies:
cd adapters
bun install
Do not commit local artifacts such as artifacts/quality-runs/, node_modules/, or desktop/node_modules/.
Gate Tiers
| Tier | Trigger | What runs | Constraint |
|---|---|---|---|
| Local | manual | The narrowest relevant tests, then whatever bun run check:impact selects | seconds |
| PR (required) | pull_request | The deterministic lanes the impact report selects, including check:agent-flow | no model, no provider, no secret, runs on an untrusted fork |
| Full sweep | Maintainer-triggered (workflow_dispatch) | Every deterministic lane with no path selection, plus module-graph health and check:desktop-ui-smoke | still no model, no secret |
| Release | maintainer-run bun run quality:release (not release-desktop.yml) | Everything above, plus native/packaging smoke and maintainer-authorized live provider baselines | live models only here, only with explicit authorization |
Note: release-desktop.yml deliberately runs no quality gate — tagging must not be blocked by bun run verify, and scripts/pr/release-workflow.test.ts guards that decision. Release-time evidence therefore comes from the PRs that were merged, plus whatever full sweeps the maintainer ran, plus the manual quality:release. The full sweep is deliberately not scheduled: spending ~90 minutes of CI is a decision, not a default, and pr-quality-workflow.test.ts fails if a schedule: is added back.
The split follows from what each tier can prove. A per-PR gate only ever covers what the diff reaches, so it is structurally blind to checks no recent PR selected and to failures that only appear when the whole suite runs together — the full sweep closes both, when the maintainer asks for it. Live model quota is spent only at release time, so every contributor can pass the required gate with no provider at all.
Path-Aware PR Checks
First ask the repository which deterministic checks match the changed paths:
bun run check:impact
Selection is import-aware. Besides the changed paths themselves, the router adds every surface that imports a changed file (scripts/pr/module-graph.ts). This closes holes that prefix-only routing could not see: editing src/shared/modelReasoning.ts now selects check:desktop because desktop/src/lib/runtimeSelection.ts imports it, and editing desktop/src/lib/browserSafePort.ts now selects check:native because desktop/electron/services/sidecarManager.ts imports it while desktop/tsconfig.json does not compile desktop/electron/. The report's ## Cross-surface impact section names the importer behind each extra check.
The graph only widens check selection. Areas, labels, and every blocking rule stay scoped to the actual diff, so editing a hub file never demands tests for files you did not touch. If the graph cannot be built, the run selects every surface and says so rather than silently reverting to prefix routing.
Deterministic Agent Gate (no model required)
bun run check:agent-flow # real server + real WebSocket + mock CLI
bun run check:desktop-ui-smoke # real desktop UI + real permission dialog + mock CLI
Neither needs a provider, credentials, or the public network. check:agent-flow covers session creation, runtime selection, first-turn streaming, tool execution, permission allow/deny, tool failure, API error, interrupt, reconnect permission replay, and session recovery. check:desktop-ui-smoke clicks the real Allow button in a real browser; it needs agent-browser and installed desktop dependencies and skips with a printed reason when either is missing.
agent-browser belongs to that committed lane (which runs headless on Linux CI) and to the maintainer-run desktop/scripts/e2e-*-agent-browser.sh scripts. For ad-hoc browser work (manual verification, screenshots, exploratory UI checks), use the ego-browser skill instead; do not treat agent-browser as a general-purpose browser tool just because it appears in the repository.
Every quality-gate lane that boots the real server runs against a sandbox config dir (scripts/quality-gate/sandbox.ts) and fails if it wrote to the developer's real ~/.claude.
Run the selected focused commands while developing. For PR-ready or full validation, use the unified entrypoint directly without first running all of its lanes separately:
bun run verify
bun run verify is equivalent to bun run quality:pr. It runs the selected policy, desktop, server, adapter, native, provider contract, chat contract, persistence, docs, and coverage lanes, without calling real models. Small external contributions do not need to run unrelated modules locally; GitHub CI runs the exact path-aware gate again.
The main quality report embeds the current test scope, result matrix, coverage summary, and links to the full coverage/JUnit/log artifacts:
artifacts/quality-runs/<timestamp>/report.md
artifacts/quality-runs/<timestamp>/report.json
artifacts/quality-runs/<timestamp>/junit.xml
artifacts/quality-runs/<timestamp>/logs/*.log
artifacts/coverage/<timestamp>/coverage-report.md
artifacts/coverage/<timestamp>/coverage-report.json
Include the commands you ran and the report summary in your PR description. quality:pr / quality:verify remain available for contributors who prefer explicit quality command names, but docs and AI prompts should prefer bun run verify.
The coverage gate does four things: measures source-only coverage, enforces the baseline ratchet, reports target gaps against 75-80%+ maintained-area goals, and enforces changed-line coverage for new or modified executable production lines. The current baseline lives in scripts/quality-gate/coverage-baseline.json, and CI compares against the base branch baseline when available. New PRs must not lower coverage beyond the allowed window. Changes to coverage-baseline.json or coverage-thresholds.json require the maintainer-only allow-coverage-baseline-change label. Quarantine is reserved for maintainer baseline/release tracking and must never hide deterministic provider/chat contract tests; the normal PR gate does not depend on quarantine to pass.
AI Coding Agent Fix Loop
Completion means implementing the intended behavior, running the checks required for the current diff, and fixing failures caused by the change. Scoped local edits, isolated fixture checks, and related repairs do not need approval at each step. Commits, pushes, releases, repository settings, and live-model quota still follow the root AGENTS.md authorization boundaries.
Use bun run check:impact to determine the check scope. Run the selected checks for ordinary tasks; use bun run verify directly for PR-ready/full validation without first running all of its lanes separately. During repairs, rerun affected focused checks, then complete the evidence needed for the final diff. Do not repeat passing checks without subsequent edits or unresolved risks. Report unrelated existing failures or environment blockers instead of expanding the change merely to make everything green.
When a check fails, consult the evidence for that failure:
| Failure | Evidence and action |
|---|---|
| Failed lane | Summary / Result Matrix in artifacts/quality-runs/<timestamp>/report.md and logs/<lane>.log |
| Path-aware PR checks | Check same-area tests, CLI core, and coverage policy; maintainer overrides require an explicit decision |
| Coverage gate | artifacts/coverage/<timestamp>/coverage-report.md or .json; address changedLines.failures / failures, while targetGaps signal technical debt |
| Build, types, lint, docs, or native | Fix issues caused by the change identified in the relevant log and rerun affected checks |
Claim PR-ready/full validation only after bun run verify passes for the final diff. Do not lower coverage baselines/thresholds or rewrite test expectations to hide failures.
Regression Test Design
A same-area test file is the gate's minimum signal; tests also need to prove behavior:
- Drive state transitions. When testing a transition, produce the state through
handleServerMessage, real store actions, or user events instead of directly assigning the expected result withsetState. Direct state setup is still appropriate for fixture initialization. - Assert behavioral invariants. Check which session or model the displayed data belongs to, rather than copying today's screen text. Test inputs and expectations must follow the intended behavior contract; do not change them to hide failures.
- Cover both dropping and keeping. Test what a deduplication, merging, or filtering rule should discard and retain. Message deduplication in particular must reject replays and preserve legitimate repeats; forward upstream identities such as
uuid/toolUseIdinstead of guessing identity from text. - Test the connections across boundaries. Separate green server, store, and component tests do not prove that messages drive the UI. Exercise risky connections through real entry points and do not mock the module under test.
Coverage reports have limits: desktop/vitest.config.ts collects only src/**, excluding the Electron main process. The repository's current Bun coverage baseline has zero branch records, and coverage.ts displays 0/0 as 100%; that does not prove all branches were exercised. Inspect current configuration and reports instead of using historical coverage figures as evidence for a new change.
Coverage References
External reference points:
- Google Testing Blog: 60% acceptable, 75% commendable, 90% exemplary; 90% is a reasonable lower threshold for changed/per-commit coverage.
- Microsoft Visual Studio / Azure DevOps docs: teams typically target about 80%, typical project requirements can be 75%, and generated code may be relaxed.
- ChromiumOS EC: new or changed lines require at least 80% coverage.
Maintaining Agent Instructions
Keep project constraints and entry points in root AGENTS.md, specialized rules near the code, and explanations/examples in on-demand documentation. Shared guidance must work for contributors using different models. Revisit duplicated workflows and broad stopping conditions as capabilities change, while preserving current safety and CI contracts. This cleanup draws on Eric Provencher's Rethinking skills and prompts for GPT-6 Astra (2026-09-04).
Repository skill descriptions should identify the applicable task and necessary distinctions; put operational detail in the body or referenced files. Use a short router for multiple workflows and avoid broadening triggers just to match more keywords. Model defaults, tool formats, and compaction behavior describe product implementation, so check the source before updating those docs.
Feature Quality Contract
Every feature, bugfix, and behavior change must ship with verifiable evidence. This rule applies to human authors and AI coding agents:
- Name the changed surface first:
desktop,server,adapter,native,docs,provider/runtime,agent-loop, orrelease. - Executable JS/TS production changes must include same-area tests in the same PR.
scripts/pr/change-policy.tschecks four areas separately:desktop/src/,src/server/, the rest ofsrc/, andadapters/, unless a maintainer explicitly appliesallow-missing-tests. Non-executable files such as prose or CSS do not independently require new tests under this rule; all impact-selected checks still apply. - Pure logic needs unit tests. Server/API/provider/runtime behavior needs API or request-shape tests. Desktop UI/store/API behavior needs Vitest or Testing Library coverage. Cross-boundary user flows through UI, WebSocket, provider proxying, native sidecars, or release packaging need E2E or desktop UI smoke.
- Agent loop, tool execution, provider routing, model selection, file editing, permissions, session resume, and desktop chat changes need mock/fixture tests in PR. Run live smoke or baseline only after deterministic checks pass and a maintainer explicitly authorizes quota use. Finding a local provider is not authorization; report when live checks were not run.
- Coverage is part of the feature. This project follows a Google/Microsoft-style policy: generated/build output is not counted as product coverage, maintained product areas should move toward 75-80%+, and new or changed executable production lines must pass the changed-line coverage threshold in
coverage-thresholds.json. - Do not lower
coverage-baseline.jsonorcoverage-thresholds.jsonjust to pass the gate; real baseline/threshold changes requireallow-coverage-baseline-changeand a reason. Legacy low-coverage areas are debt; new PRs must leave touched areas better than they found them. - The PR description must record changed files, tests added, coverage report path, E2E/live report path or blocker, and remaining risk.
Local Pre-Push Reminder
push no longer runs a local quality gate. Run checks manually when needed:
bun run quality:push
bun run quality:push reuses the PR gate impact, policy, and path-aware checks, but skips the expensive coverage lane by default; full coverage remains in bun run verify, bun run quality:pr, and CI.
You can still install the local pre-push hook, but it only prints a non-blocking reminder and never blocks git push:
bun run hooks:install
Maintainers with a trusted repository environment and model quota can run real provider smoke and desktop agent-browser smoke manually:
bun run quality:providers
bun run quality:smoke -- --provider-model minimax:main:minimax-main
To run the full live baseline, use:
bun run quality:gate --mode baseline --allow-live --provider-model minimax:main:minimax-main
PR CI Merge Gate
.github/workflows/pr-quality.yml runs for PR opened, synchronize, reopened, ready_for_review, labeled, and unlabeled events. scope-plan installs no dependencies and only produces the stable impact plan. policy-enforcement installs the frozen dependency graph independently and runs policy, so a policy failure cannot swallow product-test results. Product jobs depend only on scope-plan and select desktop, server, adapter, native, provider contract, chat contract, persistence, docs, and coverage lanes by path. The final pr-quality-gate validates every result strictly: selected jobs must succeed, unselected jobs must be skipped, and cancelled or missing results cannot be mistaken for success.
Repository settings should protect main with GitHub branch protection / rulesets and require the pr-quality-gate status check. CODEOWNERS requires maintainer review for workflows, quality policy, and high-risk provider/WebSocket boundaries. The local hook only reminds; the PR gate is what blocks low-quality merges.
Area-Specific Checks
Run the checks that match the files you changed:
bun run check:server # Server API, WebSocket, providers, sessions, and related tests
bun run check:desktop # Desktop lint, Vitest, and production build
bun run check:adapters # IM adapter tests
bun run check:native # Desktop sidecars, Electron host, and package-smoke checks
bun run check:provider-contract # Offline provider/runtime/proxy contract tests
bun run check:chat-contract # WebSocket, session, and desktop chat-store contracts
bun run check:persistence-upgrade # Persistence migrations and old-fixture compatibility
bun run check:docs # Isolated install, build, and validation for the site/ React docs
bun run check:quarantine # Maintainer baseline/release quarantine audit
bun run check:coverage # Root, desktop, and adapter coverage reports plus ratchet enforcement
Focused tests are the normal development loop. Run bun run verify locally when claiming PR-ready/full validation; hosted CI still executes every selected required lane.
Executable JS/TS production changes must include matching tests. See the Feature Quality Contract above and scripts/pr/change-policy.ts for area boundaries; missing same-area tests block the change unless a maintainer applies allow-missing-tests. Coverage baseline/threshold changes are also blocked unless a maintainer applies allow-coverage-baseline-change.
Live Model Baseline
quality:baseline runs real Coding Agent tasks: it starts the local server, creates isolated fixtures, asks a model through chat to fix code, runs tests, and saves transcripts, diffs, verification logs, and a report. It also runs provider live smoke: saved or active OpenAI-compatible providers validate connectivity, proxy conversion, and streaming proxy behavior; env-only provider smoke validates upstream connectivity and the transform pipeline.
The default baseline command does not call real models:
bun run quality:baseline
To actually call models, pass --allow-live and choose a local provider.
First list your local providers and copyable selectors:
bun run quality:providers
Example output:
Saved providers:
MiniMax
selector: minimax
main: MiniMax-M2.7-highspeed
--provider-model minimax:main:minimax-main
Copy one of the listed values:
bun run quality:gate --mode baseline --allow-live --provider-model minimax:main:minimax-main
To run only provider smoke plus desktop agent-browser smoke, use:
bun run quality:smoke --provider-model minimax:main:minimax-main
You can run multiple models in one pass:
bun run quality:gate --mode baseline --allow-live \
--provider-model codingplan:main:codingplan-main \
--provider-model minimax:main:minimax-main
Provider selectors come from the providers saved in your local Desktop Settings > Providers page. Contributors do not need the maintainer's provider UUIDs or vendor accounts. They can add their own provider locally, run bun run quality:providers, and choose their own model.
If you do not have a saved provider, you can run one unsaved provider smoke with environment variables:
QUALITY_GATE_PROVIDER_BASE_URL=https://example.com \
QUALITY_GATE_PROVIDER_API_KEY=... \
QUALITY_GATE_PROVIDER_MODEL=model-id \
QUALITY_GATE_PROVIDER_API_FORMAT=openai_chat \
bun run quality:gate --mode baseline --allow-live
When To Run The Baseline
After deterministic contract/E2E checks pass, a trusted maintainer should run the live baseline for changes touching:
- Desktop chat, session resume, WebSocket, or the CLI bridge
- Provider, model, or runtime selection
- Permissions, tool calls, file edits, and task execution
- agent-browser smoke, Computer Use, Skills, or MCP
- Release preparation or broad cross-module refactors
External PRs from forks do not receive repository secrets, and contributors are not expected to pay for model calls. Record live model: not run (untrusted fork / no provider) in the PR. A maintainer should add live evidence before merging or releasing high-risk changes; missing live evidence must not make deterministic PR lanes flaky.
Release Gate
Before a release, run release mode:
bun run quality:gate --mode release --allow-live --provider-model <selector>:main
Release mode composes PR checks, baseline catalog validation, live baseline cases, provider smoke, native checks, and current-platform canonical release package-smoke --package-kind release. Reports are written to artifacts/quality-runs/<timestamp>/. release-desktop.yml builds and publishes artifacts without running bun run verify. Pre-release quality evidence comes from PR gates and explicitly run maintainer full checks and release gates.
In release mode, live lanes are not allowed to be silently skipped. Missing providers, model quota, or external account access will fail the gate and must be recorded as a release blocker.
Releases and Auto-Update
desktop/package.json is the single source of the desktop version number. A real release requires the version, the Git tag, and release-notes/vX.Y.Z.md to match exactly.
In-app updates are driven by electron-updater, with artifacts hosted on GitHub Releases:
| Platform | Install / update target | Metadata |
|---|---|---|
| macOS arm64 / x64 | dmg for first install, zip for Squirrel.Mac updates | latest-mac.yml |
| Windows x64 / ARM64 | NSIS .exe | latest.yml |
| Linux x64 | .AppImage for updates, .deb for manual install | latest-linux.yml |
| Linux arm64 | .AppImage for updates, .deb for manual install | latest-linux-arm64.yml |
The release workflow generates latest*.yml inside each platform matrix job, renames colliding metadata to latest-<platform>.yml, and finally lets scripts/release-update-metadata.ts merge them back into the standard filenames electron-updater expects. Do not change this so each matrix job publishes the GitHub Release directly — the metadata files would overwrite each other.
Signing secrets
macOS signing and notarization depend on these GitHub Actions repository secrets:
MACOS_CERTIFICATE
MACOS_CERTIFICATE_PASSWORD
APPLE_ID
APPLE_APP_SPECIFIC_PASSWORD
APPLE_TEAM_ID
MACOS_CERTIFICATE is the base64 content of a Developer ID Application .p12. The project does not ship a .pkg, so no Developer ID Installer certificate is needed.
Windows signing is optional:
WINDOWS_CERTIFICATE
WINDOWS_CERTIFICATE_PASSWORD
Auto-update still works without Windows signing; users may just see a SmartScreen prompt.
Pre-release checks
bun run scripts/release.ts <version> --dry
bun test scripts/pr/release-workflow.test.ts scripts/release-update-metadata.test.ts scripts/quality-gate/package-smoke/index.test.ts
bun run check:policy
Confirm release-notes/v<version>.md exists before running bun run scripts/release.ts <version> for real.
Verify one real update path
Every release should be verified by upgrading from the previous stable build at least once:
- Install the previous stable release from GitHub Releases.
- Push the tag and let the
Release Desktopworkflow finish green. - Open the old build and wait for the startup check, or check for updates manually in settings.
- Confirm the new version is offered, then install and restart.
- After restart, confirm the version in About, and that providers, sessions, skills, agents, memories, custom pets, and a custom data directory all still work.
- Confirm historical attachment context, subagent details, and task state restore correctly; open a pet window and check the overlay and current-session navigation.
Platforms differ in what matters: on macOS confirm the release job used the signed artifacts and the launch-policy check passed; on Windows confirm latest.yml, .exe, and .exe.blockmap are all in the release assets, and remember that a SmartScreen prompt on an unsigned build does not mean the updater failed; on Linux verify auto-update through the AppImage, since .deb ships as a manual installer only.
PR Workflow
- Create a product branch such as
fix/session-reconnectorfeat/provider-quality-gate. - Install dependencies and make the change.
- Add tests for behavior changes.
- Run focused checks for the affected area.
- Optional: run
bun run hooks:installto show a non-blocking reminder before later pushes. - Run
bun run verifyif you are claiming PR-ready/full validation. - A trusted maintainer runs the live baseline for high-risk changes; external contributors only record why it was not run.
- In the PR description, include user impact, verification commands, coverage/quality report summary, and known risks.
FAQ
Can I run checks without a provider?
Yes. Run the impact report and its selected deterministic checks:
bun run check:impact
bun run verify also needs no real model. Only the live baseline does. Maintainers can add a provider in Desktop Settings > Providers, then run:
bun run quality:providers
What if provider selectors conflict?
If two provider names produce the same selector, quality:providers falls back to the provider ID. Copy the --provider-model ... value it prints.
What if a model ID contains a colon?
Prefer role selectors:
--provider-model custom:haiku:custom-haiku
The runner resolves haiku to the real model ID from your local provider configuration.