Files
pig/AGENTS.md
T
karti a6167629cc
CI / verify (push) Successful in 3m23s
Move from npm to pnpm across the workspace, CI and the image
The monorepo was on npm workspaces. pnpm gives it a content-addressed store
shared between the eight packages, a lockfile that records the whole graph
rather than a flattened view of it, and — the reason this mattered in practice —
`workspace:*`, which makes an internal dependency unambiguous instead of a
version range that npm may satisfy from the registry.

Mechanics:

  - `packageManager: pnpm@11.21.0` pins the version; corepack installs it in CI
    and in the image, so all three environments resolve identically.
  - The npm `workspaces` array is replaced by `pnpm-workspace.yaml`. pnpm
    ignores the former, and keeping both would leave two sources of truth.
  - All six internal dependencies moved to `workspace:*`.
  - Root scripts use `pnpm -r --if-present` and `pnpm -F <pkg>`.

Two findings worth recording, both from running it rather than reading it:

`tsx` was a devDependency, but the server runs TypeScript directly in
production — the container's command is `pnpm exec tsx apps/api/src/server.ts`.
Under npm this was concealed by the runtime stage re-installing tsx by hand
after pruning dev dependencies. Under `pnpm install --prod` that sleight of
hand stops working and the image simply fails to start. tsx is now declared in
`dependencies`, which is what it has always actually been.

The first image build failed with ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY.
That is not a pnpm bug: it had decided the modules directory was stale and
wanted confirmation before deleting it, which a non-interactive build cannot
give. The trigger was the host's `node_modules` reaching the build context —
there was no `.dockerignore` at all. pnpm's tree is symlinks into a
content-addressed store, so copying it into an image produces dangling links
and a directory pnpm rightly considers corrupt. Fixed by adding
`.dockerignore` and setting `CI=true`, which is required in any non-interactive
pnpm build.

`esbuild` is denied install scripts via `allowBuilds`. Its platform binary
arrives through the optional dependency `@esbuild/linux-x64` and the postinstall
only verifies it; confirmed by running the binary directly, which reports
0.25.12.

Verified under pnpm: typecheck clean, 150 tests / 0 failures, e2e passes, web
builds. The image was built and booted against a real Postgres — health ok,
`/api/dashboard` 401 with an issuer configured, `/` and `/capacity` serve the
SPA, `/og.png` serves as image/png, and the migrator runs from the pruned
runtime stage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 04:15:54 -07:00

