Onboarding doc for fresh agent sessions: stack, commands, layout, conventions, and an Always/Never list of gotchas hit during the AI overhaul. Current state section tracks the 1.1.0:7 checkpoint. ROADMAP.md holds the longer-term backlog. CLAUDE.md symlinks AGENTS.md so Claude Code loads it. Secrets kept out — private registry/file-host URLs and creds referenced by file location, not value.
7.7 KiB
AGENTS.md — Proof of Work
Self-hosted multi-user workout logger (Next.js app) packaged as a StartOS 0.4 s9pk, published to a private Start9 registry.
Stack (versions that matter)
- Next.js 14 (App Router, server components + server actions, SSE streaming)
- React 18, TypeScript 5, TailwindCSS 3
- Prisma 5 ORM over SQLite (WAL mode; tuned at boot)
- bcrypt (native — NOT bcryptjs), zod 3 for validation
- Vitest 4 for tests
- @start9labs/start-sdk for the 0.4 packaging layer
- Node >= 20 to build
Layout (two projects in one repo)
proof-of-work/ ← the Next.js app (THIS is where you run npm)
app/ ← App Router routes; app/api/** = route handlers
app/main/ ← authed UI; navigation.tsx = sidebar
components/ ← React components (workouts/, ai/, settings/)
lib/ai/ ← AI subsystem (see below)
lib/ai/providers/ ← claude.ts openai.ts gemini.ts ollama.ts + index.ts (getProvider)
prisma/schema.prisma ← schema (mirror; real DB migrates via entrypoint ALTERs)
prisma/*.seed.json ← curated exercise library + AI templates (reconciled each boot)
tests/ ← Vitest specs (ai-*.test.ts, routes-*.test.ts, ...)
start9/0.4/ ← StartOS packaging wrapper
docker_entrypoint.sh ← boot: first-boot seed, additive ALTERs, library reconcile
Makefile / s9pk.mk ← s9pk build (ARCHES := x86)
startos/versions/ ← one file per ExVer version + index.ts (the version graph)
~/.proof-of-work/ ← publish.sh + unpublish.sh (NOT in repo; self-hosted registry)
workout-planner/ is scratch (only logs/) — ignore. start9/0.4/*.s9pk are build artifacts.
Commands
Run app commands from proof-of-work/ (running tsc/vitest/next from repo root fails — wrong cwd):
cd proof-of-work
npm run dev # local dev server
npm run build # next build (run this to catch route/type errors before shipping)
npm run lint # next lint
npm test # vitest run (full suite)
npx vitest run tests/ai-pricing.test.ts # single file
npx vitest run -t "findPrice" # single test by name
npx tsc --noEmit # typecheck only
npx prisma generate # REQUIRED after editing schema.prisma (else TS can't see new fields)
Build/sideload the s9pk (from start9/0.4/): make x86 then make install.
Publish to the registry: ~/.proof-of-work/publish.sh (builds, uploads to FileBrowser, registers). Unpublish: ~/.proof-of-work/unpublish.sh.
Conventions
- Versioning is ExVer:
1.1.0:4(note the colon). Every release = a newstart9/0.4/startos/versions/vMAJOR.MINOR.PATCH.N.tsfile, imported intoversions/index.tsand promoted tocurrent(previouscurrentmoves intoother[]). - Bump the version BEFORE building the s9pk — Start9 0.4 won't recognize a rebuild as an update otherwise.
- Schema changes are additive ALTERs in
docker_entrypoint.sh, guarded byPRAGMA table_infochecks. Keepschema.prismain sync as the mirror, but the entrypoint is what migrates live/data. Never write a destructive migration. - Commit subject =
vX.Y.Z:N — short summary, imperative, body explains the why. - Git remote is self-hosted (a private Start9 registry + a FileBrowser artifact host), NOT GitHub. The actual registry/file-host URLs are constants in
~/.proof-of-work/{publish,unpublish}.sh; FileBrowser creds live in~/.keysat/filebrowser.env(outside the repo, gitignored). Default branch ismaster. - AI provider abstraction: each provider yields an async iterable of
GenerateChunk(text/usage/done/error); add new ones underlib/ai/providers/and register inindex.ts. - Streaming AI uses SSE; partial JSON is recovered with
lib/ai/lenientJson.ts. - Tests live in
proof-of-work/tests/; mock server-action deps withvi.hoisted()+vi.mock. - Pricing/model menus live in
lib/ai/pricing.ts(PRICES,MODEL_MENU) — keep them paired so every menu model has a price entry (there's a test enforcing this).
Always
- Run
npx prisma generateafter anyschema.prismaedit, thennpx tsc --noEmit. - Run
npm testANDnpm run buildbefore shipping a version. - Add the boot-time
ALTER TABLE(with an existence guard) for any new column, indocker_entrypoint.sh. - Treat API keys / secrets as plaintext in
/dataBY DESIGN (threat model: the operator owns/data). Reference env-var names (DATABASE_URL, etc.); never hardcode values. - Keep migrations idempotent and additive; data already on a user's server must survive upgrades.
- Verify the published file actually changed (size / 404 / Last-Modified) after publish.sh.
Never
- Never add
Co-Authored-By/ "Generated with" trailers to commits — the user authors commits solo. (This was done wrong in earlier commits; do not repeat.) - Never reintroduce nonce-based CSP — it broke first paint. Use the static
'unsafe-inline'CSP innext.config.js. - Never run app commands from the repo root — always
cd proof-of-workfirst. - Never export non-HTTP-method symbols from a
route.ts— Next.js rejects the build (helpers go inlib/, e.g.lib/ai/activateConfig.ts). - Never commit
app.db,*.bak, or any user data — they're gitignored; double-checkgit statusbeforegit add. - Never click Uninstall on a StartOS package during a data cutover — it destroys the volume; use Stop.
- Never assume GitHub — don't add a GitHub remote or push there.
AI subsystem cheat-sheet
generate/route.tskicks off a detached background runner (generationRunner.ts) and returns an id; the client attaches via SSE (generations/[id]/stream) and can also poll the row. Navigating away does NOT cancel generation.- System prompt =
systemPromptBase.ts(output contract: JSON-only, libraryexerciseIds only, suggested weights) + the template's coaching prompt +PROGRAM_OUTPUT_SHAPE+ library + optional history block (historyContext.ts). - Multi-config:
AIConfigProfilerows per user;UserPreferences.activeAIConfigIdpoints at the active one and is mirrored into the legacyai*columns for back-compat.
TODO (verify before relying on)
- Exact
maketarget set beyondx86(Makefile setsARCHES := x86;make installsideloads — confirm host config). - Whether
npm run db:seedis needed in any current workflow (entrypoint handles seeding on the server).
Current state
Latest version is 1.1.0:7 (built locally, installed on the StartOS server). The registry is currently empty — all versions were unpublished; nothing is downloadable until publish.sh runs again.
Working: workout logging, programs (manual + AI), multi-user, curated library, full AI subsystem (5 providers, multi-config, background generation, history detail, cost/duration, Ollama auto-detect, infinite-scroll exercise history).
In progress: none — repo is at a clean checkpoint.
Decided but not implemented: tiered AI prompt formatting — JSON-Schema enforcement (Ollama format / OpenAI response_format), pipe-separated library, XML-tagged sections, Ollama-only few-shot. Targets local-model output quality; would ship as 1.1.0:8.
Known issues: earlier commits (8f149d3–5b0535f) carry Co-Authored-By trailers to scrub from history. publish.sh Step 3 (registry register) silently no-op'd on 1.1.0:6 and :7 — uploaded the file but didn't register; investigate before relying on those versions appearing in the registry.
Next steps:
- Scrub
Co-Authored-Bytrailers from history (filter-branch or rebase). - Re-publish current version once the Step-3 registry-register failure is diagnosed.
- Implement the tiered AI prompt formatting (1.1.0:8).