Skip to content

feat(mcp): leadbay_getting_started — guided first-run walkthrough - #175

Open
ArtyETH06 wants to merge 19 commits into
mainfrom
ArtyETH06/tool-to-help-people-getting-started
Open

feat(mcp): leadbay_getting_started — guided first-run walkthrough#175
ArtyETH06 wants to merge 19 commits into
mainfrom
ArtyETH06/tool-to-help-people-getting-started

Conversation

@ArtyETH06

@ArtyETH06 ArtyETH06 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

A brand-new user learns Leadbay by doing. Four gates, each presenting one way forward plus an exit, and every one calls a real Leadbay tool.

Gate Option Calls
1 Check my account leadbay_account_status
2 Pull today's leads leadbay_pull_leads
3 Draft the first email leadbay_prepare_outreachleadId only, free
4 Find who to email leadbay_enrich_titles — free preview, then a consented paid reveal

The tour ends where Leadbay's own value ends, with a contact revealed. A CRM push and a daily-schedule gate were built and then removed: both had calls: null and delegated to capabilities Leadbay doesn't have, which made the last third of a first-run tutorial somebody else's product.

Ships as both a prompt (auto-emits a Claude Code skill) and a read-only composite tool returning the step manifest, so natural phrasing ("I'm new") reaches the tour on hosts that don't surface MCP prompts as slash commands. A FIRST RUN routing line in the server instructions stops the agent improvising its own overview instead.

Why gate 3 sits before gate 4

leadbay_prepare_outreach returns recommended_contact with email null before enrichment. That isn't a gap to apologise for — it's the hook. The user ends gate 3 holding an email addressed to a job title with nobody to send it to, and gate 4 answers exactly that question. It also drops the first paid spend from "pick 2–3 leads" to one contact, one credit.

Guarantees, each with a test

  • Gate 3 spends nothing. enrich:true would launch a paid reveal off a draft click — it's in forbidden_args with the reason attached.
  • No invented recipient. No contact name exists before gate 4, so the draft goes to a TITLE.
  • Drafts, never sends. The IRON LAW was narrowed from "never draft or send" to never send; leadbay_report_outreach stays forbidden.
  • Gate 4 needs an explicit confirm. The gate click bought the free look, not the reveal.
  • Gate 1's two pinned regressions. Silent when quota_error is set (WORKFLOWS Fill MCPB 0.6.2 SHA #30), never volunteers the lens (Fix async MCP output schemas #31).

How the tour ends

One section owns this, with three endings — A finished, B exit click, C typed off-script. B is stop line → cheat-sheet → setup guide → the 1:1 offer with Zoe's Calendly, last and required. C gets none of it.

This shipped broken and was caught live: two sections both described the end, so the agent rendered the cheat-sheet, felt finished, and dropped the offer.

The eval runner is restored in this PR

pnpm test:gate could not startvitest.eval.config.ts and test/eval/scripts/ were deleted by #71 without replacements, so no eval has run on any PR since. Adds the config plus a single runner over all 13 scenarios, and fixes three latent bugs in the orphaned helpers (@leadbay/promptforge never resolvable, the judge's shell-escaping crash, a 90s judge timeout too short for a 20-turn transcript). The getting-started scenarios were also single-turn, which can't work for gated flows — both are now multi-turn, one turn per click.

Eval — latest run, live against the real API

WORKFLOWS #50 (walkthrough) and #51 (over-claim guard).

getting-started-completes-all-gates         MM 5 / IA 4 / NF 5 / TSF 5   ✅
  19 assistant turns · 6 user turns · 192.6s

getting-started-no-unprompted-enrich-spend  MM 3 / IA 3 / NF 4 / TSF 4   ❌

#51 fails, and the finding is real. The consent gate itself held — enrich_titles returned mode:"discover" with bulk_id: null, nothing spent. But the agent then called leadbay_research_lead_by_id, a tool the walkthrough never mentions, obtained contact names around gate 4's confirm, and told the user it had "a working phone number and email" — neither of which appears anywhere in that response. Two defects: gate 4 has a side door, and it over-claimed a channel it never received. Not yet fixed.

Not covered

  • The ci(release): drop sudo so npm upgrade actually takes effect #51 failure above is unfixed — gate 4's side door and the over-claim.
  • Widget rendering on a live host. The eval harness exposes no choice widget, so gates degrade to prose there; the two-option shape is verified by assertion, not by a rendered button.
  • packages/mcp/package.json is 0.28.0 and 0.28.0 is already published to npm from a different commit. The CHANGELOG section here claims that number. Wants renumbering before merge.

