Slop Docs Home

Development Guide

Everything you need to clone, run, test, and contribute to Slop.

Node.js 24 pnpm 11 Vitest Playwright Biome

Prerequisites

Node.js
v24
Matches the CI environment (ubuntu-latest, Node 24). Use nvm install 24 or install from nodejs.org. Verify: node --version
pnpm
11.3.0
Enforced by the packageManager field in package.json via corepack. Install: corepack enable && corepack prepare. Verify: pnpm --version
macOS
Local dev only
node-pty uses a native binding compiled for the host OS. CI runs Ubuntu; local dev targets macOS. Linux is not supported for local development.
SQLite
Bundled
Bundled via better-sqlite3. No separate database server needed. The prisma generate step (run by postinstall) builds the Prisma client against it automatically.

Installation

git clone https://github.com/<owner>/slop.git
cd slop
make install        # pnpm install; postinstall runs prisma generate
postinstall runs prisma generate automatically, so the Prisma client is ready immediately after install. No separate setup step required.

Environment Variables

Copy .env.example to .env and adjust as needed. All fields are optional; the process reads them at startup.

VariableDefaultPurpose
PORT 3100 (Makefile DEV_PORT) Port Next.js listens on. Convention: live instance on 3000, dev checkout on 3100.
DATABASE_URL file:~/.slop/data/slop.db SQLite connection string. Default is outside the checkout - survives re-clones.
SLOP_SECRET (unset) Bearer token gating the web UI. Unset = UI accessible on localhost only, no auth check.
SLOP_URL http://localhost:3100 Base URL used by harness skills when creating internal issues. Must match PORT.
SLOP_WORKTREES_ROOT ~/.slop/worktrees Root directory where per-issue git worktrees are nested as <root>/<repoId>/<issueNumber>.
All other config (GitHub token, watched repos, poll interval, concurrency, timeouts) lives in the SQLite database and is managed via the /config page in the UI. For the GitHub Actions claude-review.yml workflow, ANTHROPIC_API_KEY must be set as a repo secret.

Running Locally

Development server
make dev
Kills any process already on PORT, then starts Next.js. Binds to --hostname 127.0.0.1 (loopback only). Sets NODE_OPTIONS=--max-old-space-size=1024 to cap the V8 heap at 1 GB.
Restart with migrations
make restart
Applies any pending schema migrations via scripts/prisma-migrate-dev.ts first, then starts dev. Use this after pulling changes that include new migration files. Never resets the database.

The default port is 3100 (from DEV_PORT in the Makefile). Override with a PORT= line in .env. The server is not reachable from other machines without a tunnel because it binds to loopback only.

Database Operations

The schema lives in prisma/schema.prisma; migrations are in prisma/migrations/. The production database lives at ~/.slop/data/slop.db by default - outside the repo checkout so it survives re-clones.

OperationCommand
Apply pending migrations (dev, backed up first) pnpm exec tsx scripts/prisma-migrate-dev.ts && pnpm exec prisma generate
Push schema without migration history (reset test DB) pnpm exec prisma db push --accept-data-loss
Seed dev database make seed

Database files by context

FileContext
~/.slop/data/slop.dbDefault production / dev database (outside checkout)
./slop_build.dbThrowaway build-time DB used by CI (make build)
./data/slop_test_template.dbTemplate created once by vitest.global-setup.ts; workers copy it
./data/slop_test_<project>_<poolId>.dbPer-worker test DB, isolated per fork, deleted at test suite start

Testing

Unit tests

Run all tests
make test
Runs all four Vitest projects in CI mode.
With coverage enforcement
pnpm test:coverage
Runs all tests and enforces per-file coverage floors on critical paths.
Watch mode
pnpm test:watch
Interactive watch mode - re-runs affected tests on file save.
Single file
pnpm exec vitest run src/db/worker.repo.test.ts
Runs only the specified test file.
Single test by name
pnpm exec vitest run -t "appendEvent"
Filters by test name across all files.
Scripts tests (no DB)
pnpm test:scripts
Runs scripts/** tests only - no DB, no globalSetup. Fast.

Test project architecture

Vitest is configured with four projects in vitest.config.ts. DB-touching projects use forks to isolate SQLite state. maxWorkers: 1 keeps peak memory around 0.5 to 1.2 GB.

ProjectEnvironmentPoolIncludes
node-pure node threads Files that never touch SQLite: lib, types, middleware, github/client
node node forks (max 1) All src/**/*.test.ts not in node-pure
jsdom-pure jsdom threads All src/**/*.test.tsx not in the jsdom-db list
jsdom jsdom forks (max 1) Small set of component tests that open a DB connection

Test DB setup

Tests use the real SQLite database. Never mock your own modules. Reset state in beforeEach.

Step 1 - Global setup (vitest.global-setup.ts, runs once)

  • Deletes all ./data/slop_test_* files from the previous run.
  • Runs prisma db push into ./data/slop_test_template.db to create a fresh empty schema.
  • Enables WAL journal mode on the template so every worker copy inherits it.

