Asked how many capacity commitments were on the book, Piggy answered "3".
Production holds 5. It had called the idle-capacity tool, which filters to
blocks above an idle threshold, and read the length of that list as the size
of the book.
The system prompt already forbade this in terms — "never report a filtered
count as a total; pig_get_idle_capacity returns the blocks with idle hours,
not the book" — and the model did it anyway. That is the second time this
argument has been lost in the prompt, so it is settled in the payload
instead: a result that cannot describe its own scope will be misread
eventually, however firmly the prompt objects.
Every tool that returns a count or a collection now carries one shape:
what it covers, how many matched, out of how many, under which filters, and
whether the list was truncated. The denominators are read from the database
rather than inferred. The pre-formatted headline states the scope too, since
that is the sentence a small model quotes most readily — the idle tool now
opens "3 of 5 live capacity commitments on the book", which is the sentence
that makes the original mistake impossible to phrase.
Two details worth keeping. Record reads enumerate rather than filter, so
their scope states a boundary instead of a ratio: these are that record's own
figures, never book-wide totals. And the workspace summary's idle threshold
is deliberately recorded as 0, distinct from the idle tool's 0.25 — that
mismatch is why three different idle figures appeared across the UI, and
naming it in the data is how it stops being invisible.
Verified against the live model: the failing question now answers 5, demand
deals 13 and contracts 20 — each drawn from a payload whose filtered figure
was smaller — while "which blocks are sitting idle" still names exactly the
blocks that are.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Work in progress from the Codex session, committed so nothing sits undeployed.
Verified before committing: typecheck clean across all packages, 139 unit tests
and the e2e suite green, migrations apply to an empty Postgres.
Adds the HubSpot integration boundary (OAuth, client, contracts, webhook
signature verification, sync), a growth route, customer-lifecycle service,
Piggy lifecycle tools, a Growth page, and shared lifecycle/hubspot types.
Two things are deliberately incomplete and should not be mistaken for finished:
`packages/db/src/schema/hubspot.ts` is NOT exported from the schema index, so it
is inert — no tables, no migration. That is the correct order (the shape can
settle before it becomes a migration), but it does mean the HubSpot routes have
no persistence behind them yet.
`pnpm-workspace.yaml` and `pnpm-lock.yaml` are left uncommitted on purpose. The
workspace file contains a literal unanswered placeholder — "esbuild: set this
to true or false" — and this repository installs with npm, which is also what
CI runs. Committing a second package manager's lockfile would make the install
ambiguous. If the move to pnpm is intended it should be a deliberate change
that updates CI and the Dockerfile together.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>