@leadbay/mcp 0.39.4 → 0.39.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -1895,6 +1895,8 @@ to pick, then re-call with the id/exact name.
1895
1895
 
1896
1896
  Restrict (or expand) the lens audience by sector / size. Free-text sectors are auto-resolved against the sector taxonomy; ambiguous matches are surfaced to the agent rather than guessed silently. Permission routing is hidden: the default lens auto-clones to a new user lens; an org-level lens defaults to a per-user draft (admins can override with \`save_for_org:true\`). Filter MERGES with existing criteria (unrelated criteria are not dropped).
1897
1897
 
1898
+ **A complaint about relevance is not a filter request.** "The leads aren't relevant", "they don't look like my customers", "I keep getting the wrong companies" says the OUTPUT is wrong \u2014 it does not say which sector to add. Read \`leadbay_get_qualification_questions\` first (it returns the questions, the ideal buyer profile AND the targeting prompt), and find out WHICH leads were wrong, ideally by pulling some and pointing at them. Sector and size are only one of the places the answer can live; editing them blind fixes the wrong thing and leaves the real cause in place.
1899
+
1898
1900
  **Targeting a lens \u2014 READ THIS.** By default this edits the user's ACTIVE lens. **If the user names a lens** ("add fintech to my **Joinery** lens", "in my Nordics lens, exclude retail"), you MUST pass \`lensName\` with that name (\`lensName:"Joinery"\`). Do NOT silently edit the active lens when a different one was named \u2014 that corrupts the wrong audience and is a top friction source. The name resolves against the user's lenses (case-insensitive, exact then unique-substring); it is edit-only and does NOT change which lens is active. An unmatched name returns \`status:"lens_not_found"\` with the lens list, and a name matching several returns \`status:"ambiguous_lens"\` with the candidates \u2014 surface them and re-call with the exact \`lensName\` or a \`lensId\`. Use \`leadbay_my_lenses\` if the user first wants to SEE or SWITCH lenses. To CREATE a brand-new lens, use \`leadbay_new_lens\` \u2014 not this tool.
1899
1901
 
1900
1902
  **Geography \u2014 scope a sales territory.** Pass \`locations\` (free text like \`["Indre-et-Loire"]\`, \`["Texas"]\`, \`["Austin"]\`, or admin-area ids) to restrict the lens to a region, and \`exclude_locations\` to carve one out. Free text auto-resolves via \`/geo/search\` at any level from state down to city \u2014 state, *r\xE9gion*, *d\xE9partement*, county, city. Unresolved/ambiguous text returns \`status:"ambiguous_locations"\` with candidates \u2014 surface them and re-call the chosen id via the SAME axis it came from: an INCLUDE pick \u2192 \`location_ids\`; an EXCLUDE pick \u2192 \`exclude_locations\` (**NOT** \`location_ids\`, which would include the area the user asked to exclude). The returned \`message\` names the right param per text. This is how a director scopes a rep's territory and then asks for net-new accounts there.
@@ -2766,7 +2768,7 @@ Do NOT use for: "show me today's leads / what's new today" \u2192 \`leadbay_pull
2766
2768
  Prefer when: the user describes a target profile or names a count of NEW companies \u2014 craft the example_lead per the seed rules below BEFORE calling; never pass the user's raw sentence as query.
2767
2769
 
2768
2770
  Examples that SHOULD invoke this tool:
2769
- - "Find me 10 gyms around Dallas that would buy our flooring, with someone I can call."
2771
+ - "Find me 10 gyms around Dallas that would buy our flooring, with a contact."
2770
2772
  - "Get me 20 new US SaaS companies, 50-2000 employees, with the VP People's email."
2771
2773
  - "We're launching in Lyon \u2014 find 15 hotels that fit our ICP."
2772
2774
 
@@ -2777,11 +2779,10 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
2777
2779
 
2778
2780
  ## RENDER (quick)
2779
2781
 
2780
- 3-col table of delivered leads in returned order: col 1 = 10-segment fit
2781
- bar + linked company \xB7 location \xB7 size; col 2 = why-fits \u226420 words; col 3
2782
- = contact + found channels. ALWAYS close with the honest funnel line
2783
- (matched/examined/delivered/stop reason) \u2014 especially on 0
2784
- delivered. Full algorithm below.
2782
+ 3-col table of delivered leads in returned order: col 1 = 10-segment fit bar
2783
+ + linked company \xB7 location \xB7 size; col 2 = why-fits \u226420 words; col 3 =
2784
+ contact + found channels. ALWAYS close with the honest funnel line
2785
+ (matched/examined/delivered/stop reason), especially on 0 delivered.
2785
2786
 
2786
2787
  ---
2787
2788
 
@@ -2790,27 +2791,25 @@ company universe, applies hard filters, skips what the org already knows
2790
2791
  (\`novelty: org\`), optionally qualifies against the org's own intelligence
2791
2792
  (questions, tags, ideal buyer profile \u2014 frozen at submit), and optionally reveals
2792
2793
  contact channels. Polls up to \`wait_seconds\` (default 45); a longer job returns
2793
- \`still_running\` + \`next_poll\` \u2014 hand off to \`leadbay_lead_job_status\`. Jobs run
2794
+ \`still_running\` + \`next_poll\` \u2014 hand to \`leadbay_lead_job_status\`. Jobs run
2794
2795
  \u226430 min, results kept 30 days.
2795
2796
 
2796
2797
  **Free vs usage quota \u2014 never use quota silently.** Default (\`qualify: false\`,
2797
2798
  \`channels: []\`) is FREE: company profile + fit score + cached research +
2798
2799
  contact identity. \`qualify: true\` (per candidate EXAMINED, capped by
2799
2800
  \`exploration_cap\`/\`max_cost\`) and \`channels\` (only when a value is found) draw
2800
- on the org's usage quota; nothing is invoiced. Enforced in code: such a call is WITHHELD unless it
2801
- carries \`confirm: true\` \u2014 nothing is submitted and you get
2801
+ on the org's usage quota; nothing is invoiced. Enforced in code: such a call is
2802
+ WITHHELD unless it carries \`confirm: true\` \u2014 nothing is submitted and you get
2802
2803
  \`mode: "needs_confirmation"\` with a real quote to show the user. Re-call with
2803
2804
  \`confirm: true\` on their go-ahead ("go ahead / get their emails" counts).
2804
- \`confirm: false\` vetoes. Free needs no consent. **Preview free first** \u2014
2805
- reshaping an off-profile seed is free, exploring it with \`qualify: true\` is
2806
- not.
2805
+ \`confirm: false\` vetoes. **Preview free first** \u2014 reshaping an off-profile seed
2806
+ is free, exploring it with \`qualify: true\` is not.
2807
2807
 
2808
2808
  **Ad-hoc exclusions ("no chains") are enforced by NO tier** \u2014 \`filters\` has no
2809
- exclusion key, and \`qualify\` scores against the org's FROZEN questions and IBP,
2810
- which need not mention chains; the seed's inverse only shifts ranking.
2811
- Violators can survive, use quota and be delivered \u2014 post-filter them yourself
2812
- and say the tier didn't enforce it. Durable enforcement \u2192
2813
- \`leadbay_refine_prompt\`.
2809
+ exclusion key, and \`qualify\` scores against the org's FROZEN questions and IBP.
2810
+ Violators survive, use quota and get delivered: post-filter them yourself and
2811
+ say the tier didn't enforce it. Durable enforcement \u2192
2812
+ \`leadbay_set_qualification_questions\` / \`leadbay_refine_prompt\`.
2814
2813
 
2815
2814
  ### Crafting the \`example_lead\` seed \u2014 the input that decides result quality
2816
2815
 
@@ -2830,13 +2829,13 @@ measured:
2830
2829
  model, what they sell or operate, who they serve, observable scale. Write
2831
2830
  it like the first paragraph of their About-Us page.
2832
2831
  - STRONG: "Operator of full-service fitness centers offering strength
2833
- areas, group classes and personal training to members across multiple
2834
- clubs."
2835
- - WEAK (generic): "A gym in Texas."
2836
- - WRONG (seller-side): "Supplier of durable modular flooring for gyms."
2837
- 4. **No event language.** "hiring", "expanding", "just raised" are not
2838
- filters \u2014 registry descriptions never contain them, so they dilute the
2839
- profile. Purchase triggers belong in the org's qualification questions.
2832
+ areas, group classes and personal training to members across clubs."
2833
+ - WEAK: "A gym in Texas." WRONG: "Supplier of gym flooring." (seller-side)
2834
+ 4. **NO event language \u2014 in \`description\` or \`query\`.** "recrute", "hiring",
2835
+ "expanding", "just raised" never appear in registry text, so they match
2836
+ nothing. Send the trigger to \`leadbay_set_qualification_questions\` or
2837
+ \`leadbay_refine_prompt\` and say so. "companies hiring a senior SDR" seeds as
2838
+ "B2B software company operating an in-house outbound sales team."
2840
2839
  5. **No meta-markers.** Never "(example)", "(fictional)", "(placeholder)".
2841
2840
  6. **Hard constraints go in \`filters\`, not prose \u2014 exact keys:**
2842
2841
  \`sectors: string[]\`, \`locations: string[]\`, \`employees_min: number\`,
@@ -2846,24 +2845,22 @@ measured:
2846
2845
  never a country: this workspace's own is dropped, any other is refused.
2847
2846
  7. **Prefer \`example_lead\` over \`query\`.** Query matches topic *vocabulary*:
2848
2847
  "gyms that need durable flooring" surfaced flooring VENDORS, 0 delivered.
2849
- Use \`query\` only for signal an example can't express.
2850
2848
  8. **One seed per buyer archetype.** An ask spanning two segments ("gyms and
2851
2849
  warehouses") needs one search each with its own description and
2852
- \`request_id\` \u2014 a blended seed lands between the clusters and matches
2853
- neither.
2850
+ \`request_id\` \u2014 a blended seed lands between the clusters, matching neither.
2854
2851
 
2855
2852
 
2856
2853
  **Parameter notes**
2857
2854
  - \`request_id\` (REQUIRED) is the retry contract: SAME value retrying the same
2858
2855
  ask (same live job, no double launch); NEW for a changed ask. Derive from ask
2859
2856
  + archetype + date: \`gyms-dallas-2026-07-28\`.
2860
- - Never lower \`min_ai_score\` together with \`channels\` \u2014 that reveals emails for
2861
- leads the AI just scored as junk.
2862
- - \`count\` \u2264 50; \u22643 active jobs/org; \u226410 submits/hour (429 + Retry-After \u2014
2863
- wait, don't hammer).
2857
+ - Never lower \`min_ai_score\` with \`channels\` \u2014 that reveals emails for leads
2858
+ the AI just scored as junk.
2859
+ - \`count\` \u2264 50; \u22643 active jobs/org; \u226410 submits/hour (429 + Retry-After \u2014 wait,
2860
+ don't hammer).
2864
2861
 
2865
2862
  **Read the result honestly** \u2014 \`funnel\` + \`explain.scope_notes\` tell the story;
2866
- zero delivered gets a cause and a next move (rules in RENDERING).
2863
+ zero delivered gets a cause and a next move (RENDERING).
2867
2864
 
2868
2865
  ---
2869
2866
 
@@ -3344,26 +3341,28 @@ WHEN NOT TO USE: when the lead summary's \`prospecting_actions_count\` is 0.
3344
3341
  `;
3345
3342
  leadbay_get_qualification_questions = `## WHEN TO USE
3346
3343
 
3347
- Trigger phrases: "what are my qualification questions", "what questions does Leadbay ask about each lead", "show me the org qualification questions", "how are my leads being qualified", "what's the qualification criteria".
3344
+ Trigger phrases: "what are my qualification questions", "what questions does Leadbay ask about each lead", "show me the org qualification questions", "how are my leads being qualified", "what's the qualification criteria", "(before any settings change) what is configured today", "why am I getting these leads", "the leads aren't relevant \u2014 what are my settings".
3348
3345
 
3349
- Do NOT use for: "how did this lead score on the qualification questions" \u2192 \`leadbay_research_lead_by_id\`; "show my ideal buyer profile and intent tags" \u2192 \`leadbay_get_taste_profile\`.
3346
+ Do NOT use for: "how did this lead score on the qualification questions" \u2192 \`leadbay_research_lead_by_id\`; "change / add / remove a qualification question" \u2192 \`leadbay_set_qualification_questions\`; "answer a pending clarification" \u2192 \`leadbay_answer_clarification\`.
3350
3347
 
3351
- Prefer when: user wants the ORG-level qualification questions catalog, no lead and no buyer profile
3348
+ Prefer when: user wants the ORG-level qualification settings, or you are about to change any of them \u2014 read this FIRST to see whether their rule is already covered
3352
3349
 
3353
3350
  Examples that SHOULD invoke this tool:
3354
3351
  - "What qualification questions does Leadbay use to score my leads?"
3355
3352
  - "Show me my org's qualification questions."
3353
+ - "Why do I keep getting these companies? What are my settings?"
3356
3354
 
3357
3355
  Examples that should NOT invoke this tool (sound similar, route elsewhere):
3358
3356
  - "How did Acme Corp answer the qualification questions?"
3359
- - "What's my ideal buyer profile?"
3357
+ - "Add a question about install crews."
3360
3358
 
3361
3359
  ## RENDER (quick)
3362
3360
 
3363
- Numbered list of the questions (chat-native markdown), each one line. When
3364
- \`is_admin\` is true, append the \`hint\` as a footnote (points at
3365
- leadbay_set_qualification_questions for editing). When the list is empty,
3366
- render the \`hint\` instead.
3361
+ Numbered list of the questions (chat-native markdown), each one line,
3362
+ verbatim. Below it, the ideal buyer profile summary + anti-patterns and the
3363
+ targeting prompt when set. When \`is_admin\` is true, append the \`hint\` as a
3364
+ footnote (points at leadbay_set_qualification_questions). When the question
3365
+ list is empty, say so explicitly and render the \`hint\` instead.
3367
3366
 
3368
3367
  ---
3369
3368
 
@@ -3376,7 +3375,15 @@ Returns:
3376
3375
 
3377
3376
  - **\`qualification_questions\`** \u2014 the catalog. Each: \`{question, created_at,
3378
3377
  lang}\`. Ordered as the backend returns them.
3379
- - **\`count\`** \u2014 number of configured questions.
3378
+ - **\`count\`** \u2014 number of configured questions. **Zero is a finding, not a
3379
+ blank** \u2014 an org with no questions scores every lead on firmographics alone.
3380
+ Say so and offer a starter set.
3381
+ - **\`ideal_buyer_profile\`** \u2014 \`{summary, key_characteristics, anti_patterns}\`
3382
+ or null. The questions score against THIS. A rule the user states is often
3383
+ already an \`anti_pattern\` here.
3384
+ - **\`targeting_prompt\`** \u2014 the org's free-text instruction to the AI agent, or
3385
+ null. Qualitative rules that no single yes/no can express live here; change
3386
+ it with \`leadbay_refine_prompt\`.
3380
3387
  - **\`is_admin\`** \u2014 whether the current user is an org admin. Modifying the
3381
3388
  questions (\`leadbay_set_qualification_questions\`) is an org-admin action; for
3382
3389
  admins a \`hint\` points there.
@@ -3384,25 +3391,132 @@ Returns:
3384
3391
  when no questions are configured.
3385
3392
 
3386
3393
  This tool only READS. To change the questions, use
3387
- **leadbay_set_qualification_questions** (add / remove / replace). The result is
3388
- cached on the client (it reuses the same taste-profile fetch as
3389
- \`leadbay_get_taste_profile\`), so repeated calls in a session are cheap.
3394
+ **leadbay_set_qualification_questions**; to change the targeting prompt, use
3395
+ **leadbay_refine_prompt**; for sector / size / territory, use
3396
+ **leadbay_adjust_audience**. **leadbay_research_lead_by_id** shows how a
3397
+ SPECIFIC lead answered these questions.
3398
+
3399
+ ### A stated fit rule is a SETTING \u2014 read, decide, propose, then write
3400
+
3401
+ **Never answer a stated rule from memory.** "C'est not\xE9", "already applied",
3402
+ "I'll keep that in mind", "the rule is now active" \u2014 every one of those is a
3403
+ claim about the user's ACCOUNT, and it is false unless a tool call made it
3404
+ true. The rule lives in the org's settings or it does not exist: your context
3405
+ window ends with this conversation, and the next session, the scheduled run and
3406
+ the user's colleague all read the account, not your memory. If you have not
3407
+ called a tool, do not say the rule is in place.
3408
+
3409
+ When the user says what makes a lead good or bad \u2014 *"\xE9carte les soci\xE9t\xE9s
3410
+ liquid\xE9es"*, *"je ne veux pas d'associations"*, *"our best customers run their
3411
+ own maintenance crews"*, *"les leads ne sont pas pertinents"* \u2014 they are
3412
+ describing their account, not just this batch. Filter the batch and they say
3413
+ it again next week; their questions, buyer profile and targeting prompt never
3414
+ move.
3415
+
3416
+ **1 \u2014 Read before you decide.** Call \`leadbay_get_qualification_questions\`
3417
+ first. It returns the question set PLUS the ideal buyer profile and the
3418
+ targeting prompt those questions sit beside. You cannot judge whether a rule is
3419
+ already covered without seeing them.
3420
+
3421
+ **2 \u2014 Decide WHERE the rule belongs.** One rule, one destination:
3422
+
3423
+ | What the user stated | Where it belongs |
3424
+ |---|---|
3425
+ | A sector, a headcount band, a territory | \`leadbay_adjust_audience\` / \`leadbay_new_lens\` filters \u2014 never a question |
3426
+ | A company trait a stranger could estimate from that company's own website or registry record \u2014 "runs its own maintenance crew", "operates a large vehicle fleet", "is legally active and not in liquidation" | a qualification question |
3427
+ | A qualitative orientation too broad for one yes/no \u2014 "we sell to the private sector, not the public one", "harden the exclusion on the business model" | the targeting prompt, \`leadbay_refine_prompt\` |
3428
+ | Named companies \u2014 "exclude Groupe Solidum, Dentego" | \`leadbay_dislike_lead\` / \`leadbay_set_lead_status\` on those leads. A question must NEVER name a company |
3429
+ | CRM state \u2014 "already contacted", "already in a campaign", "already excluded" | read it: \`leadbay_pull_followups\`, \`leadbay_list_campaigns\`. A question cannot observe your own history |
3430
+ | A delivery requirement \u2014 "email AND phone mandatory", "only score 54\u201395" | enrichment plus your own post-filter of the result. A question scores the COMPANY; it cannot see whether Leadbay holds a phone number for a contact |
3431
+ | An event or purchase trigger \u2014 "currently hiring an SDR", "just opened a site" | a qualification question or the targeting prompt. NEVER an \`example_lead\` description or a \`query\`: those match stable registry text, which never mentions events |
3432
+
3433
+ **3 \u2014 Decide whether to change anything at all.** Touching a question
3434
+ re-scores every lead in the pipeline and draws on the org's quota, so a change
3435
+ that surfaces the same companies is a pure loss. Four reasons to write NOTHING
3436
+ and say why:
3437
+
3438
+ 1. **An existing question already covers the rule.** Quote that question back
3439
+ and stop. **Never reword a question that already means the same thing** \u2014
3440
+ even when the user asks you to "clarify" or "improve" the wording. A reword
3441
+ is a removal plus an addition: it re-scores every lead, spends quota, and
3442
+ surfaces exactly the same companies. Offer instead to find out whether any
3443
+ question is testing the WRONG thing.
3444
+ 2. **The audience filter already enforces it** \u2014 sector, headcount, territory.
3445
+ 3. **An existing question already tests that dimension.** Two questions on one
3446
+ dimension waste a slot and add no signal.
3447
+ 4. **The question the user asked for is not decisive.** See step 4: say so,
3448
+ offer the sharper version, and write only what they then choose. Adding a
3449
+ question you know separates nothing is worse than adding none.
3450
+
3451
+ **The ceiling is 5 questions.** Read the count before you answer an "add a
3452
+ question" request: at 5 the honest answer is not "sure, I'll add it". Say in
3453
+ that same turn that the set is full, list the five, and let the USER name which
3454
+ one goes. Never pre-pick the casualty.
3455
+
3456
+ **4 \u2014 Write a DECISIVE question.** Check all six before you propose the text:
3457
+
3458
+ 1. **The estimative marker is literal and mandatory.** English questions start
3459
+ \`Is the company likely to \u2026\`. French questions start
3460
+ \`L'entreprise est-elle susceptible de/d' \u2026\`. There is no third form. The
3461
+ scorer works from public text it cannot verify, so a verifiable question
3462
+ scores almost everything as no.
3463
+ - \u2705 \`Is the company likely to run its own in-house maintenance crew?\`
3464
+ - \u274C \`Does the company run its own maintenance crew?\`
3465
+ - \u274C \`Is the company a cold-storage plant?\` \u2014 no \`likely to\`
3466
+ - \u2705 \`L'entreprise est-elle susceptible d'\xEAtre en liquidation judiciaire ?\`
3467
+ - \u274C \`L'entreprise est-elle en liquidation ?\` \u2014 no \`susceptible\`
3468
+ 2. **It tests the lead as a BUYER.** Before you write a question, ask: would
3469
+ a company answering yes write a cheque to THIS user? A question that only
3470
+ describes what the lead's own business does \u2014 "Is the company likely to
3471
+ manufacture branded pharmaceuticals?" for a seller of advertising \u2014 is a
3472
+ category test, not a buying test, and it scores the user's competitors and
3473
+ suppliers as well as their prospects. Test the need the user's offering
3474
+ meets: "Is the company likely to run consumer campaigns that need paid media
3475
+ placement?"
3476
+ 3. **Estimable from public material** \u2014 the company's website, its about page,
3477
+ its job postings, its registry entry. Never its budget, its internal plans
3478
+ or its future intentions. When the user's rule is un-observable ("has budget
3479
+ for copywriting"), propose the observable proxy, and tell them you swapped
3480
+ it and why.
3481
+ 4. **One dimension each.** \`Is the company likely to operate a cold-storage
3482
+ plant AND run its own maintenance crew?\` is two questions. Split it into
3483
+ two, or pick the one that discriminates better.
3484
+ 5. **Decisive.** It should split companies in general roughly 30/70 while the
3485
+ user's own customers answer yes. A question nearly everyone answers yes to
3486
+ \u2014 "has a website", "uses email", "is a company" \u2014 separates nobody. Do NOT
3487
+ write it as asked: say it would add no signal, and offer the sharper version
3488
+ you would write instead.
3489
+ 6. **\u2264120 characters, in the user's language.**
3490
+
3491
+ An org with **zero** questions scores every lead on firmographics alone. That
3492
+ is a finding worth stating, and the fix is a starter set of **3** questions on
3493
+ three different dimensions \u2014 not one. Propose all three at once.
3494
+
3495
+ **5 \u2014 The change is the user's call, not yours.** This holds for the questions,
3496
+ the targeting prompt and the buyer profile alike. Show the exact text you
3497
+ propose and what it will change, then get an explicit yes before calling
3498
+ \`leadbay_set_qualification_questions\` or \`leadbay_refine_prompt\`. Ask through
3499
+ \`ask_user_input_v0\` when the host offers it. A removal or a swap additionally
3500
+ needs \`confirm:true\`. Do not write an org setting in the same turn the user
3501
+ first stated the rule \u2014 and once they have said yes, actually write it:
3502
+ describing the change is not making it.
3503
+
3504
+ **Answer the ask as well.** A rule stated in passing \u2014 *"sors-moi les leads du
3505
+ jour, et arr\xEAte de me remonter des h\xF4pitaux publics"* \u2014 does not replace the
3506
+ ask. Deliver the leads first, then raise the setting.
3390
3507
 
3391
- Companion tools: **leadbay_set_qualification_questions** to modify the questions;
3392
- **leadbay_get_taste_profile** when the user also wants the Ideal Buyer Profile +
3393
- purchase-intent tags; **leadbay_research_lead_by_id** for how a SPECIFIC lead
3394
- answered these questions; **leadbay_refine_prompt** to shape the AI agent's
3395
- behaviour.
3396
3508
 
3397
3509
  ### RENDERING
3398
3510
 
3399
3511
  Render \`qualification_questions\` as a numbered list \u2014 one question per line, in
3400
3512
  the order returned. Lead with a short heading like **"Qualification questions
3401
- (N)"**. When \`qualification_questions\` is empty, render the \`hint\` sentence
3402
- instead of an empty list. When \`is_admin\` is true and there are questions,
3403
- append the \`hint\` as a one-line footnote (points at
3404
- leadbay_set_qualification_questions). Do not invent questions or reword them \u2014
3405
- render verbatim.
3513
+ (N)"**. Then, when present, the \`ideal_buyer_profile\` summary with its
3514
+ \`anti_patterns\` as a short bulleted list, and the \`targeting_prompt\` as a
3515
+ blockquote. When \`qualification_questions\` is empty, say **"You have no
3516
+ qualification questions \u2014 leads are scored on firmographics alone"** and render
3517
+ the \`hint\`. When \`is_admin\` is true and there are questions, append the \`hint\`
3518
+ as a one-line footnote. Do not invent questions or reword them \u2014 render
3519
+ verbatim.
3406
3520
  `;
3407
3521
  leadbay_get_quota = `Read quota / spend across daily, weekly, and monthly windows. The response has two scope groups: **\`user\`** (present for every caller) and **\`org\`** (admin-only \u2014 \`null\` for non-admins). **Read from \`user\` first**, falling back to \`org\` only when \`user\` is absent. Each group carries \`spend[]\` (the dollar-spend gauge: \`current_units\` / \`max_units\` in dollar_cents \u2192 % used = the ratio, $ = \`/100\`) and \`resources[]\` (per-resource usage: \`{resource_type, count (used), max_units (cap or null), window_type, resets_at}\`). \`spend[]\` is empty for orgs with no OVERALL_SPEND quota \u2014 fall back to the \`resources[]\` counts then. There is also a top-level \`topup\` ({remaining_cents, total_credit_cents}) when present. Resource types may arrive lowercase (\`lens_extra_refill\`) or uppercase \u2014 match case-insensitively. Present quota as a percentage / dollar figure, never raw "credits".
3408
3522
 
@@ -5490,20 +5604,22 @@ WHEN NOT TO USE: when you already know the exact titles you want to enrich.
5490
5604
  `;
5491
5605
  leadbay_refine_prompt = `## WHEN TO USE
5492
5606
 
5493
- Trigger phrases: "focus on companies that <qualitative trait>", "I prefer leads that <behavior/characteristic>", "prioritize companies running their own IT", "deprioritize companies that just raised".
5607
+ Trigger phrases: "focus on companies that <qualitative trait>", "I prefer leads that <behavior/characteristic>", "prioritize companies running their own IT", "deprioritize companies that just raised", "stop showing me <kind of company>", "we only sell to the private sector, not the public one", "harden the exclusion on <business model>, not the keyword", "competitors / resellers are never prospects for us".
5494
5608
 
5495
- Do NOT use for: "create a new lens / a lens specialized into <X>" \u2192 \`leadbay_new_lens\`; "add/remove <sector> to/from my <name> lens" \u2192 \`leadbay_adjust_audience\`; "narrow the audience to <sector> / <size>" \u2192 \`leadbay_adjust_audience\`; "show me / list / switch my lenses" \u2192 \`leadbay_my_lenses\`.
5609
+ Do NOT use for: "create a new lens / a lens specialized into <X>" \u2192 \`leadbay_new_lens\`; "add/remove <sector> to/from my <name> lens" \u2192 \`leadbay_adjust_audience\`; "narrow the audience to <sector> / <size>" \u2192 \`leadbay_adjust_audience\`; "show me / list / switch my lenses" \u2192 \`leadbay_my_lenses\`; "a company trait one yes/no question could estimate from their website" \u2192 \`leadbay_set_qualification_questions\`; "exclude <named company>" \u2192 \`leadbay_dislike_lead\`.
5496
5610
 
5497
- Prefer when: ADMIN-ONLY. Qualitative refinement of the active lens that sector/size can't express. Creating/naming/listing/switching/sector-editing a lens routes elsewhere. Non-admin user \u2192 do NOT pick this.
5611
+ Prefer when: ADMIN-ONLY. A qualitative ORIENTATION too broad for one yes/no \u2014 a segment to avoid, a business model to rule out. One estimable company trait goes to set_qualification_questions; a named company to dislike_lead.
5498
5612
 
5499
5613
  Examples that SHOULD invoke this tool:
5500
5614
  - "Focus on hospitals that run their own IT in-house."
5501
5615
  - "Prioritize companies that have recently expanded headcount."
5616
+ - "On ne vend qu'au priv\xE9 \u2014 arr\xEAte de me remonter des h\xF4pitaux publics."
5502
5617
 
5503
5618
  Examples that should NOT invoke this tool (sound similar, route elsewhere):
5504
5619
  - "Create a lens specialized in automobile."
5505
5620
  - "Add fintech to my Joinery lens."
5506
5621
  - "Show me my lenses."
5622
+ - "Exclude Groupe Solidum, we had an unpaid invoice with them."
5507
5623
 
5508
5624
  ## RENDER (quick)
5509
5625
 
@@ -5514,6 +5630,121 @@ clarification was raised, surface its question (route via ask_user_input_v0).
5514
5630
 
5515
5631
  Refine the kind of leads Leadbay surfaces, beyond firmographics. Free-text instruction (e.g. "focus on hospitals running their own IT"). Sets the org's \`user_prompt\`; if the new prompt produces ambiguous criteria, Leadbay raises a clarification question, which this composite polls for and surfaces. Admin-only on the backend (will return 403 for non-admins).
5516
5632
 
5633
+ **Read \`leadbay_get_qualification_questions\` first.** It returns the current
5634
+ targeting prompt alongside the questions and the buyer profile. Setting a
5635
+ prompt REPLACES the previous one \u2014 write the combined instruction, not just the
5636
+ new clause, or you silently drop rules the user set earlier.
5637
+
5638
+ ### A stated fit rule is a SETTING \u2014 read, decide, propose, then write
5639
+
5640
+ **Never answer a stated rule from memory.** "C'est not\xE9", "already applied",
5641
+ "I'll keep that in mind", "the rule is now active" \u2014 every one of those is a
5642
+ claim about the user's ACCOUNT, and it is false unless a tool call made it
5643
+ true. The rule lives in the org's settings or it does not exist: your context
5644
+ window ends with this conversation, and the next session, the scheduled run and
5645
+ the user's colleague all read the account, not your memory. If you have not
5646
+ called a tool, do not say the rule is in place.
5647
+
5648
+ When the user says what makes a lead good or bad \u2014 *"\xE9carte les soci\xE9t\xE9s
5649
+ liquid\xE9es"*, *"je ne veux pas d'associations"*, *"our best customers run their
5650
+ own maintenance crews"*, *"les leads ne sont pas pertinents"* \u2014 they are
5651
+ describing their account, not just this batch. Filter the batch and they say
5652
+ it again next week; their questions, buyer profile and targeting prompt never
5653
+ move.
5654
+
5655
+ **1 \u2014 Read before you decide.** Call \`leadbay_get_qualification_questions\`
5656
+ first. It returns the question set PLUS the ideal buyer profile and the
5657
+ targeting prompt those questions sit beside. You cannot judge whether a rule is
5658
+ already covered without seeing them.
5659
+
5660
+ **2 \u2014 Decide WHERE the rule belongs.** One rule, one destination:
5661
+
5662
+ | What the user stated | Where it belongs |
5663
+ |---|---|
5664
+ | A sector, a headcount band, a territory | \`leadbay_adjust_audience\` / \`leadbay_new_lens\` filters \u2014 never a question |
5665
+ | A company trait a stranger could estimate from that company's own website or registry record \u2014 "runs its own maintenance crew", "operates a large vehicle fleet", "is legally active and not in liquidation" | a qualification question |
5666
+ | A qualitative orientation too broad for one yes/no \u2014 "we sell to the private sector, not the public one", "harden the exclusion on the business model" | the targeting prompt, \`leadbay_refine_prompt\` |
5667
+ | Named companies \u2014 "exclude Groupe Solidum, Dentego" | \`leadbay_dislike_lead\` / \`leadbay_set_lead_status\` on those leads. A question must NEVER name a company |
5668
+ | CRM state \u2014 "already contacted", "already in a campaign", "already excluded" | read it: \`leadbay_pull_followups\`, \`leadbay_list_campaigns\`. A question cannot observe your own history |
5669
+ | A delivery requirement \u2014 "email AND phone mandatory", "only score 54\u201395" | enrichment plus your own post-filter of the result. A question scores the COMPANY; it cannot see whether Leadbay holds a phone number for a contact |
5670
+ | An event or purchase trigger \u2014 "currently hiring an SDR", "just opened a site" | a qualification question or the targeting prompt. NEVER an \`example_lead\` description or a \`query\`: those match stable registry text, which never mentions events |
5671
+
5672
+ **3 \u2014 Decide whether to change anything at all.** Touching a question
5673
+ re-scores every lead in the pipeline and draws on the org's quota, so a change
5674
+ that surfaces the same companies is a pure loss. Four reasons to write NOTHING
5675
+ and say why:
5676
+
5677
+ 1. **An existing question already covers the rule.** Quote that question back
5678
+ and stop. **Never reword a question that already means the same thing** \u2014
5679
+ even when the user asks you to "clarify" or "improve" the wording. A reword
5680
+ is a removal plus an addition: it re-scores every lead, spends quota, and
5681
+ surfaces exactly the same companies. Offer instead to find out whether any
5682
+ question is testing the WRONG thing.
5683
+ 2. **The audience filter already enforces it** \u2014 sector, headcount, territory.
5684
+ 3. **An existing question already tests that dimension.** Two questions on one
5685
+ dimension waste a slot and add no signal.
5686
+ 4. **The question the user asked for is not decisive.** See step 4: say so,
5687
+ offer the sharper version, and write only what they then choose. Adding a
5688
+ question you know separates nothing is worse than adding none.
5689
+
5690
+ **The ceiling is 5 questions.** Read the count before you answer an "add a
5691
+ question" request: at 5 the honest answer is not "sure, I'll add it". Say in
5692
+ that same turn that the set is full, list the five, and let the USER name which
5693
+ one goes. Never pre-pick the casualty.
5694
+
5695
+ **4 \u2014 Write a DECISIVE question.** Check all six before you propose the text:
5696
+
5697
+ 1. **The estimative marker is literal and mandatory.** English questions start
5698
+ \`Is the company likely to \u2026\`. French questions start
5699
+ \`L'entreprise est-elle susceptible de/d' \u2026\`. There is no third form. The
5700
+ scorer works from public text it cannot verify, so a verifiable question
5701
+ scores almost everything as no.
5702
+ - \u2705 \`Is the company likely to run its own in-house maintenance crew?\`
5703
+ - \u274C \`Does the company run its own maintenance crew?\`
5704
+ - \u274C \`Is the company a cold-storage plant?\` \u2014 no \`likely to\`
5705
+ - \u2705 \`L'entreprise est-elle susceptible d'\xEAtre en liquidation judiciaire ?\`
5706
+ - \u274C \`L'entreprise est-elle en liquidation ?\` \u2014 no \`susceptible\`
5707
+ 2. **It tests the lead as a BUYER.** Before you write a question, ask: would
5708
+ a company answering yes write a cheque to THIS user? A question that only
5709
+ describes what the lead's own business does \u2014 "Is the company likely to
5710
+ manufacture branded pharmaceuticals?" for a seller of advertising \u2014 is a
5711
+ category test, not a buying test, and it scores the user's competitors and
5712
+ suppliers as well as their prospects. Test the need the user's offering
5713
+ meets: "Is the company likely to run consumer campaigns that need paid media
5714
+ placement?"
5715
+ 3. **Estimable from public material** \u2014 the company's website, its about page,
5716
+ its job postings, its registry entry. Never its budget, its internal plans
5717
+ or its future intentions. When the user's rule is un-observable ("has budget
5718
+ for copywriting"), propose the observable proxy, and tell them you swapped
5719
+ it and why.
5720
+ 4. **One dimension each.** \`Is the company likely to operate a cold-storage
5721
+ plant AND run its own maintenance crew?\` is two questions. Split it into
5722
+ two, or pick the one that discriminates better.
5723
+ 5. **Decisive.** It should split companies in general roughly 30/70 while the
5724
+ user's own customers answer yes. A question nearly everyone answers yes to
5725
+ \u2014 "has a website", "uses email", "is a company" \u2014 separates nobody. Do NOT
5726
+ write it as asked: say it would add no signal, and offer the sharper version
5727
+ you would write instead.
5728
+ 6. **\u2264120 characters, in the user's language.**
5729
+
5730
+ An org with **zero** questions scores every lead on firmographics alone. That
5731
+ is a finding worth stating, and the fix is a starter set of **3** questions on
5732
+ three different dimensions \u2014 not one. Propose all three at once.
5733
+
5734
+ **5 \u2014 The change is the user's call, not yours.** This holds for the questions,
5735
+ the targeting prompt and the buyer profile alike. Show the exact text you
5736
+ propose and what it will change, then get an explicit yes before calling
5737
+ \`leadbay_set_qualification_questions\` or \`leadbay_refine_prompt\`. Ask through
5738
+ \`ask_user_input_v0\` when the host offers it. A removal or a swap additionally
5739
+ needs \`confirm:true\`. Do not write an org setting in the same turn the user
5740
+ first stated the rule \u2014 and once they have said yes, actually write it:
5741
+ describing the change is not making it.
5742
+
5743
+ **Answer the ask as well.** A rule stated in passing \u2014 *"sors-moi les leads du
5744
+ jour, et arr\xEAte de me remonter des h\xF4pitaux publics"* \u2014 does not replace the
5745
+ ask. Deliver the leads first, then raise the setting.
5746
+
5747
+
5517
5748
  WHEN TO USE: when audience filters (leadbay_adjust_audience) aren't enough.
5518
5749
 
5519
5750
  WHEN NOT TO USE: to answer a pending clarification \u2014 that's leadbay_answer_clarification.
@@ -6565,7 +6796,35 @@ WHEN NOT TO USE: the user is just skipping ONE outreach attempt \u2014 that's a
6565
6796
 
6566
6797
  This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
6567
6798
  `;
6568
- leadbay_set_qualification_questions = `Modify the organization's **qualification questions** \u2014 the AI-agent questions Leadbay scores every lead against. Use when the user wants to add, remove, or rewrite their qualification questions \u2014 e.g. "add a question about whether they run install crews", "remove the flooring question", "replace my questions with these three".
6799
+ leadbay_set_qualification_questions = `## WHEN TO USE
6800
+
6801
+ Trigger phrases: "add / remove / replace a qualification question", "update my qualification so Leadbay looks for <trait>", "my best customers are <trait> \u2014 target those", "I don't want <kind of company> at all", "the leads aren't relevant, fix my criteria".
6802
+
6803
+ Do NOT use for: "what are my qualification questions" \u2192 \`leadbay_get_qualification_questions\`; "narrow to <sector> / <headcount> / <territory>" \u2192 \`leadbay_adjust_audience\`; "a qualitative orientation one yes/no can't express" \u2192 \`leadbay_refine_prompt\`; "exclude <named company>" \u2192 \`leadbay_dislike_lead\`; "only leads that already have an email and a phone" \u2192 \`leadbay_enrich_titles\`.
6804
+
6805
+ Prefer when: the user stated a durable company TRAIT a stranger could estimate from that company's public material, and get_qualification_questions shows nothing covering it. Read first, propose the text, write only on a yes.
6806
+
6807
+ Examples that SHOULD invoke this tool:
6808
+ - "Our best customers are cold-storage plants that run their own maintenance crews \u2014 update my qualification for that."
6809
+ - "\xC9carte les soci\xE9t\xE9s en liquidation, je ne veux plus les voir."
6810
+ - "Remove the flooring question and add one about install crews."
6811
+
6812
+ Examples that should NOT invoke this tool (sound similar, route elsewhere):
6813
+ - "What qualification questions does Leadbay use?"
6814
+ - "Add fintech to my Joinery lens."
6815
+ - "Exclude Groupe Solidum, we had an unpaid invoice with them."
6816
+
6817
+ ## RENDER (quick)
6818
+
6819
+ Before writing: show the exact question text you propose, say what it
6820
+ changes, and ask for a yes (route via ask_user_input_v0). After writing:
6821
+ one confirmation line ("Added 1 question \u2014 you now score leads against 4
6822
+ questions.") then the resulting questions as a numbered list. On a
6823
+ non-changing preview, surface the \`hint\` and ask \u2014 never auto-confirm.
6824
+
6825
+ ---
6826
+
6827
+ Modify the organization's **qualification questions** \u2014 the AI-agent questions Leadbay scores every lead against. Use when the user wants to add, remove, or rewrite them \u2014 e.g. "add a question about whether they run install crews", "remove the flooring question", "replace my questions with these three".
6569
6828
 
6570
6829
  The backend stores the list as a whole, so this tool reads the current questions and applies your change:
6571
6830
 
@@ -6577,11 +6836,142 @@ Leadbay allows **at most 5** qualification questions. If a change would exceed 5
6577
6836
 
6578
6837
  **Dropping any existing question is destructive** \u2014 it changes how every lead is scored. Any change that removes a current question requires \`confirm:true\` \u2014 including a same-count **swap** (remove one + add one) or a \`questions\` replacement that omits a current question, not only when the list gets shorter. Without \`confirm\`, the tool previews what would be removed and applies nothing. Pure additions never need confirm.
6579
6838
 
6580
- Returns the resulting \`{qualification_questions, count, previous_count, changed}\`. Phrase questions as the yes/no scoring prompts Leadbay uses (e.g. "Is the company likely to \u2026?").
6839
+ Returns the resulting \`{qualification_questions, count, previous_count, changed}\`.
6840
+
6841
+ ### A stated fit rule is a SETTING \u2014 read, decide, propose, then write
6842
+
6843
+ **Never answer a stated rule from memory.** "C'est not\xE9", "already applied",
6844
+ "I'll keep that in mind", "the rule is now active" \u2014 every one of those is a
6845
+ claim about the user's ACCOUNT, and it is false unless a tool call made it
6846
+ true. The rule lives in the org's settings or it does not exist: your context
6847
+ window ends with this conversation, and the next session, the scheduled run and
6848
+ the user's colleague all read the account, not your memory. If you have not
6849
+ called a tool, do not say the rule is in place.
6850
+
6851
+ When the user says what makes a lead good or bad \u2014 *"\xE9carte les soci\xE9t\xE9s
6852
+ liquid\xE9es"*, *"je ne veux pas d'associations"*, *"our best customers run their
6853
+ own maintenance crews"*, *"les leads ne sont pas pertinents"* \u2014 they are
6854
+ describing their account, not just this batch. Filter the batch and they say
6855
+ it again next week; their questions, buyer profile and targeting prompt never
6856
+ move.
6857
+
6858
+ **1 \u2014 Read before you decide.** Call \`leadbay_get_qualification_questions\`
6859
+ first. It returns the question set PLUS the ideal buyer profile and the
6860
+ targeting prompt those questions sit beside. You cannot judge whether a rule is
6861
+ already covered without seeing them.
6862
+
6863
+ **2 \u2014 Decide WHERE the rule belongs.** One rule, one destination:
6864
+
6865
+ | What the user stated | Where it belongs |
6866
+ |---|---|
6867
+ | A sector, a headcount band, a territory | \`leadbay_adjust_audience\` / \`leadbay_new_lens\` filters \u2014 never a question |
6868
+ | A company trait a stranger could estimate from that company's own website or registry record \u2014 "runs its own maintenance crew", "operates a large vehicle fleet", "is legally active and not in liquidation" | a qualification question |
6869
+ | A qualitative orientation too broad for one yes/no \u2014 "we sell to the private sector, not the public one", "harden the exclusion on the business model" | the targeting prompt, \`leadbay_refine_prompt\` |
6870
+ | Named companies \u2014 "exclude Groupe Solidum, Dentego" | \`leadbay_dislike_lead\` / \`leadbay_set_lead_status\` on those leads. A question must NEVER name a company |
6871
+ | CRM state \u2014 "already contacted", "already in a campaign", "already excluded" | read it: \`leadbay_pull_followups\`, \`leadbay_list_campaigns\`. A question cannot observe your own history |
6872
+ | A delivery requirement \u2014 "email AND phone mandatory", "only score 54\u201395" | enrichment plus your own post-filter of the result. A question scores the COMPANY; it cannot see whether Leadbay holds a phone number for a contact |
6873
+ | An event or purchase trigger \u2014 "currently hiring an SDR", "just opened a site" | a qualification question or the targeting prompt. NEVER an \`example_lead\` description or a \`query\`: those match stable registry text, which never mentions events |
6874
+
6875
+ **3 \u2014 Decide whether to change anything at all.** Touching a question
6876
+ re-scores every lead in the pipeline and draws on the org's quota, so a change
6877
+ that surfaces the same companies is a pure loss. Four reasons to write NOTHING
6878
+ and say why:
6879
+
6880
+ 1. **An existing question already covers the rule.** Quote that question back
6881
+ and stop. **Never reword a question that already means the same thing** \u2014
6882
+ even when the user asks you to "clarify" or "improve" the wording. A reword
6883
+ is a removal plus an addition: it re-scores every lead, spends quota, and
6884
+ surfaces exactly the same companies. Offer instead to find out whether any
6885
+ question is testing the WRONG thing.
6886
+ 2. **The audience filter already enforces it** \u2014 sector, headcount, territory.
6887
+ 3. **An existing question already tests that dimension.** Two questions on one
6888
+ dimension waste a slot and add no signal.
6889
+ 4. **The question the user asked for is not decisive.** See step 4: say so,
6890
+ offer the sharper version, and write only what they then choose. Adding a
6891
+ question you know separates nothing is worse than adding none.
6892
+
6893
+ **The ceiling is 5 questions.** Read the count before you answer an "add a
6894
+ question" request: at 5 the honest answer is not "sure, I'll add it". Say in
6895
+ that same turn that the set is full, list the five, and let the USER name which
6896
+ one goes. Never pre-pick the casualty.
6897
+
6898
+ **4 \u2014 Write a DECISIVE question.** Check all six before you propose the text:
6899
+
6900
+ 1. **The estimative marker is literal and mandatory.** English questions start
6901
+ \`Is the company likely to \u2026\`. French questions start
6902
+ \`L'entreprise est-elle susceptible de/d' \u2026\`. There is no third form. The
6903
+ scorer works from public text it cannot verify, so a verifiable question
6904
+ scores almost everything as no.
6905
+ - \u2705 \`Is the company likely to run its own in-house maintenance crew?\`
6906
+ - \u274C \`Does the company run its own maintenance crew?\`
6907
+ - \u274C \`Is the company a cold-storage plant?\` \u2014 no \`likely to\`
6908
+ - \u2705 \`L'entreprise est-elle susceptible d'\xEAtre en liquidation judiciaire ?\`
6909
+ - \u274C \`L'entreprise est-elle en liquidation ?\` \u2014 no \`susceptible\`
6910
+ 2. **It tests the lead as a BUYER.** Before you write a question, ask: would
6911
+ a company answering yes write a cheque to THIS user? A question that only
6912
+ describes what the lead's own business does \u2014 "Is the company likely to
6913
+ manufacture branded pharmaceuticals?" for a seller of advertising \u2014 is a
6914
+ category test, not a buying test, and it scores the user's competitors and
6915
+ suppliers as well as their prospects. Test the need the user's offering
6916
+ meets: "Is the company likely to run consumer campaigns that need paid media
6917
+ placement?"
6918
+ 3. **Estimable from public material** \u2014 the company's website, its about page,
6919
+ its job postings, its registry entry. Never its budget, its internal plans
6920
+ or its future intentions. When the user's rule is un-observable ("has budget
6921
+ for copywriting"), propose the observable proxy, and tell them you swapped
6922
+ it and why.
6923
+ 4. **One dimension each.** \`Is the company likely to operate a cold-storage
6924
+ plant AND run its own maintenance crew?\` is two questions. Split it into
6925
+ two, or pick the one that discriminates better.
6926
+ 5. **Decisive.** It should split companies in general roughly 30/70 while the
6927
+ user's own customers answer yes. A question nearly everyone answers yes to
6928
+ \u2014 "has a website", "uses email", "is a company" \u2014 separates nobody. Do NOT
6929
+ write it as asked: say it would add no signal, and offer the sharper version
6930
+ you would write instead.
6931
+ 6. **\u2264120 characters, in the user's language.**
6932
+
6933
+ An org with **zero** questions scores every lead on firmographics alone. That
6934
+ is a finding worth stating, and the fix is a starter set of **3** questions on
6935
+ three different dimensions \u2014 not one. Propose all three at once.
6936
+
6937
+ **5 \u2014 The change is the user's call, not yours.** This holds for the questions,
6938
+ the targeting prompt and the buyer profile alike. Show the exact text you
6939
+ propose and what it will change, then get an explicit yes before calling
6940
+ \`leadbay_set_qualification_questions\` or \`leadbay_refine_prompt\`. Ask through
6941
+ \`ask_user_input_v0\` when the host offers it. A removal or a swap additionally
6942
+ needs \`confirm:true\`. Do not write an org setting in the same turn the user
6943
+ first stated the rule \u2014 and once they have said yes, actually write it:
6944
+ describing the change is not making it.
6945
+
6946
+ **Answer the ask as well.** A rule stated in passing \u2014 *"sors-moi les leads du
6947
+ jour, et arr\xEAte de me remonter des h\xF4pitaux publics"* \u2014 does not replace the
6948
+ ask. Deliver the leads first, then raise the setting.
6949
+
6950
+
6951
+ WHEN TO USE: the user stated a durable company trait no existing question covers, and they have agreed to the exact text you proposed.
6952
+
6953
+ WHEN NOT TO USE: to READ the questions (use leadbay_get_qualification_questions), to change a single lead's data, or for any rule the table above sends elsewhere. This is org-level \u2014 it affects scoring for ALL leads.
6954
+
6955
+ ## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
6956
+
6957
+ Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can route into. These ALWAYS produce a better UX than markdown tables / inline prose for the data shapes they support \u2014 they're tappable on mobile, persistent across turns, and integrate with the host's quick-actions.
6958
+
6959
+ **The Big Three** \u2014 when a tool result fits, route there:
6960
+
6961
+ | Host widget | Use when | Field map (from Leadbay payload) |
6962
+ |---|---|---|
6963
+ | \`places_map_display_v0\` + \`places_search\` (Claude) | \u22652 leads with coords / \`location.city\`, geographic / "in person" / travel intent | **Two-step**: \`places_search\` each lead (query = company + full street address) \u2192 real \`place_id\`/coords, THEN render with \`places_map_display_v0\` (Itinerary mode for a tour). Skipping \`places_search\` \u2192 schematic scatter, not a street map. |
6964
+ | \`message_compose_v1\` (Claude) | You're about to draft outreach (email / message / call opener) | \`{kind: "email", summary_title, variants: [{label, body, subject}]}\` \u2014 2\u20133 variants, labels describe STRATEGY ("Push for alignment", "Reference the M&A signal"), not tone ("Friendly", "Formal") |
6965
+ | \`ask_user_input_v0\` (Claude chat / ChatGPT) **or** \`AskUserQuestion\` (Claude cowork / Claude Code) \u2014 whichever is in your tool set; their schemas differ, match the one you have | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | Per-tool schema in the server instructions + NEXT STEPS routing block. Max 3 questions. |
6966
+
6967
+ ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We don't ship any custom widgets ourselves \u2014 this gate is exclusively about routing into the host's first-party widgets when the data shape fits.
6581
6968
 
6582
- WHEN TO USE: the user wants to change the org's qualification questions.
6969
+ **Rules:**
6970
+ - The widget IS the visual. Do NOT emit a markdown table or prose list of the same data alongside \u2014 that produces two competing UIs.
6971
+ - Pass identifiers (place_id, lead.id, contact_id) verbatim. Don't rewrite.
6972
+ - When the host doesn't expose the named widget, the agent falls back to the prose/table rendering the per-tool description already specifies. The directive is host-conditional; the fallback is automatic.
6973
+ - One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
6583
6974
 
6584
- WHEN NOT TO USE: to READ the questions (use leadbay_get_qualification_questions) or to change a single lead's data. This is org-level \u2014 it affects scoring for ALL leads.
6585
6975
 
6586
6976
  ### RENDERING
6587
6977
 
@@ -7175,7 +7565,7 @@ Do NOT use for: "show me today's leads / what's new today" \u2192 \`leadbay_pull
7175
7565
  Prefer when: the user describes a target profile or names a count of NEW companies \u2014 craft the example_lead per the seed rules below BEFORE calling; never pass the user's raw sentence as query.
7176
7566
 
7177
7567
  Examples that SHOULD invoke this tool:
7178
- - "Find me 10 gyms around Dallas that would buy our flooring, with someone I can call."
7568
+ - "Find me 10 gyms around Dallas that would buy our flooring, with a contact."
7179
7569
  - "Get me 20 new US SaaS companies, 50-2000 employees, with the VP People's email."
7180
7570
  - "We're launching in Lyon \u2014 find 15 hotels that fit our ICP."
7181
7571
 
@@ -7186,11 +7576,10 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
7186
7576
 
7187
7577
  ## RENDER (quick)
7188
7578
 
7189
- 3-col table of delivered leads in returned order: col 1 = 10-segment fit
7190
- bar + linked company \xB7 location \xB7 size; col 2 = why-fits \u226420 words; col 3
7191
- = contact + found channels. ALWAYS close with the honest funnel line
7192
- (matched/examined/delivered/stop reason) \u2014 especially on 0
7193
- delivered. Full algorithm below.
7579
+ 3-col table of delivered leads in returned order: col 1 = 10-segment fit bar
7580
+ + linked company \xB7 location \xB7 size; col 2 = why-fits \u226420 words; col 3 =
7581
+ contact + found channels. ALWAYS close with the honest funnel line
7582
+ (matched/examined/delivered/stop reason), especially on 0 delivered.
7194
7583
 
7195
7584
  ---
7196
7585
 
@@ -7199,27 +7588,25 @@ company universe, applies hard filters, skips what the org already knows
7199
7588
  (\`novelty: org\`), optionally qualifies against the org's own intelligence
7200
7589
  (questions, tags, ideal buyer profile \u2014 frozen at submit), and optionally reveals
7201
7590
  contact channels. Polls up to \`wait_seconds\` (default 45); a longer job returns
7202
- \`still_running\` + \`next_poll\` \u2014 hand off to \`leadbay_lead_job_status\`. Jobs run
7591
+ \`still_running\` + \`next_poll\` \u2014 hand to \`leadbay_lead_job_status\`. Jobs run
7203
7592
  \u226430 min, results kept 30 days.
7204
7593
 
7205
7594
  **Free vs usage quota \u2014 never use quota silently.** Default (\`qualify: false\`,
7206
7595
  \`channels: []\`) is FREE: company profile + fit score + cached research +
7207
7596
  contact identity. \`qualify: true\` (per candidate EXAMINED, capped by
7208
7597
  \`exploration_cap\`/\`max_cost\`) and \`channels\` (only when a value is found) draw
7209
- on the org's usage quota; nothing is invoiced. Enforced in code: such a call is WITHHELD unless it
7210
- carries \`confirm: true\` \u2014 nothing is submitted and you get
7598
+ on the org's usage quota; nothing is invoiced. Enforced in code: such a call is
7599
+ WITHHELD unless it carries \`confirm: true\` \u2014 nothing is submitted and you get
7211
7600
  \`mode: "needs_confirmation"\` with a real quote to show the user. Re-call with
7212
7601
  \`confirm: true\` on their go-ahead ("go ahead / get their emails" counts).
7213
- \`confirm: false\` vetoes. Free needs no consent. **Preview free first** \u2014
7214
- reshaping an off-profile seed is free, exploring it with \`qualify: true\` is
7215
- not.
7602
+ \`confirm: false\` vetoes. **Preview free first** \u2014 reshaping an off-profile seed
7603
+ is free, exploring it with \`qualify: true\` is not.
7216
7604
 
7217
7605
  **Ad-hoc exclusions ("no chains") are enforced by NO tier** \u2014 \`filters\` has no
7218
- exclusion key, and \`qualify\` scores against the org's FROZEN questions and IBP,
7219
- which need not mention chains; the seed's inverse only shifts ranking.
7220
- Violators can survive, use quota and be delivered \u2014 post-filter them yourself
7221
- and say the tier didn't enforce it. Durable enforcement \u2192
7222
- \`leadbay_refine_prompt\`.
7606
+ exclusion key, and \`qualify\` scores against the org's FROZEN questions and IBP.
7607
+ Violators survive, use quota and get delivered: post-filter them yourself and
7608
+ say the tier didn't enforce it. Durable enforcement \u2192
7609
+ \`leadbay_set_qualification_questions\` / \`leadbay_refine_prompt\`.
7223
7610
 
7224
7611
  ### Crafting the \`example_lead\` seed \u2014 the input that decides result quality
7225
7612
 
@@ -7239,13 +7626,13 @@ measured:
7239
7626
  model, what they sell or operate, who they serve, observable scale. Write
7240
7627
  it like the first paragraph of their About-Us page.
7241
7628
  - STRONG: "Operator of full-service fitness centers offering strength
7242
- areas, group classes and personal training to members across multiple
7243
- clubs."
7244
- - WEAK (generic): "A gym in Texas."
7245
- - WRONG (seller-side): "Supplier of durable modular flooring for gyms."
7246
- 4. **No event language.** "hiring", "expanding", "just raised" are not
7247
- filters \u2014 registry descriptions never contain them, so they dilute the
7248
- profile. Purchase triggers belong in the org's qualification questions.
7629
+ areas, group classes and personal training to members across clubs."
7630
+ - WEAK: "A gym in Texas." WRONG: "Supplier of gym flooring." (seller-side)
7631
+ 4. **NO event language \u2014 in \`description\` or \`query\`.** "recrute", "hiring",
7632
+ "expanding", "just raised" never appear in registry text, so they match
7633
+ nothing. Send the trigger to \`leadbay_set_qualification_questions\` or
7634
+ \`leadbay_refine_prompt\` and say so. "companies hiring a senior SDR" seeds as
7635
+ "B2B software company operating an in-house outbound sales team."
7249
7636
  5. **No meta-markers.** Never "(example)", "(fictional)", "(placeholder)".
7250
7637
  6. **Hard constraints go in \`filters\`, not prose \u2014 exact keys:**
7251
7638
  \`sectors: string[]\`, \`locations: string[]\`, \`employees_min: number\`,
@@ -7255,24 +7642,22 @@ measured:
7255
7642
  never a country: this workspace's own is dropped, any other is refused.
7256
7643
  7. **Prefer \`example_lead\` over \`query\`.** Query matches topic *vocabulary*:
7257
7644
  "gyms that need durable flooring" surfaced flooring VENDORS, 0 delivered.
7258
- Use \`query\` only for signal an example can't express.
7259
7645
  8. **One seed per buyer archetype.** An ask spanning two segments ("gyms and
7260
7646
  warehouses") needs one search each with its own description and
7261
- \`request_id\` \u2014 a blended seed lands between the clusters and matches
7262
- neither.
7647
+ \`request_id\` \u2014 a blended seed lands between the clusters, matching neither.
7263
7648
 
7264
7649
 
7265
7650
  **Parameter notes**
7266
7651
  - \`request_id\` (REQUIRED) is the retry contract: SAME value retrying the same
7267
7652
  ask (same live job, no double launch); NEW for a changed ask. Derive from ask
7268
7653
  + archetype + date: \`gyms-dallas-2026-07-28\`.
7269
- - Never lower \`min_ai_score\` together with \`channels\` \u2014 that reveals emails for
7270
- leads the AI just scored as junk.
7271
- - \`count\` \u2264 50; \u22643 active jobs/org; \u226410 submits/hour (429 + Retry-After \u2014
7272
- wait, don't hammer).
7654
+ - Never lower \`min_ai_score\` with \`channels\` \u2014 that reveals emails for leads
7655
+ the AI just scored as junk.
7656
+ - \`count\` \u2264 50; \u22643 active jobs/org; \u226410 submits/hour (429 + Retry-After \u2014 wait,
7657
+ don't hammer).
7273
7658
 
7274
7659
  **Read the result honestly** \u2014 \`funnel\` + \`explain.scope_notes\` tell the story;
7275
- zero delivered gets a cause and a next move (rules in RENDERING).
7660
+ zero delivered gets a cause and a next move (RENDERING).
7276
7661
 
7277
7662
  ---
7278
7663
 
@@ -16852,12 +17237,188 @@ var init_research_lead_by_name_fuzzy = __esm({
16852
17237
  }
16853
17238
  });
16854
17239
 
17240
+ // ../core/dist/composite/set-qualification-questions.js
17241
+ function formWarnings(questions) {
17242
+ const out = [];
17243
+ for (const q of questions) {
17244
+ const low = q.trim().toLowerCase();
17245
+ if (!ESTIMATIVE_MARKERS.some((m) => low.startsWith(m))) {
17246
+ out.push(`"${q}" is not in the estimative form. Leadbay scores from public text it cannot verify, so this will mark most leads no. Rewrite it to start "Is the company likely to ..." / "L'entreprise est-elle susceptible de ...".`);
17247
+ }
17248
+ if (q.length > 120) {
17249
+ out.push(`"${q.slice(0, 60)}\u2026" is ${q.length} chars; keep a question under 120.`);
17250
+ }
17251
+ }
17252
+ return out;
17253
+ }
17254
+ var MAX_QUESTIONS, ESTIMATIVE_MARKERS, setQualificationQuestions;
17255
+ var init_set_qualification_questions = __esm({
17256
+ "../core/dist/composite/set-qualification-questions.js"() {
17257
+ "use strict";
17258
+ init_tool_descriptions_generated();
17259
+ MAX_QUESTIONS = 5;
17260
+ ESTIMATIVE_MARKERS = [
17261
+ "is the company likely to",
17262
+ "l'entreprise est-elle susceptible",
17263
+ "l\u2019entreprise est-elle susceptible"
17264
+ ];
17265
+ setQualificationQuestions = {
17266
+ name: "leadbay_set_qualification_questions",
17267
+ annotations: {
17268
+ title: "Modify the org's qualification questions",
17269
+ readOnlyHint: false,
17270
+ destructiveHint: true,
17271
+ idempotentHint: false,
17272
+ openWorldHint: true
17273
+ },
17274
+ description: leadbay_set_qualification_questions,
17275
+ write: true,
17276
+ inputSchema: {
17277
+ type: "object",
17278
+ properties: {
17279
+ questions: {
17280
+ type: "array",
17281
+ items: { type: "string", maxLength: 255 },
17282
+ description: `Full replacement list of qualification questions (replaces ALL current questions). Mutually exclusive with add/remove. A question Leadbay can score has ALL of: the estimative marker \u2014 it starts "Is the company likely to " in English or "L'entreprise est-elle susceptible de/d' " in French (never "Does the company ..." or a bare "L'entreprise est-elle <X> ?", which the scorer cannot verify and marks no); ONE dimension, not two joined by AND; something estimable from the company's public material, never its budget or its internal plans; and enough bite to split companies roughly 30/70 \u2014 a question nearly everyone answers yes to ("has a website") adds no signal, so say that and propose a sharper one rather than writing it. Max 120 chars, in the user's language. Never name a specific company in a question. Never re-send an existing question with reworded text that means the same thing \u2014 a reword is a delete plus an add, it re-scores every lead in the pipeline against the org's quota, and it surfaces exactly the same companies.`
17283
+ },
17284
+ add: {
17285
+ type: "array",
17286
+ items: { type: "string", maxLength: 255 },
17287
+ description: `Questions to append to the current list (deduped). Mutually exclusive with \`questions\`. A question Leadbay can score has ALL of: the estimative marker \u2014 it starts "Is the company likely to " in English or "L'entreprise est-elle susceptible de/d' " in French (never "Does the company ..." or a bare "L'entreprise est-elle <X> ?", which the scorer cannot verify and marks no); ONE dimension, not two joined by AND; something estimable from the company's public material, never its budget or its internal plans; and enough bite to split companies roughly 30/70 \u2014 a question nearly everyone answers yes to ("has a website") adds no signal, so say that and propose a sharper one rather than writing it. Max 120 chars, in the user's language. Never name a specific company in a question. Read leadbay_get_qualification_questions first: skip anything an existing question already tests, and anything the lens already filters by sector, headcount or territory.`
17288
+ },
17289
+ remove: {
17290
+ type: "array",
17291
+ items: { type: "string" },
17292
+ description: "Exact question strings to remove from the current list. Mutually exclusive with `questions`. A removal requires confirm:true."
17293
+ },
17294
+ confirm: {
17295
+ type: "boolean",
17296
+ description: "Required whenever the change DROPS ANY existing question \u2014 including a same-count swap or a `questions` replacement that omits a current question, not only when the list gets shorter (removing a question changes how every lead is scored). Without it, such a change is previewed and not applied. Pure additions never need confirm."
17297
+ }
17298
+ },
17299
+ additionalProperties: false
17300
+ },
17301
+ outputSchema: {
17302
+ type: "object",
17303
+ properties: {
17304
+ qualification_questions: {
17305
+ type: "array",
17306
+ description: "The questions AFTER the change. Each: {question}.",
17307
+ items: { type: "object" }
17308
+ },
17309
+ count: { type: "number" },
17310
+ previous_count: { type: "number" },
17311
+ changed: {
17312
+ type: "boolean",
17313
+ description: "True when the list was actually written; false on a no-op or an unconfirmed shrink."
17314
+ },
17315
+ region: { type: "string" },
17316
+ form_warnings: {
17317
+ type: "array",
17318
+ items: { type: "string" },
17319
+ description: "Present when a written question will score poorly \u2014 not in the estimative 'Is the company likely to ...' form, or over 120 chars. The change WAS applied; tell the user and offer the rewrite."
17320
+ },
17321
+ hint: {
17322
+ type: "string",
17323
+ description: "Operator note \u2014 confirm prompt on a shrink, or a no-op explanation."
17324
+ },
17325
+ _meta: { type: "object" }
17326
+ },
17327
+ required: ["qualification_questions", "count", "changed"]
17328
+ },
17329
+ execute: async (client, params, ctx) => {
17330
+ const hasSet = Array.isArray(params.questions);
17331
+ const hasAdd = Array.isArray(params.add) && params.add.length > 0;
17332
+ const hasRemove = Array.isArray(params.remove) && params.remove.length > 0;
17333
+ if (hasSet && (hasAdd || hasRemove)) {
17334
+ throw client.makeError("QUALIFICATION_QUESTIONS_BAD_ARGS", "`questions` (full replace) is mutually exclusive with add/remove", "Pass EITHER `questions` (the full new list) OR `add`/`remove`, not both.", "POST /organizations/{orgId}");
17335
+ }
17336
+ if (!hasSet && !hasAdd && !hasRemove) {
17337
+ throw client.makeError("QUALIFICATION_QUESTIONS_NO_CHANGE", "nothing to change \u2014 pass `questions`, `add`, or `remove`", "Provide a full `questions` list, or `add`/`remove` entries.", "POST /organizations/{orgId}");
17338
+ }
17339
+ const orgId = await client.resolveOrgId();
17340
+ const current = await client.request("GET", `/organizations/${orgId}/ai_agent_questions`);
17341
+ const currentQs = (current ?? []).map((q) => q.question);
17342
+ const norm = (s) => s.trim();
17343
+ let next;
17344
+ if (hasSet) {
17345
+ next = params.questions.map(norm).filter((s) => s.length > 0);
17346
+ } else {
17347
+ next = [...currentQs];
17348
+ if (hasRemove) {
17349
+ const drop = new Set(params.remove.map(norm));
17350
+ next = next.filter((q) => !drop.has(norm(q)));
17351
+ }
17352
+ if (hasAdd) {
17353
+ const seen2 = new Set(next.map(norm));
17354
+ for (const q of params.add.map(norm)) {
17355
+ if (q.length > 0 && !seen2.has(q)) {
17356
+ next.push(q);
17357
+ seen2.add(q);
17358
+ }
17359
+ }
17360
+ }
17361
+ }
17362
+ const seen = /* @__PURE__ */ new Set();
17363
+ next = next.filter((q) => {
17364
+ const k = norm(q);
17365
+ if (seen.has(k))
17366
+ return false;
17367
+ seen.add(k);
17368
+ return true;
17369
+ });
17370
+ if (next.length > MAX_QUESTIONS) {
17371
+ throw client.makeError("QUALIFICATION_QUESTIONS_LIMIT", `too many questions: ${next.length} (max ${MAX_QUESTIONS})`, `Leadbay allows at most ${MAX_QUESTIONS} qualification questions. Remove some first (pass fewer in \`questions\`, or use \`remove\`), then add.`, "POST /organizations/{orgId}");
17372
+ }
17373
+ const previousCount = currentQs.length;
17374
+ const noChange = next.length === currentQs.length && next.every((q, i) => norm(q) === norm(currentQs[i] ?? ""));
17375
+ if (noChange) {
17376
+ return {
17377
+ qualification_questions: currentQs.map((q) => ({ question: q })),
17378
+ count: currentQs.length,
17379
+ previous_count: previousCount,
17380
+ changed: false,
17381
+ region: client.region,
17382
+ hint: "No change \u2014 the resulting list is identical to the current one. Pass different `add`/`remove` entries, or call leadbay_get_qualification_questions to review the current questions."
17383
+ };
17384
+ }
17385
+ const removed = currentQs.filter((q) => !next.some((n) => norm(n) === norm(q)));
17386
+ if (removed.length > 0 && params.confirm !== true) {
17387
+ return {
17388
+ qualification_questions: currentQs.map((q) => ({ question: q })),
17389
+ count: currentQs.length,
17390
+ previous_count: previousCount,
17391
+ changed: false,
17392
+ region: client.region,
17393
+ hint: `Re-call with confirm:true to apply. This would remove ${removed.length} question(s): ${removed.map((q) => `"${q}"`).join(", ")}. Removing a question changes how every lead is scored.`
17394
+ };
17395
+ }
17396
+ await client.requestVoid("POST", `/organizations/${orgId}`, {
17397
+ ai_agent_lead_questions: next
17398
+ });
17399
+ client.invalidateTasteProfile();
17400
+ const written = hasSet ? next : (params.add ?? []).map(norm).filter((q) => next.includes(q));
17401
+ const warnings = formWarnings(written);
17402
+ return {
17403
+ qualification_questions: next.map((q) => ({ question: q })),
17404
+ count: next.length,
17405
+ previous_count: previousCount,
17406
+ changed: true,
17407
+ region: client.region,
17408
+ ...warnings.length > 0 ? { form_warnings: warnings } : {}
17409
+ };
17410
+ }
17411
+ };
17412
+ }
17413
+ });
17414
+
16855
17415
  // ../core/dist/composite/get-qualification-questions.js
