Commit Graph

29 Commits

Author SHA1 Message Date
karti c2c7fb9c19 Make mutations confirm themselves, and seed the evidence trail
CI / verify (push) Successful in 2m51s
Two demo gaps, both of which made working features look like they were not
there.

**Toasts fired into nothing.** RecordSheets already called toast.success on
every save, but <Toaster /> was never mounted, so nothing appeared. It could
not be mounted, either: the shadcn original imports next-themes, which PIG does
not use — it has its own provider so a chosen theme is persisted server-side
and follows a user between devices. Rewired to PIG's useTheme, mounted inside
ThemeProvider, and offset clear of the phone tab bar and the home indicator.

Feedback added where the interface otherwise gives none: allocation and hold
report the GPU-hours actually written, because the sheet closes on success and
the only other evidence is a number moving off-screen; releasing a hold says
the capacity is sellable again; fact decisions say what the decision meant, and
that approving evidence is not the same as writing it to a record; the profile
form confirms rather than just clearing itself, which otherwise reads as the
input being discarded.

**The fact table was empty**, so the review queue and every provenance tooltip
had nothing to show — the mechanism that makes an agent-written CRM
trustworthy, invisible. Six agent-derived facts seeded with a deliberate mix:
two applied, showing what a confident agent writes unprompted, and four
proposed, including one weak claim that a reviewer should reject, so the queue
is not a row of obvious approvals. Each carries a score, a band, evidence and
where available a source. Idempotent on subject+field+value; verified over two
runs.

Verified: toast confirmed firing in a real browser on a 393px viewport, 135
unit tests and e2e green, typecheck clean, CSP hash unchanged, 0px horizontal
overflow across 12 routes at both breakpoints.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 03:39:00 -07:00
karti 2763531ce4 Align shadcn's accent token with what shadcn means by it
CI / verify (push) Successful in 2m53s
shadcn uses `bg-accent` for its SUBTLE surfaces — dropdown item hover, command
row selection, ghost and outline button hover, the dialog close affordance. The
brand colour in shadcn is `primary`.

PIG's Tailwind config mapped `accent` to `--accent`, which is the brand. That
inverted the meaning, so every shadcn hover and selection state painted a
full-strength brand block. With the monochrome "pig" palette in dark mode the
brand is near-white, so a selected command row rendered as a white slab against
a near-black sheet. Measured before the change: selected row rgb(250,250,250)
on a rgb(9,9,11) body.

`accent` now aliases `--accent-subtle` and `accent-foreground` aliases
`--accent-fg`, which is what those tokens were created for. The eleven places
where PIG's own components wanted a solid brand fill — filled chips, selected
card borders, progress bars — move to `primary`, which still resolves to
`--accent`. A `brand` alias is added for clarity.

After: selected row rgb(39,39,42) in dark and rgb(244,244,245) in light, both a
subtle tint above the body; the pipeline's active stage chip stays a solid
rgb(250,250,250) fill, unchanged.

Found by opening overlays, which earlier screenshot sweeps never did — every
route had been checked, but a dropdown or a command palette only misbehaves
once it is open. Worth remembering: page-level sweeps do not exercise portals.

Typecheck clean, 135 unit tests and e2e green, CSP hash unchanged, 0px
horizontal overflow across 12 routes at 393px and 1440px.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 03:17:49 -07:00
karti c821b2ca07 Authenticate against any OIDC provider, for on-premises installs
CI / verify (push) Successful in 2m55s
The seam existed with only a Supabase implementation, so an on-prem deployment
had no way to authenticate. A customer running PIG inside their own network
already has Okta, Entra, Keycloak, Auth0 or Google Workspace; asking them to
stand up a second identity system is a serious adoption tax and in a regulated
environment usually refused outright.

Setting PIG_OIDC_ISSUER is normally the whole configuration — the JWKS is
discovered from the issuer's well-known document. PIG_OIDC_JWKS_URI skips
discovery entirely for an air-gapped network. OIDC takes precedence over
Supabase so an on-prem install can leave the hosted values in its environment
file without them quietly taking over.

Three decisions worth stating:

Discovery is resolved lazily and the FAILURE is not cached. Doing it per
request would put the customer's identity provider on the critical path of
every API call; doing it eagerly at boot would mean their IdP rebooting takes
the CRM down with it. So it happens on first use and retries on the next
request.

The audience check is optional but warned about loudly. Without it, a token the
provider issued for ANY other application in the same tenant verifies here — a
token minted for an unrelated internal tool would be accepted as a PIG session.
It cannot be mandatory because some providers legitimately issue
single-audience tokens.

