Files
pig/README.md
T
karti 10d79fc35d Shoot the README screenshots, and make them regenerable
The screenshot section had been a placeholder since the shell was rebuilt as
three panes, because there was no cheap way to re-shoot and a stale image is
worse than no image. So this ships the capture, not just the captures:
scripts/screenshots.mjs takes all ten pages at 1440x900 and 393x852, in light
and dark, and screenshots-encode.py halves and re-encodes them to WebP — 16MB
of PNG becomes 1.9MB in the tree.

Two things would silently ruin a run, and the script exists to encode both.
Seeding localStorage['pig.themeMode'] is not enough: the appearance preference
is authoritative server-side and adopted after hydration, so every dark capture
snapped back to light a beat after first paint. The /api/me/profile response is
rewritten instead. And pig.sidebarOpen / pig.piggyDockOpen are per-device, so
whatever the last human left behind would otherwise leak in. The run also fails
on a wrong theme, a horizontal scrollbar or a console error — three things a
screenshot cannot show you.

Piggy is not pictured mid-conversation. It is off by default and no inference
credential exists, so such an image would be a staged transcript rather than a
capture. The README says that rather than implying the feature is missing.

Correcting what the README asserted while shooting against the running code:
read authorisation IS enforced — createReadGuardRoutes is mounted ahead of the
feature routes, and routes/learn.ts and routes/activities.ts are mounted too,
so only the HubSpot pair is still unreachable. The remaining read gap is that a
grant cannot be narrowed, there being no row-level team filter in the query
layer. Counts refreshed against the tree: 275 tests, ~47k lines, 14 migrations.

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

30 KiB
Raw Blame History

🐷 PIG — Prime Intellect Growth

An open-source, agent-native CRM for two-sided AI-compute companies.

License: Apache 2.0

Self-hostable. Auditable. Built for teams that buy GPU capacity on one side and sell it on the other.


Why this exists

A company that aggregates GPU capacity and resells it does not run one pipeline. It runs two, and its business is the spread between them.

Today that spread is usually managed in a spreadsheet with a margin calculator in column K, a document of supplier terms, and a general-purpose CRM that has no idea what an H100-hour is. Salesforce, HubSpot and Attio model a single pipeline of deals against companies. They have no concept of inventory, no concept of a commitment you already bought and are paying for whether or not it sells, and therefore no way to answer the question the business turns on:

Which contracted capacity is sold, to whom, at what margin — and what is idle right now?

PIG is one ledger that knows the domain. The load-bearing table is allocations, which joins a capacity_commitment (what you bought, at a known cost) to a demand_deal (what you sold, at a known price). Margin, utilisation and idle capacity all fall out of that one join. Everything else is plumbing that keeps the ledger honest.

Cost is charged against the full commitment, not only the hours that sold. Unsold hours are already paid for. Charging only the allocated share 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 it.

Who it is for

PIG models three teams, because two-sided compute companies have three constituencies competing for the same scarce capacity.

Team Job to be done
Supply Source, qualify, price and contract GPU capacity from providers
Demand Sell compute and post-training; renew and expand accounts
Research Consume capacity internally — real burn, no revenue

Research is a first-class tenant rather than an afterthought: internal burn competes with revenue for the same GPUs, and margin arithmetic that cannot see it is wrong.

The team set is configurable in packages/core/src/ontology.ts. PIG ships with these three because they match the structure of the company it was designed for, not because they are universal.

Screenshots

Captured against the current shell — header, collapsible sidebar rail, docked Piggy — running locally on the seed plus demo book (db:seed and db:demo), so every number below is computed by the code in this repository rather than drawn. Records prefixed DEMO — are fictional; the rest are the sourced, cited seed.

Each image follows your own system theme. Both themes are shown explicitly further down, and the full gallery has all ten pages at 1440px and 393px, in light and dark. Desktop captures are the 1440×900 viewport rather than the full scroll height — what you see is what fits above the fold. node scripts/screenshots.mjs re-shoots the set.

Overview — margin, sold ratio and idle capacity across the book, with the commitments you are paying for and not selling ranked by cost exposure.

