Imported from Dillpickleschmidt/nihongo-ninja (
AGENTS.md). Install upstream withnpx skills add Dillpickleschmidt/nihongo-ninja. Copyright stays with the author.
AGENTS.md
Instructions for AI agents (and humans) working in this repo. Read README.md first for the structure overview.
This file is a failure log — each trap below exists because an agent hit it. When you hit a new trap or a gap in these rules, record it in the right place: dependency rules in docs/dependencies.md, every-session rules here. Keep this file lean.
Commands
nub run dev:tanstack-start / dev:expo / dev:electron # per-platform dev
nub run build # topological build (convex codegen + web SSR)
nub run typecheck / lint / format # run before considering work done
nub run generate:routes # regenerate native routes (see below)
nub run -F @nn/tanstack-start build:electron # desktop bundle
nub run -F @nn/electron setup # fetch castLabs Chromium (once per install)
The toolchain is nub for everything: installing (nub install, lockfile is
nub.lock), running scripts, and workspace orchestration. nub run -r is
topological, so there is no separate task runner (no turbo). Workspace config
lives in neutral package.json fields only: workspaces, overrides,
allowBuilds — no tool-branded config files.
Shared dependency versions live in the root package.json catalog
(workspaces.catalog, workspaces.catalogs.react19). Packages reference them
with catalog: / catalog:react19. Add a package to the catalog when a second
package needs it. Keep peerDependencies inline.
Before you add, upgrade, or pin any dependency, read docs/dependencies.md. Several pins are deliberate (typescript, the Expo SDK 54 set, vite, lightningcss, nativewind, the TanStack family) — do not "upgrade to latest" without reading it.
Architecture rules
- Pages live in
packages/features, organized by domain (features/homepage,features/lessons,features/watch) — each domain'sindex.tsxis the page both routers mount. They contain all UI and fetch data withuseQuery(convexQuery(...))(see the Convex section). Route files in apps are thin: web routes declare path/loader/ssr options and import the page from@nn/features/<domain>; native routes are generated — never edit them. - Navigation only via
@nn/router(Link,useRouter,useParams). Never import@tanstack/react-routerorexpo-routerinside shared code.hrefis typed:Hrefis a union generated from the web route tree (packages/router/src/routes.gen.ts, emitted by the route generator), so a bad path is a compile error.$paramsegments become${string}. - Feature UI is written once, in React Native primitives (
View,Text,Pressable+ NativeWind) plus@nn/uicomponents — no DOM elements and no@base-ui/reactimports insidepackages/features. Non-overlay@nn/uicomponents are a single RN implementation; only the overlay family (Dialog, Menu, ContextMenu, Popover, Select) splits presentation per platform (Base UI on web, sheets/modals on native) behind one shared API. Seedocs/adr/0001-rn-first-ui-with-web-overlay-exception.md. Pre-pivot web-only domains are converting incrementally — match this rule in new and touched code. - Use the semantic components from
@nn/uifor page structure (Heading,Paragraph,Main,Nav,Header,Footer,Article,Section,Aside). On web, react-native-web maps theirroleto real HTML tags (<h1>,<main>,<p>, …) for SEO and accessibility. A bare<View>or<Text>renders as<div>. Do not use a bare<Text>for a heading. - Platform splits use relative imports. Metro resolves package-
exportssubpaths to an exact file with NO.native.tsxsubstitution. Export a neutral file that re-exports from a relative path; putfoo.tsx(web) andfoo.native.tsx(native) behind it.packages/features/watch/index.tsxis the canonical example. - Per-route rendering control is the point of TanStack Start. Static lesson
pages SSR by default; personalized/tool routes can set
ssr: false(seeapps/tanstack-start/src/routes/watch.tsx). Do not globalize either mode. - Adding a route: add the page under
packages/features/<domain>/, add a web route file that imports it from@nn/features/<domain>, run the web app once (regeneratesrouteTree.gen.ts), thennub run generate:routes. The generator fails loudly on zero parsed routes or a web route with no@nn/featuresimport — fix the cause, don't weaken the check. Hand-write only_layout.tsxfiles on native. - The desktop app is castLabs Electron only because of Widevine. Keep it a
thin shell: main process (
apps/electron/src/main.cjs), preload, no React. - Comments state constraints, not narration. Write a comment only for what the code cannot say: platform quirks, ordering/race requirements, deliberate divergence from the source app, port-status placeholders. Never describe what the next line does, restate a component's design in prose, or cite where code came from — those drift as the code changes. When porting, carry the source's comments; hold new ones to this bar.
Traps (all empirically hit — do not rediscover)
Lint and format (oxc, not ESLint/Prettier)
- oxlint is the linter (
nub run lint→oxlint --type-aware). Config is the root.oxlintrc.json. It runs once over the repo (not per package) and respects.gitignore. Theno-restricted-importsguard (bare react-native Text) lives here, with anoverridesentry turning it off forpackages/ui/src/**. - Type-aware rules need
oxlint-tsgolint(a root devDep) plus the--type-awareflag. tsgolint uses the TS7 engine, so it rejects TS options removed in TS7 — that is whyapps/tanstack-start/tsconfig.jsonhas nobaseUrl(paths resolve relative to the tsconfig without it, in both tsc 5.x and tsgolint). - oxfmt is the formatter (
nub run format/format:fix). Config is the root.oxfmtrc.json; it sorts imports and Tailwind classes (functionscn,cva) and respects.gitignore. - Always put a blank line between a file-header comment and the imports. Without it, oxfmt's import sorter attaches the comment to the first import and drags it into the sorted imports.
tscdoes full type checking (nub run typecheck); oxlint does not replace it.
NativeWind v5 / styling
- The web (Vite) wiring lives in
apps/tanstack-start/vite.config.ts. Its comments hold the babel preset,require.resolve, andssr.noExternaltraps. Do not remove them. - Types for
classNameon RN components come from/// <reference types="react-native-css/types" />(nativewind-env.d.ts in each package), NOTnativewind/types. - Known utility gaps on native:
text-start(usetext-left), spot-checkline-clamp. Animations route through Reanimated and are low priority. - NativeWind's web output emits utilities with
!important, which beats CSS animations in the cascade — an animated property's initial state must live in the keyframes (animation-fill-mode: both), not in a utility class likeopacity-0.
Expo CLI
- Expo actively manages
expo-env.d.tsand its tsconfig include entries — with typed routes off it DELETES them on every run. That's whyprocess.envtyping lives inapps/expo/env.d.ts(a name Expo doesn't own). Don't recreateexpo-env.d.tsby hand. - Expo's tsc has no DOM lib but follows the generated native route stubs into
web-only feature code (its
moduleSuffixespicks.nativevariants only where they exist). Web files reachable from a native stub must not touchwindowor DOM element types — use platform-suffixed wrappers (seevocab-hub/components/web-dialogs.ts) or give the importing layout/page a.nativeplaceholder so the web graph drops out of expo's typecheck.
TanStack Start
- SPA mode (
spa.enabled) is not used — its prerenderer races its own preview server on nitro 3 alpha (ECONNREFUSED/500, then hangs). The desktop bundle comes fromscripts/export-electron.ts, which snapshots the built SSR server's/output. Revisit when nitro 3 stabilizes. routeTree.gen.tsis gitignored and produced by the vite plugin — run the web dev server or build beforenub run generate:routeson a fresh clone.
castLabs Electron / desktop
- After each dependency install, run
nub run -F @nn/electron setup— nothing runs it automatically. - Before you change
apps/electron/src/main.cjs, read its comments. They hold the Widevine startup, Linux restart, WebContentsView overlay, andapp://scheme traps. Do not remove them.
DRM / Crunchyroll (/watch page)
- The header comment in
packages/features/src/watch/watch-page.native.tsxholds the verified Android and iOS WebView setup (protected media, user agents, touch-point mask). Do not remove it. - Expect software Widevine (L3); Crunchyroll accepts it. Netflix is permanently out of scope (approved-device list).
Convex
- Function files use the thin-api/fat-model split (ported from the source
app):
convex/api/*.tsfiles only validate args and delegate; the logic lives inconvex/model/*.ts. Add new functions the same way. - Static content (textbooks, chapters, modules) comes from
@nn/data— the backend imports it; do not duplicate content into the database. packages/convexis the only Convex surface. Codegen output (convex/_generated) is gitignored;nub run -F @nn/convex buildregenerates it and needs a configured deployment (CONVEX_AGENT_MODE=anonymous npx convex dev --onceprovisions a local one without an account).- Convex data goes through TanStack Query with
@convex-dev/react-query. Pages read data withuseQuery(convexQuery(api.x.y, args)), notconvex/react. This lets web route loaders prime the cache on the server, so the SSR HTML ships with data. Web wiring:apps/tanstack-start/src/router.tsx(client setup) and each route'sloader(context.queryClient.ensureQueryData(...)). Native wiring:apps/expo/src/utils/convex.ts+ the providers inapps/expo/src/app/_layout.tsx. Add aloaderto every new web route that reads Convex data, or that data will not be in the server HTML.
Agent skills
Issue tracker
Issues live in GitHub Issues via the gh CLI. See docs/agents/issue-tracker.md.
Domain docs
Single-context: one CONTEXT.md + docs/adr/ at the repo root. See docs/agents/domain.md.
Port status
v0 proved the architecture end-to-end on all three targets. The port from
../nihongo-ninja-tanstack (branch feat/supabase-to-convex) is now underway,
tracked as GitHub issues. Do not start deferred pieces (billing, animeAuth /
discover, learning_paths, subtitle overlay) unprompted — each has an issue.