298 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Working on PIG
Onboarding for an agent or engineer joining this codebase cold. Read this
first, then [`docs/build-plan.md`](./docs/build-plan.md) for what to build.
Named `AGENTS.md` because that is the file coding agents look for by
convention. Everything here applies equally to humans.
---
## 1. What PIG is, in three sentences
A company that aggregates GPU capacity and resells it runs **two pipelines**,
and its business is the spread between them. Generic CRMs model one pipeline
against companies — they have no concept of inventory, no concept of a
commitment you already paid for, and so cannot answer the only question that
matters: *which contracted capacity is sold, to whom, at what margin, and what
is idle right now.*
PIG is built around that question, it is Apache 2.0, and it is designed to be
self-hosted by the customer.
**The load-bearing table is `allocations`**, which joins a `capacity_commitment`
(what we bought, at a known cost) to a `demand_deal` (what we sold, at a known
price). Margin, utilisation and idle capacity all fall out of that one join.
Everything else is plumbing that exists to keep that ledger honest.
---
## 2. Orientation
```
packages/core Ontology (stages, tiers, enums) + margin arithmetic + palette
packages/db Drizzle schema, migrations, seeds
packages/prime Typed client for the Prime Intellect compute API
apps/api Hono HTTP API, auth, capacity service
apps/web React + Vite + Tailwind + shadcn-idiom components
apps/mcp MCP server (stdio) — 9 tools
docs/ ontology.md, build-plan.md, agents.md, deploy.md, seed-data.md
```
~11,000 lines. 39 tests. Node 22+.
| | |
|---|---|
| Repo | `PIG/pig` on git.karti.ai (Gitea) |
| Live | https://primeintellectgrowth.com |
| CI | Gitea Actions, `.gitea/workflows/ci.yml`, ~2 min, must stay green |
| Deploy | `bash scripts/deploy.sh` on the host — deliberately manual |
---
## 3. Running it
**PIG uses pnpm**, pinned by the `packageManager` field. Do not run `npm
install` — it will write a `package-lock.json` that nothing reads and resolve a
dependency tree that neither CI nor the image uses. Corepack ships with Node and
installs the pinned version for you:
```bash
corepack enable
pnpm install
# Postgres. PIG needs its own database — never point it at a shared one.
docker run -d --name pig-dev -p 5432:5432 \
-e POSTGRES_USER=pig -e POSTGRES_PASSWORD=pig -e POSTGRES_DB=pig \
postgres:16-alpine
export DATABASE_URL=postgres://pig:pig@localhost:5432/pig
pnpm run db:migrate
pnpm run db:seed # sourced, cited people — optional
pnpm run db:demo # a plausible demo book — optional, prefixed "DEMO — "
pnpm run dev:api # :8920
pnpm run dev:web # :5173, proxies /api to 8920
```
With no `SUPABASE_URL` set, **authentication is disabled in development** and
every request runs as the first user in the table. `loadConfig` refuses to start
in production without it, so this cannot leak.
Before pushing:
```bash
pnpm run typecheck && pnpm test
```
---
## 4. Rules that must not be broken
These are load-bearing. Breaking one produces a subtle failure, not an error.
**Intelligence never lives in the API.** Handlers validate, authorize, call a
service, serialise. Research, enrichment, scoring and matching heuristics live
in the service layer or in the agent. The API signals the agent by *writing a
row to `agent_tasks`*, never by calling it — so the queue survives the agent
being down and no request thread ever blocks on a model.
**There are two auth providers, behind one interface.** Supabase for the
hosted deployment, OIDC for on-premises — see `apps/api/src/lib/auth-provider.ts`.
Both reduce to "verify a bearer token, return a subject and an email", because
that is all PIG needs. Never reach for a provider SDK outside that file.
**Authentication is not authorization.** A verified JWT proves someone has an
account in an identity provider that PIG *shares with another application*. It
does not prove they belong here. Access requires a row in PIG's own `users`
table. A token without one gets `403 needs_profile`, which the front end turns
into the join flow — never a login screen they have already completed.
**Cost is charged against the full commitment, not the hours that sold.**
Unsold hours are already paid for. Any other treatment reports a healthy margin
on a block that is losing money, which is precisely the failure PIG exists to
prevent. There is a test pinning this; if you "fix" it, read the test first.
**Sold and held are different.** A live hold removes capacity from everyone
else's availability — otherwise two sellers promise the same GPUs — but it is
not revenue and must never count toward utilisation.
**Money is integer cents.** Never floats. Round, never truncate: 2.43 is
2.4299999 in binary and a lost cent compounds across millions of GPU-hours.
**Agent claims carry evidence.** Enrichment writes to `facts` with a score, a
band, a source URL and a status — never directly to the record. Only `verified`
self-applies; anything weaker waits for a human. An agent allowed to write
unattributed claims will eventually write a wrong one and nobody will be able
to tell which.
**Never invent data about real people.** Seed records carry a confidence grade
and a source URL, both shown in the UI. No email address is ever inferred.
Authorship is not employment — contributors, residency participants and alumni
are recorded as what the evidence shows. Demo data is prefixed `DEMO — ` and
its customers are fictional.
---
## 5. Traps that have already bitten
Each of these cost real time. None produced an error message.
**`onConflictDoNothing()` is a no-op without a matching unique constraint.**
It has silently duplicated seed data twice. If there is no unique index to
conflict on, do an existence check instead.
**`z.coerce.boolean()` turns the string `"false"` into `true`.** Every feature
flag set to false was silently on. Use the `envBoolean` helper in
`apps/api/src/lib/config.ts`.
**Mutations must confirm themselves.** `<Toaster />` is mounted in `App.tsx`
inside `ThemeProvider`; use `toast.success` / `toast.error` in every mutation's
`onSuccess` / `onError`. The shadcn Toaster ships wired to `next-themes`, which
PIG does not use — it was rewired to PIG's `useTheme`. Before that it was never
mounted, so toasts already written in RecordSheets fired into nothing and every
save completed in silence.
**shadcn's `accent` is a SUBTLE surface, not the brand.** shadcn uses
`bg-accent` for hover, focus and selected states — dropdown items, command
rows, ghost buttons. The brand is `primary`. In `tailwind.config.js`, `accent`
is therefore aliased to `--accent-subtle` and `primary` to `--accent`. Use
`bg-primary` for a solid brand fill; never `bg-accent`. Mapping them the other
way makes every hover state paint a full-strength brand block, which in dark
mode with the monochrome palette is a glaring white slab.
**Grid and flex children need `min-w-0`.** They default to
`min-width: auto`, meaning they refuse to shrink below their content — and a
`tabular-nums` figure, a `whitespace-nowrap` badge or a `truncate` title is all
it takes. The page then scrolls sideways on a phone and nothing reports an
error. This was fixed three separate times at individual call sites before
`Card` was given `min-w-0` on its base class; **any new container primitive
needs the same**. Check with:
```js
document.documentElement.scrollWidth - document.documentElement.clientWidth
```
It should be 0 on every route at 393px wide.
**pnpm needs `CI=true` in any non-interactive build.** When it decides a
modules directory is stale it asks before removing it; with no TTY it cannot
ask, so it aborts with `ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY`. This reads
like a pnpm bug and is not — it is pnpm refusing to delete files nobody
confirmed. Both the Dockerfile and CI set it. The usual trigger is a host
`node_modules` reaching the build context, which is why `.dockerignore` exists:
pnpm's tree is symlinks into a content-addressed store, so copying it into an
image yields dangling links and a modules directory pnpm considers corrupt.
**`tsx` is a production dependency, not a dev one.** The server runs TypeScript
directly — `pnpm exec tsx apps/api/src/server.ts` is the container's command —
so pruning it away breaks the image. It lives in `dependencies` deliberately;
moving it back to `devDependencies` because "it's a build tool" makes
`pnpm install --prod` produce an image that cannot start.
**Drizzle-generated migrations are not always valid SQL.** A `jsonb → integer`
cast was emitted without the `USING` clause Postgres requires. Always apply a
new migration to a real empty database before pushing — CI does this, but find
out before CI does.
**The proxy allows exactly one inline script, by hash.** `index.html` carries a
pre-paint theme script that prevents a white flash for dark-mode users. Editing
it changes its hash and the browser silently blocks it. CI asserts the hash;
if it fails, update the CSP in `deploy/Caddyfile.example`, on the server, and
the expected value in the workflow.
**`prices.onDemand` from the Prime Intellect API is the TOTAL FOR THE NODE.**
Verified: 1× A100 at 1.79, 2× A100 at 3.58. `gpuMemory` is likewise a node
total. There is an open bug for this — the mapper currently stores both as if
per-GPU, so an 8-GPU node reads eight times too expensive.
**The SPA fallback must never answer an `/api/` path.** Without an explicit
guard, an unknown API route returns `200 text/html` — the app shell — and the
caller sees `response.ok === true` before failing on `JSON.parse` with
"Unexpected token '<'", a long way from the cause. Both the static-file
middleware and the SPA fallback in `apps/api/src/server.ts` carry the guard;
anything added after them needs it too.
**Prime Intellect has two API hosts.** `api.primeintellect.ai` is compute and
pods. Inference is `api.pinference.ai/api/v1`, OpenAI-compatible.
**Piggy's default model thinks aloud.** `nvidia/nemotron-3-nano-30b-a3b` is a
hybrid reasoning model; under a tight `max_tokens` it rambles and truncates.
Pass `reasoning_effort: "none"` for tool use, routing and extraction.
**Deployment traps** live in `deploy/README.md` — chiefly that every Caddy site
block on that host needs `bind 10.0.0.2`, and that the CI runner uses
`container.network: host` so dependencies must be published on `127.0.0.1`.
---
## 6. Conventions
**Comments explain *why*, never *what*.** The code says what it does. Comments
carry the reasoning that would otherwise be lost — why this treatment and not
the obvious one, what breaks if it changes, what was tried and rejected. Match
the density already in the file; do not add narration.
**Match the surrounding style.** British spelling in prose and comments
(`utilisation`, `normalise`). Types are explicit at module boundaries. No
default exports.
**Enums come from `@pig/core`.** Never retype a stage list or a tier into a zod
schema — import it, so removing a value stops validating rather than silently
persisting.
**Tests pin decisions, not implementations.** The valuable cases are the ones
that would pass under a plausible-but-wrong version. Look at
`packages/core/test/margin.test.ts` for the register.
**Commits explain the reasoning**, including what was tried and rejected, and
say plainly when something was found by running the code rather than reading
it. Read `git log` for the register.
**Verify by running, not by assuming.** Several of the worst defects here were
found only by fetching a URL from another machine or applying a migration to a
real database. "It should work" has been wrong repeatedly.
---
## 7. Where to start
[`docs/build-plan.md`](./docs/build-plan.md) has 24 tasks in three waves with
real dependency edges.
**Do these first, alone, before anything fans out:**
- **F1** — install the shadcn primitive set
- **F3** — the RBAC permission model
- **F2** — the shared API write-path convention (needs F3 to call into)
They are small and they are the interface every other track codes against.
Starting parallel work before they settle is how it turns into merge conflict.
**Then the two that unblock a demo:**
- **A1** — allocation and commitment write paths. Today the core table can only
be populated by seed, so a visitor can look at the demo book but cannot enter
a deal of their own.
- **A2** — API keys. Nothing mints one, so the MCP server — the headline
feature — is unreachable in production.
**A4 (Piggy) is fully independent** and can start immediately alongside the
foundation. It touches no UI and no shared API conventions.
---
## 8. What not to do
- Do not copy component files from `trycompai/crm`. Most are shadcn/ui
originals — take them from upstream where they are canonical. Borrow the
compositions as ideas; the debt is credited in `NOTICE`.
- Do not open self-registration on the identity provider. It is shared with
another application. PIG mints accounts itself, gated on an invite.
- Do not put a production SSH key on the CI runner. Deployment is manual on
purpose.
- Do not weaken the production guard that refuses to start without an identity
provider.
- Do not seed or infer email addresses for real people.