PIG Overview: gross margin $675,871.37, 79.1% sold ratio, 1.3M idle GPU-hours, and a ranked list of capacity bought and unsold.

Margin — revenue from what was sold against the full cost of what was bought, per commitment. Cost covered and a break-even price are the two states that matter; charging only the allocated share of cost would report a healthy margin on a block that is losing money.

PIG Margin: revenue $12.4M against $11.7M of full committed cost, broken down by commitment with sold ratio, cost per hour and break-even price.

Capacity → Match a requirement — the matcher. Ask what a customer needs and PIG scores it against capacity already under commitment, saying why each block fits, and hands you straight to the allocation that records the sale.

PIG capacity matcher: a requirement for 64 H100_80GB with high-speed interconnect, scored against two commitments at 64% and 58% fit with an Allocate this capacity action on each.

Growth — deterministic attention scores over customer paper, deal activity and sold or reserved capacity. Every point is an explained signal with its sources named; nothing here is a model's guess at a win probability.

PIG Growth: accounts ranked by attention score, each tagged deployed, expansion candidate, at risk or coverage gap, with the scoring signals listed underneath.

Calendar — what closes, what renews, what expires and when capacity lands, projected from the records that already carry the dates. Export authorisations expire on this timeline too, because an expired one converts lawful business into unlawful business.

PIG Calendar for 2026-Q3: weighted pipeline, deals closing, renewals, obligations due and authorisations expiring, above a quarter timeline with one lane per kind.

Light and dark

Theme is a stored preference that follows a person between devices, resolved before first paint by an inline script so dark-mode users never get a white flash. Both tunings of the accent palette are defined in @pig/core and applied as CSS variables at runtime, so there is one definition of each colour.

The demand pipeline, in both. Legal sits second rather than last, because MSA and DPA execution gates delivery rather than closing the deal — most CRMs put contracts at the end of the funnel and are wrong about it for this market.

Light:

PIG demand pipeline in light mode: ten stages from qualification through legal, scoping, proposal, procurement, POC and deployment, with deal cards showing ACV, product line and MSA/DPA badges.

Dark:

The same demand pipeline in dark mode.

Side by side at 393px, where both tunings have to survive a smaller surface:

Supply pipeline · light Supply pipeline · dark
Supply pipeline at 393px in light mode Supply pipeline at 393px in dark mode

Mobile

PIG is responsive to 393px — the sidebar becomes a bottom tab bar, tables become cards, and the safe-area insets are handled. It is not a native app.

Overview · light Overview · dark Margin · dark
PIG Overview at 393px in light mode, with a bottom tab bar PIG Overview at 393px in dark mode PIG Margin at 393px in dark mode, the commitment table reflowed into cards

Piggy is deliberately not pictured mid-conversation. It is off by default (PIGGY_ENABLED=false, and the Compose service sits behind a profile), and showing it answering would mean staging a transcript rather than capturing one. What it may and may not do is described under the agent surface.

The two-sided data model

Forty-seven tables, but the shape is small. These are the ones that carry the thesis:

Table What it holds Why it is not in a generic CRM
capacity_commitments What you bought: term, GPU-hours, cost per GPU-hour, floor and ceiling, and a shape ({intervals[], quantities[]}) Real contracts ramp across tranches and step down at checkpoints; a single start/end/total reports availability that does not exist in the month someone wants it
demand_deals What you are selling: ACV, product line, MSA/DPA state, stage The paper state is a separate axis from the stage, because paper gates delivery
supply_deals The other pipeline: sourcing a provider through diligence to live Generic CRMs have one pipeline and call the supplier a vendor
allocations The join. Commitment × deal × GPU-hours × window × status This is the whole product. Margin, utilisation and idle all derive from it
inventory_listings Market availability mirrored from the Prime Intellect API Sync is a straight field mapping, not an ETL project
capacity_requests What a customer asked for, whether or not it could be served Unservable demand is the signal for what to buy next
contracts + sla_terms + sla_metric_targets + contract_obligations Polymorphic over party and type — MSA, DPA, SLA, order form, capacity commitment — with negotiated SLA terms and dated obligations The supply side negotiates heavyweight paper; the self-serve demand side runs on a reliability tier and a credits policy instead
export_authorizations, compliance_artifacts, compliance_decisions Export-control determinations recorded on the allocation edge, with reasoning and rule version US controls apply an ultimate-parent test that reaches through the corporate tree, so country of incorporation is not a valid key
facts Every agent-derived claim, with score, band, evidence excerpt and source URL An agent allowed to write unattributed claims will eventually write a wrong one and nobody will be able to tell which
agent_tasks / agent_runs / agent_actions The queue the API writes to and the agent drains, plus what it did The API never calls the model; it writes a row