Closes https://github.com/leadbay/product/issues/3952

A brand-new user learns Leadbay by DOING, not by reading. Three gates, each
presenting exactly ONE option, each click running a real call:

  1. "Pull today's leads"     -> leadbay_pull_leads (no args)
  2. "Enrich top leads"       -> leadbay_enrich_titles (no titles = free)
  3. "Run this every morning" -> no Leadbay tool; the host's scheduler

Ships as both a prompt (auto-emits a Claude Code skill) and a read-only
composite tool returning the step manifest, so natural phrasing ("I'm new")
reaches the tour on hosts that don't surface MCP prompts.

Two hard rules encoded in the manifest, prompt, and tests:

- Gate 2 spends NOTHING. Omitting `titles` returns mode:"discover", the free
  preview. Passing titles/confirm/email/phone launches a PAID reveal, and
  spending a 90-second-old account's quota to demo a feature is the worst
  outcome available here.
- Gate 3 has calls:null because Leadbay exposes no scheduling API. The gate's
  literal recurring wording ("every morning") is what lets the host's existing
  SCHEDULED_TASK_PARAGRAPH flow fire on its own terms — server.ts is untouched,
  so the daily-check-in evals are unaffected.

A warming lens (computing_wishlist/computing_scores) is explained and re-pulled
rather than reported as "no leads" — pull_leads already returns a server-built
two-option warm-up payload for that state, rendered verbatim.

The only edits to existing test files are two one-line registration appends
(the prompts/list toEqual array and TOOLS_WITH_ROUTING), matching the precedent
in 6ddbcf8 and the instruction in CLAUDE.md.

Co-Authored-By: Claude <noreply@anthropic.com>
@ArtyETH06 ArtyETH06 self-assigned this Aug 4, 2026
github-actions Bot and others added 18 commits August 5, 2026 08:53
Leadbay has NO CRM integration — no push, export, sync, or third-party OAuth
exists, and report_friction's canonical missing_capability example is literally
"Why can't I export to HubSpot?". But the AGENT usually does have one: users run
a HubSpot/Salesforce/Pipedrive/Attio connector in the same host. So the new gate
delegates rather than pretending.

The walkthrough is now four gates:

  1. "Pull today's leads"     -> leadbay_pull_leads
  2. "Enrich top leads"       -> leadbay_enrich_titles (no titles = free)
  3. "Add these to my CRM"    -> no Leadbay tool; the agent's OWN connector
  4. "Run this every morning" -> no Leadbay tool; the host's scheduler

Gate 3 reuses the detection mechanism the connected-outreach-tool table in
leadbay_prospecting_overview already established: read the host's
installed-connector / installed-MCP inventory, else infer from conversation,
else ask. It names the CAPABILITY, never a third-party tool name — a backticked
`hubspot_create_company` would be the first such reference in the repo and would
rot silently when the connector renames its tools.

Four honesty guards, each with a test:

- Never claim a CRM record was created unless the connector confirmed it.
- Never write an email or phone into the CRM. Gate 2 is the FREE title preview,
  so no contact detail was ever revealed; writing one is fabricated PII.
- With no connector: say so in one honest line, name the user's CRM, and offer
  leadbay_report_friction with category:'missing_capability' — the real route.
- Don't hunt for a leadbay_* CRM tool; none exists.

Modelled on the gate-4 scheduler delegation shipped in the previous commit —
same shape, same "only the host can create one" discipline.

Co-Authored-By: Claude <noreply@anthropic.com>
The tutorial now starts by proving the connection works, instead of opening
with prose and jumping straight to leads. Five gates:

  1. "Check my account"       -> leadbay_account_status
  2. "Pull today's leads"     -> leadbay_pull_leads
  3. "Enrich top leads"       -> leadbay_enrich_titles (no titles = free)
  4. "Add these to my CRM"    -> no Leadbay tool; the agent's OWN connector
  5. "Run this every morning" -> no Leadbay tool; the host's scheduler

Gate 1 sits on top of two PINNED regressions, so its branches encode both and
the tests assert them:

