# CLAUDE.md Working notes for Claude Code on pansy. [DESIGN.md](DESIGN.md) is the architecture document and stays authoritative; this file is the operational stuff you'd otherwise rediscover every session. ## This is a vibe-coded project — say so **pansy is written by an LLM, with a human directing.** That is not a footnote, it is a property of the software people should know before they trust it with their garden plans. Rules, not preferences: - The README carries a prominent, near-the-top disclosure. It does not get moved below the fold, softened into "AI-assisted", or quietly dropped in a rewrite. - If you rewrite the README, the disclosure survives the rewrite. - Anywhere else the project introduces itself (a landing page, a docs site, a package description), it says the same thing. If you find yourself editing that section, the only acceptable direction is clearer and more honest, never quieter. ## Keep the docs true Docs rot silently and nobody notices until someone follows them and it doesn't work. Treat them as part of the change, not follow-up work: - **README.md** — update it in the *same commit* whenever you add or change an environment variable, a route worth knowing about, a build or run command, or anything in the Docker/Compose example. The env var table and the compose snippet are the two things people copy, so they're the two that hurt most when they're stale. - **DESIGN.md** — update it when the architecture actually changes: a new table, a new package, a new API surface, a decision that supersedes one written there. Not for every implementation detail. - **CLAUDE.md** — this file. Add a convention here the moment you find yourself rediscovering it. - **Examples must run.** If you change something an example depends on, fix the example. A snippet that references an issue number as "once #5 lands" after #5 has landed is a bug in the docs. When you touch a file, glance at whether the comments around your change are still true. Stale comments are worse than none — the next reader believes them. ## Build and test pansy is a standalone Go module inside a parent workspace, so **`GOWORK=off` is required** or the build picks up sibling modules: ```sh GOWORK=off go build ./... GOWORK=off go test ./... cd web && npx tsc --noEmit && npx vitest run && npm run build make test # both halves make build # web bundle → embed → CGO_ENABLED=0 static binary ``` `gofmt -l internal/` before committing. Note `internal/service/plants_test.go` is already unformatted on `main` — leave it alone unless you're touching it, so the diff stays about your change. ## Architecture in one paragraph `internal/store` (hand-written SQL, `modernc.org/sqlite`, pure Go) → `internal/service` (**the seam: every permission check and invariant**) → `internal/api` (thin gin handlers: decode, call service, encode). Agent tools in `internal/agent` are equally thin adapters over the *same* service methods, so they inherit permission enforcement for free. If you are about to put a rule in a handler, put it in the service instead. Frontend: React 19 + Vite + Tailwind 4 + TanStack Router/Query, built into `internal/webdist/dist` and embedded with `embed.FS`. ## Conventions that bite if you miss them - **Everything is centimeters**, stored as SQLite `REAL`. Imperial is a display and entry concern only, in `web/src/lib/units.ts`. Two distinct scales live there: *dimension* (m/ft — gardens, objects, the garden grid) and *spacing* (cm/in — plant spacing, bed grid). Mixing them is what #47 was about. - **Optimistic concurrency everywhere.** Every mutable row has `version`; PATCH and DELETE carry it; the store returns `(current row, ErrVersionConflict)` on mismatch and the API answers 409 with the row under `"current"`. - **No-access is `ErrNotFound`, not `ErrForbidden`.** Existence is masked deliberately. `ErrForbidden` means "you can see it but may not do that". - **Plops (plantings) live in their parent object's local frame**, origin at the object's center, `-y` is north. Moving or rotating a bed moves its plants free. - **Soft removal**: "clear bed" sets `removed_at`; the editor reads `removed_at IS NULL`. Hard delete is a different operation. - **Migrations** are numbered `.sql` files in `internal/store/migrations/`, run at startup, embedded. Never edit one that has shipped. - **Every service mutation lands in history** (#48). If you add one, record it — see `internal/service/revisions.go`. Multi-row operations pass all their changes to a single `record` call so they undo as one unit. ## Workflow Branch → PR → **Gadfly** reviews it automatically → consider every finding and fix what's real → merge when the pipeline is green. Do not grade Gadfly findings. A push to `main` builds the image and deploys to Komodo; the live instance at `pansy.orgrimmar.dudenhoeffer.casa` updates a few minutes later. Workflow- and config-only changes (CI, this file, docs) go straight to `main` without the PR dance. Planning happens in Gitea issues first: standalone issues under a tracking epic, implemented in later per-issue sessions. ## Environment `PANSY_PORT`, `PANSY_DB`, `PANSY_BASE_URL`, `PANSY_REGISTRATION`, `PANSY_LOCAL_AUTH`, `PANSY_OIDC_*` — note it's `PANSY_DB` and `PANSY_PORT`, not the `_PATH`/`_ADDR` names you might guess. Authentik is the primary IdP; OIDC-first with local passwords as fallback. Agent config (v2): `OLLAMA_CLOUD_API_KEY` and `PANSY_AGENT_MODEL` (default `ollama-cloud/glm-5.2:cloud`), set in Komodo. Model strings pass verbatim to `majordomo.Parse`, so a comma-separated spec gives failover for free. `majordomo` and `executus` are sibling repos at `../`.