Two pipelines, with stages taken from how the market operates:

Demand:  qualification → legal → scoping → proposal → procurement
         → POC → deployment → expansion            (+ closed_won / closed_lost)

Supply:  sourced → qualifying → technical diligence → financial diligence
         → pricing → contracting → onboarding → live → renewal
                                                 (+ churned / rejected)

Legal sits second in the demand pipeline. MSA and DPA execution gates the deal rather than closing it. Most CRMs put contracts at the end of the funnel and are wrong about it for this market.

Three further decisions worth knowing before you read the schema:

  • Holds reserve; they do not sell. A live hold removes capacity from everyone else's availability — otherwise two sellers promise the same GPUs — but never counts toward utilisation or revenue.
  • Security tiers are ranked, not labelled. community_cloud < secure_cloud < government, and a requirement is satisfied only from at or above its tier.
  • Money is integer cents, rounded exactly once, at the boundary.

Self-hosting

Requirements

Node 22+, pnpm 11+ (pinned by packageManager; corepack enable installs it), and a PostgreSQL 16 database that PIG owns exclusively.

Development

corepack enable
pnpm install

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     # optional — sourced, cited, confidence-graded people
pnpm run db:demo     # optional — a plausible demo book, prefixed "DEMO — "

pnpm run dev:api     # :8920
pnpm run dev:web     # :5173, proxies /api to :8920

With no identity provider configured, authentication is disabled in development and every request runs as the first user in the table. loadConfig refuses to start with NODE_ENV=production in that state, so it cannot leak into a deployment.

Production

cp .env.example .env    # then edit
docker compose -p pig up -d db
docker compose -p pig run --rm --no-deps app pnpm exec tsx packages/db/src/migrate.ts
docker compose -p pig up -d --build app

Migrate from a one-off container before the app starts, not with exec: a release that queries a table its migration has not yet created crash-loops before you can attach to it. Full deployment notes, including the reverse proxy, the release poller and rollback semantics, are in deploy/README.md.

Every environment variable

Read from apps/api/src/lib/config.ts (API), apps/piggy/src/config.ts (Piggy) and docker-compose.yml. Bold means no default.

Required

Variable Default Notes
DATABASE_URL The only unconditionally required value. PIG owns this database exclusively
POSTGRES_PASSWORD Compose only; docker-compose.yml refuses to start without it

In production you must additionally set either SUPABASE_URL or PIG_OIDC_ISSUER. The API throws at boot with neither.

Identity

Variable Default Notes
SUPABASE_URL unset Hosted path. Absent in development ⇒ auth disabled
SUPABASE_ANON_KEY unset Public by design; served to the browser via /api/config
SUPABASE_SERVICE_KEY unset Only for administrative provisioning and self-registration. Warns at boot when set
PIG_OIDC_ISSUER unset On-premises path. Takes precedence over SUPABASE_URL
PIG_OIDC_JWKS_URI discovered Set it to skip discovery on an air-gapped network
PIG_OIDC_AUDIENCE unset Strongly recommended: without it, any token your provider issued for any application in the same tenant is accepted here. Warns, does not refuse
PIG_OIDC_EMAIL_CLAIMS provider defaults Comma-separated, in preference order

Server

Variable Default Notes
PIG_PORT 8920
PIG_PUBLIC_URL http://localhost:8920 The single origin the app is served from; CORS and the Google redirect are validated against it
NODE_ENV development production activates the identity-provider guard
PIG_ADMIN_EMAILS '' Comma-separated. Every address must already have an account — an unregistered address here is a standing offer of admin rights to whoever claims it first
PIG_INVITE_CODE unset Set it to gate signup
PIG_SETTINGS_ENCRYPTION_KEY unset Base64-encoded 32 bytes. Required for Notion and Google OAuth; secrets written in the admin UI need it

