Prepares the repo for parallel agent work. No application code. - CLAUDE.md: conventions, branch naming, and the six non-negotiable invariants from the design (immutable raw bytes, no stored odometers, SI integers, dual-layer user isolation, secret containment, single ingestion path). Also records a model-allocation policy: the orchestrator runs Opus 5, workers default to Sonnet, and Opus is reserved for review plus the areas where a mistake is silent and expensive (ingest, wear SQL, auth/RLS, the Bryton protocol client). And the Gitea Actions gotchas, so nobody rediscovers them: GITEA_TOKEN cannot push to the container registry, jobs.*.environment is ignored, and cron needs a workflow_dispatch pair. - CONTRIBUTING.md: day-to-day flow, worktrees for parallel branches, review expectations. - .gitea/workflows/ci.yml: repo hygiene (branch naming, secret scan, no ride data in git), plus API/web/migration jobs that guard on whether the code exists yet, so CI is meaningful now and grows into the real thing rather than being rewritten. - .gitea/PULL_REQUEST_TEMPLATE.md: forces an honest "how this was verified" and an invariant checklist. - scripts/pr.sh, scripts/review.sh: open and inspect PRs via the Gitea API. - Directory scaffold with placeholder READMEs. Agents open PRs; humans merge them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8.1 KiB
CLAUDE.md
Conventions for this repo. Agents and humans both follow these. Read before making changes.
What this is
A self-hosted cycling app: ride sync from a Bryton Rider 650, mileage tracking, spare-parts
inventory, maintenance records with mileage-milestone reminders. See docs/PLAN.md for the full
design, docs/DECISIONS.md for settled decisions, docs/RESEARCH.md for source material.
Read docs/DECISIONS.md before proposing an architectural change. If a decision there is wrong,
say so and argue it — but don't silently contradict it.
Layout
apps/api/ Python 3.12 / FastAPI / SQLAlchemy async / Alembic
apps/web/ SvelteKit static SPA (installable PWA)
packages/openapi/ openapi.json — COMMITTED contract artefact, CI enforces it matches the code
deploy/ docker-compose, Caddyfile, systemd units, backup scripts
docs/ plan, decisions, research
scripts/ repo tooling (PR helpers, etc.)
.gitea/workflows/ CI
Non-negotiable invariants
These are load-bearing. Breaking one is a correctness bug, not a style choice.
- Raw bytes are the only truth. Every ingested file is written to the content-addressed blob
store before parsing, and is never mutated or deleted. Every table is a rebuildable projection:
deleting everything derived from a
raw_file_idand re-parsing must be semantically a no-op. - No odometer columns. Component wear is always derived by replaying activities against
time-ranged
component_installs. Never add a stored running total to a component. - All physical quantities are SI integers in storage — metres, seconds, mm/s, centimetres, grams, minor currency units. Imperial is display-only. Never store a float mile.
- Every user-owned table has
user_id, an RLS policy, and a repository-layer scope. Both layers, always. Never rely on the query alone. - Secrets never leave the server. The Bryton credential is password-equivalent. It must not appear in any API response model, any log line, or any error message.
- One ingestion path. All sources funnel through
ingest_bytes(). Never add a second parse path for a new source.
Style
- Python:
ruff(lint + format),mypy --strict. Type everything. Async throughout; no sync DB calls in request handlers. - SQL: migrations via Alembic only, never manual DDL. Every migration must survive
upgrade -> downgrade -1 -> upgrade. - Tests: pytest against a real Postgres service container, never mocks for DB behaviour.
Parser changes need a golden fixture in
apps/api/tests/fixtures/fit/. - Commits: imperative mood, explain why in the body. Conventional-commit prefixes
(
feat:,fix:,refactor:,test:,docs:,chore:,ci:). - Match surrounding code. Don't introduce a new pattern when one exists.
Branching
Never commit directly to main. main is protected and only moves via merged PRs.
feat/<scope>-<short-desc> new capability feat/ingest-fit-parser
fix/<scope>-<short-desc> bug fix fix/wear-wet-multiplier
refactor/<scope>-<desc> no behaviour change
test/<scope>-<desc> tests only
docs/<desc> documentation
chore/<desc> tooling, deps, CI
Scope is usually the area: ingest, wear, auth, notify, web, deploy, ci.
One branch = one reviewable change. If a branch grows past roughly 400 changed lines, it should probably have been two.
PR workflow
- Branch from an up-to-date
main. - Commit in logical steps. Push the branch.
- Open a PR with
scripts/pr.sh(see below) or the web UI. Fill in the template honestly — especially "How this was verified." - CI must be green. A red PR is not ready for review, and shouldn't be requested.
- Review happens on the PR. Address feedback with new commits, don't force-push over review history unless asked.
- Squash-merge into
main. Delete the branch.
Agents: you open PRs, you do not merge them. Merging is a human decision.
Model allocation
The orchestrator runs Opus 5. Worker agents do not default to it.
The rule is match the model to the cost of being wrong, not the size of the task. A big, well-specified job with obvious failure modes is cheap work. A small change to dedupe logic that silently corrupts data for six months is expensive work.
Opus 5 — orchestration, and anything subtly wrong-able
- Task decomposition, planning, and all code review (see below — this is where it pays for itself)
- Anything touching the six invariants above
ingest/— FIT parsing, the three dedupe layers, activity-vs-course discrimination. Errors here are silent and corrupt the archive.wear/— the wear SQL. Wrong numbers that still look plausible are the worst failure mode in the product, because nobody notices.auth/, RLS policies — security, and a mistake exposes another user's data.sources/bryton/— a reverse-engineered protocol with no spec to check against.- Schema migrations that alter or drop existing columns.
- Debugging anything that two Sonnet attempts have already failed to fix.
Sonnet — the bulk of implementation
Default for normal feature work. docs/PLAN.md is detailed enough that most implementation is
careful transcription plus ordinary judgement, and CI catches the rest:
- CRUD endpoints, Pydantic models, repository methods
- SvelteKit components, routes, styling, the service worker
- Tests against an already-decided behaviour
- Additive migrations, docker-compose and Caddy config, CI workflows
- Documentation
Haiku — mechanical work
- Dependency bumps, formatting, renames, changelog entries
- Log triage, "find every call site of X"
- Anything where a script would also work
The economics
Sonnet implements, Opus reviews is the default pairing, and it is much cheaper than Opus implementing while catching most of the same problems. Review reads a focused diff; implementation reads the whole repo and writes for hours. If budget is tight, cut Opus from implementation before you cut it from review.
Escalate a task to a stronger model when it has actually failed, not preemptively. Two failed Sonnet attempts is a real signal; "this feels hard" is not.
Never run more agents in parallel than there are genuinely independent branches of work. Parallel agents that touch the same files cost more than one agent working in sequence, because the merge conflicts and re-review are paid twice.
Working as a parallel agent
Each agent works in its own git worktree so parallel branches don't collide:
git worktree add ../bike-app-<branch> -b feat/<scope>-<desc>
# work there, push, open PR
git worktree remove ../bike-app-<branch>
Rules:
- Stay in your lane. Touch only files your task needs. If you need a change in someone else's area, note it in the PR rather than making it.
- Never rebase or force-push another agent's branch.
- Rebase on
mainbefore opening the PR, so the reviewer sees a clean diff. - Don't invent scope. If the task is ambiguous, ask rather than guess — a wrong guess costs a full review cycle.
- Report honestly. If tests fail, say so with the output. If you skipped something, say that. "Done" means done and verified.
What needs a human decision
Don't do these autonomously:
- Merging a PR
- Anything touching
docs/DECISIONS.md(propose it in a PR, argue the case) - Schema changes that drop or rewrite existing columns
- Anything that sends data off the server, or adds a third-party runtime dependency on one
- Rotating or changing secrets
- Force-pushing anything, ever, to
main
Environment
- Gitea at
http://192.168.0.3:3000, repoBBergle/bike-app, act_runner on the same host. - SSH remote
git@192.168.0.3:BBergle/bike-app.git, key pinned in~/.ssh/config. secrets.GITEA_TOKENcannot push to the Gitea container registry — use theREGISTRY_TOKENPAT secret (package:write). This is a documented Gitea limitation, not a misconfiguration.jobs.*.environmentis silently ignored by Gitea Actions. Don't build a gate on it.- Always pair
schedule:withworkflow_dispatch:— Gitea's cron has shipped flaky.