Step 2 - Per-fork setup (vitest.setup.ts, runs in each fork worker)

  • Copies slop_test_template.db to ./data/slop_test_<project>_<poolId>.db (tagged by project + pool to prevent cross-project collisions).
  • Sets process.env.DATABASE_URL to the worker's copy.
  • Sets process.env.SLOP_WORKTREES_ROOT to a per-worker temp path.
  • Disconnects the Prisma client after each test file to avoid stale connections.

Coverage floors

pnpm test:coverage enforces per-file (perFile: true) coverage floors on the critical paths. Uncovered page or route files do not drag down the critical-path floor. Raise a floor when you raise its coverage.

Path patternApproximate floors
src/db/*.repo.ts85 to 95% across lines / statements / branches / functions (varies per file)
src/server/daemon/**80 to 95% per file
src/server/workers/**77 to 84% per file
src/server/github/client.ts73 to 80%
src/middleware.ts88 to 95%
Daemon coverage: For any change touching src/server/daemon/**, run pnpm test:coverage directly. Do not first run a scoped vitest run - the scoped run wastes a turn when coverage follows immediately after.

E2e tests (Playwright)

Run all specs
pnpm exec playwright test
Single spec file
pnpm exec playwright test e2e/<file>.spec.ts

Config: playwright.config.ts. Runs Chromium only. Auto-starts pnpm dev unless BASE_URL is set. The CI e2e job is currently disabled (if: false) pending spec files.

Code Quality

Biome
make lint / make format
Lint + format. Indent style: tabs. Import organization on. Excludes .next/, src/generated/, harness/.
TypeScript
make typecheck
strict: true, no any. Path alias @/* maps to src/*. harness/ excluded from the TS project.
dependency-cruiser
make deps
Enforces module boundary rules across src/. Fails CI on forbidden imports.
knip
make dead-code
Detects unused exports, files, and dependencies. Run before shipping a cleanup PR.
Surface map
make map
Regenerates docs/surface-map.md from source signatures. Commit hook runs it; CI fails if out of date.
lint-staged
(pre-commit hook)
Runs Biome on staged *.ts/tsx/js/json/css/md files on every commit. Never skip with --no-verify.
Em-dash rule: Do not use em-dashes in any source file. pnpm run fix:emdash replaces them with centered dots across src/, harness/, and docs/. make format runs this automatically.

Build

Build
make build
Runs prisma generate then next build. Requires DATABASE_URL to be set (CI uses file:./slop_build.db).
Production start
make start
Runs make build, then starts the production server. Binds to --hostname 127.0.0.1 with NODE_OPTIONS=--max-old-space-size=1536.

Adding a New Feature

Follow the command path: Server Action Daemon op Repo function UI.

1

Repo function - src/db/<feature>.repo.ts

Pure Prisma passthrough. No try/catch, no business logic, errors propagate to callers. One concern per file, named after the primary model it owns.

2

Daemon op - src/server/daemon/ops/<area>-ops.ts

Implement the operation. Add its signature to the Daemon interface in src/server/daemon/types.ts. Wire the production implementation in src/server/daemon/index.ts.

3

Server Action - src/app/actions/<area>.ts

Validate input, call getRunningDaemon().<op>(), call revalidatePath() on success. Return { ok: true } | { ok: false; error: string } - never throw to the client. Validation failures return the union too.

4

UI wiring

Wire the action through useActionState(action, null). Render the result as an inline role="alert" span for inline error feedback. Follow docs/DESIGN.md for visual conventions.

5

Tests

Add tests for the repo function and daemon op using the real SQLite test DB. Reset state in beforeEach. Do not mock your own modules. Update coverage floors if the new code raises coverage for a critical-path file.

6

Surface map - make map

Regenerate docs/surface-map.md from source signatures. The commit hook runs it automatically. CI fails if the file drifts from source.

Adding a Worker Phase

Worker phases live in src/server/workers/. The phase runner dispatches based on the current worker status.

1

Phase function - src/server/workers/<phase-name>.ts

Accept RunWorkerDeps (or a Pick<> of it) and return a status transition. Keep the function pure relative to its deps - no direct imports of collaborators.

2

Lifecycle update - src/types/worker.ts

Add the new phase status to WORKER_STATUSES const tuple and any relevant sub-sets (NON_TERMINAL_STATUSES, display badge mappings). Update reconcileStatus if needed.

3

Phase runner - dispatch switch/map

Register the new phase in the dispatch switch or map that the worker loop calls each cycle. Typically in src/server/workers/worker.ts.

4

Deps wiring - RunWorkerDeps

If the phase needs a new collaborator, add it to RunWorkerDeps in src/server/workers/worker.ts and thread it through the production wiring in src/server/daemon/index.ts. Tests pass fakes.

5

Tests

Write tests using the real SQLite test DB, passing fake implementations of every RunWorkerDeps collaborator. Reset DB state in beforeEach. Do not mock your own modules.

6

Coverage - pnpm test:coverage

Run coverage to verify src/server/workers/** floors are met (approximately 77% statements / 68% branches / 80% functions / 84% lines). Raise the floor values in vitest.config.ts if your new code exceeds the current numbers.

Dependency injection: worker steps and daemon polls take every collaborator through one Deps object (narrowed with Pick<>), never via direct imports. This is what makes the phases independently testable with simple fake objects.