Rebuild Piggy's interface, and give the demo book a business to describe
CI / verify (push) Successful in 4m57s
CI / publish (push) Has been skipped

Piggy answered in raw markdown, threw away every tool result it streamed,
and fought the reader's scroll on every token. The three surfaces that
made it worth having — what it read, how it reasoned, what it cost — were
all on the wire and none of them reached the screen.

The transcript is now composed of five parts under components/piggy:
answers render through streamdown, the container sticks to the bottom
without pinning the reader there, tool steps say what they read and link
to the record, and each turn carries its model and token count. Three
lifecycle bugs went with them: Stop left a permanent spinner, a truncated
stream was indistinguishable from thinking, and a failed send destroyed
the message it failed to send.

Underneath, the inference path grew timeouts, jittered retries on 429 and
5xx, tolerance of the malformed frames a 30B model emits, and an
agent_runs row per turn so chat spend is observable. The system prompt now
states that a field ending in Cents is cents — without it nemotron renders
costPerGpuHourCents: 189 as "$189 per GPU-hour", which is a 100x error on
the most scrutinised number in the room.

The demo book was arithmetically incoherent: every deal's value
contradicted its own allocation revenue by up to 3.6x, nothing had ever
closed, no customer had any paper, and the marketplace was empty. Deal
value is now derived from the allocation, the book clears 5.3% across five
blocks with one deliberately underwater, and the renewal, compliance and
agent-provenance machinery finally has rows to act on. A --clear that
deleted every obligation, SLA term and capacity request in the database
regardless of origin is scoped to the demo's own ids.

