A requirements agent that lives in Slack. It interviews each stakeholder privately, finds the contradictions between them, and answers developers' questions only from requirements a named human confirmed — or asks the person who owns the decision and brings the answer back to the developer's own thread.
There is no dashboard. Slack is the product. One read-only web page renders the final plan.
The agent starts the conversations. It DMs four people who never messaged it, adapts each interview to what that person just said, posts a contradiction into the team channel where only the two people who disagree can settle it, and carries an answer from a stakeholder's DM back into a thread somewhere else. None of that is reproducible in a single chat window.
Hero 1 — the contradiction nobody knew about. Operations says a certificate is issued
automatically on completion. Finance says it needs payment confirmation. Neither ever
mentioned the other's condition, and the synthesizer is forbidden from merging them into one
rule. It posts a conflict card in the channel; only the two people tagged on it can resolve
it; resolving it creates the confirmed requirement, with an Approval row recording who
decided and when.
Hero 2 — "I don't know, and here is who does." A developer asks @BridgeAI can an admin revoke a certificate after download?. No confirmed requirement states it, so the agent
refuses to answer, names Operations as the owner, DMs Anna one answerable question, and posts
her answer back into the developer's original thread — now a confirmed requirement with her
as the source.
The gaps are filled before the team sees the plan. Synthesis posts the conflict card to the
channel immediately — two people disagreeing is the team's business — but holds the plan
link and DMs every open question privately to the stakeholder who owns it, through the same
Answer-button loop a developer's question uses. Each answer confirms that requirement in
place, with them as the source. When the last one lands, the plan posts itself to the channel,
exactly once. /bridgeai plan publishes early if you don't want to wait.
Needs discussion is a conversation, not a shrug. Pressing it opens a modal asking what has to be discussed; the owner's own words are stored on the approval record, shown on the plan page, and quoted back when BridgeAI asks them to agree the final wording for each item. Those items are held out of the plan until they do — an owner parking something is not a way for it to quietly disappear.
- The synthesis schema has no
confirmedvalue. The model physically cannot confirm anything. Requirement.status = 'confirmed'is only ever written in the same transaction as anApprovalrow. Three paths write one: a sign-off, a conflict resolution, a routed answer.- The model cites short aliases (
A7,R2), never database ids, and every alias that comes back is validated. A requirement citing a source that does not exist is downgraded to an assumption, and the fake alias is kept and shown — on the plan page and on the channel message. - The Router writes its analysis before its answer. If any part of the question has no evidence, code trusts the analysis over the answer and routes to a human. An answer with an unverifiable citation is never posted.
Two processes, one repo, one database. No HTTP between them.
pnpm install
createdb bridgeai # local Postgres
cp .env.example .env # fill in the Slack, database and OpenAI values
pnpm db:push
pnpm bot # Bolt over Socket Mode — no public URL needed
pnpm dev # the plan page on http://localhost:3000Socket Mode on. Bot scopes commands, chat:write, im:write, users:read,
users:read.email, app_mentions:read, channels:history, channels:read. App-level token
with connections:write. Event subscription: app_mention only. Interactivity on. App Home
Messages Tab on. Slash command /bridgeai. Invite the bot to the project channel —
/bridgeai new refuses to start otherwise.
/bridgeai new |
create a project and interview its stakeholders (new demo prefills the demo) |
/bridgeai status |
who has answered, and what has been decided |
/bridgeai plan |
open the drafted plan |
/bridgeai synthesize |
draft the plan now, without waiting for everyone |
/bridgeai ask |
re-send any open question that hasn't reached its owner (synthesis does this automatically) |
/bridgeai ping |
check the bot, the database and the model |
@BridgeAI … |
ask a question in the project channel |
pnpm seed:reset # wipe every project (everything cascades)
pnpm seed:fill # give the live project three complete interviews (Finance, Support, Engineering)
pnpm ai:check # the demo-reliability tests, against the real modelseed:fill never triggers synthesis — synthesis has to post the conflict card, and a script
has no Slack client. Run Anna's interview live, or /bridgeai synthesize.
TESTING.md is the click-by-click runbook: pre-flight, the demo sequence in order, the exact
answers Anna must give for the conflict to appear, and a symptom → cause troubleshooting
table.
Each is a narrow operation with a fixed input and a Zod-validated output schema. None of them
knows Slack exists, and none touches the database — which is what makes pnpm ai:check
possible without either. Every one writes structured analysis before its answer, and code
checks the two agree.
| Agent | Thinks first | Then answers |
|---|---|---|
| Interviewer | every capability in the brief, what this role decides, what the brief never states | 4–6 round-1 questions |
| Follow-up Interviewer | a verdict per answer, the decisions still open | done, or 1–3 questions quoting the person's own words |
| Synthesizer | a model of the whole product and its coverage | requirements, conflicts, open questions |
| Router | each part of the question and the requirement that states it | answered, or unknown + the owner |
The interview is adaptive, not a form: round 1 is 4–6 questions, and every later question is generated from what that person just said, quoting it back ("Because you said …"), for up to three rounds. The Follow-up agent never sees another stakeholder's answers, so contradictions can only surface in public, on the conflict card.
bot/ Slack ↔ lib translation. No business logic. lib/ never imports @slack/*.
lib/ services + lib/ai/* (pure: context in, validated output out, no DB)
app/ one server-rendered route: /plan/[projectId]
scripts/ seed, seed data, ai:check
| Variable | Effect |
|---|---|
MAX_ROUNDS |
3 = round 1 + two follow-up rounds; 2 if you are short on time |
FOLLOWUPS_ENABLED |
false turns the adaptive interview back into a form — don't |
PLAN_PAGE_ENABLED |
false posts the plan as a Slack message instead of linking the page |
APPROVALS_MODE |
minimal (default) is the one "Approve all my items" button per owner. full (per-requirement approvals) is cut item #1 in the plan and has no service contract, so the flag currently records the mode without changing behaviour. |
OPENAI_BASE_URL |
set only for a sponsor-provided gateway |
DEMO_SLACK_USER_IDS |
prefills the people pickers for /bridgeai new demo. Use the labelled form — Operations:U…,Finance:U…,Support:U…,Engineering:U… — so order cannot silently put the wrong person in a role. The bare positional form still works. Entries that don't name a role (a developer account, say) are ignored. |
temperatureis not sent. The chosen model rejects it (400 Unsupported parameter). Each operation still declares its intended temperature, andlib/ai/client.tsdocuments how to restore the parameter for a model that accepts it.- Array bounds are validated in code, not in the schema. Structured Outputs strict mode
does not honour
minItems/maxItems, sosanitizeQuestionsenforces 4–6 (and ≤ 3 for follow-ups) after parsing. - Blank evidence is not evidence. The Router sometimes returns
""or".none"for a part it cannot support.lib/ai/answerDeveloperQuestion.tsaccepts a value only if it is a realR<n>alias — anything else becomesnulland forces routing. - An
open_questionis exempt from the citation downgrade. It carries categorybusiness_rule(the category the decision will have once it is made) and may legitimately cite nothing; downgrading it to an assumption would erase the gap the plan exists to show. An asserted rule with no valid source is still downgraded. ai:checkreports one result asKNOWN: the answered path occasionally routes "When should the certificate be generated?" to Operations instead of answering from a requirement about when a certificate may be issued. Loosening the prompt to force an answer would weaken exactly the evidence-strictness Hero 2 depends on, so it is reported and not counted as a failure.- pnpm 10 blocks dependency build scripts.
@prisma/client's own postinstall is what generates the client, so a plainpnpm installleft it ungenerated and Next failed withCan't resolve '@prisma/client'. Fixed two ways:onlyBuiltDependenciesinpnpm-workspace.yamlapproves Prisma's scripts, and apostinstall: prisma generatescript inpackage.jsonregenerates it regardless. Questionhas nobasedOncolumn, so follow-up rounds keep each question'sbasedOnonInterview.analysis, keyed by position — the interview modal needs it for its hint.
Prepared the night before, as the event rules allow for prompts and starter code: the Slack
workspace and app configuration, the create-next-app scaffold and dependency install, the
empty bridgeai database, and the four system prompts in lib/ai/prompts/.
Everything else — the Prisma schema, every service in lib/, every handler and Block Kit
surface in bot/, the plan page, the seed data and the reliability tests — was written
during the build window.