Email falls back through email, preferred_username and upn, because providers
disagree, but a preferred_username without an "@" is ignored — PIG keys
membership on the address, and a bare username must never become an account
identity.

Also fixed a warning that claimed "authentication is DISABLED" on a correctly
configured OIDC deployment. That is worse than silence: an operator who reads
it on a secure install learns to ignore the warnings. The dev bypass itself was
already correct — it keys on the resolved provider rather than on Supabase.

18 new tests, most of them about what the provider must REFUSE: a foreign
signing key, a foreign issuer, a token for a different application, an expired
token, a token with no subject, and a discovery outage that must not become
permanent. Keys are generated per test and the JWKS is served locally, so they
run offline.

Verified: production refuses to start with neither provider, starts with OIDC
alone, enforces 401 on an unauthenticated request, and warns only about the
genuinely missing admin list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 02:32:28 -07:00
karti 54edee30ed Unknown /api paths returned the SPA with HTTP 200
An authenticated GET to any unrecognised API route — a typo, a renamed
endpoint, an older client — fell through to the SPA fallback and returned
200 text/html containing the app shell.

This is close to the worst failure shape for an API consumer. `response.ok` is
true, so nothing treats it as an error; the caller then dies on `JSON.parse`
with "Unexpected token '<'" far from the actual cause. The MCP server, the CLI
and Piggy all consume this API and would all have hit it. It was masked from
casual testing because unauthenticated requests are rejected earlier by the
auth middleware, so it only appears once you hold a valid token.

Found by probing production with Scott's token: GET /api/keys (the real path is
/api/api-keys) returned 200 text/html.

The static-file middleware already carried this guard — added for the same
reason when og.png was being served as HTML — but the SPA fallback beneath it
did not. Same guard, one place missing.

Verified: unknown API paths now return 404 application/json, real API paths
still answer, client-side routes still receive the shell, and static assets
still serve with their own content types. Typecheck clean, 124 unit tests and
the e2e suite green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 02:05:00 -07:00
karti 6bd5526675 Give Card min-w-0 so the page stops scrolling sideways on a phone
The Overview page overflowed 80px at 393px wide. Traced to the "The book" card:
the grid column was a correct 361px, the card inside it was 457px and refused
to shrink. Confirmed by forcing `min-width: 0` on grid children in the live
page, which took the overflow to 0.

Fixed on the Card base class rather than at the call site, because this is the
third time the same trap has been fixed individually — grid and flex children
default to `min-width: auto` and cards routinely hold something unshrinkable, a
tabular-nums figure or a nowrap badge. `min-width: 0` is inert for a
block-level card outside a flex or grid parent, so applying it always costs
nothing and removes the whole class of bug.

Verified by running the stack locally against the demo data: 0px overflow
across all 12 routes at both 393px and 1440px.

AGENTS.md updated to say any NEW container primitive needs the same, with the
one-line browser check to confirm it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 02:01:17 -07:00
karti d4d7095605 Migrate before starting the application
CI / verify (push) Successful in 2m32s
2026-08-13 01:48:05 -07:00
karti 853bde2265 Build the agent-native compute CRM platform
CI / verify (push) Successful in 3m6s
2026-08-13 01:39:01 -07:00
karti bfd2f8d95a Add AGENTS.md — onboarding for whoever picks this up next
CI / verify (push) Successful in 1m32s
Named by convention so a coding agent finds it without being told. Written to
get someone productive from a cold start without having to reconstruct the
reasoning from the diff.

Four sections carry the weight. The architecture rules that must not be broken,
each of which fails silently rather than loudly — intelligence never lives in
the API, authentication is not authorization, cost is charged against the full
commitment, sold and held are different things. The traps that have already
cost time here, with the specific symptom each produces: onConflictDoNothing
being a no-op without a constraint, z.coerce.boolean turning "false" into true,
grid children needing min-w-0, Drizzle emitting a cast Postgres rejects, the
CSP allowing exactly one inline script by hash, and the Prime Intellect API
quoting node totals rather than per-GPU prices. The conventions, including that
comments explain why rather than what. And an explicit start order.

The last section says what not to do, which is the part most easily lost: do
not copy component files from the MIT project we borrowed ideas from, do not
open self-registration on a shared identity provider, do not put a production
key on the CI runner, do not weaken the guard that refuses to serve the CRM
unauthenticated, and do not invent email addresses for real people.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 22:25:57 -07:00
karti cf3117e458 Record verified Prime Intellect API facts, and log a real pricing bug
CI / verify (push) Successful in 1m33s
The token was found on cloud-1 after all, in a Claude memory note. Verifying its
claims against the live API turned up a defect in code already shipped.

