Contributing

June 25, 2026 · View on GitHub

← Docs index

Thanks for contributing to Fusion.

Development Setup

Prerequisites

  • Node.js (current LTS recommended)
  • pnpm (packageManager is pnpm)
  • Git
  • pi runtime/auth configured for AI features

Install dependencies

pnpm install --frozen-lockfile

pnpm build-scripts approval policy

pnpm v10 blocks dependency preinstall/install/postinstall scripts by default and reports any uncategorized packages in the install output.

When a package is flagged:

  • Approve it only when this repo genuinely needs that dependency's build script for supported runtime/build paths.
  • Ignore it when prebuilt artifacts, optional-native fallbacks, or release-only tooling make the script unnecessary for normal workspace verification.
  • Record every reviewed package in exactly one bucket: onlyBuiltDependencies or ignoredBuiltDependencies.

Fusion currently keeps the reviewed decision set documented in the root package.json pnpm block and mirrored in pnpm-workspace.yaml, which is the effective pnpm v10.33 install-time config read by pnpm install.

The guard test node --test scripts/__tests__/pnpm-build-scripts-config.test.mjs (also covered by pnpm test:scripts) enforces that the reviewed dependencies stay categorized, deduped, and non-overlapping so the ignored-build-scripts warning cannot silently return.

Build workspace packages

pnpm build      # default build (excludes desktop/mobile)
pnpm build:all  # full recursive build including desktop/mobile

Git LFS (demo reel GIFs)

