Files
pansy/CLAUDE.md
T
steveandClaude Opus 4.8 3f3a5b057c
Build image / build-and-push (push) Successful in 25s
Gadfly review (reusable) / review (pull_request) Canceled after 5m56s
Adversarial Review (Gadfly) / review (pull_request) Canceled after 5m56s
Agent runtime: majordomo in-process, Ollama Cloud config, chat endpoint (#56)
Everything below the run loop already existed. This is the thing that runs a
model.

The build tag is gone, deliberately. internal/agent's doc comment promised two
separations — cmd/pansy not importing the package, and the tool wiring behind
//go:build majordomo — and both have been rewritten rather than left as a stale
aspiration. A tag that keeps the agent out of the binary only earns its keep if
you would ever ship a build without the agent, and the agent is the point;
keeping it meant an untagged CI that never compiled the code that matters.
majordomo is a real dependency now, resolved from the Gitea instance as a
pseudo-version with no replace directive, so the Docker build (which has no
sibling checkout) resolves it the same way this machine does. It is stdlib-first
and pure Go, so CGO_ENABLED=0 and the single static binary survive.

A TURN IS ONE CHANGE SET. That is the whole reason acting without a confirmation
prompt is defensible: "empty the garlic bed and plant cucumbers" is one object
edit and a dozen planting inserts, and it has to undo as one action rather than
thirteen. The scope is opened even for a turn that turns out to be a question,
because a change set with no revisions is never written — so asking costs
nothing and history isn't littered with empty entries.

The model spec goes to majordomo.Parse verbatim. That grammar, including
comma-separated failover chains, is majordomo's; re-implementing any of it here
would only mean two places to update when it grows. The key needs a bridge
though: majordomo's ollama-cloud preset reads OLLAMA_API_KEY while pansy (like
gadfly) is configured with OLLAMA_CLOUD_API_KEY, so the provider is registered
explicitly on a private registry rather than depending on ambient environment.

Runs are bounded by a step cap, a timeout and majordomo's loop guards. This is
loop safety, not cost control — pansy is a personal tool and spend caps are
explicitly not a v2 concern. A capped run does NOT fail: it kept whatever it
managed to do, that work is recorded and undoable, and the reply says it stopped
early rather than going silent.

The chat endpoint streams. A turn that clears a bed and replants it makes a
dozen tool calls over tens of seconds, and without streaming that is a long
silence followed by everything at once — which reads as a hang, and defeats a
design that rests on watching the canvas change as it happens.

Conversations persist per (user, garden). Client-held history would be lost on a
refresh, which is exactly when someone reloads to check whether the agent's
change landed. Only the user/assistant TEXT is stored, not the model's full
transcript: continuity needs what was said and what came back, and replaying a
stored tool call would replay a decision made against a garden that has since
moved on. It also keeps majordomo's message shape out of the schema.

An instance with no key starts, serves the app, and doesn't advertise the agent
— the routes aren't registered at all, the same shape as OIDC 404ing when
unconfigured. A configured-but-unresolvable model logs and disables the
assistant rather than refusing to boot: a garden planner that won't start
because of a chat feature is worse than one without chat.

Tool refusals reach the model as tool results it can explain, not 500s. The ACL
story only works if it can narrate the refusal.

Closes #56

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
2026-07-21 02:15:22 -04:00

6.1 KiB

CLAUDE.md

Working notes for Claude Code on pansy. 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:

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: OLLAMA_CLOUD_API_KEY, PANSY_AGENT_MODEL (default ollama-cloud/glm-5.2:cloud) and PANSY_AGENT_ENABLED, set in Komodo. Model strings pass verbatim to majordomo.Parse, so a comma-separated spec gives failover for free — don't parse that grammar in pansy.

majordomo is a real dependency now, resolved from the Gitea instance as a pseudo-version. There is no replace directive and there must not be one: a replace pointing at ../majordomo builds on your laptop and breaks the Docker build, which has no sibling checkout. executus is a sibling repo at ../ and is not a dependency.

The majordomo build tag is gone. Don't reintroduce it — an untagged CI that never compiles the agent is worse than a slightly larger binary.