This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
- Install:
vp install(delegates to pnpm viapackageManager) - Build:
vp run build:allor per package:pnpm --filter @dotabod/{package} run build - Lint + format + typecheck:
vp check(auto-fix:vp check --fix) - Test:
vp testfor the whole workspace, or a specific path:vp test packages/path/to/file.test.ts - Runtime: services are Node 24; dev with
tsx watch src/index.ts, prod withnode dist/index.js
- TypeScript: Use strict types with explicit return types for exported functions
- Formatting: 2-space indentation, 100 char line width, single quotes
- Imports: Use ESM module syntax with explicit file extensions
- Error handling: Use try/catch for async operations, with Winston for logging
- Testing: Files end with
.test.tsor in__tests__directories
- Monorepo with packages in
packages/and microservices inservices/ - Avoid circular dependencies (see CIRCULAR-DEPS.md)
- Docker Compose is used for development and production environments
Each service must run as exactly one replica. They are stateful singletons and running two at once breaks things: dota holds per-token game state in memory and fires non-idempotent side effects with no cross-instance coordination (no socket.io Redis adapter, so overlay emits only reach the process the overlay's socket is on; no Redis lock around bet open/close, so two instances can create duplicate Twitch predictions; map.game_state can double-create clips). steam logs into one Steam account (a 2nd login mutually kicks the Dota GC session). twitch-chat registers an EventSub conduit shard per instance (two → duplicate chat messages). twitch-events is the singleton conduit manager. This also rules out Coolify rolling deploys (which briefly run old+new together); deploys are a hard cutover, which is fine — each service recovers in single-digit seconds and the GSI/socket/webhook paths all self-heal via retry/reconnect. To ever scale dota past one replica you'd first need a socket.io Redis adapter + a cross-instance bet lock (covering both GSIHandler and events/gsi-events/hero.name.ts).
Each service runs on Coolify with node --cpu-prof gated by the CPU_PROF env var. Coolify app UUIDs: dota i8gccg8, twitch-events zwg4g4c, twitch-chat zwgkg48, steam wsgwk8s.
To capture: in Coolify → <service> app → set env CPU_PROF=1 → Restart → let it serve traffic ~5 min → Stop (SIGTERM triggers process.exit(0), node flushes). Retrieve:
ssh oracle 'sudo cat /var/lib/docker/volumes/<uuid>-profiles/_data/CPU.*.cpuprofile' > svc.cpuprofileOpen in Chrome DevTools → Performance → Load profile. Source maps resolve frames to src/.... Set CPU_PROF= back to empty + Restart when done.
Monitoring — why no in-container New Relic APM agent: The Node APM agent (loaded via -r newrelic) burned excessive CPU for little benefit — dota alone sat at ~40% container CPU emitting ~540k APM datapoints/30min — so it was removed in 2026-05. Observability now comes entirely from the Oracle host newrelic-infra agent: nri-docker emits ContainerSample (CPU/mem/restart, faceted by label.coolify.resourceName) and host fluent-bit forwards container logs to NR with a container_name attribute. Tradeoff: we lost APM-only HTTP signals (response time, apdex, throughput, transaction error rate) with no host equivalent — backend alerts/dashboards were migrated to ContainerSample/Log queries accordingly. Do NOT re-add the newrelic package or -r newrelic to the services.
Rollback to a prior image: look up a previous master digest with gh api "/orgs/dotabod/packages/container/<service>/versions?per_page=5" --jq '.[] | select(.metadata.container.tags == ["master"]) | .name', then paste sha256-<digest> (dash, not colon) in Coolify's image tag/hash field and Redeploy.
Chat rosters are assembled from up to three Twitch clips, each read by a different detector and each carrying different data. Most "wrong roster" bugs are really "which clip landed?".
| clip | fires at | endpoint | contains |
|---|---|---|---|
| draft | PLAYER_DRAFT +46s |
/detect_draft |
names only, no heroes |
| strategy | STRATEGY_TIME +43.75s |
/detect |
names + ranks + heroes |
| in-game | GAME_IN_PROGRESS +60s |
/detect_in_game |
heroes only, no names |
Only the strategy/loadout panel carries names + ranks + heroes together — miss it and nothing
downstream recovers names. Scheduling: packages/dota/src/dota/events/gsi-events/map.game_state.ts.
Non-obvious constraints, all verified against production data — re-deriving them costs hours:
- The draft name strip is not in slot order. Three verified alignments give three different
permutations; only draft index 0 (Radiant captain) is stable. Draft↔roster merging must go
through the fuzzy name matcher (
_align_players_with_draft), never a positional join. An in-game result withdraft_alignment.mapping == {}is correct behaviour, not a bug — the in-game top bar shows no names to match on. - In-game hero confidence runs low (mean ~0.66; only ~20% of slots reach the 0.75
HERO_CONFIDENCE_THRESHOLD), and no clean cutoff exists — a measured wrong read scored 0.416 while a correct one scored 0.386. Don't add score gates; anchor on GSI, which knows the streamer's own hero exactly (correctSelfHeroWithGsiinVisionResolver.ts). - Draft rows in
processing_queueare alwaysstatus='failed', even when they succeed. Checkclip_resultsfor the real outcome. - GLOBAL_DELAYat thescheduleClipsites intentionally cancels the+ GLOBAL_DELAYinsidegetStreamDelay()— that 7s is for chat, not clips.
Debugging tools live in scripts/clip-debug/ (query_match.sh for a match's full clip history,
scan_clip.py to see what a clip's frames actually contain plus the STRATEGY TIME countdown
that tells you where it landed). The dota-vision-roster-debug skill has the full workflow,
the DB/API recipes, and a list of plausible-sounding fixes that measurement has already
disproved — read it before changing clip timing or alignment code.
This project is using Vite+, a unified toolchain built on top of Vite, Rolldown, Vitest, tsdown, Oxlint, Oxfmt, and Vite Task. Vite+ wraps runtime management, package management, and frontend tooling in a single global CLI called vp. Vite+ is distinct from Vite, and it invokes Vite through vp dev and vp build. Run vp help to print a list of commands and vp <command> --help for information about a specific command.
Docs are local at node_modules/vite-plus/docs or online at https://viteplus.dev/guide/.
- Run
vp installafter pulling remote changes and before getting started. - Run
vp checkandvp testto format, lint, type check and test changes. - Check if there are
vite.config.tstasks orpackage.jsonscripts necessary for validation, run viavp run <script>. - If setup, runtime, or package-manager behavior looks wrong, run
vp env doctorand include its output when asking for help.