The demo reel GIFs in demo/assets/*.gif are stored via Git LFS so the binaries live outside the packfile and re-shot captures stop bloating .git (see .gitattributes).

Install Git LFS before cloning or pulling, otherwise these files arrive as small text pointer stubs instead of real GIFs:

# macOS: brew install git-lfs   (or your platform's package manager)
git lfs install        # one-time, per machine

If you already cloned without LFS, run git lfs install then git lfs pull to hydrate the GIFs. New GIFs added under demo/assets/ are routed to LFS automatically by the .gitattributes filter — no extra steps.

Workspace Package Overview

PackagePurpose
@fusion/coreShared domain types, stores, persistence, and core utilities
@fusion/dashboardExpress API + React UI (including dashboard TUI in CLI)
@fusion/engineScheduling, planning, execution, merge orchestration
@fusion/desktopElectron shell around Fusion dashboard/client
@fusion/mobileCapacitor + PWA mobile packaging
@fusion/plugin-sdkPlugin SDK for building Fusion extensions
@runfusion/fusionPublished CLI + pi extension (includes merged TUI)

Development Workflow

pnpm local             # fast local dashboard/API + AI engine startup on a safe localhost port
pnpm local --no-engine # fast local dashboard/API-only startup
pnpm local --prebuild <none|client|full>  # local dashboard/API + AI engine startup with an explicit prebuild level
pnpm dev               # source-mode CLI; dashboard gets a client-only prebuild, other commands skip it
FUSION_DEV_PREBUILD=full pnpm dev dashboard  # production-like full workspace prebuild
pnpm dev:ui            # dashboard dev server only
pnpm dev:hmr           # dashboard API + Vite HMR UI, with no startup prebuild
pnpm lint              # lint all packages
pnpm test              # merge-gate suite + changed-only affected tests (bounded; never full-suite)
pnpm test:gate         # the merge gate: curated engine-core suite + CI-shape test
pnpm smoke:boot        # boot smoke: CLI --help + real serve /api/health
pnpm test:full         # full workspace suite (explicit opt-in; clean-worktree compatible)
pnpm build             # workspace builds (excludes desktop/mobile)
pnpm build:all         # full workspace build (includes desktop/mobile)
pnpm verify:workspace  # deep opt-in verification: lint -> test:full -> build
pnpm typecheck         # workspace typechecks

Deterministic workspace verification bootstrap

Fusion codifies workspace verification as a deterministic contract:

  • Use pnpm install --frozen-lockfile for clean bootstrap and dependency repair paths.
  • pnpm test:full must be runnable in a clean worktree without requiring a prior pnpm build.
  • Root test entrypoints (pnpm test via scripts/test-changed.mjs and pnpm test:ci:shard via scripts/ci-test-shard.mjs) call scripts/ensure-test-artifacts.mjs, which deterministically builds only missing/stale required workspace dist artifacts (@fusion/core, @fusion/dashboard, @fusion/engine, @fusion/plugin-sdk, and @fusion-plugin-examples/{dependency-graph,hermes-runtime,openclaw-runtime,paperclip-runtime}).
  • Package-scoped hooks now mirror this bootstrap for fresh worktrees where needed: @fusion/dashboard and @fusion-plugin-examples/dependency-graph run pretest: node ../../scripts/ensure-test-artifacts.mjs.
  • This includes clean states where those required dist directories are absent.
  • pnpm test:gate is the merge gate: the curated engine-core suite plus the CI-shape test. CI blocks PRs on exactly Lint, Typecheck, Build, and Gate (boot smoke + pnpm test:gate) — see .github/workflows/pr-checks.yml and docs/testing.md.
  • pnpm verify:workspace is the deep opt-in verification (not the merge gate) and runs in strict order:
    1. pnpm lint
    2. pnpm test:full
    3. pnpm build

GitHub Actions runs deterministic test sharding via pnpm test:ci:shard --shard <index> --total <count> in the non-blocking full-suite.yml workflow (push to main only — never a PR gate), while keeping local semantics unchanged:

  • pnpm test remains gate + changed-only local iteration.
  • pnpm test:full remains the explicit full workspace suite; dashboard exhaustive coverage is explicit via pnpm --filter @fusion/dashboard test:deep.
  • pnpm verify:workspace remains the deep opt-in lint -> test -> build verification.

test:ci:shard is a CI-focused entrypoint (scripts/ci-test-shard.mjs) that deterministically balances workspace packages with test scripts by counting package-local **/__tests__/**/*.test.{ts,tsx,mjs} files, auto-splitting oversized packages into virtual shard entries ({ name, shardIndex, shardCount }), then assigning entries in descending weight order with best-fit placement for unsplit entries (closest under-budget fit, otherwise minimum overshoot) while keeping slices of the same package on different shards when possible. Whole entries run as grouped pnpm --filter <pkg> test calls, and virtual entries run one-by-one via pnpm --filter <pkg> test --shard <index>/<count> (no bare --, because Vitest's cac parser would otherwise treat the shard flag as a filter separator). This keeps coverage reproducible while improving shard balance.

pnpm test now uses a changed-only entrypoint (scripts/test-changed.mjs) for faster local iteration. It resolves the comparison base from .changeset/config.json (baseBranch) and runs only affected workspaces from pnpm-workspace.yaml (both packages/* and plugins/**) using safe package-first filtering (pnpm --filter <pkg> test). It runs the merge-gate suite first, then the affected set. The full suite runs only on explicit opt-in (--full / pnpm test:full); shared-infrastructure changes and unresolvable diffs widen the affected set but never escalate to an implicit full-suite run (the old escalation was the local OOM path).

Root test entrypoints (pnpm test, pnpm test:full, and pnpm test:ci:shard) now use a shared CPU-aware default worker budget instead of fixed low values. By default, Fusion sets FUSION_TEST_TOTAL_WORKERS to max(4, min(12, cpuCount - 1)) and FUSION_TEST_CONCURRENCY to 2 (clamped to the total budget), while still honoring explicit overrides from VITEST_MAX_WORKERS, FUSION_TEST_TOTAL_WORKERS, and FUSION_TEST_CONCURRENCY.

Test isolation contract (required)

Fusion tests must run against disposable test data, never live local state:

  • The canonical Vitest bootstrap is packages/core/src/__test-utils__/vitest-setup.ts.
  • Workspace/package Vitest configs should use package-local src/__tests__/setup-test-isolation.ts shims that call into the shared core bootstrap rather than re-implementing HOME/cwd isolation.
  • Test runs must use temp HOME and temp workspace/project roots so global settings resolve under temporary directories instead of real ~/.fusion.
  • The repository .fusion directory is treated as protected live data; root test entrypoints run scripts/check-test-isolation.mjs to fail if tests mutate protected Fusion data paths.
  • pnpm test (scripts/test-changed.mjs) now creates a disposable temp HOME/USERPROFILE for the entire run (including cache-hit no-op guard checks), so concurrent writes from an active local Fusion session to your real ~/.fusion do not trigger false positives.

If you add or change test entrypoints, keep this isolation guard path intact and ensure guard + test execution share the same disposable HOME so changed/full/cached paths stay consistent.

Spec authoring: provenance evidence for outside tooling

Any task that wires in an outside command-line program, daemon, separately-fetched program, or package-managed dependency must include provenance evidence in its PROMPT.md. The deterministic spec-validation gate (detectExternalIntegrationEvidenceGaps) REVISEs specs that mention this kind of outside tooling without enough provenance to audit where it comes from and what command or artifact is expected.

Use a dedicated ## External Integration Evidence or ## External-Integration Evidence section when possible. The gate accepts semantically labeled bullets; labels may include or omit a trailing URL/name, may use / or : separators (for example Docs / homepage URL: or Docs/homepage:), and URLs may be bare or backtick-wrapped.

Include all five evidence fields:

  1. Canonical upstream repo URL — a GitHub URL with distinct owner/repo; duplicate owner/owner placeholders are rejected.
  2. Docs / homepage URL — a distinct non-GitHub, non-artifact URL.
  3. Release / download URL — a GitHub …/releases/… URL, a generic …download… URL, an npm registry.npmjs.org/<pkg>/-/<name>-<ver>.tgz URL, or any .tgz/.tar.gz artifact URL.
  4. Binary / CLI name — the command name in backticks, such as `wt`.
  5. Checksum — a sha256/sha512 digest, a pinned-manifest token, or the literal upstream-pending-verification marker. The marker is accepted for the checksum field only; never use it in place of source, docs, or artifact URLs.

Never fabricate source URLs, command names, release locations, or checksums. Cite real provenance, or use upstream-pending-verification only for the checksum field while the digest is being pinned.

## External Integration Evidence

- Canonical upstream repo URL: https://github.com/max-sixty/worktrunk
- Docs / homepage URL: https://worktrunk.dev/
- Release / download URL: https://github.com/max-sixty/worktrunk/releases/latest/download/wt-linux-x64.tar.gz
- Binary / CLI name: `wt`
- Checksum: `sha256-<digest>` (or `upstream-pending-verification` until the checksum is pinned)

Quality Gate Checklist

Before submitting changes, verify:

  • pnpm test:gate — the merge gate (curated engine-core suite + CI-shape test)
  • pnpm verify:workspace — deep opt-in lint → test:full → build verification
  • pnpm typecheck — type checking passes

Realtime/SSE change note

If your change touches dashboard realtime behavior (/api/events, SSE hooks, proxy event streams, or dedicated stream endpoints), review and update the canonical contract doc:

Do not create parallel SSE architecture docs. Keep ownership/scoping/cleanup guidance centralized there.

Testing Requirements

Use real test runs (not manual verification substitutes):

pnpm test
pnpm test:coverage
pnpm test:coverage:core
pnpm test:coverage:engine
pnpm test:coverage:cli
pnpm test:coverage:dashboard

Test File Organization

All test files live in __tests__/ subdirectories alongside the code they test:

  • Test for src/foo.tssrc/__tests__/foo.test.ts
  • Test for app/components/Bar.tsxapp/components/__tests__/Bar.test.tsx

When adding new tests, follow this convention. The monorepo has been standardized on __tests__/ organization.

Build Standalone Executables

Fusion supports standalone binary builds through Bun compile scripts in the CLI package.

pnpm build:exe      # build host-target executable
pnpm build:exe:all  # build multi-target executables

CLI Integration Test Lanes

Default workspace verification stays lean and deterministic:

  • pnpm test runs the standard suite and does not require Bun cross-build integration tests.
  • pnpm verify:workspace remains the deep opt-in lint -> test -> build verification.

Slow/pre-release CLI coverage is explicit and opt-in:

pnpm test:slow-cli                                  # workspace entrypoint
pnpm --filter @runfusion/fusion test:slow-cli       # agent-export integration (FUSION_TEST_SLOW_CLI=1)
pnpm --filter @runfusion/fusion test:build-exe      # native binary cross-build integration (FUSION_TEST_BUILD_EXE=1)
pnpm --filter @runfusion/fusion test:pre-release    # combined CLI slow lane (slow-cli + build-exe)

Additional audited suite:

pnpm --filter @runfusion/fusion test:extension-integration

test:extension-integration enables FUSION_TEST_EXTENSION_INTEGRATION=1 and runs the full fn pi extension integration suite. It remains an explicit opt-in lane so default workspace verification stays fast, while still providing a discoverable command for full extension-tool integration coverage.

Dashboard Test Lanes

Dashboard tests are split into explicit local lanes. The default dashboard package gate is a curated quality gate that keeps representative app/API coverage without running every exhaustive modal, view, and route permutation on every local or PR pass:

pnpm --filter @fusion/dashboard test                # curated app/API quality gate
pnpm --filter @fusion/dashboard test:deep           # exhaustive app + API suite
pnpm --filter @fusion/dashboard test:app            # exhaustive React/jsdom app tests
pnpm --filter @fusion/dashboard test:api            # exhaustive Node API/server tests
pnpm --filter @fusion/dashboard test:browser-smoke  # local browser layout smoke
pnpm --filter @fusion/dashboard test:build          # built client output contract

Use the default lane for normal local iteration before PRs. Run test:deep when changing broad dashboard architecture, shared modal/view infrastructure, or route registration behavior where the exhaustive permutations are still useful. The built-client contract remains a separate lane because it performs its own production build. pnpm build remains an explicit PR gate, and PR test shards avoid a redundant pre-test workspace build to save GitHub Actions minutes.

Release Process

Fusion uses Changesets + version PR workflow.

  • See RELEASING.md for release flow details.
  • For published package behavior changes, include a changeset using the structured body format (summary, category, optional dev fields). See the changeset format guide in .changeset/README.md.

Code Signing

Release binary signing setup is documented here:

Git / Commit Conventions

Use task-ID-scoped conventional commits:

  • feat(FN-XXX): ...
  • fix(FN-XXX): ...
  • test(FN-XXX): ...
  • docs(FN-XXX): ... (for documentation-only changes)

Project Memory

When enabled, Fusion uses OpenClaw-style memory files:

  • .fusion/memory/MEMORY.md — long-term project memory
  • .fusion/memory/YYYY-MM-DD.md — daily running notes
  • .fusion/memory/DREAMS.md — dream-processing memory file
  • The legacy top-level memory file is a deprecated migration fallback (seed/alias behavior) and should not be treated as canonical

Use project memory for reusable patterns, constraints, and pitfalls that should persist across tasks.

Background Memory Summarization

Fusion can automatically extract insights from memory and prune transient content. Enable via insightExtractionEnabled setting:

  • .fusion/memory/MEMORY.md — Canonical long-term memory source (inside the layered .fusion/memory/ workspace) compacted/pruned by extraction jobs
  • .fusion/memory/memory-insights.md — Distilled insights output
  • .fusion/memory/memory-audit.md — Audit report after each extraction (includes pruning outcome)

See Settings Reference for configuration details.

Localization (i18n)

User-facing strings are localized with react-i18next across both the dashboard and the terminal UI. English is the source-of-truth; catalogs and shared config live in the @fusion/i18n package. To translate, add a language, or wire new strings, see the Localization contributor guide. When adding user-facing copy, prefer t("namespace:key", "English default") over a hardcoded string and run pnpm i18n:extract to register the key.

Dashboard CSS Organization

The dashboard's CSS has been modularized:

  • Global stylesheet (packages/dashboard/app/styles.css, ~4,500 lines)
    • Design tokens, primitives (.btn, .card, .modal, .form-input), global cross-component rules
  • Per-component stylesheets (56+ files in packages/dashboard/app/components/)
    • Each component needing CSS has a co-located ComponentName.css
    • Each ComponentName.tsx must import: import "./ComponentName.css";

Rule: New component CSS goes in the component's .css file, not in styles.css. Only truly global rules belong in the root stylesheet.

Icon-only button sizing contract

Dashboard icon-only controls should use the shared .btn-icon contract instead of ad-hoc Lucide size={12} / size={14} props:

  • .btn-icon defaults icon glyphs to --icon-size-md via --btn-icon-size
  • compact icon-only buttons (.btn-icon.btn-sm, .btn.btn-icon.btn--sm) automatically use --icon-size-sm
  • keep intentionally smaller directional affordances (for example split-button chevrons or non-button inline indicators) as explicit local exceptions in component CSS/JSX

Use component-local overrides only when a surface has a deliberate visual exception; ordinary icon-only actions should inherit the shared contract.

Todo/list action-row pattern

For dense list rows (for example TodoView items), keep action buttons in a dedicated second row instead of cramming controls beside the primary text line:

  • define row-action layout in the component-local stylesheet (for example TodoView.css), not styles.css
  • use spacing/layout tokens (--space-*, --radius-*, --transition-*) instead of literal spacing values
  • if row actions are hover-revealed on desktop, include a required mobile override under @media (max-width: 768px) that forces visibility (opacity: 1) so touch devices can always access controls

CSS Testing

For CSS regression tests, use the helper at packages/dashboard/app/test/cssFixture.ts:

import { loadAllAppCss, loadAllAppCssBaseOnly } from "../test/cssFixture";

// Load all CSS (styles.css + all component .css)
const allCss = await loadAllAppCss();

// Load base rules only (strips @media/@supports)
const baseOnly = await loadAllAppCssBaseOnly();

Never directly readFileSync('../styles.css') — the ESLint rule no-restricted-syntax in eslint.config.mjs blocks this in test files and directs you to cssFixture.ts.

SQLite Test Runner Pitfall

When running engine tests with Vitest and node:sqlite, ensure the engine Vitest config uses thread pool mode:

  • pool: "threads"
  • pool: "vmThreads"

node:sqlite fails under Vitest VM contexts; using threads avoids that failure mode.