**prices.onDemand is the total for the whole node, not per-GPU.** Confirmed:
datacrunch lists 1x A100 at 1.79 and 2x A100 at 3.58, and gpuMemory scales the
same way (640 for 8x 80GB). packages/prime/src/map.ts stores both as if they
were per-GPU, so an 8-GPU node reads eight times too expensive. It would have
silently poisoned inventory search, the max-price filter and every margin
comparison against bought capacity — and nobody would have noticed, because the
numbers still look plausible. Logged rather than fixed, per the instruction to
hold; it needs a regression test built from the real 1x/2x pair.

**Inference is a different host.** api.primeintellect.ai is compute and pods;
inference is api.pinference.ai/api/v1, OpenAI-compatible. PIG's config knows
only the first, so A4 and A13 need both.

**Piggy's default model** is nvidia/nemotron-3-nano-30b-a3b, and the important
detail is that it is a hybrid reasoning model which thinks aloud by default and
truncates under a tight max_tokens. `reasoning_effort: "none"` gives ~1s terse
output for tool use and extraction, which is what Piggy does nearly all of the
time.

One claim did NOT reproduce: the note warns of Cloudflare 403ing non-browser
user-agents, but PIG's own UA and curl's both returned 200. Recorded as history
in case a 403 ever appears.

Also logged: the key is a broad, never-expiring credential sitting in plaintext
in a memory markdown file. PIG's sync should hold a separate narrower key
scoped to availability reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 21:52:33 -07:00
karti 3398345109 Plan: add RBAC, capacity tiers, imports, admin settings; expand contracts
CI / verify (push) Successful in 2m36s
Six additions, and one promoted to foundation.

RBAC becomes a Wave 0 task rather than something absorbed into each CRUD
track. Authorization today stops at "is a member", and eight parallel write
tracks plus a bulk-import feature are about to land. Import especially: one bad
column mapping can rewrite thousands of records, so it must not ship before
there is a real answer to who may run it. Suggested default is team leads and
platform admins only — easy to loosen later, unpleasant to tighten.

A government/sovereign capacity tier joins secure and community. It is a schema
change, so it is cheaper before the tables carry real data. Matching must never
satisfy a government requirement with community capacity, and the tier
interacts with the export-control predicate already modelled in compliance.ts.

Imports are split so the work is reusable: a CSV/Excel framework carrying the
upload, mapping, dry-run preview and idempotent commit, with Notion and Google
Sheets as thin OAuth front-ends onto the same mapping step. Notion databases
are tables with typed properties, so treating them as a separate importer would
duplicate the hard part.

Admin settings gains Piggy's model selection, defaulting to a Nemotron model on
Prime Intellect inference, with the endpoint configurable so an on-prem install
can point at the customer's own.

Contracts is expanded from "surface the schema" to the full field set — the
hierarchy with order-form-beats-MSA precedence, negotiated SLA terms including
fee abatement and its trigger, spare-pool scope, maintenance classes and the
reasonable-endeavours carve-out, plus take-or-pay and termination tier. Written
as a first draft to be corrected by someone who negotiates these for a living.

Also records three open questions, including that the Prime Intellect API token
could not be found on cloud-1 — searched ~/.prime, ~/.config/prime, /opt and
/etc.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 21:33:38 -07:00
karti e4698e4d0c Add a build plan, informed by reading Comp AI CRM properly
CI / verify (push) Successful in 1m32s
Cloned trycompai/crm (MIT) and inventoried it rather than assuming. Three
findings shaped the plan.

Their component library is far deeper: 68 primitives to our 9, including
data-table, command, sheet, drawer, combobox, chart, and a set of agent-chat
components that map almost exactly onto what Piggy needs.

Their SourcedValue/Provenance pattern is worth adopting outright — a dotted
underline on any agent-derived value with a tooltip carrying the claim, the
reasons, when it was observed and the source URL. PIG already stores all of
that in `facts` and surfaces none of it.

But we are ahead of them on mobile, not behind. Measured across both repos:
211 responsive utilities across their 329 tsx files (0.6 per file) against 58
across our 16 (3.6 per file); zero safe-area handling to our five; no drawer or
sheet used for navigation, no viewport-fit. Their app is effectively
desktop-only. So the plan takes depth from them, not mobile behaviour.

