Repository navigation
feat: let a new account actually do something - #11
Merged
Merged
Conversation
Adds `packages/ai` — the only package in the tree that talks to a model. The
composer writes the message body for a recommendation, and everything around
it exists to make that output safe to show a reviewer.
Two properties are structural rather than requested:
1. It can only see grounded inputs. The prompt is assembled from stored
evidence, stored facts and the customer's own offering. There is no path
by which the model learns something it may not cite.
2. Its output is checked, not trusted. Eight deterministic §14.2 gates run
on every draft — grounding overlap, unsupported claims, identity
confidence, flattery, spam patterns, cross-prospect duplication,
sensitive topics, policy. A draft that fails is rewritten once, naming
the exact invented fragments; still failing, it is withheld.
Withheld is a working state, not an error. The card keeps the prospect, the
evidence and the recommended action, and the reviewer writes the message. A
bad draft next to a caveat is still a bad draft someone might approve.
The undecidable question "is this claim true?" is inverted into a decidable
one: does every specific assertion — quoted phrase, number, mid-sentence
proper noun — appear in stored evidence? That is what `checks.ts` answers,
and it is why no model output reaches a human unexamined.
Also:
- `POST /recommendations/:id/draft` composes or recomposes on demand, returns
200 with `drafted: false` and a reason when withholding, and audits it.
A user-edited draft is never discarded by a recompose.
- The approval card gains "Write draft" / "Rewrite", and states plainly why
no draft exists rather than offering a retry that cannot help.
- Prompt caching splits the stable offering/voice prefix from the
per-prospect half, so a campaign pays for the prefix once.
- The composer is optional infrastructure: without ANTHROPIC_API_KEY the
queue still runs end to end and drafting returns 503. Refusing to boot
would take the system down for a feature designed to produce nothing.
No model decides a merge, a score, or a policy outcome. 367 tests pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A fresh login was an empty app with no way to fill it. The cause was not a missing screen: registration created a user, organisation and workspace, but no offering and no campaign — and a campaign requires an offering, a prospect requires a campaign. There was no route that could add a person at all. The pipeline had only ever been driven from a local script. - Registration now provisions a starter offering and campaign. Existing accounts are backfilled on first use rather than told to create something the UI does not expose. - POST /api/v1/prospects runs the full chain for a GitHub handle. It is synchronous because a first-run user needs to see something appear, and it reports identities linked and signals found rather than "queued". A handle that resolves to nothing answers 200 with a reason: a typo is information, not a server fault. - GET /api/v1/people backs a real prospect list and detail page, so the evidence behind a recommendation is inspectable. - Today's empty state now distinguishes "no prospects yet" from "no recommendations yet" and offers the action that fixes the first. The pipeline moves from apps/worker to packages/pipeline. Both the API and the background loop run it, and an app importing another app's source made the dependency direction a lie. apps/worker held nothing else, so it is gone. Email verification ships alongside it. Format was already checked; what was missing was any proof the address exists. Tokens are stored as a SHA-256 digest like sessions, superseded rather than accumulated on resend, and every failure answers identically so a guessed token learns nothing. The gate is on sending, not on signing in — research and drafting still work while the mail is in flight. Accounts predating this are grandfathered, and a send failure never fails the signup that triggered it. Resend is reached over plain HTTP; with no key the link is logged instead, so a fresh checkout still completes a signup. 395 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts: # .env.example # README.md # apps/api/src/app.test.ts # apps/api/src/app.ts # apps/server/src/index.ts # bun.lock # packages/pipeline/package.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Logging in gave an empty app with no way to fill it. The cause was structural, not cosmetic.
What was actually broken
Registration created a user, organisation and workspace — but no offering and no campaign. A campaign requires an
offering_id; a prospect requires a campaign. And there was no route that could add a person at all — the pipeline had only ever been driven from a local script. So the app was empty and had no path out of it.What this adds
POST /api/v1/prospectsruns the full chain for a GitHub handle. Synchronous, because a first-run user needs to see something appear, and it reports identities linked and signals found rather than "queued".@handleand pasted profile URLs are accepted. A handle that resolves to nothing answers200with a reason — a typo is information, not a server fault.GET /api/v1/peoplebacks a real prospect list and a detail page showing linked identities with confidence and signals with source links.Email verification
Format was already validated server-side (
isPlausibleEmail,z.string().email()); what was missing was any proof the address exists.403 email_unverified.Structural
apps/worker→packages/pipeline. Both the API and the background loop run the chain, and an app importing another app's source made the dependency direction a lie.apps/workerheld nothing else, so it is gone.Verification
bun run check— format, typecheck, 395 tests, all passingapps/webproduction build passes;/prospects,/prospects/[id]and/verifyall present0006was applied to production Turso during development (.envpoints at prod). It is additive — new column, new table, grandfathering update — and the container applies pending migrations at boot anyway.🤖 Generated with Claude Code