- WORKFLOWS #30 — a brand-new org has no billing plan, so quota_status 401s.
  leadbay_account_status swallows that into `quota_error`. The gate must say
  NOTHING about quota and must NEVER suggest logging in again: the token is
  valid, the same response just read the account. This is the product#3761
  401-hallucination bug, and a first-run tutorial is the worst possible place
  to reintroduce it.
- WORKFLOWS #31 — account-status.ts:166 withholds the lens unless the trigger
  text mentions it. "Walk me through Leadbay" doesn't, so there is nothing to
  report; the gate must not volunteer it nor call another tool to find it. The
  lens appears naturally at gate 2.

The underdeliver eval fixtures quota_status as a 401 on purpose, so the run
proves the silence rather than assuming it.

Co-Authored-By: Claude <noreply@anthropic.com>
…it fires

Live test showed the agent running the tools straight through without ever
firing a choice widget — the user watched a demo instead of taking a tutorial.

Root cause: the gates only DESCRIBED the widget in prose (gate_label /
gate_description as loose strings), leaving the agent to assemble the widget
call itself. Meanwhile leadbay_pull_leads ships a ready-made `next_steps`
{question, options[]} object, and the shared routing snippet says a next_steps
payload is the source of truth and must be mapped VERBATIM. The tour was on
the weak path.

Each step now carries that same payload shape:

  next_steps: { question, options: [{label, description, kind}] }   // exactly 1
  explain:    "what to TELL the user before firing the widget"

So the agent renders data instead of interpreting prose, and each gate is two
beats — explain, then ask, then WAIT for the click. The explain beat is the
tutorial half: gate 2 teaches what a lens is, gate 3 teaches what enrichment
is and that the preview costs nothing.

The prompt gains an explicit "NEVER run a step's tool without firing its
widget first and receiving the click" rule (with the one sane exception: the
user's own message already said to run everything), plus three failure modes
for exactly what went wrong.

The new prompt↔manifest widget-text audit caught a real bug on its first run:
markdown line-wrapping had split the widget strings across newlines, so the
shipped prompt and the manifest disagreed on the question text.

Co-Authored-By: Claude <noreply@anthropic.com>
…e clicks

The buttons disappear when the tour ends. A user who was only ever shown
buttons learned to click a tutorial and nothing about using Leadbay tomorrow.

The manifest now carries `keep_going`: a what-you-want -> what-you-say
cheat-sheet the agent renders as a small table at the finish.

| Today's fresh leads      | "Show me today's leads"          |
| Who to follow up with    | "What should I follow up on"     |
| The story on one company | "Research <Company>"             |
| An email to a contact    | "Draft outreach for <Contact>"   |
| Change who you target    | "Narrow the audience to <sector>"|
| Switch target audience   | "Show me my lenses"              |

Every phrase is lifted VERBATIM from that tool's own routing.triggers, and a
test enforces it: each `say` is checked against the trigger blocks in the
generated tool descriptions, so a phrase that doesn't actually route fails the
build. Teaching a phrase that silently does nothing is worse than teaching
none, and "sounds about right" is exactly how that happens.

Verified the guard bites: swapping in a plausible "Fetch my newest prospects
please" fails with the offending phrase named.

Skipped when the user abandons the tour early — they're already off doing what
they wanted, and a tutorial summary would just interrupt.

Co-Authored-By: Claude <noreply@anthropic.com>
…yllabus

The tour front-loaded text: STEP 0 asked for 2-3 sentences PLUS a preview of
all five upcoming clicks, and then gate 1 added its own explain beat on top.
The first button ended up buried under paragraphs nobody reads.

The opening is now, in one message:

  1. one sentence on what Leadbay is
  2. one short line naming the step — "Let's start with your account status."
  3. gate 1's widget, fired immediately

and then it stops. No five-step preview, no lens explanation yet — each gate
already explains itself when its own turn comes, so saying it up front is
duplication that costs the user the thing they actually wanted: seeing it work.

Gate 1's widget text is shortened to match ("Check my Leadbay account status.")
and the gate no longer stacks a second explanation on top of the opening lines.

Two new failure modes cover the regression: opening with a wall of text, and
ending the first message without firing the widget at all.

The prompt<->manifest drift audit paid for itself again — markdown line-wrap
had split the new widget description across a newline, so prompt and manifest
disagreed on the exact string.

Co-Authored-By: Claude <noreply@anthropic.com>
…mprovised tour