The plan also says not to copy their component files. Most are shadcn/ui
originals — MIT, and designed to be installed from upstream where they are
canonical and current. Borrow the compositions as ideas; the debt is already
credited in NOTICE.

Structured into waves with real dependency edges so the work can be handed to
several agents without collision. Two foundation tasks must land alone first
(the primitive set, and the API write-path convention) because eight parallel
CRUD tracks would otherwise each invent their own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 21:09:34 -07:00
karti 14417e34bc Margin table: say "Covered" rather than $0.00 for break-even
CI / verify (push) Successful in 1m51s
Same fix already applied to the capacity cards, missed here. A zero break-even
means the block's cost is fully recovered and any further sale is upside;
printing "$0.00" is technically true and reads like a rendering bug. The demo
book has three such blocks, so it was visible on every screenshot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 20:58:22 -07:00
karti 468979b303 Add a plausible demo dataset
CI / verify (push) Successful in 2m1s
So the product is legible before anyone has entered real data, and so Piggy
has something to reason about while it is being built.

Kept separate from the base seed because that one is publicly-sourced and cited
while this is invented. Two rules, both deliberate:

Every record is prefixed "DEMO — ", so a screenshot can never be mistaken for
real business. And demand-side customers are fictional. Suppliers are real
companies — they are public, and naming the actual market is the point — but
inventing customers with invented contract values against real named businesses
would be fabricating commercial records about them, which is a different thing
and not worth the extra realism.

The numbers are tuned to teach rather than to flatter. The book clears +5.4% at
79% utilisation, which is thin and about right for this industry once capacity
cost is charged honestly. Underneath, the blocks disagree: the large H200 block
carries it, the EU H100 block is underwater at 55% sold because a 46% markup
needs ~69% sold to break even, and the community pool holds a large unconverted
hold — so the difference between "sold" and "held" is visible rather than
theoretical.

An earlier tuning left the whole book at -26%. Honest, but it reads as a broken
product rather than an under-utilised book, so the totals now open healthy and
the problems appear on drill-down.

Also exercises parts of the schema nothing had touched yet: ramped capacity
shapes, negotiated SLAs with fee abatement and spare-pool scope, renewal
obligations with one deliberately near-term, EU data-residency constraints on a
capacity request, and internal research burn.

Two bugs found while testing it, both the same trap as before: the research
allocation duplicated on every run because onConflictDoNothing() is a no-op
without a matching unique constraint, and notes were double-prefixed. Verified
idempotent over three consecutive runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 20:55:41 -07:00
karti 2a30645c8d CI: Postgres on 127.0.0.1, because the runner uses host networking
CI / verify (push) Successful in 1m59s
The runner is configured with `container.network: host`. That one setting
explains all three earlier failures, and the workflow now records them so
nobody repeats the sequence:

  services:                     not resolvable by name from a host-networked
                                job — "getaddrinfo EAI_AGAIN postgres"
  --network container:$HOSTNAME /etc/hostname is the HOST's name, not a
                                container id, so the join finds nothing
  default-gateway addressing    wrong idea outright: with host networking the
                                default route is the real router, not a bridge

Sharing the host's network namespace means a published port is just on
127.0.0.1. Readiness is now checked over TCP from the job itself rather than
with pg_isready inside the container — the latter proves the server started,
not that this job can reach it, which is the thing that actually failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 20:32:06 -07:00
karti e6b4c1618e CI: reach Postgres through the gateway instead of a shared namespace
CI / verify (push) Failing after 5s
Second attempt failed differently: /etc/hostname inside the job reports the
HOST's name rather than the container id, so `--network container:$HOSTNAME`
found no such container.

Rather than hunt for our own container id through /proc, publish the port on
the host and connect through the job container's default gateway. That needs
no container identity at all. The port is derived from the run id so two
concurrent runs cannot collide, and DATABASE_URL is exported through GITHUB_ENV
once Postgres is actually accepting connections.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 20:30:24 -07:00
karti 40f6fd993d CI: start Postgres as a step rather than a service container
CI / verify (push) Failing after 5s
The first run got through install, typecheck and all 39 tests, then failed on
`getaddrinfo EAI_AGAIN postgres`. This runner does not attach service
containers to the job's network, so the `services:` hostname never resolves.

Fixed by starting Postgres with `--network container:$HOSTNAME`, sharing the
job container's own network namespace so it appears on 127.0.0.1. That works
regardless of how the runner is configured — which matters here because the
runner is shared with other repositories and should not need reconfiguring to
suit this one.

