Development Guide
Everything you need to clone, run, test, and contribute to Slop.
Prerequisites
nvm install 24 or install from nodejs.org. Verify: node --versionpackageManager field in package.json via corepack. Install: corepack enable && corepack prepare. Verify: pnpm --versionnode-pty uses a native binding compiled for the host OS. CI runs Ubuntu; local dev targets macOS. Linux is not supported for local development.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
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.
| Variable | Default | Purpose |
|---|---|---|
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>. |
/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
make dev
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.make restart
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.
| Operation | Command |
|---|---|
| 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
| File | Context |
|---|---|
~/.slop/data/slop.db | Default production / dev database (outside checkout) |
./slop_build.db | Throwaway build-time DB used by CI (make build) |
./data/slop_test_template.db | Template created once by vitest.global-setup.ts; workers copy it |
./data/slop_test_<project>_<poolId>.db | Per-worker test DB, isolated per fork, deleted at test suite start |
Testing
Unit tests
make test
pnpm test:coverage
pnpm test:watch
pnpm exec vitest run src/db/worker.repo.test.ts
pnpm exec vitest run -t "appendEvent"
pnpm test:scripts
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.
| Project | Environment | Pool | Includes |
|---|---|---|---|
| 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 pushinto./data/slop_test_template.dbto 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.dbto./data/slop_test_<project>_<poolId>.db(tagged by project + pool to prevent cross-project collisions). - Sets
process.env.DATABASE_URLto the worker's copy. - Sets
process.env.SLOP_WORKTREES_ROOTto 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 pattern | Approximate floors |
|---|---|
src/db/*.repo.ts | 85 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.ts | 73 to 80% |
src/middleware.ts | 88 to 95% |
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)
pnpm exec playwright test
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
.next/, src/generated/, harness/.strict: true, no any. Path alias @/* maps to src/*. harness/ excluded from the TS project.src/. Fails CI on forbidden imports.docs/surface-map.md from source signatures. Commit hook runs it; CI fails if out of date.*.ts/tsx/js/json/css/md files on every commit. Never skip with --no-verify.pnpm run fix:emdash replaces them with centered dots across src/, harness/, and docs/. make format runs this automatically.
Build
make build
prisma generate then next build. Requires DATABASE_URL to be set (CI uses file:./slop_build.db).make start
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Deps object (narrowed with Pick<>), never via direct imports. This is what makes the phases independently testable with simple fake objects.