Rebuild the shell, add Calendar and Learn, and govern reads
CI / verify (push) Successful in 3m45s
CI / publish (push) Has been skipped

Seven parallel agents and an adversarial verification pass. The three things
worth knowing before reading the diff:

RBAC WAS ALREADY BUILT. docs/build-plan.md marks F2 and F3 outstanding and is
stale — packages/core/src/permissions.ts and lib/mutation.ts shipped long ago.
So this does not rebuild them; it closes the gaps an audit found. The big one
is that reads were entirely ungoverned: every GET was "any authenticated
member", so a junior demand rep and a research contractor could both pull
per-block supplier cost and break-even prices from /api/capacity/margin, and
every contract's negotiated terms. For a company whose margin is the business,
that was the hole that mattered. Adds book:read / economics:read / team:read,
a readGuard middleware, and a `viewer` role below member.

THE BUTTON AND THE 403 DISAGREED — the exact thing F3 said must never happen.
Contracts.tsx never called can() at all, so its save button was always enabled
against a server requiring contract:sign; Capacity.tsx gated commitment
creation on deal:write/demand while the server wanted commitment:write/supply.

POST /api/activities was the one write bypassing executeMutation: no capability
check, and any member could mutate accounts.lastActivityAt as a side effect.
It is now a proper mutation() behind activity:write.

The shell becomes three panes — a collapsible shadcn sidebar with an account
switcher on the Piggy accent, a header with real search, and Piggy docked to
the right, page-aware and persistent across navigation. The phone keeps its
bottom tab bar, which is the thing this product already beat trycompai/crm on,
and gains the sidebar as a sheet.

Calendar is a projection over thirteen dated sources rather than a new table,
because a table would duplicate dates that already live on contracts, deals and
commitments and would drift — and one ledger answering the question is the
whole argument. It surfaces export_authorizations and compliance_artifacts,
which had indexed expires_at columns, schema comments saying they must be
alerted on, and no read endpoint or UI anywhere.

Learn carries two tracks. Concepts are members-only; the platform track can be
opened with a share code by someone with no account. The code mints a scoped
learn-only token and never a Principal — every route here resolves a principal
and then checks capabilities, so a principal-minting code would be one missing
check away from leaking the book. "Only platform-track rows may be code-visible"
is a database CHECK constraint as well as a write-path rule, and a test asserts
a valid learn token still gets 401 on /api/dashboard, /api/accounts and
/api/contracts — the same invariant scripts/deploy.sh refuses to ship without.

CD becomes tag-to-ship. CI publishes an image to the Gitea registry on a
release-* tag and cloud-2 pulls it, so no credential on the shared runner can
execute anything on production — by construction rather than by policy. Both
halves of deploy.sh's original rule survive: nothing on the runner reaches the
host, and a human still decides when it ships. deploy.sh gains a rollback and a
public-origin check, and PIG_IMAGE now reaches compose through `sudo env`,
without which sudo's env_reset silently resolved every release to pig:local.

Tests 141 -> 261.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-13 15:02:48 -07:00
parent 6cf80747cc
commit 13dec6b4b8
102 changed files with 28638 additions and 913 deletions
+45 -27
View File
@@ -30,23 +30,28 @@ 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/core Ontology (stages, tiers, enums) + permissions + margin + palette
packages/db Drizzle schema (47 tables), migrations, seeds
packages/prime Typed client for the Prime Intellect compute API
apps/api Hono HTTP API, auth, capacity service
apps/api Hono HTTP API, auth, capacity/contract/calendar services
apps/web React + Vite + Tailwind + shadcn-idiom components
apps/piggy The agent — lease-based queue worker + private chat server
apps/mcp MCP server (stdio) — 9 tools
docs/ ontology.md, build-plan.md, agents.md, deploy.md, seed-data.md
apps/cli `pig`, the HTTP surface for scripts and agent kernels
docs/ ontology.md, build-plan.md, agents.md, seed-data.md
deploy/ README.md (deployment), Caddyfile example, autodeploy units
```
~11,000 lines. 39 tests. Node 22+.
~45,000 lines including tests. 261 tests across five packages
(core 62, prime 24, api 157, piggy 13, cli 5), plus a critical-path E2E suite
under `apps/api/e2e`. 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 |
| Deploy | Push a `release-*` tag; CI publishes the image and the host's poller pulls it. A push to `main` deploys nothing. `bash scripts/deploy.sh` on the host is the manual path |
---
@@ -204,8 +209,10 @@ 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.
total. `packages/prime/src/map.ts` now divides both by `gpuCount` at the
boundary and keeps the node totals in `raw` for reconciliation — this was a
real bug that made an 8-GPU node read eight times too expensive. Anything new
that reads an upstream price must normalise the same way.
**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
@@ -221,6 +228,15 @@ pods. Inference is `api.pinference.ai/api/v1`, OpenAI-compatible.
hybrid reasoning model; under a tight `max_tokens` it rambles and truncates.
Pass `reasoning_effort: "none"` for tool use, routing and extraction.
**A route file with green tests can still be unmounted.** Every route module is
a factory returning a `Hono` app, and `createApp` has to call it. The tests
mount the factory themselves, so they pass whether or not `app.ts` ever does.
Four modules are in exactly that state right now — `read-guards.ts`,
`learn.ts`, `hubspot.ts`, `hubspot-webhook.ts` — which is why read
authorisation is unenforced and `/learn` answers 404 from a page that is in the
navigation. After adding a route file, curl the path against a running server;
the test suite cannot tell you.
**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`.
@@ -258,28 +274,30 @@ 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.
**Every task in the original three-wave plan has shipped.**
[`docs/build-plan.md`](./docs/build-plan.md) is now an audited record of that
rather than a queue, and it carries the remaining work at the bottom. The two
interfaces everything else codes against — `packages/core/src/permissions.ts`
(the RBAC model) and `apps/api/src/lib/mutation.ts` (the write path) — are
settled; read them before adding any write.
**Do these first, alone, before anything fans out:**
**The highest-value work now, in order:**
- **F1** — install the shadcn primitive set
- **F3** — the RBAC permission model
- **F2** — the shared API write-path convention (needs F3 to call into)
1. **Mount `createReadGuardRoutes`.** The read half of the permission model is
written, tabulated and tested, and does nothing, because `app.ts` never
mounts it. Until it does, every authenticated member can read supplier cost
and margin. It is one line, and it must be registered *before* the handlers
it guards — Hono runs matched handlers in registration order.
2. **Mount `learn.ts`.** `/learn` is in the navigation and its API answers 404.
3. **Enqueue the other six agent task kinds.** The worker is complete; only
`enrich_account` and `enrich_contact` are ever written to `agent_tasks`, so
Piggy does far less than the ontology implies.
4. **Mount the HubSpot routes, or delete them.** Seven tables, OAuth, sync jobs
and webhook verification, all written, tested and unreachable.
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.
`app.ts` is the one shared file. If your change needs a route mounted, a public
path allowlisted or a schema widened there, say so rather than racing another
agent for it.
---