Also queries row counts through `docker exec` rather than a local psql, since
the runner image is not guaranteed to ship postgresql-client, and removes the
container in an `if: always()` step so a failed run does not leave it behind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 20:29:25 -07:00
karti 73231a8944 Add CI, a test suite, and a deploy script
CI / verify (push) Failing after 34s
`npm test` did nothing until now. CI that runs no tests is theatre, so the
tests came first — 39 of them, over the two places where an error would be
silent and expensive.

packages/core: the margin arithmetic. Every dashboard figure, idle-capacity
alert and agent answer resolves through it, and wrong numbers still look like
numbers. The cases pin decisions rather than implementation: cost is charged
against the full commitment (a naive version reports the opposite sign on a
loss-making block), aggregation sums cents rather than averaging percentages
(averaging reports +22% on a book that is losing money), break-even prices the
remaining hours and returns null rather than Infinity when there are none, and
internal research burn counts as cost with no revenue.

packages/prime: the upstream mapping. Rounding rather than truncating cents,
because 2.43 is 2.4299999 in binary and a lost cent compounds across millions
of GPU-hours. And interconnect normalisation, where an unrecognised fabric maps
to Unknown rather than Ethernet — guessing low loses a deal, guessing high
sells a training customer a cluster that cannot train.

CI runs on push and pull request: typecheck all six packages, unit tests,
migrations applied twice to a real Postgres, a seed-idempotency assertion that
fails the build if row counts move on a second run, a server boot, the front-end
build, and a Docker build.

It also asserts the inline theme script's hash still matches the CSP the proxy
allows. That script prevents a white flash for dark-mode users; if it changes
without the CSP being updated, the browser silently blocks it and nothing
anywhere reports an error.

Deployment stays a script rather than push-to-deploy. Automating it would put
an SSH key with production write access on the CI runner — a real escalation
for a project this size. The script takes a database dump before migrating and
refuses to finish if an unauthenticated request returns anything but 401.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 20:27:47 -07:00
karti 7719850fc5 Let people register with a personal email and an invite code
The invite gate was unreachable. PIG shares its identity provider with another
application, and self-registration there is deliberately switched off — so
somebody with a personal address and a valid invite code could never obtain a
token, and therefore could never reach the endpoint that accepts the code. The
gate was real and nothing could ever arrive at it.

Opening self-registration on the provider would have opened it for the
neighbouring application too, which is precisely why it was closed. So PIG now
mints the account itself through the provider's admin API, and only after the
invite validates. The provider stays shut; the invite becomes the actual gate.

Order is deliberate: validate the invite, create the auth account, create the
profile, consume the invite. If the profile write fails the auth account is
deleted again — otherwise someone could sign in with no profile and no way to
obtain one, because their invite would look spent.

An administrator's address does NOT bypass this. The bypass in /api/signup
exists to bootstrap the first admin from an account that already exists; here
an account is created from nothing, and an ungated version of that is simply
an open registration endpoint.

Registration signs the person in on success rather than returning them to a
login form to retype the password they entered ten seconds earlier. An address
that already exists in the provider but has no PIG profile is detected and
pointed at sign-in, since /api/signup handles that case properly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 20:15:32 -07:00
karti 045e51bc5c Polish: team page title, and accent swatches that match the theme
The accent swatches always previewed the light-mode value, which made the
near-black "Pig" swatch effectively invisible on a dark card — the one place
the preview needed to be accurate. Each accent is tuned twice because a colour
that reads well on white is usually too dark on near-black; the swatch now
shows the value for the theme actually in effect.

Team and the placeholder routes also set document titles, so every route in
the app is now distinguishable in history and in a tab strip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 19:55:38 -07:00
karti 7c134140ff Fix static serving — og.png was returning index.html
The server mounted static files at /assets only, so every top-level file fell
through to the SPA catch-all. og.png, apple-touch-icon.png, icon-192.png and
manifest.webmanifest each returned `200 text/html` containing the app shell.

This is a nearly invisible failure. The app works. The tab shows an icon,
because browsers cache the SVG. Nothing errors anywhere. But a link shared to
iMessage, Slack or X fetches og.png, receives HTML, and renders a card with no
image — which is the entire point of having made one.

Now the whole dist directory is served, with /api excluded so routes are never
answered from disk, and the SPA fallback still catching client-side routes that
have no file behind them.

