36 KiB
Project Guidelines
STANDING DIRECTIVE: Buttons Are Frozen (2026-05-13)
Do not file, plan, or implement tasks that adjust button mobile-responsiveness, touch-target sizing, or mobile reflow of header/action button rows anywhere in the dashboard (TaskCard, SettingsModal, ChatView, MissionManager, AgentsView, FAB, etc.). Keep buttons as they are.
This supersedes earlier guidance about mobile touch targets, primary/secondary control sizing on mobile, and .touch-target minimums for buttons. The Frontend UX Design workflow step (WS-006) is disabled and must stay disabled.
If you find yourself opening SettingsModal.css, TaskCard.css, ChatView.css, etc. inside an @media (max-width: 768px) block to touch a .btn, .modal-close, .settings-header-actions, or .card-* button — stop. Confirm with the user in chat before proceeding.
Exception: explicit named user request in chat that overrides this directive.
Finalizing Changes
When a change affects the published @runfusion/fusion package, add a changeset:
cat > .changeset/<short-description>.md << 'EOF'
---
"@runfusion/fusion": patch
---
Short description of the change.
EOF
Bump types: patch (bug fixes / internal), minor (new features / CLI / tools), major (breaking). Commit the changeset alongside the code change.
Do NOT create changesets for internal docs (AGENTS.md, README), CI config, or behavior-preserving refactors. The other workspace packages (@fusion/core, @fusion/dashboard, @fusion/engine) are private — no changesets for them.
Releasing
Always use the repo release script:
pnpm release --yes
scripts/release.mjs is the source of truth: preflight, apply changesets, update lockfile + root changelog, build, commit, publish, push main, push tag. Do not run changeset version, pnpm publish, or manual git tags as a substitute. The script also keeps @runfusion/fusion and runfusion.ai in sync.
Package Structure
@fusion/core— domain model, task store (private)@fusion/dashboard— web UI + API server (private)@fusion/engine— triage, executor, reviewer, merger, scheduler (private)@runfusion/fusion— CLI + pi extension (published to npm)
Only @runfusion/fusion is published. The others get inlined into the CLI bundle via tsup noExternal: [/^@fusion\//].
Importing across @fusion/* packages
For the inlining to work, @fusion/* imports must be statically analyzable. The following anti-pattern silently breaks the published CLI (FN-2613, Runfusion/Fusion#9):
// ❌ BROKEN: variable specifier defeats static analysis
const engineModule = "@fusion/engine";
const engine = await import(/* @vite-ignore */ engineModule);
esbuild leaves the dynamic import in the output, the package isn't installed at runtime, the catch swallows the error, and downstream calls fail with createFnAgent2 is not a function.
Rules:
- Default to static imports:
import { createFnAgent } from "@fusion/engine". - Exception:
@fusion/corecannot statically import engine (circular). Core uses DI viasetCreateFnAgentinpackages/core/src/ai-engine-loader.ts, called frompackages/engine/src/index.ts. Don't add new dynamicimport("@fusion/engine")in core — extend the loader. - Never reintroduce the
engineModule = "@fusion/engine"trick. Treat any sighting as a bug. vi.mock("@fusion/engine", …)hoists above static imports — mocking still works.
Storage Model
Hybrid: structured metadata in SQLite (.fusion/fusion.db, WAL mode), large blobs (PROMPT.md, attachments) on disk under .fusion/tasks/{ID}/. See docs/storage.md.
Multi-Project Support
Central registry at ~/.fusion/fusion-central.db; per-project DB at .fusion/fusion.db. See docs/multi-project.md for CentralCore API, isolation modes, and global concurrency.
Testing
Tests are required. Typechecks and manual verification are not substitutes for assertions.
Use the narrowest command that exercises the behavior you changed, then broaden before reporting completion.
pnpm test # changed-only workspace tests; falls back to full gate in safety contexts
pnpm test:full # full workspace quality gate
pnpm lint # lint all packages
pnpm build # build workspace packages (excludes desktop/mobile)
pnpm verify:workspace # canonical pre-merge gate: lint -> test:full -> build
pnpm test:full runs each package's default test script with capped worker fanout (FUSION_TEST_TOTAL_WORKERS=4 FUSION_TEST_CONCURRENCY=2 pnpm -r --workspace-concurrency=2 test). Do not casually raise worker counts; dashboard/jsdom and integration-heavy packages destabilize when oversubscribed. Use VITEST_MAX_WORKERS=<n> only for targeted package-level investigation.
Fresh-worktree dist bootstrap
pnpm test auto-runs scripts/ensure-test-artifacts.mjs to rebuild missing/stale dist artifacts. Dashboard and dependency-graph package lanes auto-bootstrap too. If you hit opaque Failed to resolve import "./cli-spawn.js" (or similar), treat it as bootstrap regression against FN-4605 — don't work around with a manual pnpm build.
Dashboard Test Lanes
pnpm --filter @fusion/dashboard test # curated app/API quality gate (default)
pnpm --filter @fusion/dashboard test:deep # exhaustive app + API suite
pnpm --filter @fusion/dashboard test:app # exhaustive React/jsdom
pnpm --filter @fusion/dashboard test:api # exhaustive Node API/server
pnpm --filter @fusion/dashboard test:browser-smoke # local browser CSS/layout smoke
pnpm --filter @fusion/dashboard test:build # built client output contract
Run test:deep when changing broad dashboard architecture, shared modal/view infrastructure, or route registration. Run test:browser-smoke for layout/responsive/navigation/modal/CSS changes. Run test:build for Vite output, lazy-loading, chunking, or client-dist changes.
When adding a new test file under app/components/__tests__, also add its basename to qualityAppTests in packages/dashboard/vitest.config.ts — otherwise the curated gate silently skips it.
Targeted commands
pnpm --filter @fusion/core test
pnpm --filter @fusion/engine test
pnpm --filter @runfusion/fusion test
pnpm test:scripts
node --test scripts/__tests__/*.test.mjs
For a single Vitest file, use package-local exec vitest:
pnpm --filter @fusion/core exec vitest run src/__tests__/central-db.test.ts --silent=passed-only --reporter=dot
Engine test helper convention
packages/engine/src/__tests__/executor-test-helpers.ts defaults both isUsableTaskWorktree to true and classifyTaskWorktree to { ok: true } via a helper-level worktree-pool mock. To test failure paths, override with vi.spyOn(worktreePool, "classifyTaskWorktree").mockResolvedValueOnce({ ok: false, classification: "unregistered", reason: "..." }) (or isUsableTaskWorktree for legacy call sites). Production liveness assertions in executor.ts are unchanged.
Before Reporting Done
- Code changes: affected package tests + any directly relevant browser/build lane.
- Cross-package, shared test infrastructure, or CI changes:
pnpm test:full. - Production/bundling-sensitive changes:
pnpm build. - Substantial work:
pnpm verify:workspace. - If you skip a relevant lane, say why.
Test File Organization
Test for src/foo.ts → src/__tests__/foo.test.ts. Test for app/components/Bar.tsx → app/components/__tests__/Bar.test.tsx. __tests__/ is the standard.
What NOT to write
Tests should cover behavior a user could notice break, not implementation shape. Don't write:
- CSS-class permutation tests — use one
it.eachfor the boolean matrix, not oneitper combination. - Field-presence tests when a payload-roundtrip test already exercises the same field.
- React.memo tautologies — testing
React.memotests React, not us. Test custom comparators directly, one case. - Mock-the-world wiring tests — if a test mocks 8+ deps just to render a component, shim children with
() => nullor delete and rely on an integration test one level up. - Structural CSS assertions — "tab uses .class-name not inline style". Consolidate into one aggregate layout-contract test per component.
Prefer it.each over copy-pasted it() blocks. When trimming, keep: first case + opposite case + any precedence/override case.
What TO keep unconditionally
- Tests linked to an FN-ticket in describe/it names — these guard real regressions.
- Integration tests exercising real SQLite, real worker pool, or spawned processes.
- Lean core/engine unit tests with low mock burden.
Port 4040 is Reserved
Port 4040 is the production dashboard port. A user's live session is typically running there. Agents must NEVER:
- Run
kill,kill -9,pkill, orkillallagainst processes on port 4040. - Start a test server on port 4040 — always use
--port 0for a random free port.
Architecture invariants
Detailed mechanism logs live in docs/architecture.md and docs/design/. The contracts agents must respect:
- Orphan
fusion/*branches: prune-or-rescue, never force-delete. Subsumed branches pruned; unique-commit branches rescued into triage tasks. - Stale active branches: self-healing's
reclaim-stale-active-branchesstage prunes afusion/<task-id>branch with zero unique commits when no usable worktree mapping exists, then clearstask.branch/task.worktree/task.baseCommitSha. It must defer reclaim (emitbranch:stale-active-reclaim-deferred) when the task worktree is inactiveSessionRegistry, whenexecutionStartedAtis withinSTALE_ACTIVE_BRANCH_EXECUTION_GRACE_MS(10 minutes), or when the mapped worktree has uncommitted changes. - Worktree metadata reconcile ordering (FN-4962):
reconcile-task-worktree-metadatamust run beforereclaim-stale-active-branches; staletask.worktreemetadata is rebound to livefusion/<task-id>worktrees when present (task:auto-recover-worktree-metadata-rebound) or cleared (task:auto-recover-worktree-metadata-cleared) when absent. - Completion fan-out is synchronous:
SelfHealingManager.reconcileCompletedTask()runs onin-review → done. Downstream staleblockedBylinks and residualfusion/<task-id>branch/worktree artifacts are reconciled immediately, not on a periodic sweep. - In-review stall deadlock: identical stalls (same code + reason) repeated past
inReviewStallDeadlockThreshold(default 3) auto-pause withpausedReason: "in-review-stall-deadlock"andstatus: "failed". - Restart recovery:
RestartRecoveryCoordinatorclassifies interruptedin-progressruns. Unusable-worktree session-start failures (missing,incomplete,unregistered git worktree) are recoverable; retries are capped atMAX_WORKTREE_SESSION_RETRIES=3before escalating. - Executor pre-session liveness gate (FN-4935): the gate now skips for fresh acquisitions (
acquisition.source === "fresh"), emits structurednot_usable_task_worktree:<classification>diagnostics (including canonicalized registered-path snapshots) and aworktree:incomplete-detectedaudit event withsource: "executor-liveness-gate", while preserving the existingtaskDoneRetryCount/MAX_TASK_DONE_REQUEUE_RETRIESrequeue contract. FN-4651worktreeSessionRetryCountremains scoped to the in-review/session-start recovery path. - Stale self-owned active-session reconcile on conflict cleanup (FN-4973): when executor worktree-conflict cleanup finds only a same-task stale
activeSessionRegistryentry and no live in-memoryactiveWorktreesbinding for that task/path, it must unregister the stale entry beforeremoveWorktree(plus one-shot backstop reconcile on same-taskActiveSessionWorktreeRemovalErrorraces). Foreign-task entries remain protected by FN-4811 and must never be reconciled by the requesting task. - Task title/ID drift (FN-4898): active and archived title writes normalize foreign embedded
FN-NNNtokens viapackages/core/src/task-title-id-drift.ts. Lineage is preserved insourceParentTaskId/ description markers, not title embeds. - PR-conflict reclaim wiring (FN-4763): GitHub PR refresh now persists normalized
prInfo.mergeableconflict state and, when conflicting, funnels tasks into self-healing’s existing reclaim machinery (reclaimPrConflictForTask/reclaim-pr-conflictsstage) so branch-conflict handling stays centralized with existinginspectBranchConflictoutcomes and unrecoverable pause semantics. - Worktrunk-managed lifecycles: when
worktrunk.enabled, self-healing defers prune/idle/worktree-cap sweeps to the worktrunk backend; branch-level reclaim and orphan rescue stay native. - Post-finalize verification no-op (FN-4944): when auto-merge receives a delayed
VerificationErrorafter a task is alreadydonewithmergeDetails.mergeConfirmed === true(already-on-main fast-path), it must log one[verification] ... no actiondiagnostic and must not bounce the task back toin-progress/merging-fix.
Engine Process Rules
The engine runs the executor, merger, scheduler, IPC host, and dashboard activity loop on a single Node event loop. Blocking that loop stalls every task in-flight.
Never use execSync for User-Configured Commands
Any command from project settings — testCommand, buildCommand, workflow step scripts — must run via promisify(exec) with a timeout:
import { exec } from "node:child_process";
import { promisify } from "node:util";
const execAsync = promisify(exec);
const { stdout, stderr } = await execAsync(command, {
cwd: worktreePath,
timeout: 120_000,
maxBuffer: 10 * 1024 * 1024,
});
execSync is only acceptable for short deterministic git plumbing (git rev-parse, git branch -d, git worktree remove). User-configured command wiring lives under packages/engine/src/sandbox/ (FN-4636 seam); keep internal git plumbing on direct async exec.
Move-Task contract
User-initiated moveTask(in-progress → todo) is a hard cancel: executor listeners must abort active sessions before dispose, stop step/workflow subprocesses, and leave the task parked in todo with userPaused semantics intact. Engine-initiated rebounds (pause, stuck recovery, workflow rerun, self-healing) use default moveSource: "engine" plus the appropriate preserve* flags and must not set userPaused.
Git Conventions
- Commit messages:
feat(FN-XXX):,fix(FN-XXX):,test(FN-XXX): - One commit per step (not per file change)
- Always include the task ID prefix
Merging Branches Into Main
Hard-won rules (FN-2370 silently reverted three commits' worth of work):
- Drop duplicate commits before merging. If a branch contains commits that duplicate work already on main, rebase to drop them. Auto-resolvers cannot tell which side is canonical and will silently discard refinements.
git log main..branch --format=%sshould not overlap withgit log <base>..main --format=%s. - Rebase over squash for multi-commit branches. Fusion's direct merger defaults
directMergeCommitStrategy="auto": squash for 0–1 substantive commits, history-preserving rebase/cherry-pick otherwise. Force via project setting or**Direct Merge Commit Strategy:** auto|always-squash|always-rebasein PROMPT. - Empty cherry-picks are no-ops. Cherry-pick merges treat git's empty-pick signatures as "already on main" — empty commits skipped, fully-subsumed branches auto-complete, no empty commit created.
- Already-on-main classifier. Verification-fix finalize and self-healing both recover when a task's lineage is already landed (emits
task:auto-recover-finalize-already-on-main,task:auto-recover-branch-misbound). - Contamination auto-recovery. When every foreign-attributed commit is upstream by patch-id, the executor drops them and requeues. A second contamination event escalates to paused human adjudication. FN-4499 adds a bootstrap-misbinding safety branch (foreign-only attribution →
reanchorBranchToBase+ requeue) before the contamination classifier. FN-4887 adds a self-healing foreign-only sweep for in-review/paused tasks, with bounded auto-recovery only whenownCommitCount === 0,nonAttributedCount === 0, and every foreign commit is attributable by subject orFusion-Task-Idtrailer; this emitstask:auto-recover-foreign-only-contamination/task:auto-recover-foreign-only-contamination-skippedand leaves ambiguous cases to manual recovery (FN-4860/FN-4875 boundary). - Post-squash audit on auto-resolved conflicts.
postMergeAuditMode:warn(default),block(refuse on findings),off. Rebase-strategy overlap-only findings auto-clear when deterministic verification has proven the merged tree. When findings still block, themergeAuditAutoRecoverypipeline runs (Stages 1–5: deterministic → programmatic → ai-assisted → bounded retries → park-with-follow-up). - Pre-commit diff-volume gate. Before writing an auto-resolved squash commit, the merger compares each file's staged squash delta against branch net delta vs merge-base. Non-allowlisted files losing too much branch volume block the merge in
in-review. Guard against FN-3936-style silent drops. - Smart-prefer-main overlap guard. When
mergeConflictStrategy="smart-prefer-main", recent main commits (30-commit lookback) overlapping branch-modified files flip to prefer-branch by default (mergeStrategyOverlapBehavior="flip-to-prefer-branch").
Gitignored-path guard on squash merges
The merger strips gitignored paths from staged squash sets before commit (standard, Attempt 3 fallback, and verification-fix rebuild). Any staged ignored path is unstaged and logged.
Agents must never git add -f .fusion/... or force-add any ignored scratch artifact. Findings, diagnosis, and test-plan notes belong in task documents via fn_task_document_write, not committed files.
File-Scope invariant on squash merges
Every squash commit path enforces a file-scope invariant immediately before commit: the staged set must overlap the task's declared ## File Scope from PROMPT.md. Zero overlap with a non-empty scope throws FileScopeViolationError, resets pre-squash state, and parks the task in in-review.
Per-task opt-out: task.scopeOverride = true (log task.scopeOverrideReason when set). Empty scopes are not enforced.
File Scope entries are validated when PROMPT.md is written — non-path tokens (git refs, URLs, SHAs, bare identifiers) are rejected with InvalidFileScopeError.
Manual audit script
For post-incident inspection: node scripts/audit-squash-merge.mjs <squash-sha>. Review every flagged item yourself — for each duplicate-cherry-pick subject, diff the matching main commit against HEAD and confirm survival. Restore any silent drops on the same branch before reporting merge complete.
Pi Extension (packages/cli/src/extension.ts)
The pi extension ships as part of @runfusion/fusion and provides tools + a /fn command for chat agents.
Update when:
- CLI commands change (behavior, flags, output)
- Task store / Agent store API changes
- New user-facing features chat agents should be able to use
Don't add tools for engine-internal operations (move, step updates, logging, merge) — those are owned by the engine's own agents.
The extension has no skills — tool descriptions give the LLM everything it needs.
fn_web_fetch
Lightweight URL read from agent/chat sessions. HTTP GET, follows redirects, extracts readable text (HTML→text and JSON pretty-print), bounded.
Universal baseline: available by default across executor, step-session, reviewer, merger, triage, and heartbeat (including engineer/custom direct-report paths). Gated under the network_api action-gate category (FN-4603).
- Defaults:
timeoutMs=30000,maxBytes=512000(500 KB) - Blocks private/loopback/link-local hosts (including DNS-resolved) unless explicitly overridden in internal/test contexts
- Read-only (no JS rendering, no auth flows, no POST/cookie workflows)
- Use the
agent-browserskill when JS rendering or interactive navigation is required
Agent Coordination Tools
Seven coordination tools support spawning, provisioning, discovery, delegation, and direct-report config. Detailed parameter contracts live in tool descriptions and docs/agents.md.
spawn_agent— Parent-task-scoped ephemeral child in its own worktree. Limits viamaxSpawnedAgentsPerParent(default 5) andmaxSpawnedAgentsGlobal(default 20). Auto-terminated with parent. Gated under generictask_agent_mutation(FN-3973 explicitly excludes it from durableagentProvisioningpolicy).agent_create/agent_delete— Non-ephemeral provisioning of direct reports. Policy-gated viaprojectSettings.agentProvisioning(approvalMode,trustedRoles,trustedAgentIds,alwaysApproveDelete). Tool responses usedetails.outcome:created/deleted/pending_approval/denied. Pending requests resolve viaPOST /api/approvals/:id/decision. Audit events:agent:{create,delete}:{requested,approved,denied}.list_agents— Discovery withrole/state/includeEphemeralfilters.delegate_task— Create + assign task to a specific agent. Implementation tasks require executor-role target unlessoverride: true. Cannot target ephemeral agents (usespawn_agent).get_agent_config/update_agent_config— Read/write soul, instructions, heartbeat interval/timeout, max concurrent runs, message response mode. Authorization: caller can only act on agents wheretarget.reportsTo === caller.id. Cannot operate on ephemeral agents.
Checkout Leasing
- 409 Conflict = ownership contention. Response:
{ error, currentHolder, taskId }. Never auto-retry 409. HeartbeatMonitor.executeHeartbeat()validates checkout before work begins; mismatchedcheckedOutByexits withreason: "checkout_conflict". Heartbeat does not auto-checkout — callers obtain the lease.- With
CentralClaimStorewired, the authoritative owner is the centraltaskClaimsrow; per-project lease fields mirror it.MeshLeaseManager.recoverAbandonedLease()releases central first then local.reconcileLeaseRow(taskId)converges divergent state on the next tick (emitstask:auto-recover-lease-*). Without a claim store, behavior remains single-node per-project.
Agent Runtime Config
Per-agent overrides via runtimeConfig:
- Heartbeat:
heartbeatIntervalMs,heartbeatTimeoutMs,maxConcurrentRuns. Triggered by timer, task assignment, or on-demand (POST /api/agents/:id/runs). - Budgets: per-agent token budget tracking;
HeartbeatMonitor.executeHeartbeat()skips whenisOverBudgetorisOverThreshold(timer triggers). Hard caps pause the agent. - Performance ratings: 1–5 scale with trend analysis, injected into system prompts.
See docs/agents.md for the full contract.
Settings
Two tiers: global (~/.fusion/settings.json) overridden by project (.fusion/config.json). Configure via dashboard Settings modal or fn settings. Full reference: docs/settings-reference.md.
Model selection hierarchy
All three lanes (planning / executor / reviewer) follow the same 5-tier precedence:
- Per-task override (
planningModelProvider/Id,modelProvider/Id,validatorModelProvider/Id) - Project lane (
planningProvider/Id,executionProvider/Id,validatorProvider/Id) - Global lane (
planningGlobalProvider/Id,executionGlobalProvider/Id,validatorGlobalProvider/Id) - Project
defaultProviderOverride/defaultModelIdOverride - Global
defaultProvider/defaultModelId→ automatic resolution
Per-task token budget precedence
task.tokenBudgetOverride- Project
taskTokenBudget.perSize[task.size] - Project
taskTokenBudget.soft/hard - Global
taskTokenBudget.perSize[task.size] - Global
taskTokenBudget.soft/hard
Hard cap → pause with pausedReason: "token_budget_exceeded". Soft cap → one-shot alert per task.
Model presets
Standardize executor/validator pairs; auto-selectable by task size (Small → Budget, Medium → Normal, Large → Complex). See settings reference.
Missions
- Autopilot — watches task completion and activates the next slice. States:
inactive → watching → activating → completing. See docs/missions.md. - Planning context — feature → task triage enriches descriptions with full mission → milestone → slice → feature hierarchy.
- Planning tools —
fn_mission_create,fn_milestone_add/update,fn_slice_add/activate,fn_feature_add/update/link_task.fn_milestone_updateandfn_feature_updateaccept partial patches.
Workflow Steps
Reusable quality gates at configurable lifecycle phases. Pre-merge can block; post-merge is informational. gateMode is gate (failure blocks merge/remediation) or advisory (records advisory_failure, no block, no auto-revive). Defined as prompt (AI review) or script (deterministic command). See docs/workflow-steps.md.
Run Audit
Every engine mutation is recorded across four domains:
- Database — task:create, task:update, task:move,
room:ambiguity:branch(deictic message routing telemetry), etc. - Git — worktree:create, commit:create, merge:resolve, etc.
- Filesystem — file:write, prompt:write, attachment:create,
secret:read|create|update|delete|approval-requested|approval-granted|approval-denied|sync-push|sync-pull,secret:env-*, etc. - Sandbox —
sandbox:prepare,sandbox:run,sandbox:failure,sandbox:fallback.
Events are tied to run IDs end-to-end. See docs/architecture.md for the audit API.
Archive Cleanup
Archived tasks can be cleaned up while preserving metadata. Restored tasks keep metadata but lose attachments and agent logs. See docs/task-management.md.
Secrets
AES-256-GCM-encrypted storage in project (secrets) and global (secrets_global) scopes. Per-secret access policy (auto/prompt/deny) resolved as row → global default → "prompt". Master key via the core MasterKeyProvider abstraction. See docs/secrets.md for current capabilities and the pending agent-tool wiring (FN-4867).
Node Dashboard
Mesh-network node management UI. Settings/auth/secrets sync endpoints documented in docs/architecture.md. All remote endpoints require the target node's apiKey; inbound endpoints validate Authorization: Bearer <apiKey>.
Headless Node Mode (fn serve)
Starts API server + AI engine without a frontend. Binds 0.0.0.0 by default. Health endpoint + startup banner in docs/architecture.md.
Terminal UI
The Ink-based TUI is part of fn (no separate @fusion/tui package). Implementation: packages/cli/src/commands/dashboard-tui/.
Engine Diagnostic Logging
Structured logging via createLogger() from packages/engine/src/logger.ts. All lines prefixed with subsystem name. See docs/diagnostics.md for the full key-diagnostic-points catalog. Notable subsystems include [executor], [scheduler], [stuck-detector], [auto-claim-snapshot], [prompt-size], [wake-trigger-diagnostics], [retry-burned], and [room-ambiguity].
AgentSemaphore (packages/engine/src/concurrency.ts) has defensive guards: limit getter returns minimum 1; availableCount returns 0 for invalid limits.
Dashboard UI Styling Guide
The dashboard's CSS is split into a global stylesheet (packages/dashboard/app/styles.css) and per-component files (packages/dashboard/app/components/ComponentName.css). Each ComponentName.tsx imports its stylesheet at the top.
Rule: New CSS for a component goes in app/components/ComponentName.css, NOT styles.css. Only design tokens, primitives (.btn, .card, .modal, .form-input), and cross-component @media overrides belong in the global file.
The index.html shell is templated server-side: the server injects a per-user <link rel="modulepreload"> for the last-used taskView chunk, sourced from Vite's dist/client/.vite/manifest.json and kb:<projectId>:kb-dashboard-task-view in localStorage.
Design tokens
styles.css is the source of truth for tokens (--space-*, --radius-*, --shadow-*, --transition-*, --font-*, --header-height, --mobile-nav-height, --standalone-bottom-gap, --overlay-padding-top) and color variables (--bg, --surface, --card, --text, --text-muted, status colors --triage/--todo/--in-progress/--in-review/--done, semantic --color-success/--color-error/--color-warning/--color-info, status backgrounds --status-*-bg).
Always reference tokens. Never hardcode pixels, hex, or rgba() in component CSS — the only exception is inside :root/theme blocks where tokens are defined. For translucent backgrounds use color-mix(in srgb, var(--color) X%, transparent), not rgba().
Theme system
Dark/light modes via data-theme; 54 color themes via data-color-theme (lazy-loaded from app/public/theme-data.css).
- Base tokens (
--bg,--surface, etc.) — redefine in:root,[data-theme="light"], and every theme block. - Semantic tokens (
--autopilot-pulse,--event-error-text,--badge-mission-*,--fab-*) —:root+[data-theme="light"]only; no per-color-theme overrides. - Status tokens (
--triage,--todo, etc.) — redefine per theme block.
status-colors-theme.test.ts iterates all theme blocks to catch regressions.
Component classes
Reuse existing primitives from styles.css:
- Buttons:
.btn,.btn-primary,.btn-danger,.btn-warning,.btn-sm,.btn-icon,.btn-icon--active,.btn-badge. All inherit:focus-visiblevia--focus-ring-strongand:activeviatransform: scale(0.97). - Modals:
.modal-overlay[.open],.modal,.modal-lg,.modal-header,.modal-close,.modal-actions,.modal-actions-left/right. Overlay pads top with--overlay-padding-top. - Forms:
.form-group,.input,.select,.checkbox-label,.form-error. Inputs in.form-groupget focus styles automatically. - Cards:
.card,.card-header,.card-id,.card-title,.card-meta,.card-status-badge--{triage,todo,in-progress,in-review,done,archived}. - Utility:
.touch-target(44px min),.visually-hidden.
Don't create parallel button/form variants — add states (:hover, :focus-visible, :active) to the existing primitives.
Mobile responsive
Breakpoints: 768px (primary mobile), 1024px (tablet min-width: 769px and max-width: 1024px), 640px (compact), 480px (xs). Mobile overrides go in @media (max-width: 768px) blocks at the bottom of styles.css after base styles.
Bottom spacing: --mobile-nav-height (44px) + env(safe-area-inset-bottom, 0px) + --standalone-bottom-gap (0/8px PWA). All bottom-positioned mobile elements compose those.
Touch targets: Standing button-freeze directive supersedes per-button touch-target guidance. For non-button elements, primary controls (nav bar, FAB, tab action rows, modal CTAs, list-row tap targets, form controls) must be ≥36px on mobile. Secondary controls inside a card/list-row where the row itself is the tap target stay compact (24–28px or small chips).
Safe area: max(var(--space-md), env(safe-area-inset-left, 0px)) for notch-aware horizontal padding.
Lazy-Loaded Heavy Views
These 18 views are lazy-loaded via React.lazy() with <Suspense fallback={null}>. prefetchLazyViews() warms chunks once on mount via requestIdleCallback. Do not make these eager.
AgentsViewNodesViewChatViewMemoryViewDevServerViewInsightsViewDocumentsViewSkillsViewResearchViewReliabilityViewEvalsViewTodoViewGoalsViewStashRecoveryViewSetupWizardModalPluginManagerPiExtensionsManagerAgentDetailView
When adding or removing entries, update packages/dashboard/app/__tests__/lazy-loaded-views-docs.test.ts (expected set + count).
CSS testing
Use packages/dashboard/app/test/cssFixture.ts:
import { loadAllAppCss, loadAllAppCssBaseOnly } from "../test/cssFixture";
const allCss = await loadAllAppCss(); // styles.css + all component .css
const baseOnly = await loadAllAppCssBaseOnly(); // strips @media/@supports
Never directly readFileSync('../styles.css') — an ESLint rule (no-restricted-syntax in eslint.config.mjs) bans this and points at cssFixture.ts. vitest.config.ts has test.css: { include: [/.+/] } so component CSS imports inject into jsdom for getComputedStyle assertions.
File browser editor & autosize textarea
FileEditor.tsxis CodeMirror 6-only (no<textarea>fallback). Language resolution:packages/dashboard/app/utils/codemirror-language.ts.- For chat-style composer fields use
packages/dashboard/app/hooks/useAutosizeTextarea.ts. Pattern:height = "auto"then clampscrollHeightto min/max inuseLayoutEffect. Pair withresize: noneandoverflow-y: auto.
File-path links
Reuse packages/dashboard/app/utils/filePathLinkify.tsx and FileBrowserContext. Wrap plain text with linkifyFilePaths(...), mixed JSX with linkifyReactChildren(...). Mount under FileBrowserProvider and route clicks through its openFile(path, { workspace?, line?, col? }).
Common pitfalls
--surface-hoverundefined — reference with a fallback (var(--surface-hover, rgba(0,0,0,0.03))) or define explicitly.- BEM specificity — when a container state class and an element modifier target the same node, the container can win. Use
:not(.modifier)to scope. - CSS
@mediadetection — track brace depth to confirm a rule is mobile-scoped; don't scan backwards for the nearest@media. Many components are global even if visually mobile-only. - Mobile board scroll-snap (FN-001) —
scroll-snap-type: x mandatoryon mobile.boardcauses iOS Safari to compress the viewport when switching from ListView. Usex proximity+overflow-anchor: none. lucide-reacticon adds — updatevi.mock("lucide-react")test mocks immediately; missing exports cascade..spinis global — don't redefine the generic spin keyframes in component CSS.
Reliability Mechanism Coverage
Reliability-layer changes are in scope. Interaction regression backstops live in packages/engine/src/__tests__/reliability-interactions/ — any task that adds or changes a reliability layer must add/update interaction tests there covering each plausible pair with existing layers (merge path, workflow/pre-merge, self-healing, scheduler/watchdog/restart recovery, governance gates).
- FN-4935 backstop:
packages/engine/src/__tests__/reliability-interactions/executor-liveness-gate.test.tsguards fresh-acquisition skip behavior, structured liveness classifications, and executor-gate audit/requeue outcomes. - FN-4887 backstop:
packages/engine/src/__tests__/reliability-interactions/foreign-only-contamination-recovery.real-git.test.tscovers composition between bootstrap-misbinding, contamination dispatcher retry, misbound-in-review ordering, and FN-4811 active-session safeguards. - FN-4976 backstop:
packages/engine/src/__tests__/reliability-interactions/stale-self-owned-session-registry.test.tsguardscleanupConflictingWorktreeclearing stale same-taskactiveSessionRegistryentries before the FN-4811 foreign-owner check, while preserving refusal behavior for foreign owners and live same-task bindings.
The auto-recovery dispatcher at packages/engine/src/auto-recovery.ts (FN-4533) composes on top of existing layers (FN-4500 fast-path, FN-4508 deterministic branch-conflict, FN-4499 bootstrap-misbinding, FN-4428 contamination, mergeAuditAutoRecovery Stages 1–5, self-healing) to handle six residual classes: file-scope violation at squash, branch misbinding / ghost worktree, verification-fix scope leak, contamination, branch-conflict-unrecoverable residuals, and room-post/message-send failures. Invocation is additive — no existing layer's behavior changes.