Prime Intellect

Variable Default Notes
PRIME_API_KEY unset Scope it to Availability → Read only
PRIME_API_BASE https://api.primeintellect.ai The compute/pods host. Inference is a different host — see below
PRIME_SYNC_ENABLED false Warns if on without a key
PRIME_SYNC_INTERVAL_MINUTES 30

Piggy

The API and the Piggy container read overlapping but distinct sets.

Variable Default Read by Notes
PIGGY_ENABLED false API Gates the chat surface
PIGGY_INFERENCE_API_KEY Piggy Required by the Piggy process. The model credential never reaches the API container
PIGGY_INFERENCE_BASE https://api.pinference.ai/api/v1 both OpenAI-compatible
PIGGY_MODEL nvidia/nemotron-3-nano-30b-a3b both Admin-selectable at runtime too
PIGGY_LEASE_SECONDS 300 both Queue lease duration
PIGGY_POLL_INTERVAL_MS 2000 Piggy
PIGGY_MAX_TOKENS 1024 Piggy
PIGGY_WORKER_ID hostname:pid Piggy
PIGGY_INTERNAL_URL unset API http://piggy:8931 under Compose
PIGGY_INTERNAL_TOKEN both Min 32 chars; required by the Piggy process. Never put it in a query string
PIGGY_CHAT_HOST 127.0.0.1 Piggy
PIGGY_CHAT_PORT 8931 Piggy Never published to the host
PIGGY_CHAT_ALLOW_NON_LOOPBACK false Piggy Compose sets true, because the API reaches it across the Compose network

Integrations — all optional, all validated as a group

Setting one member of a group without the others fails at boot rather than half-working.

Group Variables
Slack SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET
Buzz BUZZ_RELAY_URL, BUZZ_PRIVATE_KEY, BUZZ_AUTH_TAG
Notion import NOTION_CLIENT_ID, NOTION_CLIENT_SECRET, NOTION_REDIRECT_URI (+ PIG_SETTINGS_ENCRYPTION_KEY)
Google Sheets import GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI (+ PIG_SETTINGS_ENCRYPTION_KEY)

GOOGLE_REDIRECT_URI must be exactly <PIG_PUBLIC_URL origin>/oauth/google/callback.

Architecture

A pnpm monorepo. Around 47k lines of TypeScript including tests, 275 tests across five packages, green CI.

apps/
  web/        React 19 + Vite + Tailwind + shadcn-idiom components
  api/        Hono HTTP API — auth, validation, capacity and contract services
  piggy/      The agent: a lease-based queue worker plus a private chat server
  mcp/        MCP server (stdio) — 9 tools
  cli/        `pig`, the HTTP surface for scripts and agent kernels
packages/
  core/       Ontology, permissions, margin arithmetic, palette — no I/O
  db/         Drizzle schema (47 tables), 14 migrations, seed and demo data
  prime/      Typed client for the Prime Intellect compute API
docs/         ontology.md, screenshots.md, build-plan.md, agents.md, seed-data.md
deploy/       Caddyfile example, autodeploy units, deployment notes

Three rules hold the shape:

Intelligence never lives in the API. Handlers validate, authorise, 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.

Authentication is not authorisation. A verified JWT proves someone has an account in an identity provider PIG may share with another application. Access additionally requires a row in PIG's own users table; a token without one gets 403 needs_profile, which the front end turns into a join flow rather than a login screen they have already completed. Both providers reduce to "verify a bearer token, return a subject and an email" behind apps/api/src/lib/auth-provider.ts.

Writes go through one chokepoint. apps/api/src/lib/mutation.ts derives zod schemas from the ontology, applies the capability check, runs the write and its audit activity in one transaction, and returns a consistent error shape.

The RBAC model, as it now stands

Eleven capabilities, in packages/core/src/permissions.ts, resolved from team membership and role and shared by the API and the browser so a disabled button and a 403 cannot disagree.

Roles are ranked, and every rule is "at or above": viewer < member < lead < admin. A platform admin (an address in PIG_ADMIN_EMAILS) holds everything, platform-wide.

Writes are team-scoped:

Capability Teams Minimum role
deal:write supply, demand member
commitment:write supply lead
contract:sign supply, demand admin
activity:write all member
data:import all admin
fact:review research admin
integration:connect all admin
settings:admin platform admin only

Reads are platform-wide, deliberately:

Capability Teams Minimum role Covers
book:read all viewer Accounts, contacts, both pipelines, contracts, growth, facts
economics:read supply, demand member Supplier cost, break-even price, margin, idle, inventory, the dashboard
team:read all viewer The roster

Read grants are not team-scoped, and that is a decision rather than an omission: no row-level team filter exists anywhere in the query layer, so a "demand only" read grant would be a promise the guard could not keep. The honest model is that a read capability is held or it is not, and the role required to hold it is what separates the roster from the cost book. economics:read is the one that matters — supplier cost per GPU-hour and break-even price are the business.

The read half is enforced. The policy table lives in apps/api/src/routes/read-guards.ts and createReadGuardRoutes is mounted in app.ts before the feature routes — Hono runs matched handlers in registration order, so a guard registered after its route would return 200 while looking correct. read-governance.test.ts pins that ordering in both directions, and fails when a GET appears that no rule covers, so a new read endpoint cannot ship ungoverned by accident.

What it still cannot do is filter within a grant: see limitations.

The agent surface

PIG is a first-class application for agents and for humans, and neither is a degraded view of the other. There are two distinct surfaces.

Piggy — the in-app agent

apps/piggy is one image running two processes' worth of behaviour:

  • The queue worker claims a task with SELECT … FOR UPDATE SKIP LOCKED inside a transaction, holds a renewable lease (default 300s, renewed at half the interval), and aborts its own work if it ever loses that lease — so two workers can never both be mid-flight on one task. Failures retry with exponential backoff capped at one hour, up to the task's maxAttempts. Every attempt writes an agent_runs row with the model, the input, the token counts and either a summary or the error. Its tool set is exactly two: pig_get_subject and pig_record_fact, and a fact is refused without both a source URL and an evidence excerpt.
  • The chat server listens on 127.0.0.1:8931 and is never published to the host. The API authenticates the user, forwards bounded context, and calls it with a shared internal bearer token. Chat is read-only: seven tools (pig_get_record, pig_get_account_lifecycle and five page-scoped summaries), each of which aggregates first and returns at most a handful of exemplar rows, because interactive chat runs at 1024 max tokens across at most four turns. Ambient coding tools are rejected before inference by an explicit boundary check.

Piggy is off by default. PIGGY_ENABLED defaults to false and the Compose service sits behind profiles: ['piggy'], so a default docker compose up starts the CRM without it:

docker compose -p pig --profile piggy up -d --build

The MCP server — for the agent you already use

apps/mcp speaks stdio and holds an API key. It calls the same HTTP API a browser does: no database credentials, no privileged path, and deliberately no tool that provisions infrastructure, spends money or emails a customer. Nine tools, because a sprawling tool list measurably degrades model performance:

Tool What it answers
pig_whoami Who am I acting for, and which teams am I on?
pig_my_pipeline Where are we? What needs attention?
pig_capacity_match What have we bought that would serve this customer?
pig_margin_report What is each block earning against what it cost?
pig_idle_capacity What are we paying for and not selling?
pig_inventory_search What could we buy to cover demand we cannot serve?
pig_search Find an account
pig_get_account Everything about one account
pig_log_activity Record a call, meeting or note

Mint a key in Settings → API keys (shown once), then run it from a clone — @pig/mcp is a workspace package and is not published to npm:

export PIG_URL=https://your-pig-host
export PIG_API_KEY=pig_...
claude mcp add pig -- pnpm --dir /path/to/pig exec tsx apps/mcp/src/stdio.ts

There is also a pig CLI with --json output for scripts and agent kernels; see docs/agents.md.

Shipping — tag to deploy

CI is Gitea Actions, one sequence, about two minutes. It typechecks every package, applies the migration chain twice to a real empty Postgres, asserts the seed is idempotent, runs 275 unit tests and the critical-path E2E, boots the server and curls it, builds the front end, checks the inline theme script still hashes to the value the proxy's CSP allows, and builds the Docker image.

Shipping is two steps and the second one is a human:

git tag release-2026-08-13 && git push origin release-2026-08-13
  1. A push to main runs verify and stops. Nothing deploys.
  2. A release-* tag runs the same verify, then publish pushes git.karti.ai/pig/pig:<tag> and :<short-sha> to the registry.
  3. Within five minutes pig-autodeploy.timer on the production host notices the newest release tag has a different digest, checks the tree out at that tag, and runs scripts/deploy.sh with PIG_IMAGE set.

The direction of travel is the point: no credential on the shared CI runner can execute anything on the production host. The host holds a pull-only token and fetches. deploy.sh dumps the database first, gates on health, the unauthenticated-401 check and a public-origin body marker, and rolls back to the previous image if a gate fails — exiting 1 when the previous image was restored and 3 when the release under test is still live, because that is the one thing an on-call needs at 04:00.

What is not built yet

Said plainly, because you are going to grep the repo anyway.

Read authorisation is enforced, but only at the grant. A capability is held or it is not. Once economics:read is held, it returns every commitment's cost — there is no filter that narrows it to one team's book, because no row-level team filter exists anywhere in the query layer. That is the gap to close before PIG serves a company where "supply can see supply's costs" is a requirement.

The HubSpot integration is written, tested and never mounted. routes/hubspot.ts and routes/hubspot-webhook.ts are both absent from app.ts, so OAuth, connections, sync jobs, webhook verification and seven hubspot_* tables are all unreachable from the running server.

Six of the eight declared agent task kinds are never enqueued. The worker is complete and generic, but only enrich_account and enrich_contact are ever written to agent_tasks (both from record creation). write_brief, match_capacity, detect_idle_capacity, summarise_pipeline, watch_renewal and research_supplier are declared in the ontology and nothing produces them. Piggy therefore does far less than the queue implies — not because the machinery is missing, but because nothing asks.

Piggy chat cannot write. By design for now, but worth stating: the interactive agent reads and cites; it cannot create or update a CRM record.

The MCP server is stdio only. There is no Streamable HTTP transport and no /mcp endpoint on the API, so remote MCP clients cannot connect over the network — each user runs the server locally against their own API key. The package is also not published to npm, so npx @pig/mcp does not work.

No row-level or team-scoped read filtering exists anywhere in the query layer. Every read returns the whole book. This is why read capabilities are platform-wide rather than per-team, and it is the thing to build before PIG serves a company where that is not acceptable.

.env.example is incomplete. POSTGRES_PASSWORD and PIG_SETTINGS_ENCRYPTION_KEY are both load-bearing and both missing from it; the table above is authoritative. ANTHROPIC_API_KEY is declared in the API config and read by nothing.

Not started at all: email or calendar ingestion, forecasting, quota and attainment, invoicing or billing reconciliation, a public API beyond what the MCP tools cover, multi-tenancy of any kind, and any mobile application. PIG is responsive to 393px; it is not a native app.

Documentation

  • AGENTS.md — start here if you are joining this codebase. Architecture rules, the traps that have already bitten, and conventions.
  • Screenshots — every page, at 1440px and 393px, light and dark
  • Ontology — the domain model, and why it is shaped this way
  • Build plan — what shipped, what remains, in dependency order
  • Agent integration — MCP clients and the CLI
  • Seed data provenance — every claim, graded and cited
  • Deployment — self-hosting, the release poller, rollback

A note on seed data

PIG ships with a roster of publicly documented people so the application is legible on first run. Every record carries a confidence grade and a source URL, both shown in the interface. No email addresses are included or inferred. Records that could not be independently sourced are marked as such rather than quietly presented as fact, and people who are demonstrably not staff — alumni, residency participants — are labelled accordingly. Seeding is opt-in (pnpm run db:seed) and never automatic. See docs/seed-data.md.

If you are seeded here and would rather not be, open an issue and the record will be removed.

Licence

Apache License 2.0 — see LICENSE and NOTICE. The architectural debts to Comp AI CRM (MIT) and Buzz (Apache-2.0) are credited in NOTICE. No source code was copied from either.