alwaysApply: true
error wolf compresses noisy stack traces and build logs. The work happens in the browser with the user's own OpenRouter key. The server does three things only: it sets the consent cookie, it reads the cookies for the /hunt gate, and it serves the app.
The product logic is in src/lib/. That code imports no framework APIs. Keep it
that way.
- Cold, professional tone. No flattery. Objective corrections only.
- Stay focused. Do not pad answers.
- Prefer existing patterns over new frameworks or parallel design systems.
- Refine touched code in place: same behavior, fewer moving parts.
- Prefer single-purpose modules, components, and functions with consistent names.
- This repo has no
.agents/skills/tree. Use this file,README.md, andcomponents.jsonas the primary conventions.
| Area | Choice |
|---|---|
| Package manager | pnpm (pnpm-lock.yaml) |
| Runtime / app | TanStack Start on Cloudflare Workers (file routes: src/routes/) |
| Routing | TanStack Router (src/routeTree.gen.ts is generated — do not edit it) |
| UI | React 19 |
| React Compiler | babel-plugin-react-compiler through @rolldown/plugin-babel (@vitejs/plugin-react v6 uses Oxc) |
| Language | TypeScript (strict: true in tsconfig.json) |
| Build / dev | Vite 8 (vite dev, vite build) |
| Styling | Tailwind CSS v4 through @tailwindcss/vite |
| Components | shadcn/ui (shadcn, components.json), @base-ui/react, class-variance-authority |
| Icons | Hugeicons (@hugeicons/react, @hugeicons/core-free-icons) |
| Fonts | @fontsource/space-mono, imported from src/globals.css |
| Theming | Local provider in src/components/theme-provider.tsx plus a pre-paint inline script |
| Images | vite-imagetools for the background photo, plain <img> for the logo |
| Animation helpers | tw-animate-css (imported from src/globals.css) |
| Lint | Oxlint (.oxlintrc.json): TypeScript and React rules, --type-aware through oxlint-tsgolint |
| Format | Oxfmt (.oxfmtrc.json): Prettier-compatible options plus sortTailwindcss |
| Test | Vitest (vitest.config.ts), unit tests beside the sources in src/lib/ |
| Error monitoring | @sentry/react (browser), @sentry/cloudflare (Worker), @sentry/core (tunnel), proxied at /wdyd |
Module system: ESM. package.json sets "type": "module".
src/routes/— file routes.__root.tsxholds the document shell, the head metadata, the theme provider, and the error boundary. A directory or file with a-prefix is route-local UI and gets no URL, such as-hunt/.src/lib/— the product. Simplify engines, the OpenRouter client, routing estimation, cost models, and token estimation. No framework imports.src/lib/server/— the only server-side modules. They adapt the TanStack request helpers to the framework-free helpers insrc/lib/.src/lib/sentry/— the DSN, org, project, tunnel path, option objects, and the scrubber. Onlybrowser.tsimports a Sentry SDK at runtime, and it is browser-only. Everywhere else the SDK imports are type-only.src/components/— shared UI.ui/holds the shadcn primitives.src/hooks/— shared hooks.src/assets/— images that the build processes. Files inpublic/ship as-is, so do not put a large source image there.src/client.tsx,src/router.tsx,src/server.ts— the browser entry, the router factory, and the Worker entry.src/start.ts— global request middleware, discovered by file name. It is bundled into both the browser and the Worker, so it must never import@sentry/cloudflare, which pulls innode:async_hooksandcloudflare:workers. Use the isomorphic helpers from@sentry/core.
Colocate by feature as the app grows. Keep route-local UI under src/routes/
with a - prefix.
| URL | File | Notes |
|---|---|---|
/ |
src/routes/index.tsx |
Marketing page and the consent button. |
/hunt |
src/routes/hunt.tsx |
The product. A loader gates it on consent. |
/privacy |
src/routes/privacy.tsx |
|
/robots.txt |
src/routes/robots[.]txt.ts |
Square brackets escape the dot in a path. |
/sitemap.xml |
src/routes/sitemap[.]xml.ts |
|
/wdyd |
src/routes/wdyd.ts |
Sentry envelope proxy. POST only. |
Do not change these URLs. The consent flow and the sitemap reference them, and the tunnel path is compiled into every browser bundle already in the wild — renaming it silently stops crash reports from older tabs.
/wdyd answers every method except POST with 405. Without those handlers the
router falls through to the SPA shell and serves the whole app as HTML with a
200, which robots.txt (Allow: /) then invites crawlers to index.
There are four. Handle each with care.
src/lib/server/consent.tssets the consent cookie. The home route then navigates to /hunt. The cookie attributes must not change.src/lib/consent.tsclears the same cookie from the browser with the same flags. Do not throw aredirectfrom this server function. An imperative server-function call receives it as a rawResponse, and the navigation never happens.src/routes/hunt.tsxreads the consent cookie and the OpenRouter key cookie in a server function. It must keep the legacy cookie names. If you drop them, existing users lose their consent.src/server.tswraps the TanStack Start server entry withwithSentry. Itsfetchdeliberately declares one parameter and its options callback deliberately takes none. Both are what let it typecheck with no cast:Envnever has to resolve, which matters becauseworker-configuration.d.tsis gitignored and absent in CI. Annotating it withExportedHandlerorEnvbreaks CI only, never a local typecheck.src/routes/wdyd.tsforwards Sentry envelopes.handleTunnelRequestvalidates each envelope's DSN against an allowlist, which is the only thing stopping it from being an open relay into somebody else's Sentry project. The Worker SDK must never settunnel: it posts to Sentry directly, so an error thrown while handling a tunnel request cannot tunnel itself.
- Tailwind v4 runs from
src/globals.css. That file holds the Tailwind import, the font imports, the@theme inlinetokens, and the:rootand.darkvariables. - Dark mode uses the
.darkclass on<html>. An inline script in__root.tsxsets the class before first paint. Without it, a dark-mode user sees a light flash. - New shadcn pieces:
pnpm dlx shadcn@latest add <component>. - Imports: use the
@/*alias. It points at./src. - Do not add a second component library or icon set.
Use pnpm.
| Command | Purpose |
|---|---|
pnpm dev |
Vite dev server on port 3000 |
pnpm build |
Production build into dist/ |
pnpm preview |
Serve the production build |
pnpm deploy |
Deploy with Alchemy |
pnpm test |
Vitest, one run |
pnpm test:watch |
Vitest in watch mode |
pnpm lint |
Oxlint (--type-aware) |
pnpm lint:fix |
Oxlint with safe fixes |
pnpm format |
Oxfmt (write) |
pnpm format:check |
Oxfmt check-only |
pnpm typecheck |
tsgo --noEmit (@typescript/native-preview) |
pnpm typecheck:tsc |
Classic tsc --noEmit (parity check) |
pnpm cf-typegen |
Generate Worker binding types |
There is no combined check script. After a substantive edit, run
pnpm format, pnpm lint, pnpm typecheck, and pnpm test.
Node version: package.json asks for 24.x. CI uses 24.
wrangler.jsonc configures the Worker. Read it before you change the build.
mainpoints atsrc/server.ts. That file wraps the TanStack Start server entry.compatibility_flagsincludesnodejs_compat.- There is no
routesblock. The site serves from*.workers.devuntil the custom domain moves.
Alchemy injects its Cloudflare Vite plugin during alchemy plan and
alchemy deploy. Local Wrangler checks use root wrangler.jsonc
(main: src/server.ts). Keep that file aligned with alchemy.run.ts.
Workers have no filesystem. Do not use node:fs or process.cwd() in code
that the server bundle reaches. Read files at build time instead. See
src/lib/example-traces.ts and src/lib/announcements/load.ts.
Bundle size. Production runs on the Cloudflare Workers Paid plan (10 MiB
gzip). After pnpm build, run
pnpm exec wrangler deploy --dry-run --name error-wolf dist/server/server.js
to print the current size. This is a read-only size check, not the deployment
path. Keep the Worker under the paid limit; treat 3 MiB as a soft target so a
plan downgrade would still fit.
To test against the Workers runtime and not the Vite dev server, run
pnpm build, then pnpm exec wrangler dev. Node API differences appear there.
Alchemy deploys this Worker. Same-repository pull requests run an Alchemy
plan after CI succeeds. Fork pull requests run CI only. A push to master
deploys production after CI succeeds. The one-time stacks/github.ts stack
creates the preview (read-only plan token + Alchemy state credentials) and
production (deploy) environment secrets used by these jobs.
Set VITE_SITE_URL as a build variable in the Cloudflare project. Vite inlines
it at build time, so it must be present in the Cloudflare build and not only in
GitHub Actions.
.github/workflows/ci.yml runs the checks on Blacksmith runners: format, lint,
typecheck, test, build, and the Worker size report. Same-repository pull
requests then run alchemy plan --stage prod with the preview environment.
.github/workflows/deploy.yml deploys production after a successful push CI on
master.
pnpm deploy runs alchemy deploy --stage prod for a deploy by hand. It needs
a Cloudflare API token with the same deployment permissions as CI.
Vite inlines every VITE_* variable into both bundles at build time.
| Name | Where | Purpose |
|---|---|---|
VITE_SITE_URL |
Build | Canonical origin for metadata and the sitemap. |
SENTRY_AUTH_TOKEN |
Build | Uploads source maps. Optional. |
SENTRY_AUTH_TOKEN is the only Sentry secret. The DSN, the org, and the project
are public by design and live in src/lib/sentry/constants.ts. Without the
token the Vite plugin disables itself and the build still succeeds.
Vite inlines a VITE_* value into the browser bundle, so never put a secret in
one. The repo is public.
- TypeScript: strict mode. The
@/*alias points at./src.tsgois the primary typechecker. Thetypescriptpackage stays forpnpm typecheck:tscand for editor tooling. - Oxfmt (
.oxfmtrc.json): LF, no semicolons, double quotes, 2 spaces, print width 80, trailing commases5. Tailwind class sorting readssrc/globals.cssand thecnandcvafunctions. - Oxlint (
.oxlintrc.json):eslint-plugin-react-compilerloads as a jsPlugin. The ignore list coversnode_modules,dist,.wrangler, the generated route tree, andscripts/example-bg-photo-tuner/**. - React: the app renders on the server and hydrates in the browser. There is
no React Server Components boundary, so a
"use client"directive means nothing here. Do not add one. - Utilities: merge Tailwind classes with
cn()from@/lib/utils.
Vitest runs in the node environment. Test files sit beside their sources in
src/lib/. Run pnpm test.
These tests cover the parts that are hard to check by hand: cost models, model endpoint fetching, preprocessing, the OpenRouter client, recent results, and run deadlines. Keep them passing. If a test needs a change, change its framework assumptions and never the expected behavior.
src/lib/sentry/scrub.test.ts and src/lib/sentry/options.test.ts are a
different kind of test. They assert the privacy contract: no user info, no
cookies, no headers, no bodies, no console breadcrumbs, and API keys redacted
out of every message. A failure there is a live data leak, not a style problem.
Do not weaken an assertion to make a change pass.
Use standard git workflows. This repo defines no mandatory commit-message prefixes.