This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This project is indexed by bodhiorchard's own code-graph indexer
(backend/app/services/code_indexer/, MIT, MIT-licensed graphify under the
hood). The cached call graph is stored in Postgres (repo_graph_cache) and
exposed to Claude Code through the code_* MCP tool group.
- MUST run impact analysis before editing any symbol. Before modifying a function, class, or method, call
code_impact({target: "symbolName", direction: "upstream", repo_id: "<uuid>"})and report the blast radius (direct callers, affected files) to the user. - MUST warn the user if impact returns more than ~20 callers across multiple modules — that's the high-risk signal.
- When exploring unfamiliar code, use
code_query({query: "concept", repo_id: "<uuid>"})to find candidate symbols by name. Pair withcode_contextfor caller/callee details. - For "what's in this feature?" questions, use
code_community({cluster_id: "c0", repo_id: "<uuid>"})— returns every file and symbol in a domain cluster.
- NEVER edit a function, class, or method without first running
code_impacton it. - NEVER rename symbols with find-and-replace — the call graph is the source of truth for usage sites.
- NEVER manually edit
cluster_cacheorrepo_graph_cacherows; they're derived state owned by the scan pipeline.
| Tool | Purpose |
|---|---|
code_impact |
Upstream/downstream BFS from a symbol (blast-radius check) |
code_query |
Substring search across symbol labels + file paths |
code_context |
Single-symbol 360°: attributes, callers, callees |
code_community |
List nodes/files in one cluster (e.g. c0) |
code_god_nodes |
Top-N highest-degree hubs (refactoring candidates) |
code_stats |
Graph stats + language extension distribution |
The indexer runs in Stage 0 (ingest) of every scan and stays SHA-keyed,
so re-runs on the same head_sha short-circuit through cluster_cache /
repo_graph_cache without re-parsing.
All development is orchestrated from the repo root via npm workspaces. Backend Python lives in backend/.venv (created by npm run setup); activate conda activate python3129 for ad-hoc Python tooling.
| Command | What it does |
|---|---|
npm run setup |
One-time: creates backend/.venv, installs Python deps, copies .env files, starts infra, runs Alembic migrations |
npm run dev |
Starts postgres+redis, waits for Postgres, then runs backend/frontend/multiplayer concurrently with colour-coded logs |
npm run dev:infra / npm run stop |
Start/stop the infra-only containers (postgres, redis) |
npm run dev:backend / dev:frontend / dev:multiplayer |
Run a single process if npm run dev is too noisy. |
docker compose up |
Full Docker mode — whole stack incl. backend in containers (see README for Hybrid vs Full Docker trade-off) |
- Lint:
.venv/bin/ruff check . --fix && .venv/bin/ruff format .(line-length 99, rulesE,F,I,N,W,UP,B,A,SIM) - Type check:
.venv/bin/mypy app/(strict mode, pydantic plugin enabled) - Tests:
.venv/bin/pytest—asyncio_mode = "auto", testpaths =tests/ - Single test:
.venv/bin/pytest tests/path/to/test_x.py::test_name -xvs - New migration:
.venv/bin/alembic revision --autogenerate -m "message"thenalembic upgrade head(also auto-runs on backend startup viaentrypoint.sh)
- Type check (the real gate):
npx vue-tsc --noEmit—npm run lintfails because no ESLint config exists; ignore the pre-existing unused-import error inSceneManager.ts - Build:
npm run build(runsvue-tsc --noEmitthen Vite) - Tests:
npm test(Vitest, one-shot) /npm run test:watch
- Dev:
npm --workspace bodhiorchard-multiplayer run start:dev(Colyseus 0.17, port 2567)
Bodhiorchard is a local-first, multi-tenant AI dev-ops platform with three cooperating processes plus managed infra. Keep these boundaries in mind — they determine where new code belongs.
backend/(FastAPI, Python 3.12, async SQLAlchemy 2.0) — REST API, agent orchestration, MCP server, repository scanning. Every request is JWT-auth'd and org-scoped; repositories enforce tenant isolation at the data layer.frontend/(Vue 3 + Vuetify 3 + Pinia + TypeScript) — SPA at:3000. Uses PlayCanvas for the 3D "Living Tree" / Garden Engine visualization. Axios interceptor attaches JWT.multiplayer/(Colyseus 0.17, TypeScript) — authoritative room server at:2567for shared 3D-world state (OrgRoom, RaceRoom). Simulation logic is server-side; clients interpolate.- Infra — PostgreSQL 16 + pgvector (vector search for BUDs/bugs/features), Redis (cache + job queue).
api/v1/ HTTP handlers — thin; validate via schemas, delegate to services
agents/ Agent definitions & orchestrators (12 agents: Triage, BUD, TechPlan, ...)
mcp/ MCP server (BUD-lifecycle writes, feature registry, code graph, team context) for Claude Code
services/ Business logic — LLM calls, scanning, synthesis, bud_closure, bug_linker
repositories/ Data access — all queries filter by organization_id
models/ SQLAlchemy ORM
schemas/ Pydantic DTOs
core/ Auth, permissions, encryption (Fernet AES-128 for secrets at rest)
Cross-cutting patterns: async jobs (register handler → return 202 → track via useJobSocket); dual Claude auth modes per org (api_key vs hybrid_host, stored in claude_auth_mode).
Event-bus fanout: event_bus.publish(topic, payload) reaches (a) in-process asyncio Queue subscribers (dashboard /ws) and (b) every external transport registered via register_transport() in main.py lifespan. The multiplayer server subscribes via services/colyseus_forwarder.py. Add new external sinks (Slack, metrics, SSE) by writing an async (topic, payload) -> None callback and registering it in lifespan — no publisher-side changes.
bud → design → development → testing → uat → prod → closed (discarded at any stage)
- Markdown doc with spec / tech spec / test plan sections, numbered per org (
BUD-001, …). - Embeddings generated at creation time; bug-linker uses pgvector cosine distance, threshold 0.40.
on_bud_closed()inservices/bud_closure.pyis the single entry point for contributor-XP, BUD-shipped SP, learning metrics, and the post-close Learning Agent — called from both manual PATCH and auto-close.- Repo scans are NOT triggered on BUD close. They are owned by the PR-merge GitHub webhook (
api/v1/github_webhook.py→services/scan/pr_merge_update.py), which gates on the repo'smain_branch. - Linked-feature
in_progress → donetransitions run inapi/v1/bud.pyviaservices/feature_lifecycle.transition_feature_for_budon every status change, independent ofon_bud_closed. - Release detection has two paths: fast (
bud_idon PR) and SHA-walk (release PRs withoutbud_id).
shared/world/zones.ts holds world-zone positions imported by both the frontend engine and the multiplayer server — keep them in sync or the simulations desync.
| Mode | Backend runs in | Claude auth | Use when |
|---|---|---|---|
| Full Docker | Container | Org-level API key in Settings → AI Config | Evaluators, Mac-mini deploys |
| Hybrid | Host venv (hot reload) | Inherits host's claude login session |
Dev loop, Claude Pro subscription |
The stored claude_auth_mode on the org decides which path agent runs take.
BODHIORCHARD-ARCHITECTURE.md— full 8400-line architecture specAGENTS.md— per-agent capabilities and triggersfrontend/src/engine/ARCHITECTURE.md— must-read before touching the Garden Engine (rules encoded in the Garden Engine section below)
- No ESLint config exists —
npm run lintfails. Usevue-tsc --noEmitas the type-check gate - Pre-existing TS error in
SceneManager.ts(unused import) — ignore in type-check output v-tabs-window-itemvaluemust exactly matchv-tabvalue— mismatches cause silent blank content- Tab values like
'uat','prod','closed'are NOT inBUD_SECTIONS— they're inNON_SECTION_BUD_TABSandSTATUS_TAB_MAP
- BUDs get embeddings at creation time (for bug linker vector search)
on_bud_closed()inbud_closure.pyhandles: contributor XP, BUD-shipped SP, BUD learning metrics, and the post-close Learning Agent (called from both manual PATCH and auto-close). Repo scans live in the PR-merge webhook, not here.- Release detection: fast path (bud_id on PR) vs SHA-walk path (release PRs without bud_id)
- Bug auto-linking:
bug_linker.pyuses pgvector cosine distance with 0.40 threshold
Two different state graph types — KayKit vs Kenney characters use DIFFERENT parameter types:
- KayKit (via KayKitCharacterFactory): Uses
LOCOMOTION_STATE_GRAPHfrom AnimUtils.ts with BOOLEANsittingand INTEGERspeed,working - Kenney (legacy blocky): Uses custom state graphs with INTEGER params for all
When setting anim params, ALWAYS check _isKayKit:
if (this._isKayKit) {
anim.setBoolean("sitting", true); // BOOLEAN for KayKit
} else {
anim.setInteger("sitting", 1); // INTEGER for Kenney
}Available KayKit animations (from characters/kaykit/animations/*.glb):
simulation.glb:Sit_Chair_Idle,Sit_Chair_Down,Sit_Floor_Idle,Lie_Down,Lie_Idle,Lie_StandUp,Cheering,Wavinggeneral.glb:Idle_A,Idle_B,Interact,Use_Item,PickUp,Death_Amovement_basic.glb:Walking_A,Running_A,Jump_Full_Shorttools.glb:Work_A,Working_A,Chop,Hammer,Fishing_Idle
Use exact track names with findAnimTrack() — not fuzzy keywords.
Before making ANY changes to frontend/src/engine/, you MUST:
- Read
frontend/src/engine/ARCHITECTURE.md— this is the single source of truth for the engine's structure, conventions, data flow, and design decisions. - Follow the PBR lighting rule — NEVER use
useLighting = falseon any material. The engine uses proper IBL + ACES tone mapping + sRGB gamma. If something looks dark, adjust exposure/material properties — don't bypass the pipeline. - Respect the boundary — only
GardenEngine(fromengine/index.ts) is imported by Vue.types.tshas zero app-layer imports. - Use MaterialFactory — don't create ad-hoc
StandardMaterialinstances. UseMaterialFactory.getColor()for cached, properly-lit materials. - Return exclusion zones — any subsystem that occupies ground space must return
{ x, z, radius }zones so grass/rocks avoid it. - Old engine backup — the previous engine lives in
frontend/src/engine_bkup/for reference. It is excluded from TypeScript compilation viatsconfig.json.
simulation.glbhas multiple tracks withSitprefix:Sit_Chair_Down(0.8s transition) andSit_Chair_Idle(3.6s loop). Always use the exact track nameSit_Chair_Idle— fuzzy keyword'Sit'matchesSit_Chair_Downfirst, which is a one-shot transition that looks like idle.findAnimTrack()inAnimUtils.tsdoes substring matching and returns the first hit. When assigning via keywords, put the most specific name first:['Sit_Chair_Idle', 'Sit'].KayKitCharacterFactory.ANIM_TRACK_MAPis the authoritative mapping for NPC characters.TakeoverAnimGraph.assignCoreAnimationsmust match these exact track names.- The same pattern applies to any future animations with shared prefixes (e.g.
Walk_ForwardvsWalk_Backward).
- E-key fires
wasPressedwhich can trigger twice rapidly — a 300ms cooldown (seatToggleCooldown) inGardenEngine.onUpdateprevents sit/stand toggle loops. TakeoverController.getAnimState()must return"sit"when_sittingis true, otherwise the periodic broadcast sends"idle"to the server and other clients see the character standing on the chair.TakeoverController.sitAt()must clearjumpProgressandanim.setBoolean('jumping', false)to cleanly transition from any prior state.
| Phase | Status | What it adds |
|---|---|---|
| 1 — Skeleton | DONE | Application boot, PBR lighting, camera, input, materials |
| 2 — World | PENDING | Environment, trees, buildings, pool, water, arcs |
| 3 — Player | PENDING | GLTF characters, WASD movement, swimming, labels |
| 4 — NPC AI | PENDING | Behavior trees, time-based schedules, activity routing |
| 5 — Interaction | PENDING | Click/hover picking, tooltips, camera focus |
| 6 — Effects | PENDING | Particles, splash, ZZZ, steam, vehicles |