Around that: accounts have a detail page, ⌘K searches the book, Settings
can mint the API keys it always claimed to, and deploy.sh actually ships
the agent instead of silently skipping its compose profile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
claude
2026-08-14 00:33:41 -07:00
parent 76e3caa1cb
commit 99d165b5e5
81 changed files with 21780 additions and 2250 deletions
+210 -1
View File
@@ -14,10 +14,20 @@
# This caught a seed that silently duplicated 27 contacts.
# 4. The unit tests pass.
# 5. The server boots against that database and answers.
# 6. The front end builds, and the CSP hash for the inline theme script still
# 6. Piggy boots against that same database, answers /internal/health, and
# the API — wired to it through the environment, not through a stub —
# reports it enabled. The relay's own tests inject a resolver, so they
# stay green whether or not the real wiring exists; only this step reads
# it. A crash on boot and an unset PIGGY_INTERNAL_URL look identical from
# the browser: the dock simply never appears.
# 7. The front end builds, and the CSP hash for the inline theme script still
# matches what the proxy is configured to allow. Editing that script
# changes its hash, and the failure mode is a silent white flash for
# dark-mode users rather than an error.
# 8. docker-compose.yml renders, and the piggy service is passed every
# environment key the worker's schema requires. That is the one failure
# nothing else here can see, because it lives between two files that are
# each individually correct.
#
# THE CSP HASH IS DUPLICATED IN THREE PLACES: the `expected` constant below,
# `deploy/Caddyfile.example`, and the LIVE Caddyfile on cloud-2. Only the first
@@ -163,6 +173,101 @@ jobs:
done
curl -sf http://127.0.0.1:8930/api/health | grep -q '"ok":true'
- name: Piggy boots, and the API reports it enabled
# apps/api's own comment admits the gap this closes: its tests inject a
# resolver, so they pass whether or not the process is really wired to a
# Piggy. Here the relay is given nothing but environment variables and
# has to reach a Piggy that actually booted.
#
# It runs after the seed on purpose: with no identity provider every
# request is the development user, and that user is a seeded row.
run: |
LOGS=$(mktemp -d)
# Derived from the run id for the same reason Postgres's port is: this
# job shares the host's network namespace, so a fixed port belongs to
# the whole machine and two concurrent runs would fight over it.
PIGGY_PORT=$(( 30000 + (${{ github.run_id }} % 5000) ))
API_PORT=$(( 36000 + (${{ github.run_id }} % 5000) ))
# Worthless, and long enough for the schema's 32-character minimum.
INTERNAL_TOKEN='piggy-ci-internal-token-0123456789'
PIGGY_PID=''
API_PID=''
# There are two processes between the job's pid and the server that
# holds the port — pnpm launches tsx, tsx launches node — so the whole
# descendant tree has to go. Verified by watching a plain `kill` leave
# a Piggy behind, still holding its Postgres connections.
#
# SIGKILL, not the polite signal: nothing here needs a clean shutdown,
# and a server still listening when the next step runs is worse than
# an abrupt one.
stop() {
for pid in "$@"; do
[ -n "$pid" ] || continue
for child in $(pgrep -P "$pid" 2>/dev/null); do stop "$child"; done
kill -9 "$pid" 2>/dev/null || true
done
}
trap 'stop "$PIGGY_PID" "$API_PID"' EXIT
# Nothing here calls a model: the task queue is empty and the status
# route never reaches one. The inference base points at the discard
# port so that a future version which DID call out would fail loudly
# rather than quietly billing somebody's real endpoint.
PIGGY_INFERENCE_API_KEY=ci-stub-key \
PIGGY_INFERENCE_BASE=http://127.0.0.1:9/v1 \
PIGGY_INTERNAL_TOKEN="$INTERNAL_TOKEN" \
PIGGY_CHAT_HOST=127.0.0.1 \
PIGGY_CHAT_PORT="$PIGGY_PORT" \
pnpm exec tsx apps/piggy/src/main.ts > "$LOGS/piggy.log" 2>&1 &
PIGGY_PID=$!
for i in $(seq 1 30); do
curl -sf "http://127.0.0.1:${PIGGY_PORT}/internal/health" >/dev/null && break
sleep 1
done
HEALTH=$(curl -sf "http://127.0.0.1:${PIGGY_PORT}/internal/health" || true)
echo "GET /internal/health -> ${HEALTH:-<no response>}"
case "$HEALTH" in
*'"ok":true'*) ;;
*)
echo 'Piggy never answered. Its configuration schema rejects an incomplete environment on start, so the reason is usually the last line here:'
tail -30 "$LOGS/piggy.log"
exit 1
;;
esac
NODE_ENV=development PIG_PORT="$API_PORT" \
PIGGY_ENABLED=true \
PIGGY_INTERNAL_URL="http://127.0.0.1:${PIGGY_PORT}" \
PIGGY_INTERNAL_TOKEN="$INTERNAL_TOKEN" \
pnpm exec tsx apps/api/src/server.ts > "$LOGS/api.log" 2>&1 &
API_PID=$!
for i in $(seq 1 30); do
curl -sf "http://127.0.0.1:${API_PORT}/api/health" >/dev/null && break
sleep 1
done
# The stored admin toggle is the inner gate, and an earlier step in
# this job has already created the settings row with Piggy off — the
# insert is ON CONFLICT DO NOTHING, so booting with PIGGY_ENABLED=true
# cannot correct it. Flip it here: what is under test is the wiring,
# not the switch.
docker exec "$PG_CONTAINER" psql -U pig -d pig \
-c 'update platform_settings set piggy_enabled = true' >/dev/null
STATUS=$(curl -sf "http://127.0.0.1:${API_PORT}/api/piggy/status" || true)
echo "GET /api/piggy/status -> ${STATUS:-<no response>}"
case "$STATUS" in
*'"enabled":true'*) ;;
*)
echo 'The API does not consider Piggy available, which is what the browser sees as a dock that never appears. PIGGY_ENABLED, PIGGY_INTERNAL_URL and PIGGY_INTERNAL_TOKEN are all read where the routes are composed; one of them is no longer reaching them.'
tail -30 "$LOGS/api.log"
exit 1
;;
esac
- name: Front end builds
run: pnpm -F @pig/web run build
@@ -189,6 +294,110 @@ jobs:
console.log('CSP hash unchanged: '+hash);
"
- name: Compose file renders, and Piggy is passed every key it requires
# `docker compose config` is the only thing that reads docker-compose.yml
# in this repository. Without it, a typo in that file is discovered by
# the production host, at deploy time, as a container that restarts for
# ever with a message only `docker logs` shows.
run: |
WORK=$(mktemp -d)
# A dummy environment file rather than a real .env: these values are
# never used, they exist only because compose refuses to render while
# a `${VAR:?}` is unset. Passing --env-file also means a stray .env on
# the runner cannot supply a key and hide its absence from the check.
cat > "$WORK/dummy.env" <<'ENVEOF'
POSTGRES_PASSWORD=ci-dummy
PIG_PUBLIC_URL=http://localhost:8920
SUPABASE_URL=http://localhost:54321
SUPABASE_ANON_KEY=ci-dummy
ENVEOF
# --profile piggy, because a profiled service is otherwise omitted
# from the rendered output entirely — and it is the service under test.
docker compose --env-file "$WORK/dummy.env" --profile piggy config -q || {
echo 'If that complained about a missing variable, add it to the dummy environment above: a `${VAR:?}` in docker-compose.yml needs a value here, not the right value.'
exit 1
}
docker compose --env-file "$WORK/dummy.env" --profile piggy config --format json \
> "$WORK/compose.json"
# .mts, not .ts: this file lives outside the workspace, so tsx has no
# package.json to tell it the module system and would treat a .ts file
# as CommonJS, where top-level await is a syntax error.
cat > "$WORK/piggy-env-keys.mts" <<'CHECKEOF'
/**
* Every key the Piggy configuration schema requires must be handed to
* the piggy service by docker-compose.yml. One that is missing is not
* a failure anywhere else in this repository: both files are
* individually valid, and the gap only appears as a container exiting
* on boot with "Invalid Piggy configuration".
*
* Both sides are read at run time — the required keys by asking the
* schema itself what an empty environment is missing, the provided
* keys from the compose file as Compose renders it. A list copied
* into this workflow would be right today and wrong by the next key.
*/
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
interface RenderedCompose {
services?: Record<string, { environment?: Record<string, string | null> }>;
}
const composeJsonPath = process.argv[2];
if (!composeJsonPath) {
console.error('Usage: piggy-env-keys.mts <rendered-compose.json>');
process.exit(1);
}
const configModule = (await import(
pathToFileURL(resolve('apps/piggy/src/config.ts')).href
)) as { loadPiggyConfig: (env: NodeJS.ProcessEnv) => unknown };
/**
* An empty environment fails on exactly the keys that have neither a
* default nor `.optional()`, and loadPiggyConfig reports one indented
* "KEY: message" line per failure.
*/
function keysWithNoDefault(): string[] {
try {
configModule.loadPiggyConfig({});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return [...message.matchAll(/^\s+([A-Z][A-Z0-9_]*):/gm)].flatMap(([, key]) =>
key ? [key] : [],
);
}
throw new Error(
'The Piggy config schema accepted an empty environment, so this check can no longer tell which keys are required.',
);
}
const rendered = JSON.parse(readFileSync(composeJsonPath, 'utf8')) as RenderedCompose;
const piggy = rendered.services?.piggy;
if (!piggy) {
console.error('The rendered compose file has no `piggy` service.');
process.exit(1);
}
const provided = new Set(Object.keys(piggy.environment ?? {}));
const required = keysWithNoDefault();
console.log(`piggy requires ${required.length} key(s) with no default: ${required.join(', ')}`);
const missing = required.filter((key) => !provided.has(key));
if (missing.length > 0) {
console.error(`docker-compose.yml never passes: ${missing.join(', ')}`);
console.error('The piggy container would exit on boot and restart for ever.');
console.error("Add each key to the piggy service's environment: block, and to .env.example.");
process.exit(1);
}
console.log('Every required Piggy key is present in the piggy service.');
CHECKEOF
pnpm exec tsx "$WORK/piggy-env-keys.mts" "$WORK/compose.json"
- name: Docker image builds
run: docker build -t pig:ci .