16856
17416
  var getQualificationQuestions;
16857
17417
  var init_get_qualification_questions = __esm({
16858
17418
  "../core/dist/composite/get-qualification-questions.js"() {
16859
17419
  "use strict";
16860
17420
  init_tool_descriptions_generated();
17421
+ init_set_qualification_questions();
16861
17422
  getQualificationQuestions = {
16862
17423
  name: "leadbay_get_qualification_questions",
16863
17424
  annotations: {
@@ -16885,6 +17446,13 @@ var init_get_qualification_questions = __esm({
16885
17446
  type: "number",
16886
17447
  description: "Number of qualification questions configured."
16887
17448
  },
17449
+ ideal_buyer_profile: {
17450
+ description: "The org's Ideal Buyer Profile {summary, key_characteristics, anti_patterns}, or null when none is configured. The questions score against this profile \u2014 read it before judging whether a user's stated rule is already covered."
17451
+ },
17452
+ targeting_prompt: {
17453
+ type: ["string", "null"],
17454
+ description: "The org's free-text targeting prompt (user_prompt) the AI agent follows, or null when unset. Qualitative rules live here rather than in a question; change it with leadbay_refine_prompt."
17455
+ },
16888
17456
  is_admin: {
16889
17457
  type: "boolean",
16890
17458
  description: "Whether the current bearer-token holder is an org admin. Admins can modify the questions via leadbay_set_qualification_questions."
@@ -16903,11 +17471,21 @@ var init_get_qualification_questions = __esm({
16903
17471
  const isAdmin = me?.admin ?? false;
16904
17472
  const orgId = me?.organization?.id ?? await client.resolveOrgId();
16905
17473
  const questions = await client.request("GET", `/organizations/${orgId}/ai_agent_questions`) ?? [];
17474
+ const [ibpResult, promptResult] = await Promise.allSettled([
17475
+ client.request("GET", `/organizations/${orgId}/ideal_buyer_profile`),
17476
+ client.request("GET", `/organizations/${orgId}/user_prompt`)
17477
+ ]);
17478
+ const ibp = ibpResult.status === "fulfilled" ? ibpResult.value ?? null : null;
17479
+ const targetingPrompt = promptResult.status === "fulfilled" ? promptResult.value?.prompt ?? null : null;
16906
17480
  let hint;
16907
- if (questions.length === 0) {
16908
- hint = "No qualification questions configured yet. Use leadbay_set_qualification_questions to add some, or leadbay_refine_prompt to shape the AI agent.";
17481
+ if (questions.length >= MAX_QUESTIONS && isAdmin) {
17482
+ hint = `${questions.length} of ${MAX_QUESTIONS} question slots are used \u2014 the set is FULL. An addition is a SWAP: tell the user the set is full, list these ${questions.length} and let THEM choose which one goes, then call leadbay_set_qualification_questions with confirm:true. Never pre-pick the one to drop.`;
17483
+ } else if (questions.length >= MAX_QUESTIONS) {
17484
+ hint = `${questions.length} of ${MAX_QUESTIONS} question slots are used \u2014 the set is FULL. Changing it means dropping one, and that is an org-admin action. Tell the user which question they would need an admin to swap out.`;
17485
+ } else if (questions.length === 0) {
17486
+ hint = `No qualification questions configured \u2014 every lead is scored on firmographics alone. Propose a starter set of exactly 3 questions in ONE leadbay_set_qualification_questions call \u2014 one question is too thin to separate anything, each on a DIFFERENT buying dimension, each starting "Is the company likely to ..." / "L'entreprise est-elle susceptible de ...", and none restating a sector or size the lens already filters on. Get the user's yes first.`;
16909
17487
  } else if (isAdmin) {
16910
- hint = "You're an org admin \u2014 use leadbay_set_qualification_questions to add, remove, or replace these questions.";
17488
+ hint = `You're an org admin \u2014 use leadbay_set_qualification_questions to add, remove, or replace these questions. ${MAX_QUESTIONS - questions.length} of ${MAX_QUESTIONS} slots are still free.`;
16911
17489
  }
16912
17490
  return {
16913
17491
  qualification_questions: questions.map((q) => ({
@@ -16916,6 +17494,12 @@ var init_get_qualification_questions = __esm({
16916
17494
  lang: q.lang
16917
17495
  })),
16918
17496
  count: questions.length,
17497
+ ideal_buyer_profile: ibp ? {
17498
+ summary: ibp.summary,
17499
+ key_characteristics: ibp.key_characteristics,
17500
+ anti_patterns: ibp.anti_patterns
17501
+ } : null,
17502
+ targeting_prompt: targetingPrompt,
16919
17503
  is_admin: isAdmin,
16920
17504
  region: client.region,
16921
17505
  ...hint ? { hint } : {}
@@ -17149,155 +17733,6 @@ var init_getting_started = __esm({
17149
17733
  }
17150
17734
  });
17151
17735
 
17152
- // ../core/dist/composite/set-qualification-questions.js
17153
- var setQualificationQuestions;
17154
- var init_set_qualification_questions = __esm({
17155
- "../core/dist/composite/set-qualification-questions.js"() {
17156
- "use strict";
17157
- init_tool_descriptions_generated();
17158
- setQualificationQuestions = {
17159
- name: "leadbay_set_qualification_questions",
17160
- annotations: {
17161
- title: "Modify the org's qualification questions",
17162
- readOnlyHint: false,
17163
- destructiveHint: true,
17164
- idempotentHint: false,
17165
- openWorldHint: true
17166
- },
17167
- description: leadbay_set_qualification_questions,
17168
- write: true,
17169
- inputSchema: {
17170
- type: "object",
17171
- properties: {
17172
- questions: {
17173
- type: "array",
17174
- items: { type: "string", maxLength: 255 },
17175
- description: "Full replacement list of qualification questions (replaces ALL current questions). Mutually exclusive with add/remove."
17176
- },
17177
- add: {
17178
- type: "array",
17179
- items: { type: "string", maxLength: 255 },
17180
- description: "Questions to append to the current list (deduped). Mutually exclusive with `questions`."
17181
- },
17182
- remove: {
17183
- type: "array",
17184
- items: { type: "string" },
17185
- description: "Exact question strings to remove from the current list. Mutually exclusive with `questions`. A removal requires confirm:true."
17186
- },
17187
- confirm: {
17188
- type: "boolean",
17189
- description: "Required whenever the change DROPS ANY existing question \u2014 including a same-count swap or a `questions` replacement that omits a current question, not only when the list gets shorter (removing a question changes how every lead is scored). Without it, such a change is previewed and not applied. Pure additions never need confirm."
17190
- }
17191
- },
17192
- additionalProperties: false
17193
- },
17194
- outputSchema: {
17195
- type: "object",
17196
- properties: {
17197
- qualification_questions: {
17198
- type: "array",
17199
- description: "The questions AFTER the change. Each: {question}.",
17200
- items: { type: "object" }
17201
- },
17202
- count: { type: "number" },
17203
- previous_count: { type: "number" },
17204
- changed: {
17205
- type: "boolean",
17206
- description: "True when the list was actually written; false on a no-op or an unconfirmed shrink."
17207
- },
17208
- region: { type: "string" },
17209
- hint: {
17210
- type: "string",
17211
- description: "Operator note \u2014 confirm prompt on a shrink, or a no-op explanation."
17212
- },
17213
- _meta: { type: "object" }
17214
- },
17215
- required: ["qualification_questions", "count", "changed"]
17216
- },
17217
- execute: async (client, params, ctx) => {
17218
- const hasSet = Array.isArray(params.questions);
17219
- const hasAdd = Array.isArray(params.add) && params.add.length > 0;
17220
- const hasRemove = Array.isArray(params.remove) && params.remove.length > 0;
17221
- if (hasSet && (hasAdd || hasRemove)) {
17222
- throw client.makeError("QUALIFICATION_QUESTIONS_BAD_ARGS", "`questions` (full replace) is mutually exclusive with add/remove", "Pass EITHER `questions` (the full new list) OR `add`/`remove`, not both.", "POST /organizations/{orgId}");
17223
- }
17224
- if (!hasSet && !hasAdd && !hasRemove) {
17225
- throw client.makeError("QUALIFICATION_QUESTIONS_NO_CHANGE", "nothing to change \u2014 pass `questions`, `add`, or `remove`", "Provide a full `questions` list, or `add`/`remove` entries.", "POST /organizations/{orgId}");
17226
- }
17227
- const orgId = await client.resolveOrgId();
17228
- const current = await client.request("GET", `/organizations/${orgId}/ai_agent_questions`);
17229
- const currentQs = (current ?? []).map((q) => q.question);
17230
- const norm = (s) => s.trim();
17231
- let next;
17232
- if (hasSet) {
17233
- next = params.questions.map(norm).filter((s) => s.length > 0);
17234
- } else {
17235
- next = [...currentQs];
17236
- if (hasRemove) {
17237
- const drop = new Set(params.remove.map(norm));
17238
- next = next.filter((q) => !drop.has(norm(q)));
17239
- }
17240
- if (hasAdd) {
17241
- const seen2 = new Set(next.map(norm));
17242
- for (const q of params.add.map(norm)) {
17243
- if (q.length > 0 && !seen2.has(q)) {
17244
- next.push(q);
17245
- seen2.add(q);
17246
- }
17247
- }
17248
- }
17249
- }
17250
- const seen = /* @__PURE__ */ new Set();
17251
- next = next.filter((q) => {
17252
- const k = norm(q);
17253
- if (seen.has(k))
17254
- return false;
17255
- seen.add(k);
17256
- return true;
17257
- });
17258
- const MAX_QUESTIONS = 5;
17259
- if (next.length > MAX_QUESTIONS) {
17260
- throw client.makeError("QUALIFICATION_QUESTIONS_LIMIT", `too many questions: ${next.length} (max ${MAX_QUESTIONS})`, `Leadbay allows at most ${MAX_QUESTIONS} qualification questions. Remove some first (pass fewer in \`questions\`, or use \`remove\`), then add.`, "POST /organizations/{orgId}");
17261
- }
17262
- const previousCount = currentQs.length;
17263
- const noChange = next.length === currentQs.length && next.every((q, i) => norm(q) === norm(currentQs[i] ?? ""));
17264
- if (noChange) {
17265
- return {
17266
- qualification_questions: currentQs.map((q) => ({ question: q })),
17267
- count: currentQs.length,
17268
- previous_count: previousCount,
17269
- changed: false,
17270
- region: client.region,
17271
- hint: "No change \u2014 the resulting list is identical to the current one. Pass different `add`/`remove` entries, or call leadbay_get_qualification_questions to review the current questions."
17272
- };
17273
- }
17274
- const removed = currentQs.filter((q) => !next.some((n) => norm(n) === norm(q)));
17275
- if (removed.length > 0 && params.confirm !== true) {
17276
- return {
17277
- qualification_questions: currentQs.map((q) => ({ question: q })),
17278
- count: currentQs.length,
17279
- previous_count: previousCount,
17280
- changed: false,
17281
- region: client.region,
17282
- hint: `Re-call with confirm:true to apply. This would remove ${removed.length} question(s): ${removed.map((q) => `"${q}"`).join(", ")}. Removing a question changes how every lead is scored.`
17283
- };
17284
- }
17285
- await client.requestVoid("POST", `/organizations/${orgId}`, {
17286
- ai_agent_lead_questions: next
17287
- });
17288
- client.invalidateTasteProfile();
17289
- return {
17290
- qualification_questions: next.map((q) => ({ question: q })),
17291
- count: next.length,
17292
- previous_count: previousCount,
17293
- changed: true,
17294
- region: client.region
17295
- };
17296
- }
17297
- };
17298
- }
17299
- });
17300
-
17301
17736
  // ../core/dist/composite/get-lead-custom-fields.js
17302
17737
  var getLeadCustomFields;
17303
17738
  var init_get_lead_custom_fields = __esm({
@@ -22409,11 +22844,11 @@ var init_find_new_leads = __esm({
22409
22844
  properties: {
22410
22845
  query: {
22411
22846
  type: "string",
22412
- description: "Natural-language ICP ask. Matches topic VOCABULARY \u2014 can surface vendors of a product as easily as buyers of it. Prefer example_lead; use query only when the user's wording carries signal an example can't."
22847
+ description: "Natural-language ICP ask. Matches topic VOCABULARY \u2014 can surface vendors of a product as easily as buyers of it. Prefer example_lead. NO event language ('hiring', 'recrute', 'expanding', 'just raised'): registry text never says what a company is DOING, so an event word matches nothing here. Send the trigger to leadbay_set_qualification_questions or leadbay_refine_prompt instead, and tell the user that is where it went."
22413
22848
  },
22414
22849
  example_lead: {
22415
22850
  type: "object",
22416
- description: "A FICTIONAL typical ideal customer used as a look-alike seed \u2014 the highest-leverage input. Put everything in `description` (registry 'About Us' style, what the company IS); leave `name` unset (a distinctive invented name pulls matches toward name-lookalikes).",
22851
+ description: "A FICTIONAL typical ideal customer used as a look-alike seed \u2014 the highest-leverage input. Put everything in `description` (registry 'About Us' style, what the company IS, never what it is DOING and never what the seller sells); leave `name` unset (a distinctive invented name pulls matches toward name-lookalikes).",
22417
22852
  properties: {
22418
22853
  name: { type: "string" },
22419
22854
  description: { type: "string" },
@@ -29230,6 +29665,7 @@ var FRICTION = `Problem reports: when the user asks you to report a Leadbay prob
29230
29665
  var MENTAL_MODEL = `How Leadbay works (mental model): Leadbay is a sales inbox, not a queryable database. Each day the user logs back in, a fresh batch of leads is delivered. Batch size is paced by how many leads the user has actually acted on recently \u2014 some workflows produce a big stream of smaller prospects, others a narrow stream of bigger ones. Pulling more won't produce more; the user acting on leads (outreach, skips, saves) does.`;
29231
29666
  var QUOTA_REFRESH = `Show the refreshed quota AFTER a quota-using action only when it matters: the user asks where they now stand, the action stopped on an exhausted window, or a top-up the user confirmed landed. Otherwise report the result and say nothing about quota. When you do show it, wait for genuine completion \u2014 leadbay_bulk_enrich_status reports the job done (all_done, OR a plateau you've decided is terminal: overall_progress.done stopped climbing across spaced polls, so some contacts are unresolvable and all_done stays false) \u2014 then call leadbay_account_status once and render the per-window %/$ gauge (Daily/Weekly/Monthly) it returns. leadbay_enrich_contacts only LAUNCHES an async reveal (it returns a hint to check back in ~60s), so do NOT refresh quota right after it \u2014 the usage isn't reflected yet. For that single-contact flow, refresh only once a re-read of the lead's contacts (leadbay_research_lead_by_id; leadbay_get_contacts where exposed) shows the REQUESTED channel actually landed \u2014 the requested email and/or phone_number present \u2014 NOT enrichment.done alone (that flag is already true for a contact enriched on the other channel earlier, so a phone reveal could otherwise trigger the refresh before phone_number arrives). This is the canonical quota surface; do NOT hand-roll a 'credits' line in its place. Skip it when account_status reports unlimited_credits, quota_error, or a null quota (nothing to show), or when billing is genuinely unavailable. Do it ONCE per completed action \u2014 not after every poll while work is still in progress.`;
29232
29667
  var QUOTA_TOPUP = `Quota & top-ups: when a tool returns QUOTA_EXCEEDED / 429, the user has TWO options \u2014 wait for the window reset (daily / weekly / monthly resets shown in leadbay_account_status), OR top up AI credits (top-ups clear the throttle IMMEDIATELY \u2014 they are not subject to the same window). Always offer BOTH options; default-recommending 'wait until tomorrow' is wrong when a 30-second top-up unblocks the same call. If the host exposes leadbay_create_topup_link, OFFER it on every quota wall: 'Want me to generate a top-up link?' \u2014 when the user says yes, call leadbay_create_topup_link and surface the returned Stripe URL as a clickable link for the user to open in their browser. (Sibling leadbay_open_billing_portal is for ongoing subscription changes, not one-shot top-ups.) AFTER the user has topped up: do NOT keep refusing operations. A top-up invalidates every prior 429 and every stale 'you're at your quota' snapshot. The moment the user signals they topped up / bought credits / added credits \u2014 even WITHOUT re-calling account_status \u2014 treat the previous quota state as void and RETRY the originally failed call. (Best practice: re-call leadbay_account_status to surface the fresh state to the user, then retry; but the retry itself does NOT require a successful account_status check first. If the retry hits the wall again, THEN you have evidence the top-up didn't land; only then re-offer top-up / wait.) The agent's job after a top-up is to RESUME the workflow the user was on, not gate-keep.`;
29668
+ var STATED_RULES = `Stated fit rules live in the account, not in your memory: when the user tells you what makes a lead good or bad ("\xE9carte les soci\xE9t\xE9s liquid\xE9es", "I don't want associations", "our best customers run their own maintenance crews", "the leads aren't relevant"), that is a change to their Leadbay settings. NEVER answer it with "c'est not\xE9", "already saved", "the rule is active" or "I'll keep that in mind" \u2014 those are claims about their account that are false unless a tool call made them true, and the next session, the scheduled run and their colleague all read the account, not your context. Call leadbay_get_qualification_questions first \u2014 it returns the questions, the ideal buyer profile and the targeting prompt together \u2014 then decide where the rule belongs, decide whether anything needs to change at all, propose the exact text, and write only on an explicit yes.`;
29233
29669
  var TRANSIENT_401 = `Transient 401s (DO NOT narrate as an auth problem): Leadbay bearer tokens do NOT expire on a timer, and the client already auto-retried this call once before surfacing it. So a lone AUTH_EXPIRED / 401 from a tool is almost always a brief Leadbay-side hiccup, NOT a sign the user must sign in again. When you see ONE, just silently retry the operation once more and carry on \u2014 do NOT tell the user to log in again, reconnect the connector, reauthorize, or 're-load Leadbay', and do NOT surface the bare '401' to them at all. Only if calls keep failing across several attempts should you mention it \u2014 and then frame it as a possible Leadbay-side issue or a possible logout (you cannot tell which from here), and offer to report it to the team. Never turn a one-off 401 into a 'your connection needs re-authenticating' message; that is the wrong read on a connection that is actually working.`;
29234
29670
  var TRIGGERED_BY = `Trigger provenance (MANDATORY): every Leadbay composite-tool call MUST carry a non-empty \`_triggered_by\` argument \u2014 the verbatim slice of the user's most recent message that this call is acting upon. Quote literally; do NOT paraphrase, summarize, or substitute a one-word label like "leads" or "request" (those are rejected). If you are acting WITHOUT a fresh user message (a memory recall, a scheduled run, a self-initiated retry), pass the actual instruction you are acting on \u2014 the recalled directive, the schedule's intent, or the original request being retried \u2014 so the value is always a real, auditable trace. Strip any secrets the user pasted (API keys, passwords, card numbers, full home addresses) \u2014 replace with [REDACTED]. A composite call missing or blanking this field is rejected with LAST_PROMPT_REQUIRED; just re-call with the field set. This is a protocol requirement on EVERY composite invocation (not just the first), independent of any telemetry setting.`;
29235
29671
  var VERIFICATION = `After every email, call, message, or meeting with a lead's contact, you MUST call leadbay_report_outreach with verification={source, ref} (gmail_message_id from the Gmail send, calendar_event_id from a booking, or user_confirmed='<the user's literal confirmation>'). Skipping or fabricating verification poisons the human team's pipeline.`;
@@ -29398,6 +29834,9 @@ function buildServerInstructions(exposed) {
29398
29834
  }
29399
29835
  parts.push(TRIGGERED_BY);
29400
29836
  parts.push(MENTAL_MODEL);
29837
+ if (has("leadbay_get_qualification_questions")) {
29838
+ parts.push(STATED_RULES);
29839
+ }
29401
29840
  if (has("leadbay_create_topup_link")) {
29402
29841
  parts.push(QUOTA_TOPUP);
29403
29842
  }
@@ -31596,7 +32035,7 @@ var OAUTH_BASE_URLS = {
31596
32035
  fr: "https://staging.api.leadbay.app"
31597
32036
  }
31598
32037
  };
31599
- var VERSION = "0.39.4";
32038
+ var VERSION = "0.39.5";
31600
32039
  var HELP = `
31601
32040
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
31602
32041