Found by fetching the asset from an outside host instead of trusting that
adding the file was enough. Verified: og.png is image/png at 53KB, the icons
and manifest carry their real types, /margin and /capacity still receive the
shell, and the API is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 19:51:58 -07:00
karti d154a78d48 Polish: per-route titles and a way to sign out
There was no sign-out control anywhere. Easy to miss when you develop
permanently signed in, and a stranded feeling for anyone on a shared machine.
Clearing the identity provider's session is sufficient — PIG holds no session
of its own — and the auth listener returns to sign-in without a reload. A hard
redirect follows regardless, so a failed provider call cannot leave a
half-signed-out interface.

Every route now sets its own document title. A single static title makes
browser history and a wall of tabs useless: every entry reads "pig" and nobody
can tell the margin view from the pipeline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 19:49:32 -07:00
karti 46d7d80f1b Fix boolean env parsing — PRIME_SYNC_ENABLED=false was reading as true
z.coerce.boolean() calls Boolean(value), so the string "false" is true. So are
"0", "no" and "off". Every feature flag set to false was silently on.

Caught by reading a startup warning that should not have been there: the
deployment logged "PRIME_SYNC_ENABLED is on but PRIME_API_KEY is unset" while
the .env plainly said false. Had the key been present, PIG would have started
polling a third-party API nobody asked it to poll.

Replaced with an explicit parser accepting 1/true/yes/on, treating an empty or
absent value as the default. Demonstrated both behaviours side by side before
committing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 19:46:21 -07:00
karti 0551e8dd6e Add link previews and icons; make PIG_INVITE_CODE actually work
Social and icons. A 1200x630 card, apple-touch-icon, and maskable PWA icons,
generated from an HTML template by a script so the mark, wordmark and tagline
cannot drift from the product. The apple-touch-icon referenced in index.html
was a 404 until now. Icons are drawn on an opaque plate with inset because iOS
rounds corners and Android may apply a circle — an edge-to-edge mark loses its
ears to that crop. og:image is absolute, which is the single most common
reason a card unfurls blank.

PIG_INVITE_CODE was a lie. Signup validates against the invites table, so
setting the variable only flipped a label in the UI — an operator would set it,
hand the code to a colleague, and watch them be rejected. It is now reconciled
into a real invite row at boot: setting it issues, changing it rotates and
revokes the predecessor, and removing it revokes. Verified all three, plus that
a restart with an unchanged code does not duplicate.

Two bugs found while doing that:

- `uses_remaining` was jsonb, so the SQL decrement could never have worked.
  Now integer. The generated migration failed because Postgres has no implicit
  jsonb->integer cast, so the USING clause is hand-written.
- Redemption keyed off `redeemedAt`, which would have made every reusable
  invite single-use — a confusing way to lock a team out. Availability now
  comes from `usesRemaining`, and redemption records who used it most recently
  without consuming it.

Sign-in gains a password option alongside the magic link, defaulting to
password since that is the daily path. PIG stores neither; both are handled by
the identity provider and PIG only ever sees the resulting token.
autocomplete is set so password managers and iOS can fill.

Verified: full migration chain applies to a fresh Postgres, the CSP hash for
the inline theme script is unchanged by the rebuild.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 19:43:10 -07:00
karti 93818a2d2c Document two proxy traps found by deploying rather than assuming
The deployment came up healthy, served a valid certificate, and returned 200 —
and was completely unreachable. Two distinct causes, both invisible from
inside the host:

1. Every other site on this proxy binds to a private VNIC address. Caddy
   groups site blocks into servers BY listen address, so a block without
   `bind` landed in a separate server on :443. The specific listener wins for
   traffic arriving on that address, which is all public traffic after NAT, so
   requests hit the server that had never heard of these hostnames and fell
   through to an empty 200. Testing from the host with --resolve 127.0.0.1
   worked perfectly, which is exactly why this was worth chasing from a third
   machine instead of trusting a local check.

2. The CSP blocked the inline pre-paint theme script, so dark-mode users would
   have seen a white flash on every load. Fixed with the script's hash rather
   than 'unsafe-inline', which would have defeated the policy, and rather than
   an external file, which would have reintroduced the flash. Editing that
   script changes its hash and silently breaks it, so that is written down.

Verified from an independent host: health returns JSON, the app serves, an
unauthenticated API call is refused, the short alias redirects, security
headers are present, and the existing sites on the proxy are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 19:31:57 -07:00
karti 7bb8835974 Add profile creation — close the gap between signing in and being a member
Deploying and then trying to actually use it surfaced a dead end: /api/signup
was exempted from auth but never implemented, so a real person could sign in,
receive 403 needs_profile, and have nowhere to go. Authentication worked;
joining did not.