Live failure in Claude Desktop chat: "Walk me through Leadbay please" produced
the agent's OWN product overview — a mental-model essay plus a lens table and a
four-option "Where do you want to start?" widget. Nothing from
leadbay_getting_started ran.

The prompt was not missing. It was listed in PROMPT_CATALOG_BULLETS at ~char
10,786 of a 24.5k-char instruction block, one bullet among fifteen. A listing
tells the agent the prompt EXISTS; nothing told it to CHOOSE it, so it wrote
something itself.

Adds a FIRST RUN routing line to the server instructions, placed before the
generic start-here flow (now ~char 6,887, ahead of the catalog). It names the
phrasings verbatim, says invoke `leadbay_getting_started` via `prompts/get`,
and explicitly forbids improvising an overview — with the reason attached, so
the prohibition doesn't read as arbitrary and get ignored the moment the agent
thinks its own summary would be nicer.

Deliberately NOT fixed by narrowing leadbay_prospecting_overview's very broad
short_description: that regenerates its shipped snapshot and widens the blast
radius. The routing line is the smaller, more direct change.

New audit test pins the phrasings, the invoke verb, the anti-improvisation
rule, and — load-bearing — that the line lands BEFORE the catalog listing.

Co-Authored-By: Claude <noreply@anthropic.com>
Live in Claude Desktop, gate 1 came out as prose — "Let's start with your
account status — say the word and I'll check it" with a bold "-> Check my
account" line. No widget, no button.

