Guidelines for working with code in this repository. These are linter-like rules and build/test instructions — not a code summary.
Last updated: 2026-06-19. Keep this in sync with the codebase; update it in the same PR when rules or commands change.
Canonical standards live in docs/DEVELOPMENT_STANDARDS.md. That document is the single source of truth for enforceable rules and aspirational targets across TypeScript, React, Zustand, MUI, TanStack Query, ReactFlow, Fastify, Drizzle, Zod, Electron security, accessibility, performance, security, observability, error handling, git/PR hygiene, and dependency management. The rules in this file are the area-specific overlay — read both.
- Development Standards — Canonical standards for the whole repo (MUST READ).
- Design System — Design token rules: SPACING, TYPOGRAPHY, BORDER_RADIUS, MOTION, Z_INDEX (MUST READ for any UI work).
- TypeScript Backend — TypeScript backend packages (
packages/) - Web UI — React web application
- Components, Stores, Contexts, Hooks, Utils, ServerState, Lib, Config
- UI Primitives Strategy — Primitives-first policy, decision tree, migration rules (MUST READ for frontend work)
- Testing — Web testing guide (Jest, React Testing Library, Playwright)
- Electron — Desktop app
- Mobile — React Native mobile app
- Agent System — Agent architecture, tools, skills, workflow nodes
- Agent Harnesses & Tooling — Validate, debug, run, single-node, browser, deploy, trace (the tools that close the build→verify loop)
- Scripts — Build and release scripts
The repo ships harnesses built for the agent edit→verify loop: check a workflow
before running it, run it and read everything it emitted, run a single node in
isolation, drive the real browser, deploy, and trace token/cost. Reach for these
before hand-rolling a script. Every CLI command runs from source with
npm run dev:nodetool -- <cmd> (no build) or from dist with
npm run nodetool -- <cmd> after npm run build:packages. The full flag
reference lives in CLAUDE.md and docs/cli.md.
| Need | CLI harness | Agent/MCP tool | Speed |
|---|---|---|---|
| Static pre-flight (unknown nodes, missing props, bad edges) — run this first | nodetool validate <id|file.json|file.ts> |
validate_workflow (inline graph or workflow_id) |
< 1 s, no DB for file targets |
| Run a workflow end-to-end and read every message/log/output/error | nodetool debug <id|file> (server surface, default) |
debug_workflow (status + outputs + errors + job logs + graph in one call) |
seconds |
| Build a mini app from a prompt and verify it end to end | nodetool app build "<prompt>" -p <provider> -m <model> |
build_app (POST /api/applications/build; poll/cancel via the debug-session endpoints) |
minutes |
| Real-browser surface (Playwright + Chromium canvas), trace, per-stage shots | nodetool debug <id> --browser --trace --stages |
— | tens of seconds (opt-in) |
| Tight edit→verify loop on a file target | nodetool debug file.ts --watch (prints a verdict diff per save) |
— | per-save |
| Run one node in isolation with a prop bag | nodetool node run <type> --props '{…}' [--no-secrets] |
— | sub-second hermetic |
Run a workflow (id, JSON, or DSL .ts) |
nodetool run <file> / nodetool workflows run <id> [--params …] |
run_workflow, start_background_job |
varies |
| Map changed files → minimal workspaces to rebuild/test | nodetool affected [--base main] |
— | instant |
| Author/inspect a graph against the live registry | — | create_workflow, search_nodes, list_nodes, get_node_info, get_example_workflow, export_workflow_digraph |
— |
| Jobs & assets | nodetool jobs … / nodetool assets … |
list_jobs, get_job, get_job_logs, list_assets, get_asset |
— |
| Agent/chat REPL (loop | plan | graph | multi-agent) | nodetool-chat --agent (npm run dev:chat) |
— | — |
| Deploy + remote ops (Docker/SSH/RunPod/GCP/Supabase) | nodetool deploy <init|plan|apply|status|logs|destroy>; deploy workflows <sync|run>, deploy database, deploy collections |
— | — |
| Trace tokens/cost/timing (OTel span tree) | --trace-file <f.jsonl> / --trace-stdout pretty|json on any CLI run |
— | — |
The agent/MCP tools above are the @nodetool-ai/agents MCP tools
(packages/agents/src/tools/mcp-tools.ts), exposed to in-product agents and over
the websocket MCP server — use them instead of shelling out when you are already
inside an agent context.
Browser workflow harness. The in-browser graph harness runs whole workflows
against the real backend and renders the actual ReactFlow canvas, recording IO,
traces, and screenshots — see In-Browser Workflow Harness
below and web/src/e2e_runner/README.md. The same
surface backs nodetool debug --browser and web's npm run test:debug-harness.
Suggested loop: validate (cheap, catches structural bugs) → node run to
isolate a suspect node → debug to run the whole graph and read messages →
debug --browser only when a bug is browser-specific → --trace when chasing
token/cost/latency.
- Node.js 22.22.1 (required — see
.nvmrc). Matches Electron 39's embedded Node (22.22.1). The one source-built native module (better-sqlite3) is rebuilt against the Node ABI by the rootpostinstallhook (electron/scripts/rebuild-native.mjs), which runs afternpm install/npm cifinishes reifying the tree. - Use
nvm useto activate the correct version. - If you see
NODE_MODULE_VERSIONerrors, runnpm run rebuild:native. - Fresh checkout or missing
node_modules: ifnpm run typecheck/lint/testfail with module-resolution errors (Cannot find module,Cannot find type definition file) on files you didn't touch, runnpm installfirst — don't spend a cycle proving the failure predates your change. Re-run the checks after install before investigating further.
Three postinstall steps break npm install in locked-down containers (CI
sandboxes, Claude Code on the web, proxied networks). A failed postinstall
makes npm roll back the entire node_modules tree, so one bad package
means no dependencies at all — including ESLint and the design-lint gate.
keytarneeds libsecret headers on Linux. Without them node-gyp fails withPackage libsecret-1 was not found. Fix first:apt-get install -y libsecret-1-dev.electrondownloads its binary in postinstall. Proxies that block the download (HTTP 403) fail the install. Skip it when you don't need to launch Electron:ELECTRON_SKIP_BINARY_DOWNLOAD=1 npm install.onnxruntime-nodedownloads CUDA binaries from GitHub releases in postinstall (it assumes CUDA whennvccis absent). Same proxy failure mode, and there is no skip env var in our pinned version.
When only the JS toolchain matters (typecheck, lint, unit tests that don't hit native modules), bypass all of the above in one step:
npm install --ignore-scripts --no-audit --no-fundThis skips every postinstall — including the root better-sqlite3 rebuild —
so anything touching the database needs npm run rebuild:native afterwards
(which will still require the downloads above to have succeeded).
The image nodes are shader-backed: every lib.image.* generator and every
nodetool.image transform reaches WebGPU through Dawn. On a machine with no
Vulkan driver they fail with:
No WebGPU adapter available (Node/Dawn). On headless Linux this usually means
no Vulkan driver (ICD) is installed — Dawn has no software fallback of its own.
This is an environment gap, not a broken test and not an unsupported
platform. CI already solves it: the test-packages leg of
.github/workflows/quality-checks.yml and the browser job in test.yml both
install mesa-vulkan-drivers, which ships lavapipe — a CPU Vulkan ICD. Do
not conclude from this error that shader-backed nodes cannot be tested, and do
not skip a test because your box hits it; the same test passes in CI.
With root:
apt-get install -y mesa-vulkan-driversWithout root (sandboxes, dev containers), extract the driver and point the
Vulkan loader at it. libvulkan1 — the loader — is usually already present;
only the ICD is missing:
apt-get download mesa-vulkan-drivers
dpkg-deb -x mesa-vulkan-drivers_*.deb /tmp/vk
# The shipped manifest names the library relatively, so rewrite it absolutely:
python3 - <<'PY'
import json
p = "/tmp/vk/usr/share/vulkan/icd.d/lvp_icd.json"
d = json.load(open(p))
d["ICD"]["library_path"] = "/tmp/vk/usr/lib/x86_64-linux-gnu/libvulkan_lvp.so"
json.dump(d, open("/tmp/vk/lvp_icd.json", "w"))
PY
export VK_DRIVER_FILES=/tmp/vk/lvp_icd.jsonThen run the tests as usual. Lavapipe is a software rasterizer, so it is slow
but exact — pixel comparisons (nodetool.compare.CompareImages) are
reproducible under it, which is what
packages/base-nodes/tests/image-examples-run.test.ts relies on.
npm install # Install all dependencies (web, electron, mobile)
npm run build # Build all packages
npm run typecheck # Type check all packages
npm run lint # Lint all packages
npm run lint:fix # Auto-fix linting issues
npm run test # Run all tests
npm run check # Run all checks (typecheck, lint, test)npm run build:packages # Build all in dependency order
npm run test:packages # Test all packages
npm run test --workspace=packages/<name> # Test single package
npm run test:watch --workspace=packages/<name> # Watch mode for single packagecd web
npm install # Install dependencies
npm start # Start dev server
npm run build # Production build
npm run typecheck # TypeScript check
npm run lint # ESLint
npm run lint:fix # Auto-fix lint issues
npm test # Run tests
npm run test:watch # Watch mode
npm run test:coverage # With coverage
npm run test:e2e # Run e2e tests (requires backend)cd electron
npm install # Install dependencies
npm start # Start electron
npm run build # Production build (tsc + vite + electron-builder)
npm run typecheck # TypeScript check
npm run lint # ESLint
npm run lint:fix # Auto-fix lint issues
npm test # Run testsnpm run dev # Backend (tsx --watch) + web Vite server
npm run dev:server # Backend dev server only (tsx --watch, port 7777)
npm run electron # Build web and start Electron app
npm run electron:dev # Electron against Vite server (requires conda env)After any code change, run:
npm run typecheck # Type check all packages
npm run lint # Lint all packages
npm run test # Run all testsAll three must pass before the task is complete.
Running suites per package instead is fine when the full run is too slow — but
run every package nodetool affected names, not the ones you remember touching.
lint passing is not test passing: a change to packages/websocket that was
linted and never tested broke two route suites, and CI found it rather than the
author.
Verification failures in this repo are rarely "forgot to run the tests". They are green signals that were never earned. Four rules, each paid for:
Prove a new check can fail. Invert the condition once and watch it go red,
then restore. nodetool validate returned ✅ on a workflow whose model id was
totally-not-a-real-model-xyz — it had never checked ids at all, and only a
deliberately-bogus input revealed it. A check that has only ever been green is
indistinguishable from one that examines nothing. For an audit that scans
files, also assert it found something, so it cannot pass by matching nothing.
This is rule 7's sibling in docs/HARNESS_FIRST.md.
Reproduce before you enforce. Rule 5 requires a bug fix to ship a reproduction; the same applies to a new rule. A validator check was written from a log warning plus code reading, shipped as an error, and would have failed the examples gate on six shipped workflows — until three reproductions of its own criterion passed cleanly and the feature was reverted. Until you have watched the failure, report it; do not enforce it.
"I checked" means you enumerated. Not one plausible file, and never a
comment — a comment is a hypothesis about code, not the code. Claims that
validateGraph was ungated in CI, and that six call sites were safe, were both
made from a sample and both wrong in method. To assert "all X do Y", produce the
list; if that is too expensive, scope the claim to what you actually read.
Distrust the measurement before the conclusion. pgrep -f matches the
shell command that contains the pattern, so a probe can report a dead process as
running. cmd | head && echo ok prints ok on failure, because head exits 0 —
capture the exit code of the command you care about. When a result is surprising,
re-measure with a different tool before believing it.
Two mechanical traps worth naming: after a programmatic edit, byte-count the
file for stray control characters (a \u0000 written as the byte it denotes
got a .ts file staged as binary), and if the code walks a graph or list,
run it once on a large input — an edges.some() inside a node loop is O(n·m)
and passed every hand-written fixture before timing out on the 20 000-node
chain in CI.
Before submitting a PR, review for:
- Existing tests still pass and cover the changes
- No new TypeScript errors or lint warnings
- No unintended side effects in related code
- Edge cases and error handling are covered
- Performance implications considered
- Decorator packages load from
dist/:base-nodes,node-sdk,fal-nodes,replicate-nodes,elevenlabs-nodes,minimax-nodesuse decorators. After changing these, runnpm run build:packagesbefore runningnpm run dev. - Package build order matters: Always use
npm run build:packages(builds in dependency order). Don't build individual packages with unbuilt dependencies. - Mobile typecheck needs protocol: Run
cd packages/protocol && npm run buildbeforenpm run typecheck:mobile. mobile/is not a root workspace: it keeps its own Expo/React Native deps, so its scripts usenpm --prefix mobile …(notnpm --workspace=mobile …, which would fail).- WebSocket uses MsgPack, not JSON: Use existing serialization helpers. Don't serialize WebSocket messages as JSON.
- Don't create WebSocket instances: Use
GlobalWebSocketManagersingleton in the frontend. - ES Modules everywhere: All packages use
"type": "module". Compiled imports need.jsextensions. - Never import from
dist/: Use@nodetool-ai/<package>workspace references in source code.
Full standards: DEVELOPMENT_STANDARDS §1 TypeScript.
- Use TypeScript for all new code. Never use
any— preferunknown+ narrowing or proper generics. - Use
constby default,letwhen reassignment is needed. Never usevar. - Use strict equality (
===/!==). Exception:== nullfor null/undefined checks. - Always use curly braces for control statements.
- Use
Array.isArray()to check for arrays, nottypeof. - Throw
Errorobjects, not strings. - Always add comments for intentionally empty catch blocks.
- No
// @ts-ignore— use// @ts-expect-error <reason>. - No
enumin new code — useas constobjects +keyof typeofunions. - Prefer discriminated unions over optional fields with implicit invariants.
- Validate untrusted input with Zod at the boundary — see DEVELOPMENT_STANDARDS §11.
- Use functional components only. No class components.
- Always define a TypeScript interface for component props.
- Never mutate state directly. Use immutable patterns.
- Don't use inline functions in JSX when passed to memoized child components.
- Test behavior, not implementation details.
| Hook | Use When | Do Not Use When |
|---|---|---|
useEffect |
Side effects (network, subscriptions, timers, DOM) | Deriving data from props/state |
useMemo |
Expensive computation, referential stability | Cheap computation |
useCallback |
Passing to memoized children, dependency of effect/memo | Function used only locally |
React.memo |
Pure component, stable props, renders often, expensive | Props change every render |
Never add these "just in case." If performance is fine, do nothing.
- Always prefix with
use. - Use descriptive names:
useWorkflowActionsnotuseActions. - Include all dependencies in
useEffect,useCallback,useMemoarrays. - Provide TypeScript types for all return values.
- Keep stores focused on a single domain.
- Use selectors to prevent unnecessary re-renders:
useStore(state => state.value). - Use shallow equality for object selections.
- Define actions within the store alongside state.
- Use
persistmiddleware for settings stored in localStorage.
- MANDATORY: Use UI primitives from
web/src/components/ui_primitives/for all frontend UI. Never import raw MUI components (Typography,Button,IconButton,Tooltip,CircularProgress,Chip,Dialog,Alert,Divider,Paper,Skeleton,Tabs,Drawer,Breadcrumbs,Select,Switch,TextField) directly in component files. These are only allowed insideui_primitives/andeditor_ui/where the primitives are defined. - See the Primitives Strategy for the full decision tree, migration rules, and 90+ available primitives.
- When touching any component file, opportunistically migrate raw MUI usage to primitives.
- Replace
display: "flex"/flexDirectionpatterns withFlexRow/FlexColumnlayout primitives. - Replace
<Typography>withText,Label, orCaptionprimitives. - Replace
<CircularProgress>withLoadingSpinner. Replace<Tooltip>withTooltipprimitive. - Use
sxprop for one-off styles on primitives. Usestyled()only insideui_primitives/for defining new primitives. - Use theme values for spacing, colors, and typography — never hardcode hex colors or pixel values.
- Prefer composition over deep prop drilling.
- If no primitive exists for your use case, create a new primitive in
ui_primitives/rather than using raw MUI.
Design Token Rules (see docs/DESIGN.md for full reference)
Every style value that falls into one of the categories below must use the corresponding token — never hardcode.
| Category | Forbidden | Use instead |
|---|---|---|
| Spacing / gap / padding | 5px, 10px, 13px, 0.25 theme units |
SPACING.* / GAP.* / PADDING.* |
| Font size | "14px", "0.85rem", any raw px/rem |
var(--fontSize*) or <Text>/<Label>/<Caption> |
| Font weight | 700, "bold", 300 |
400, 500, or 600 only |
| Border radius | 4, 10, 18, "var(--rounded-*)" |
BORDER_RADIUS.xs/sm/md/lg/xl/xxl/pill/circle |
| Transitions | "all 200ms ease", raw timing strings |
MOTION.all/border/background/… |
| Z-index | 9999, 1000, arbitrary integers |
Z_INDEX.dropdown/modal/tooltip/… |
- Use hierarchical query keys:
['workflows', workflowId]. - Set appropriate
staleTimebased on data volatility. - Use
enabledoption for conditional queries. - Use optimistic updates for mutations where appropriate.
- Always invalidate related queries after successful mutations.
- Components: PascalCase (
MyComponent.tsx) - Hooks: camelCase with
useprefix (useMyHook.ts) - Stores: PascalCase file, camelCase
useprefix for hook (useMyStore) - Utilities: camelCase (
formatDate.ts) - Constants: UPPER_SNAKE_CASE (
MAX_NODES) - Types/Interfaces: PascalCase (
NodeData) - Tests: Same as source +
.test.ts(x), placed in__tests__/directories
- React and core libraries
- Third-party libraries (MUI, TanStack Query, etc.)
- Internal stores and contexts
- Internal components
- Internal utilities and types
- Styles
- Use React Testing Library queries (
getByRole,getByLabelText). - Use
userEventfor interactions, notfireEvent. - Use
waitForfor async assertions. - Mock external dependencies and API calls.
- Test user-facing behavior, not implementation details.
- Keep tests independent and isolated.
E2E tests require the TypeScript backend and Node.js frontend. For comprehensive E2E testing documentation, see web/TESTING.md.
# Build the backend packages first (one time)
npm run build:packages
# Install and run
cd web
npm install
npx playwright install chromium
npm run test:e2e # Automatically starts servers
# Manual setup for debugging
# Terminal 1: PORT=7777 HOST=127.0.0.1 node packages/websocket/dist/server.js
# Terminal 2: cd web && npm start
# Terminal 3: cd web && npx playwright testA browser-based graph harness that runs whole workflows against the real backend and renders the actual ReactFlow canvas per workflow, recording IO, traces, and screenshots into a self-contained HTML report. Frontend lives in web/src/e2e_runner/, backend in packages/websocket/src/e2e-server.ts. See web/src/e2e_runner/README.md.
cd web
npm run test:e2e-runner # headless: boots backend + Vite, runs the suite
npm run test:e2e-runner:headed # watch it run in a browserThe Electron workspace has no Playwright suite — the main process is covered by
Jest tests in electron/src/__tests__/.
cd electron
npm testSee electron/src/AGENTS.md for Electron-specific testing.
Full standards: DEVELOPMENT_STANDARDS §16 Security and §12 Electron Security.
- Use
DOMPurify.sanitize()for user input rendered as HTML. - Never use
dangerouslySetInnerHTMLwith unsanitized input. - Use
contextBridgefor Electron IPC — never exposenodeIntegration. - Validate all IPC inputs with Zod before acting on them.
- No
eval,new Function, orsetTimeoutwith string arguments. - Secrets never appear in code, logs, or error messages.
npm auditmust pass — high/critical advisories block merge unless waived with rationale.
These three areas have full sections in the central standards doc:
- Accessibility (§14) — WCAG 2.2 AA target, semantic HTML, keyboard parity, focus management.
- Performance (§15) — Bundle and runtime budgets, lazy loading, virtualization.
- Observability (§17) — OpenTelemetry spans, structured logs, semantic conventions.
Full standards: DEVELOPMENT_STANDARDS §20.
- Conventional commits:
feat(scope):,fix(scope):, etc. Subject ≤ 72 chars, imperative mood. - One concept per commit. Body explains WHY, not WHAT.
- Never
--no-verify. Never rewrite published history. - PRs are small (target <400 LOC), self-reviewed, and CI-green before review.
Full guide: docs/WRITING_STYLE.md. Comment/README rules: DEVELOPMENT_STANDARDS §19.
- Write prose — docs, READMEs, this file, PR descriptions, comments — concise and concrete. Cut any sentence that survives deletion without losing meaning.
- No AI slop. Forbidden:
leverage,utilize,seamless,robust,powerful,comprehensive,cutting-edge,unlock,empower,streamline,it's worth noting,dive into, rule-of-three padding, "it's not just X, it's Y", emoji decoration, and the rest of the forbidden list. - Bold-label bullets must add information beyond the label. Claims are concrete: numbers, names, paths — not adjectives.
- When you edit a Markdown file, fix slop you pass in the same change. For code, use the
unslopskill.
- Node.js LTS, TypeScript 5.4+, ES Modules
- Vitest for testing
- Key packages:
@nodetool-ai/websocket(server),@nodetool-ai/kernel(runtime),@nodetool-ai/cli(CLI) - See packages/AGENTS.md for full package list
- React 18.2.0, TypeScript 5.7.2, Vite 6.4.1
- MUI v7.2.0 + Emotion, Zustand 4.5.7, ReactFlow 12.10.0
- TanStack Query v5.62.3, React Router v7.12.0
- Jest 29.7.0 + React Testing Library 16.1.0, Playwright for E2E
- Electron 39.8.8, React 19.1.0, TypeScript 5.3.3
- Zustand 5.0.3, Vite 6.4.1
- React Native / Expo - See mobile/README.md