The route is mounted before the auth middleware, because requiring membership
to reach the route that grants membership is circular. It verifies the token
itself and then requires one of two things:

- A valid invite code. Stored hashed, optionally pinned to an address,
  optionally expiring, consumed on redemption with the redeemer recorded.
- Presence in PIG_ADMIN_EMAILS. The bootstrap path, which exists because on a
  fresh deployment nobody can issue an invite since nobody can sign in to
  issue one.

The bootstrap path is narrow by construction: the address must be listed in
server-side configuration AND match the verified email claim on the token.
Admin rights are never read from the request body, so a crafted payload
cannot grant them.

Two behaviours worth noting. A row that was invited but never signed into is
claimed rather than rejected, binding it to the identity that just proved
ownership of the address. And a resubmitted form returns the existing user
instead of erroring, because a double-tap should be harmless.

The front end now treats needs_profile as a step in the flow rather than an
error, showing a team picker. Sending someone back to a login screen they have
already completed is a loop with no exit.

Also: the seed no longer creates the dev@localhost admin row under
NODE_ENV=production. It was unreachable (no auth subject, so nobody can sign
in as it), but an admin-flagged placeholder in a real deployment is a trap.

Verified: rejects a missing token, rejects an invalid body, claims a pre-existing
row, and is idempotent on resubmission.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 19:26:43 -07:00
karti c747eb2aa7 Add deployment: Dockerfile, compose, proxy config, and docs
One container plus a Postgres behind any TLS-terminating proxy. Nothing is
specific to a particular host.

The app and API are served from a SINGLE origin. This is not tidiness: browser
auth sessions live in per-origin storage, so splitting them across two
hostnames makes sign-in loop in a way that presents as a server fault. The
short alias redirects rather than serving a second origin.

Two safety properties verified by running the image, not by reading the code:

- With NODE_ENV=production and no SUPABASE_URL, the process refuses to start
  and says why. Serving the whole CRM unauthenticated is a worse outcome than
  failing to deploy, so the failure is deliberate and loud.
- In production the development auth bypass does not apply: an unauthenticated
  request to /api/dashboard returns 401 rather than adopting the first user in
  the table.

The Dockerfile typechecks all six packages as a build gate, so a deploy that
does not compile fails at build time rather than in front of a user. Runtime
runs unprivileged as `node`, and Postgres is not published to the host.

Docs cover the ontology and why it is shaped this way, agent connection for
Claude Code / Codex / prime-agent / Buzz, and the provenance rules governing
seed data about real people — including how to have your record removed.

Verified: image builds, container reports healthy, serves the SPA, enforces
auth, and the production guard exits non-zero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 19:19:53 -07:00
karti de33a03524 Add the web app, seed data, and user-selectable theming
apps/web — React, Vite, Tailwind, shadcn-idiom components. Mobile Safari is a
first-class target, not an afterthought:

- Two navigation treatments rather than one compromise. A bottom tab bar on
  phones, because the top of a large phone is out of thumb reach; a persistent
  sidebar from lg upward, so an iPad in portrait gets it too.
- Safe-area insets throughout, so the tab bar clears the home indicator and the
  last row of a list is actually reachable.
- Inputs are pinned to a 16px minimum, which is the correct fix for Safari
  zooming on focus. user-scalable=no is not used: it breaks pinch-zoom for
  everyone and recent iOS ignores it anyway.
- The pipeline board becomes a stage picker on phones. An eight-column board
  scrolling horizontally on a 390px screen is technically responsive and
  practically useless.

Theming: users pick an accent and the whole interface re-tints. Accent values
live once, in @pig/core, and are written onto the root element at runtime —
there is no CSS copy to drift from the TypeScript. Preferences are stored
server-side so they follow a person between laptop and phone, mirrored into
localStorage only so the pre-paint script can avoid a white flash. Status
colours stay fixed regardless of accent: if "at risk" re-tinted to whatever
someone picked, the signal would be gone.

Seed data is public research, every record carrying a confidence grade and a
source URL. No email addresses are seeded or inferred — none are published, and
guessing them from a name and a domain is unreliable and rude. Authorship is
not promoted to employment: contributors, residency participants and alumni are
recorded as what the evidence actually shows, and a name that could not be
sourced at all is listed as unresolved rather than invented.

Three defects found and fixed by actually running it rather than assuming:

1. The seed was not idempotent. onConflictDoNothing() with no target is a no-op
   without a matching unique constraint, so a second run duplicated 27
   contacts. There is deliberately no unique index on (account, name) — two
   people at one company can share a name — so idempotency is enforced in the
   seed instead of by bending the schema.
