Status: Current. Commands are owned by package.json.
Run:
git rev-parse --show-toplevel
git branch --show-current
git status --short --branchThe first command is the active repository root. Stay there. Superset sessions are linked worktrees and may live under ~/.superset/worktrees/, not /home/codex/Projects/DebateLab.
Never overwrite unrelated changes. Do not switch to the primary checkout because an old note names it.
- Node.js 22.x
- npm and
package-lock.json - Deno 2.x for Edge Function tests
- Docker and Supabase CLI 2.121.0, pinned in
package.json(npm ciinstalls it) for database work - Playwright browser binaries for browser gates
Install the locked JavaScript tree with the Node version from .nvmrc:
nvm use
npm ciThe repository is an npm workspace: npm ci at the root installs the web app and every package under packages/, and links @debatelab/core into node_modules. Run every script from the root.
Outside Superset, start Vite directly:
npm run devvite.config.ts defaults to port 8080 and maps @ to src/. Superset's Run action calls ./.superset/dev.sh, which allocates an available port from 8080-8179 for the current worktree and releases it through ./.superset/teardown.sh. Do not assume a Superset workspace uses 8080.
For another explicit local port:
npm run dev -- --host 127.0.0.1 --port 8081Automated browser tests always start a fresh server. npm run verify puts it on this worktree's port from 18100-18179 (bash .superset/e2e-port.sh); a bare Playwright run falls back to 127.0.0.1:18080.
A plain npm run dev talks to the production Supabase project unless the browser toggle says staging. For anything Adam opens from another device, run the staging preview instead. This local server is not a Vercel deployment. Hosted Vercel previews are opt-in; operations.md has the command.
npm run dev:preview -- --port 9201 --strictPortdev:preview binds to 127.0.0.1 and sets VITE_STAGING_PREVIEW=true. In that mode Vite also accepts *.ts.net hostnames, so tailscale serve can forward to it however it was started. The app is then pinned to staging and calls Supabase at /__staging-supabase on its own origin, which Vite proxies to the staging project (see stagingPreviewProxy in vite.config.ts). Requests stay same-origin, so Edge Function CORS lists need no preview entries. The staging strip shows, without the switch to production.
Two ports are involved. The dev server listens locally on a port in 9200-9299. The tailnet-preview skill publishes it on a tailnet HTTPS port in 9100-9149, and that HTTPS origin is what the browser sees. Staging Auth allows redirects to https://codex-remote.tailab0ab3.ts.net:91*/**, so Google sign-in returns to the preview only when the published port starts with 91. If you run tailscale serve by hand, publish on 9100-9199. Demo codes and email codes must be staging ones: staging has its own users.
The checked-in .env.example contains optional client observability variables:
VITE_SENTRY_DSNVITE_POSTHOG_KEYVITE_POSTHOG_HOST
Two more build variables turn on the no-login mini round on /ai-debate: VITE_DEMO_ENABLED=true and VITE_TURNSTILE_SITE_KEY (Cloudflare Turnstile's public site key). Without both, the page shows its static sample round. The demo-rebuttal Edge Function has its own DEMO_ENABLED switch; see supabase/README.md.
Production builds also read the Sentry build variables SENTRY_AUTH_TOKEN, SENTRY_ORG and SENTRY_PROJECT. Local builds leave them unset and skip source map upload. See operations.md.
The frontend Supabase environment is resolved by src/lib/supabase/env.ts, not by those variables. Public browser configuration is distinct from server secrets.
Never put service-role keys, AI provider keys, Stripe secrets, webhook secrets, demo-code peppers, or integrity secrets in Vite variables. Any VITE_* value can reach the browser bundle.
The main entry points are:
src/main.tsxfor startup and platform integration.src/App.tsxfor routes and top-level providers.src/pages/for route-level composition.src/components/features/for feature UI.src/lib/stores/for state and Supabase hydration.src/lib/ai/client.tsfor AI requests.packages/core/src/ai/schemas/for response validation.
Platform-neutral code shared with the iOS app lives in packages/core/. It mirrors src/lib/ paths, and each old path re-exports it, so web code keeps importing @/lib/.... To add a module, put it under packages/core/src/, import siblings by relative path, leave a re-export at the old src/lib/ path if web code used it, and follow the package's no-DOM rule. The stores, the AI client, and the i18n catalogs stay in src/ for now.
Follow existing functional React, Zustand, Zod, and shadcn/Radix patterns. Reuse current helpers before adding another abstraction.
Prove a change by using it: run the closest Playwright spec or drive it with the verify-debatelab skill. There are no unit tests to update (testing.md). Run lint, type checking, and build before handing work off.
The local PostgreSQL-backed Hocuspocus service lives under services/collaboration/. It backs the collaborative Workspace, which is hidden and on hold; its routes register only when VITE_COLLABORATION_URL is set. Run its focused tests with:
npm run test:collaborationStart it only with explicit local Supabase values for COLLABORATION_DATABASE_URL, COLLABORATION_PORT, COLLABORATION_ALLOWED_ORIGINS, SUPABASE_URL, and SUPABASE_ANON_KEY. The service binds loopback, takes one PostgreSQL session advisory lock per loaded document epoch, and closes sessions on access or restore notifications. It is not a production deployment.
Edge Functions live under supabase/functions/. Shared server code belongs in _shared only when at least two functions use the same boundary or policy.
Run native Deno tests with:
npm run test:edgeThese tests intentionally use Deno permissions declared in the npm script. Do not broaden permissions just to make a test pass.
Handler authentication remains necessary where supabase/config.toml disables gateway JWT verification. See ../supabase/README.md for function groups and auth boundaries.
Migrations live in supabase/migrations/ and run in filename order. Database and policy tests live under supabase/tests/.
In a Superset workspace, use the repository's locking wrapper:
./.superset/db-gate.shOutside Superset, run the underlying commands directly:
npx supabase db start
npm run test:db
npx supabase stop --no-backupThe wrapper and verify.sh share two local Supabase slots across worktrees. Slot 1 keeps today's ports (API 54321, database 54322, app 18081, provider 18090, collaboration 12345) and ~/.superset/debatelab-db.lock. Slot 2 adds 100 to ports and uses ~/.superset/debatelab-db.slot2.lock plus the provider container debatelab-fake-openrouter-s2. The authoritative port table is .superset/lib/slot.sh. Allocation takes the first free slot or waits up to 600 seconds for either; DEBATELAB_STACK_SLOTS=1 restricts it to slot 1. Supabase containers and networks are named per worktree. The helper reserves subnets 10.254.231.0/24 and 10.254.232.0/24 for slots 1 and 2, avoiding the Pi's Tailscale route that overlaps Docker's next default 192.168.0.0/20 network. It labels each project network so Supabase cleanup removes it. An existing network on a different subnet is refused; stop its owning stack before retrying. A plain lock file, a dead owner, or a directory without a PID older than 120 seconds is reclaimed automatically. Lock operations require Python 3 and use only its standard library. A persistent .guard file serializes recovery and PID publication with a kernel file lock; the file itself never holds a slot, and process death releases the kernel lock. Leave guard files in place so every caller locks the same inode. Use separate worktrees, and use the wrapper rather than starting Supabase directly when another task may be active. The task that starts a stack stops it before finishing, including after failures.
Memory-heavy jobs share ~/.superset/debatelab-heavy.lock. npm run verify wraps its build in .superset/heavy.sh, and the full database gate wraps its browser phase. Use bash .superset/heavy.sh npx expo export for a local Expo export too. The helper skips locking when CI is set or flock is unavailable. It serializes these jobs without preventing two database stacks from running.
When several agents need a database at once, each can run its own Docker-free project with scripts/agent-stack.sh. It uses the Supabase CLI's experimental stack engine (CLI 2.121 or newer) on the native runtime: no lock, no shared ports, and a cold start of about 20 seconds.
scripts/agent-stack.sh --name my-task start # applies every migration; prints API_URL and DB_URL
scripts/agent-stack.sh --name my-task test # what npm run test:db runs: pgTAP and the concurrency scripts
scripts/agent-stack.sh --name my-task psql -c 'select 1'
scripts/agent-stack.sh --name my-task advisors # Supabase security advisors
scripts/agent-stack.sh --name my-task reset # re-apply migrations after editing them
scripts/agent-stack.sh --name my-task serve fn.env # Edge Functions with fn.env's variables (foreground)
scripts/agent-stack.sh --name my-task destroy # always, including after failuresPorts are assigned by the CLI; read them from the env file start prints. The stack cannot serve the magic-link email template, so the script runs the project from an overlay directory under ~/.cache/debatelab-stacks/ that drops that block and symlinks everything else. Use it for migrations, pgTAP, the concurrency scripts and RLS queries. The full-stack browser journeys still need ./.superset/db-gate.sh, which is also what CI runs.
Never point local tests at staging or production. Never apply a migration to an unverified project.
playwright.config.ts starts Vite on an isolated port and refuses to reuse an existing server.
P=$(bash .superset/e2e-port.sh) && CI=1 PORT=$P PLAYWRIGHT_BASE_URL=http://127.0.0.1:$P npm run test:e2eFailure artifacts go to ignored test-results/ and playwright-report/. Protected staging disables traces, screenshots, and video.
Read testing.md before running WebKit or staging.
Agentic tests (agentic/*.e2e.ts, the e2e runner) drive the verification stack, never a dev server you started. Run them with .claude/skills/verify-debatelab/bin/verify.sh e2e run after verify.sh up. See testing.md.
Run:
npm run docs:checkThe checker includes new Markdown by default. It validates supported local link forms and rejects actionable retired checkout paths. It does not validate remote URLs, Markdown syntax, or heading fragments.
When a command or boundary changes, update the focused document that owns it. Avoid duplicating source trees, schema listings, dependency catalogs, or model lists.
Run git rev-parse --show-toplevel. Use that result. Do not trust a copied cd command over the current worktree.
Choose a free explicit development port. Do not point browser automation at an unrelated server.
Use the required CI=1, PORT, and PLAYWRIGHT_BASE_URL values. Playwright sets reuseExistingServer: false.
Check whether another worktree owns the default containers or ports. Coordinate and stop the existing local stack rather than deleting containers blindly.
Check the active client environment and environment-specific browser storage. Do not copy production sessions into staging or alter server authorization to hide a client configuration error.
All three staging values must be present together. Partial configuration is rejected. A disabled or skipped staging workflow is not a pass.