Imported from cweber12/bouldering-beta-app (
AGENTS.md). Install upstream withnpx skills add cweber12/bouldering-beta-app. Copyright stays with the author.
Bouldering Beta — Agent Rules
Stack Snapshot
| Concern | Library / Version |
|---|---|
| Framework | Next.js 16.2.1 — App Router, "use client" boundary, webpack 5 |
| UI | React 19.2.4 |
| Language | TypeScript strict, "module": "esnext", "moduleResolution": "bundler" |
| Styling | Tailwind CSS v4 |
| Computer vision | @techstark/opencv-js ^4.12.0 (WASM, main thread only) |
| Pose estimation | @mediapipe/tasks-vision ^0.10.34 (MediaPipe Pose Landmarker, GPU delegate) |
| Testing | Vitest ^4.1.1 + jsdom + @testing-library/react ^16.3.2 |
| Path alias | @/* → project root |
⚠ This is NOT the Next.js you know. APIs, conventions and file structure may all differ from training data. Read the relevant guide in
node_modules/next/dist/docs/before writing any code. Heed deprecation notices.
Project Architecture
pipeline/ Framework-agnostic processing modules (NO React imports), grouped by
concern — module map and pipeline rules in pipeline/CLAUDE.md
hooks/ React hooks that wire pipeline modules to UI state
useOpenCV.ts loads /public/opencv.js; exposes { ready, cv }
usePoseModel.ts loads MediaPipe Pose Landmarker; exposes { ready, model }
useVideoProcessor.ts seek loop → pose estimation → ORB extraction
useImageMatcher.ts upload image → extractFeatures → matchOrbFeatures
usePoseVideo.ts auto-renders annotated WebM from match result
useClickOutside.ts close-on-outside-click seam (mousedown/pointerdown)
useEscapeKey.ts ESC-to-close seam
useMeasuredHeight.ts ResizeObserver callback ref → measured px height
components/ React components grouped by why they live there
ui/ generic primitives (LoadingSpinner, ThemeToggle, InfoDropdown,
ComboInput, ImageCropper, LocationAutocomplete, Modal,
FullscreenModal)
layout/ app chrome & page shells (NavBar, AccountMenu, Preloader,
Providers, LoadingGate, ToolPageShell, ToolRouteHeader)
skeleton/ skeleton-overlay UI (FramePlayer, SkeletonStylePanel)
capture/ crop + camera (CropBoxOverlay, CameraRecorderModal)
run/ run-type domain primitives (RunTypeBadge, RunStatusDot)
scan/ compare/ route/ routes/ map/ feature-owned components
shared/ ClimbDetailModal only (pending removal in redesign)
storage/
sessionStore.ts in-memory Map; exports RunType, RouteAttempt (includes runType, rating?, notes?)
utils/
poseConstants.ts MP_KP indices, MP_KP_NAMES, MP_SKELETON_EDGES (MediaPipe/BlazePose topology)
cropFraction.ts CropFraction type + DEFAULT_CROP (plain data, no React)
leaflet.ts initLeafletMap() — CartoDB tiles + icon fix (ClimbsMap, MapPicker)
cvHelpers.ts
workers/ Legacy Web Worker files (keep, do not delete)
Critical Coding Rules
Color system and theming
- All colors must use semantic CSS tokens defined in
app/globals.css(@theme inlinefor dark defaults,.theme-lightclass for light overrides). - Never use raw Tailwind palette classes for status/semantic colors: no
red-400,amber-900,emerald-500,black/60etc. where a semantic token exists. - Semantic token classes available:
text-danger,bg-danger-surface,border-danger-border,text-caution,bg-caution-surface,border-caution-border,text-send,bg-send,bg-send-surface,text-attempt,bg-attempt,bg-attempt-surface,text-fg-inverse. - Run-type chips:
bg-send/80 text-fg-inverse(send) andbg-attempt/80 text-fg-inverse(attempt). Run-type badges:bg-send-surface text-send/bg-attempt-surface text-attempt. - Error banners:
bg-danger-surface border-danger-border text-danger. Warning banners:bg-caution-surface border-caution-border text-caution. - Modal loading overlays:
bg-surface/70 backdrop-blur-sm(notbg-black/40). - Theme is toggled via
useTheme()fromhooks/useTheme.tsx.ThemeProvideris mounted incomponents/layout/Providers.tsx. ThemeTogglecomponent lives incomponents/ui/ThemeToggle.tsx— import and place it in the NavBar right-side controls.- A FOUC-prevention inline script in
app/layout.tsxreadslocalStorageand appliestheme-lightortheme-darkclass to<html>before React hydrates. - Canvas drawing values (map pins, skeleton overlays) use
utils/theme.tsdark/lightobjects — keep them in sync withglobals.csstokens.
OpenCV and pipeline modules
- See
pipeline/CLAUDE.mdfor OpenCV (cv) rules, pipeline module rules, the execution chain, and pipeline testing rules. Auto-loaded when working underpipeline/.
Hooks
- Hooks consume pipeline functions; they own state transitions and error boundaries.
- Expose
orbStatus: "idle" | "extracting" | "ready" | "failed"fromuseVideoProcessorso the UI never shows image upload until ORB extraction has completed. imageFilestate lives in the parent component and is passed tousePoseVideo— hooks do not own File objects.- Dismiss/modal seams — do not hand-roll close-on-outside-click or ESC effects. Use
useClickOutside(ref, onOutside, enabled, eventType?)anduseEscapeKey(onEscape, enabled?). For dialogs/sheets usecomponents/ui/Modal(portal + backdrop) and for the crop fullscreen viewscomponents/ui/FullscreenModal— both already compose the two hooks.
TypeScript
- Never use
any. The only permitted exceptions (type CV = any,type PoseDetector = anyfor WASM bindings) are documented inpipeline/CLAUDE.md.
Run classification & S3 key format
RouteAttempt.runTypeis"attempt" | "send"(re-exported asRunType).- Optional
rating?: stringandnotes?: stringare stored alongside each run. - S3 key format:
RouteData/{userId}/{state}/{area}/{route}/run-{timestamp}-{attempt|send}.json. - ID format:
run-{timestamp}(without the type suffix). - Legacy
attempt-{timestamp}.jsonfiles are still loadable — defaultrunTypeto"attempt". - UI colours: amber for attempts, emerald for sends.
Profile & social
- Profile and following data live in S3 under the
ProfileData/prefix (same bucket as route data) — there is no separate database service. - Storage formats, API routes, and validators: see
docs/agents/profile.mdbefore profile/social work.
Authentication (Firebase)
- Auth uses Firebase Auth (client SDK) with server-issued HTTP-only session cookies — no localStorage tokens, no Supabase.
utils/firebase/client.ts— browser Firebase app/auth (getFirebaseAuth). Client sign-in/sign-up viasignInWithEmailAndPassword/createUserWithEmailAndPassword.utils/firebase/admin.ts— server-only Firebase Admin SDK singleton (getAdminAuth). Initialised fromFIREBASE_PROJECT_ID,FIREBASE_CLIENT_EMAIL,FIREBASE_PRIVATE_KEY(server-side env, neverNEXT_PUBLIC_).utils/firebase/constants.ts— Edge-safe shared constants:SESSION_COOKIE_NAME(__session),SESSION_COOKIE_MAX_AGE_MS(14 days). Never importfirebase-adminhere (used by Edge middleware).- Session flow: client
signIn()(hooks/useAuth.tsx) gets a Firebase ID token →POST /api/auth/sessioncallsadminAuth.createSessionCookie()and sets the__sessionHTTP-only cookie →DELETE /api/auth/sessionclears it on sign-out. proxy.tsruns in the Edge runtime and only checks__sessioncookie presence to redirect unauthenticated users away from/scan,/compare,/profile(UX guard, not full verification — firebase-admin is unavailable in Edge).app/api/s3/shared.tsverifySession()performs the real check server-side viagetAdminAuth().verifySessionCookie(cookie, true)(checks signature + revocation).hooks/useAuth.tsxprovidesAuthProvidercontext +useAuth()hook. File must stay.tsx— it contains JSX.- All S3 API routes call
getAuthUserId()(verifies the session cookie) and return 401 when unauthenticated. isValidKey()andisValidPrefix()enforce that every S3 key is scoped to the authenticated user:RouteData/{userId}/....hooks/useS3Storage.tsderives user-scoped keys viaderiveS3Key(userId, attempt).components/layout/NavBar.tsxshowsPUBLIC_TABS(Home, Docs) for unauthenticated users andAUTH_TABS(all tabs) for authenticated users.
Testing
- Test files mirror the source tree under
__tests__/. - Use
vi.stubGlobal+vi.unstubAllGlobals()in afterEach for DOM globals. - Pipeline-specific testing rules (jsdom
ImageDatacasts, OpenCV module-boundary mocks,FakeOrbWorker): seepipeline/CLAUDE.md.
Media previews with crop overlays
- See
components/CLAUDE.mdfor the scan/compare page flows and all media-preview/crop-overlay patterns (object-fill rule, viewport-fit/fullscreen/height-filling styles, expand button, fullscreen video ref). Auto-loaded when working undercomponents/.
Security Review Checklist
When adding or changing code, verify the following:
- Open redirect — Any
router.push(url)orredirect(url)using user-supplied input must validate the target is a relative path (startsWith("/"), notstartsWith("//"), no://). - User-scoped data — Every S3 key or prefix must include the authenticated user ID. Server-side API routes must call
isValidKey(key, userId)/isValidPrefix(prefix, userId)before any S3 operation. - Input length limits — User-supplied strings (state, area, route names, notes) must be length-limited before storage. S3 keys must not exceed 1024 bytes.
- Error sanitisation — AWS/infrastructure error details must not be returned to the client in production. Use
awsErrorMessage()which logs details server-side and returns a generic message. - Auth gating — Protected routes (
/scan,/compare,/profile) must be guarded byproxy.ts. API routes must callgetAuthUserId()and return 401 when null. - File extensions — Any file containing JSX must use
.tsx(not.ts). Verify after renaming or creating hook/component files. - Cookie security — The Firebase
__sessioncookie ishttpOnlywithSameSite=strictandSecure(in production). Never store ID tokens or session cookies inlocalStorage. - No secrets in client code — Only
NEXT_PUBLIC_*env vars may be referenced in client components. AWS credentials and the Firebase Admin service-account vars (FIREBASE_PROJECT_ID,FIREBASE_CLIENT_EMAIL,FIREBASE_PRIVATE_KEY) must stay server-side.
After Every Code Change
Run these checks in order, then commit without prompting the user:
# 1. Type-check (zero output = success)
npx tsc --noEmit
# 2. Lint
npx eslint .
# 3. Targeted tests for changed files/features
npx vitest run <targeted test files>
# 4. Stage and commit
git add .
git commit
Fix TypeScript errors before proceeding. Do not disable tsc checks.
The agent MUST run npx tsc --noEmit, npx eslint ., targeted npx vitest run ...,
and git add . + git commit after every code change session without waiting to be asked.
Branch isolation workflow (required for non-interference)
Non-interference is enforced by keeping each agent's working tree separate, not by every agent minting a worktree:
- Copilot always creates a dedicated worktree + branch per task
(
git worktree add ..\beta-scanner-<task> -b <type>/<task-name>), so it never touches the primary checkout. - Copilot completion is PR-first (push branch + open PR + cleanup after merge);
see
.github/copilot-instructions.mdfor Copilot-only completion and cleanup rules. - Claude Code (this interactive session) works on the primary checkout with a per-issue task branch — no separate worktree. Because Copilot is isolated in its own worktree, editing the primary checkout cannot collide with it. Only create a worktree here when a second Claude effort is already live on the primary checkout.
Shared rules for both agents:
- Start each task branch from
main. - Keep one issue per branch; do not batch unrelated fixes.
- Avoid destructive git commands (
reset --hard, force-push, rewriting shared history). - Clean up on completion. A task is not complete until its now-merged task
branch — and any worktree created for it — is removed. Once the branch is
merged into
main, delete it (git branch -d <task-branch>) and, if a worktree was created, remove it first (git worktree remove <path>) while it is clean and offmain. Never leave a merged branch or its worktree behind. - Verify the cleanup. After merging and closing the issue, run
node scripts/audit-issues.mjsand resolve every warning it emits, including merged-but-undeleted local branches. A clean audit is the completion gate.
Claude setup (primary checkout, task branch):
git fetch origin
git switch main
git pull --ff-only
git switch -c <type>/<task-name>
Copilot setup (dedicated worktree):
git fetch origin
git switch main
git pull --ff-only
git worktree add ..\beta-scanner-<task> -b <type>/<task-name>
Local merge policy
- Default behavior is local validation + local commit only (no automatic push).
- Do not merge automatically unless the user explicitly requests completion merge.
- If requested, merge locally using
git merge --no-fffrom the primary checkout after checks pass. - A completion merge is not finished until cleanup runs. Immediately after
the merge (and after closing the issue), delete the merged task branch and
remove any worktree that was created for the effort, then confirm the
audit-issues.mjscleanup is clean. Do not end the session with a merged branch or worktree still present.
Merge-on-complete sequence when explicitly requested:
# in task branch (primary checkout) or task worktree
npx tsc --noEmit
npx eslint .
npx vitest run <targeted test files>
git add .
git commit
# in primary checkout
git switch main
git merge --no-ff <task-branch>
# cleanup — required to finish the task
git branch -d <task-branch>
# if a worktree was created for this effort (must be clean and off main first):
git worktree remove ..\beta-scanner-<task>
node scripts/audit-issues.mjs # resolve every warning, incl. merged branches
Issue tracking
- When implementing a
.scratch/issue, follow the PRD lifecycle loop indocs/agents/issue-tracker.md: tackle exactly one issue per branch, sequence issues by number, branch each one frommain, write the activeBranch:line into the.scratch/.../issues/*.mdfile when work starts, then merge withgit merge --no-ffand close the issue in that same.scratchfile by settingStatus: done+Merged: <sha>in the same step. - If a PR/issue is already closed or merged, immediately reconcile the
corresponding
.scratch/.../issues/*.mdfile in that same session: setStatus: done, preserveBranch:, addMerged: <sha>, and tick satisfied acceptance checkboxes before ending work. - An issue is never
doneuntil its code is merged, and merged code never lands without moving its issue todone— the two happen together. This includes batch commits: a commit that lands several issues' work closes every one of them (status +Branch:+Merged:+ ticked checkboxes) immediately. - The PRD's own
Status:moves with its issues (ready-for-agent→in-progresson first landing →donewhen all issues are terminal), and an issue replaced by newer work is closedwontfixwith aSuperseded-by:pointer — seedocs/agents/issue-tracker.md. PRD folders are grouped by disposition (.scratch/actionable/,.scratch/parked/,.scratch/done/); when disposition changes, move the folder and update the PRD'sDisposition:line in the same commit. - Before ending a PRD work session, run
node scripts/audit-issues.mjsand resolve any drift it reports (unclosed/unmerged issues, incomplete tracking blocks, PRD status drift, dangling supersession pointers); delete local branches it warns about.
README maintenance
- When a code change adds, removes, or renames user-visible features, pages,
storage formats, or API behaviour, update
README.mdin the same commit. - Keep the S3 key format example, Pages table, and feature summary in the README consistent with the actual code.
Commit Message Convention
<type>: <imperative summary under 72 chars>
- bullet describing what changed
- bullet describing what changed
one or two sentences explaining why the change was made
Formatting rules:
- No quotation marks anywhere in the commit message.
- No explicit What, How, or Why labels.
- Use a short summary line, then a blank line, then bullets, then a blank line, then a brief why paragraph.
Types: feat, fix, refactor, test, chore