2. /capacity scrolled sideways on a phone. Grid items default to
   min-width:auto and `truncate` sets nowrap, so a long title became
   unshrinkable content and widened the track. Fixed with min-w-0 on every
   truncating grid child.
3. The idle-capacity alert silently failed to fire at exactly 80% utilisation,
   losing a float comparison against a 0.2 threshold. Moved to 0.15, which is
   also a more sensible line for "worth attention".

The worked example is tuned to teach rather than to flatter: 70% sold at a 53%
markup lands at +6.7% margin with 20% still idle, so both the healthy number
and the alert are visible. Drop the sold share to 55% and the same block goes
underwater — that sensitivity is the argument for the product.

Verified in a real browser at 393px and 1440px, light and dark: zero horizontal
overflow on every route, zero console errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 19:16:43 -07:00
karti 7aeec0c632 Add Prime Intellect client, API, and MCP server
packages/prime — a hand-written typed client, because the first-party SDK is
Python only. Deliberately narrow: PIG reads availability and nothing else, and
the key it holds should be scoped so it could not provision even if the code
tried. Rate limits are undocumented upstream, so it backs off empirically with
full jitter and honours Retry-After. Unknown fields survive in `raw` rather
than being dropped.

apps/api — Hono, with authentication and authorization kept firmly apart. A
verified JWT proves someone has an account in the identity project, which may
be shared with other applications; it does NOT prove they belong here. Access
requires a row in PIG's own users table, and a token without one gets 403
needs_profile rather than entry.

The capacity service is the business logic: availability counts sold and held
separately, so a live hold removes inventory from everyone else's availability
without inflating utilisation. Expired holds are ignored at read time, so the
numbers stay right even when the sweeper is behind. Matching treats
interconnect as a hard filter and excludes Unknown as well as Ethernet —
unverified is not the same as adequate.

apps/mcp — nine tools over stdio, so a team member drives PIG from Claude
Code, Codex, prime-agent, or a Buzz agent. It holds an API key and calls the
same HTTP API the browser does, with no database credentials, so an agent can
never reach further than the person it acts for. Results are formatted as
prose rather than raw JSON.

Theme preferences live in the database rather than localStorage, so a chosen
accent follows someone from laptop to phone. Status colours stay independent
of the accent: if "at risk" re-tinted to whatever a user picked, the signal
would be gone.

Note on the SDK import: its package exports use a `./*` wildcard whose types
entry resolves server/mcp.js to server/mcp.js.d.ts, which does not exist. The
runtime specifier must keep the .js suffix, so the types are mapped via
tsconfig paths rather than by writing an import that would fail at runtime.

Verified: all five packages typecheck; the MCP server constructs and registers
its tools.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 19:02:45 -07:00
karti d36762f264 Scaffold PIG and model the compute-GTM ontology
PIG is an agent-native CRM for two-sided AI-compute companies: businesses
that buy GPU capacity from providers and resell it. Their business is the
spread between two pipelines, which is precisely what a generic CRM cannot
represent.

The load-bearing decision is the `allocations` table, joining 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. Cost is charged against the full commitment
rather than only the hours that sold, because unsold hours are already paid
for and any other treatment flatters a block that is losing money.

Domain decisions worth noting, each grounded in how this market operates:

- Demand stages put `legal` second, not last. Customers do not hand
  workloads to an infrastructure provider before paper is executed.
- Supply qualification splits technical from financial diligence, recorded
  attributably. Accepting capacity is a two-key decision.
- Capacity carries a time SHAPE (intervals + quantities), not a window.
  Commitments ramp and step down; a rectangle reports availability that
  does not exist in the month someone wants it.
- SLAs model three distinct shapes: none, a reliability tier plus credits
  policy, and a negotiated agreement. Aggregators generally cannot promise
  uptime on resold capacity, but negotiate heavyweight paper upstream.
  Remedies include fee abatement, which is materially better than a capped
  credit and is not expressible as one.
- Export control is a predicate on the allocation edge, evaluated against
  the ULTIMATE parent's jurisdiction. Country of incorporation is not a
  valid key, so this cannot live as a flag on an account.
- Agent-derived claims land in `facts` with a confidence band and evidence.
  Only verified claims self-apply; weaker ones await review.
- The API never calls the agent. It writes to a leased queue, guarded by a
  partial unique index on unfinished work.

Verified: typechecks clean, migration generates and applies to Postgres 16
(31 tables, 24 enums, 117 indexes).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 18:41:41 -07:00