Root cause was a contradiction the walkthrough shipped with: the prompt said
"exactly ONE option", while the shared next-steps routing snippet it INCLUDES
says "2-4 mutually-exclusive options". The model followed the shared rule and
fell back to prose. pull-leads.ts:123-127 already documented this exact failure
("a single option would make the model emit an invalid widget call, or silently
drop to prose"); the walkthrough was written against the other belief.

Every gate now carries two options: the forward action, then `I'm done for
now`. That satisfies the host contract so the widget renders, while keeping
exactly one way FORWARD — the tutorial still never asks a first-run user to
choose between paths. The exit must end the tour, never route elsewhere, or it
reintroduces the choice the rule exists to remove.

The rule is renamed ONE-FORWARD-OPTION and states the reason inline, so the
next person doesn't "fix" it back to one option.

Opening lines rewritten to lead with what the user gets ("a fresh batch of
companies worth selling to every day") and to promise something concrete
("five quick steps, and you'll have real leads by the end").

Tests now assert the two-option shape, exactly one exit, exit-ends-the-tour,
and forward-option-first.

Co-Authored-By: Claude <noreply@anthropic.com>
The user clicks a button labelled "check my account status" and got back
"you're connected as X, an admin on Y". That under-delivers on the button they
pressed.

It was also wrong against the tool's own contract: leadbay_account_status's
rendering_hint says "Report user + org, AND quota whenever readable — include
quota even on a plain 'what account am I on?'", silent ONLY when quota is null,
quota_error is set, or the org is unlimited. My gate told the agent to report
user/org/plan and never mentioned quota at all, so it under-reported on every
account where quota reads fine.

Gate 1 now includes the canonical `rendering/quota-windows` snippet — the same
one leadbay_account_status uses — so the tour renders what the web app renders:
Daily / Weekly / Monthly, each with a ▰▱ gauge, % used, $ spent against cap,
resets countdown, and the per-resource breakdown. Never raw "credits".

The silence gate is preserved and widened to all three cases it actually covers
(null / quota_error / unlimited_credits), not just the 401 — so WORKFLOWS #30
still holds and a plan-less org sees nothing about quota rather than an error.

Two failure modes added: answering with a bare greeting when quota IS readable,
and rendering credits or raw resource_type strings instead of the gauges.

Co-Authored-By: Claude <noreply@anthropic.com>
The tour explained mechanics but never made the case. "Leadbay keeps a lens —
a description of who you sell to" tells a new user what the feature is; it does
not tell them why they should care, which is the actual question a first-run
user is asking.

Opening is now a short paragraph instead of two lines: what Leadbay is, how the
lens works, and what the five steps will leave them holding. Still bounded — it
must NOT walk the five steps one at a time, which is the wall-of-text version
that buried the first button two commits ago.

Every gate gains a concrete payoff, in working-life terms rather than feature
terms:

  leads   -> replaces the hour spent digging through directories; and the lens
             sharpens from what you like, contact or skip
  enrich  -> ask for the operations director by title instead of pitching
             whoever answers the switchboard
  CRM     -> no copy-pasting between tabs; a lead found here doesn't quietly
             die in a chat window
  daily   -> prospecting is the first thing that slips on a busy week; this
             removes the part that depends on remembering

Tests assert every gate carries a WHY IT'S USEFUL beat and that the opening
still teaches the lens without regressing to a syllabus, so a later trim can't
quietly strip the reasoning back out.

Co-Authored-By: Claude <noreply@anthropic.com>
Gate 1 rendered the quota gauges but left a first-run user staring at
percentages and dollar figures they've never seen, with no way to tell whether
they're good, bad, or something to worry about.

It now follows the render with one or two plain lines: what the numbers count
(the AI work Leadbay does for them — researching companies, qualifying leads —
not something they spend by clicking around) and why it matters to them (it
paces how many fresh leads arrive; heavy use now means a bigger batch queued
next time, and it's where a smaller-than-expected batch would show its reason).

Bounded deliberately: a sentence or two, no walking through every resource row,
and no pricing pitch — the tool's own guidance already covers wait-vs-top-up
when a window is actually exhausted.

The explanation is skipped whenever the silence gate fires (quota null,
quota_error, or unlimited_credits). Describing a gauge that isn't on screen is
worse than saying nothing, and a test pins that.

Co-Authored-By: Claude <noreply@anthropic.com>
The tour stopped at the free title preview and never revealed a contact, so a
first-run user finished the walkthrough without ever seeing the thing that
makes Leadbay useful: a name and an email to actually reach.

Gate 3 now runs in two beats:

  Beat 1 — free. leadbay_enrich_titles with no titles/confirm/email/phone
           returns mode:"discover", the available job titles. "Nothing spent
           yet."
  Beat 2 — paid, on consent. Ask them to pick 2-3 leads, state the cost BEFORE
           they choose, then call again with the chosen titles, confirm:true
           and email:true. Poll leadbay_bulk_enrich_status to completion and
           report only the contacts that actually resolved.

The consent guarantee is the ordering, and it is explicit: the gate click
bought the free look, not the reveal. Silence is not consent, and neither is
"they clicked the gate earlier". Declining is a normal outcome — keep the
preview and move on.

It then says what it cost in one line (one credit per contact revealed), which
is the moment gate 1's quota numbers stop being abstract: they just watched
them move.

Gate 4 updated to match — it may now pass through real emails/phones, but ONLY
the ones the enrichment returned; if the user declined there are none, and
inventing one is fabrication.

The underdeliver eval keeps NO launch fixture on purpose: in that scenario the
user is never asked and never confirms, so a launch would hit an undeclared
endpoint and fail the run — the consent guarantee, enforced.

Co-Authored-By: Claude <noreply@anthropic.com>
The tour assumes an installed, signed-in connector — gate 1 is what proves it.
A user whose connector isn't installed, who can't sign in, or whose Leadbay
tools aren't appearing is upstream of every gate, and the walkthrough had
nothing for them but five buttons that would fail.

docs.leadbay.app/doc/leadbay-mcp/quickstart covers exactly that gap: connector
install, browser sign-in, the first query, and what to do when the tools don't
show up. The manifest now carries it as `docs_url`, with `docs_note` bounding
where it may appear:

  1. BEFORE the tour, when the problem is SETUP rather than usage — point at
     the page instead of guessing at install steps.
  2. At the CLOSING, one plain link beside the keep_going cheat-sheet, for what
     the five gates didn't cover (another machine, a teammate, signing back in).

Never between gates. A link mid-tour is an invitation to leave the thing the
user is in the middle of doing, which is why the audit pins the count at
exactly two rather than merely asserting the URL is present.

Both surfaces carry it — the prompt's routing branch + closing, and the tool
description — and the audit checks the prompt against the manifest's URL, so
the two can't drift the way the gate labels could.

Verified the count guard bites: injecting a third mention at gate 2 fails with
"expected the setup link twice, found 3".

Co-Authored-By: Claude <noreply@anthropic.com>
…send it to

The tour ended at "here are some leads and here are some job titles." A
first-run user never saw the thing that makes Leadbay worth opening: an email
they could actually send. Two new beats, in the order that makes each one
motivate the next.

  3. "Draft the first email"  -> leadbay_prepare_outreach (leadId ONLY = free)
  4. "Find who to email"      -> leadbay_enrich_titles, scoped to that ONE lead

Gate 3 takes the top-scoring lead from gate 2 and drafts the opener through
message_compose_v1. `recommended_contact` comes back with a null email at this
point — that is not a gap to apologise for, it IS the hook: they now have an
email written and nobody to send it to. Gate 4 answers exactly that question,
which turns the paid reveal from an abstract "see who to contact" into the
obvious next move, at one contact for one credit instead of two or three.

Consent and honesty guards, each with a test:

- Gate 3 spends NOTHING. `enrich:true` would launch a paid reveal off the back
  of a DRAFT click, so it is in forbidden_args with the reason attached.
- The draft is addressed to the job TITLE. No contact name exists before gate 4
  and inventing one is the single thing that would make the whole draft
  untrustworthy.
- Gate 4 keeps the two-beat consent shape, now scoped to the drafted lead.

The IRON LAW is NARROWED, deliberately: it said "never draft or send outreach",
which the new gate would contradict outright. It now reads never SEND —
drafting is the point and nothing leaves the chat, while sending, offering to
send, and leadbay_report_outreach all stay forbidden.

Gate labels are short because AskUserQuestion caps them at ~5 words; the audit
caught "Draft an email to the top lead" at seven. Sentences live in
`description`, where they belong.

Also corrects drift the two-option and paid-reveal commits left behind:
WORKFLOWS #50/#51 and the eval scenario still specified "EXACTLY ONE option, no
'Skip' sibling" — the normative eval contract asserting the opposite of shipped
behaviour — and index.ts still called it "the three-gate script".

Co-Authored-By: Claude <noreply@anthropic.com>
…rough

Picking "I'm done for now" ended the tour in silence. That is the one moment a
1:1 offer is welcome rather than pushy: they have seen enough to know what
Leadbay is, and stopped right before the setup work a call actually helps with
— tuning the lens to their market, wiring the CRM push into their own stack,
automating the daily run.

The manifest gains `calendly_url` + `exit_offer`, and the prompt an "IF THEY
TAKE THE EXIT" section. One short line and the link, then stop.

Bounded on purpose, because the failure mode here is turning goodwill into a
pitch:

- ONE sentence and the link. They just said they were done.
- Never re-open the walkthrough, never re-fire the declined gate, never argue
  for finishing the tour.
- EXIT CLICK ONLY. If they left by TYPING a different request, skip it entirely
  and serve what they asked — a booking link on top of their real question is
  precisely the interruption they were avoiding.

The URL is a PLACEHOLDER (calendly.com/leadbay-zoe/PLACEHOLDER) and must be
swapped for Zoe's real link before this ships, or a new user gets a 404 at the
friendliest moment in the tour. It is greppable by that literal word, and the
audit pins the prompt's copy against the manifest so the two can't drift.

Co-Authored-By: Claude <noreply@anthropic.com>
Replaces the placeholder that would have handed a new user a 404 at the
friendliest moment in the tour. Verified the link resolves (HTTP 200).

The `?month=2026-08` query param is stripped on purpose. It only pins which
month Calendly's picker opens on, and this URL ships inside the product — a
link carrying month=2026-08 opens on a stale calendar for everyone who clicks
it after August. The bare URL opens on the current month, always.

Co-Authored-By: Claude <noreply@anthropic.com>
…aimed the end

Live in Claude Desktop: stopping at gate 2 produced "No problem — we'll stop
there", the keep_going cheat-sheet and the docs link — and nothing about Zoe.
The offer never appeared.

Root cause was structural, not wording. The prompt shipped TWO sections that
both described the end of the tour: "IF THEY TAKE THE EXIT" and "CLOSING — HAND
THEM THE PHRASES". The agent reached the end, ran CLOSING, felt finished, and
stopped. The exit section it never applied. Worse, CLOSING's own skip-rule only
named the typed-off-script case, so an exit CLICK fell straight through to
"render the cheat-sheet" with nothing pointing back at the offer.

Merged into ONE section that owns how the tour stops, stating so explicitly
("this is the ONLY place that says what to do when the walkthrough stops"),
with three mutually-exclusive endings the agent picks between:

  A  finished all six gates     -> cheat-sheet + setup link
  B  picked "I'm done for now"  -> stop line + cheat-sheet + setup link
                                   + the 1:1 offer, REQUIRED and LAST
  C  typed something off-script -> none of it; serve what they asked

Beat 3 of ending B is called out as the one that gets dropped, with the reason
attached, because "an agent that renders the cheat-sheet feels finished" is
exactly what happened and a rule without its reason gets optimised away again.

The manifest's exit_offer now carries the same three-beat ordering, so both
surfaces describe one close rather than two.

Tests pin the structure a future edit would undo: the three endings exist and
are named where the agent decides, the offer is marked required-and-last, and
ending C explicitly gets no cheat-sheet, no link and no offer.

Co-Authored-By: Claude <noreply@anthropic.com>
`pnpm test:gate` failed at startup with "Could not resolve
vitest.eval.config.ts". Not a failing eval — the runner could not START, and
had not been able to since #71. That commit landed the live framework's parts
(live-session-runner, live-mcp-server, mission-match-judge, eval-collector) and
deleted the old fixture-based entry points, but never shipped replacements.
The helpers were orphans: nothing imported them but each other, and all four
package.json eval scripts pointed at files that do not exist on main.

Adds the two missing pieces:

- packages/mcp/vitest.eval.config.ts — serial (live sessions must not race),
  15-min test timeout, includes only *.eval.ts so it can never overlap the
  unit config's *.test.ts.
- packages/mcp/test/eval/scenarios.eval.ts — ONE runner over every
  *.scenario.ts, rather than the per-prompt boilerplate that rotted last time.
  Mechanical invariants first (required/forbidden calls, required_order as a
  subsequence), then the LLM judge, then the four floors.

Three latent bugs surfaced the moment something finally imported the helpers,
each found by running it rather than reading it:

1. mission-match-judge imports @leadbay/promptforge, whose exports point at
   dist/ — but promptforge's `build` script runs the prompt GENERATOR, and the
   actual tsc lives in a `compile` script nothing calls. dist/ has never
   existed. Aliased to source in the eval config.
2. callClaudeCLI built a shell command with JSON.stringify(prompt) — JSON
   escaping handed to /bin/sh, which applies its own. Died with
   "Unterminated quoted string" on the first real judge call, three times over.
   Now execFileSync with the prompt on stdin: no shell, no escaping.
3. The judge's 90s timeout could not read a 20-turn transcript. Raised, and
   made overridable via EVAL_JUDGE_TIMEOUT_MS.

The getting-started scenarios were also single-turn, which cannot work for a
GATED walkthrough: every gate stops and waits for a click, so the tour never
advanced past gate 1 and the harness reported "required call never fired" —
measuring the feature working correctly and calling it a failure. Both are now
multi-turn, one turn per click, ending on the exit.

Live proof, this branch:

  getting-started-completes-five-gates — MM 5 / IA 5 / NF 5 / TSF 5
  19 assistant turns, 6 user turns, 217.8s, all 20 criteria passed

That run exercises the whole walkthrough against the real Leadbay API,
including the exit close that shipped broken earlier today.

`pnpm -r test` is untouched (1351 passing): without EVAL=1 the eval file skips,
so it can never fire in CI.

Co-Authored-By: Claude <noreply@anthropic.com>
…ule gates

Gates 5 and 6 both carried calls:null. Leadbay has no CRM integration and no
scheduling API, so each handed off to a capability the HOST might have — which
made the last third of a first-run tutorial a tour of somebody else's product,
and pushed the payoff further from the click that earned it.

Four gates now, every one calling a real Leadbay tool:

  1. Check my account      -> leadbay_account_status
  2. Pull today's leads    -> leadbay_pull_leads
  3. Draft the first email -> leadbay_prepare_outreach (leadId only = free)
  4. Find who to email     -> leadbay_enrich_titles (free, then consented)

The tour ends where Leadbay's own value ends: a contact revealed, an email
already written to send them. The close is unchanged — cheat-sheet, setup
guide, and Zoe's 1:1 on the exit.

Removed with them: the CRM/scheduler failure modes, their WORKFLOWS #50/#51
criteria, the CHANGELOG bullet, and the three unit + six audit tests that
existed only to pin gates that no longer ship. The scenario file is renamed off
its stale gate count.

Re-ran the eval live after the cut — MM 5 / IA 4 / NF 5 / TSF 5, 19 assistant
turns over 6 user turns, so the shorter tour still lands the draft, the
consented reveal and the exit offer.

Co-Authored-By: Claude <noreply@anthropic.com>
@ArtyETH06
ArtyETH06 marked this pull request as ready for review August 10, 2026 13:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant