Merge gitea/main into the Motion branch

Motion was written against a base five commits behind main, so the
integration is the interesting part of this commit:

- The migration is renumbered 0014 -> 0015. Main shipped
  0014_piggy_conversations, and two migrations sharing an index is a
  journal that applies one of them.
- The seed-idempotency gate keeps main's all-tables diff rather than the
  motion_templates counter this branch added; the general check subsumes
  the specific one.
- Nav gains a Motion group alongside main's new Workspace group, and
  Piggy keeps the mark main gave it.
- Stat keeps main's container-scaled figure, which already carries the
  min-w-0 this branch added for the same reason.
- Piggy's page labels keep main's refusal wording for the four pages with
  no tool of their own, and gain the three Motion routes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 18:30:45 -07:00
149 changed files with 37440 additions and 3502 deletions
+79 -18
View File
@@ -293,7 +293,7 @@ In production you must additionally set **either** `SUPABASE_URL` **or**
| Variable | Default | Notes |
|---|---|---|
| `PRIME_API_KEY` | unset | Scope it to `Availability → Read` only |
| `PRIME_API_KEY` | unset | Scope it to `Availability → Read`, plus inference if Piggy is on — the same key buys the agent's tokens. Nothing that can provision |
| `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` | |
@@ -310,9 +310,15 @@ means editing `.env` and restarting the container.
| Variable | Default | Read by | Notes |
|---|---|---|---|
| `PIGGY_ENABLED` | `false` | API, `deploy.sh` | Gates the chat surface, and tells `scripts/deploy.sh` to ship the `piggy` Compose profile with the app |
| **`PIGGY_INFERENCE_API_KEY`** | — | Piggy | Required by the Piggy process. Missing, it exits at boot and crash-loops. 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 | The API reads it to display; Piggy reads it to call |
| **`PRIME_API_KEY`** | — | API, Piggy | One key, two hosts: the availability sync calls `api.primeintellect.ai`, the agent calls `api.pinference.ai`. Required by the Piggy process; missing, it exits at boot and crash-loops |
| `PIGGY_INFERENCE_API_KEY` | — | Piggy | The legacy spelling of `PRIME_API_KEY`, still accepted so a `.env` written before the harness swap keeps starting. Set one, not two |
| `PIGGY_AGENT_MODEL` | `nvidia/nemotron-3-nano-30b-a3b` | Piggy | The default answer model. Must be one of the five ids in `apps/piggy/src/agent/models.json`, or Piggy refuses to start — an unlisted model is not registered with the harness and would fail on a user's first question instead |
| `PIGGY_AGENT_MODE` | `confirm` | Piggy | `read_only`, `confirm` or `auto`. Contracts, commitments, allocations and compliance require a click in every mode |
| `PIGGY_AGENT_THINKING` | `off` | Piggy | `off``max`. **Read [the trap](#the-thinking-level-trap) before raising it or changing the model** |
| `PIGGY_AGENT_MAX_TOKENS` | `4096` | Piggy | Output tokens per agent turn, reasoning included. Clamped down to the model's own ceiling |
| `PIGGY_AGENT_DIR` | `~/.pig/piggy-agent` | Piggy | The harness's own directory. Compose pins it to `/var/lib/piggy-agent`; it must never be a checkout, because the harness reads context files from its cwd |
| `PIGGY_INFERENCE_BASE` | `https://api.pinference.ai/api/v1` | both | OpenAI-compatible. The agent reads its base URL from `models.json`; this one still drives the queue worker |
| `PIGGY_MODEL` | `nvidia/nemotron-3-nano-30b-a3b` | both | The queue worker's model. The agent uses `PIGGY_AGENT_MODEL` and the picker |
| `PIGGY_LEASE_SECONDS` | `300` | both | Queue lease duration |
| `PIGGY_POLL_INTERVAL_MS` | `2000` | Piggy | How often an idle worker looks for a task |
| `PIGGY_MAX_TOKENS` | `1024` | Piggy | Per queued task |
@@ -328,6 +334,24 @@ means editing `.env` and restarting the container.
| `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 |
##### The thinking-level trap
Worth its own heading, because it costs an afternoon otherwise.
The harness defaults `thinkingLevel` to `medium`, which is tuned for a coding
agent. On the default nemotron model that produced **6,195 output tokens of
reasoning and an empty answer** — the turn hit its ceiling mid-thought and
returned `finish_reason: length`. `low` was worse. `off` maps, for that model,
to the endpoint's `reasoning_effort: none`, and the same question came back
correct in **149 output tokens**.
The mapping is per model, in `thinkingLevelMap` in
`apps/piggy/src/agent/models.json`. The nemotron entries have one; deepseek,
opus and gpt-5.6 do not, so at `off` they send no reasoning parameter at all and
inherit the endpoint's default. **If you change `PIGGY_AGENT_MODEL` and start
getting empty or truncated answers, this is why** — give the new model a
`thinkingLevelMap` before touching `PIGGY_AGENT_THINKING`.
#### Integrations — all optional, all validated as a group
Setting one member of a group without the others fails at boot rather than
@@ -351,7 +375,8 @@ 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
piggy/ The agent: a Prime Agent session over the CRM tools, served by a
private chat server, plus a lease-based queue worker
mcp/ MCP server (stdio) — 10 tools
cli/ `pig`, the HTTP surface for scripts and agent kernels
packages/
@@ -439,7 +464,23 @@ 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:
`apps/piggy` runs **Prime Agent** — Prime Intellect's own agent harness
(`@earendil-works/pi-coding-agent`, MIT), embedded as a Node library rather than
shelled out to — with PIG's CRM tools and nothing else. Models come from Prime
Intellect inference (`api.pinference.ai`) on `PRIME_API_KEY`; the picker offers
five, defined in `apps/piggy/src/agent/models.json`, priced and sized in the one
file the runtime and the UI both read.
**The harness has no shell, no filesystem and no Python.** It is constructed
with `noTools: 'all'` and an explicit allowlist, and there are three independent
gates behind that: PIG's own boundary check on the tool list before a session
opens, a comparison of the harness's live `state.tools` against exactly what was
handed in — a startup error if they differ, so a future harness release cannot
widen the set quietly — and a test that pins the same comparison. Prompt
templates, skills, extensions and context-file discovery are all disabled, and
the harness's cwd is a dedicated directory that holds no code.
It 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
@@ -450,20 +491,36 @@ degraded view of the other. There are two distinct surfaces.
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 2048 max tokens across at
most four turns. Ambient coding tools are rejected before inference by an
explicit boundary check.
- **The chat server** listens on `8931` and is never published to the host. The
API authenticates the user, forwards bounded context and the caller's
principal, and calls it with a shared internal bearer token. Its tools are
scoped to what the user is looking at: a focused record reader or one of the
page-scoped summaries, plus lookups (`pig_search_records`,
`pig_get_record_by_id`, `pig_list_renewals`, `pig_list_inventory`,
`pig_get_account_lifecycle`). Each aggregates first and returns at most a
handful of exemplar rows.
**Chat can now write**, which it could not before: `pig_log_activity`,
`pig_create_contact`, `pig_create_task`, `pig_update_deal_stage` and
`pig_update_record_fields`. Every one of them runs through the same
`executeMutation` path the HTTP API uses, as the calling user's own
`Principal` — so Piggy holds no privilege of its own and cannot touch a record
its user could not. `PIGGY_AGENT_MODE` decides how far it may go on its own
(`read_only`, `confirm`, `auto`), and in `confirm` a change is proposed as a
card the user applies. Contracts, commitments, allocations and compliance
records require a click in **every** mode; that rule is one function,
`requiresApproval` in `packages/core/src/piggy-protocol.ts`, so it cannot be
true in one place and false in another.
Conversations persist in `piggy_conversations` and `piggy_messages`
(migration 0014), and `/piggy` is a full workspace rather than a docked panel
alone.
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. Turning it on is three values in `.env`
`PIGGY_ENABLED=true`, `PIGGY_INFERENCE_API_KEY` and a 32-character
`PIGGY_INTERNAL_TOKEN` — and then a deploy:
`PIGGY_ENABLED=true`, `PRIME_API_KEY` (or the legacy `PIGGY_INFERENCE_API_KEY`)
and a 32-character `PIGGY_INTERNAL_TOKEN` — and then a deploy:
```bash
bash scripts/deploy.sh
@@ -560,8 +617,12 @@ ever written to `agent_tasks` (both from record creation). `write_brief`,
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.
**Piggy's write surface is five tools, not the whole CRM.** It can log an
activity, create a contact or a task, move a deal stage and update fields on a
record it can already read. Everything else — creating an account, a
commitment, a contract, an allocation — is still a human's job in the UI, and
the mutations it does have route through the same `executeMutation` path and
the same capability checks as the HTTP API.
**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