@leadbay/mcp 0.39.3 → 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.
@@ -4843,6 +4843,8 @@ to pick, then re-call with the id/exact name.
4843
4843
 
4844
4844
  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).
4845
4845
 
4846
+ **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.
4847
+
4846
4848
  **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.
4847
4849
 
4848
4850
  **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.
@@ -5714,7 +5716,7 @@ Do NOT use for: "show me today's leads / what's new today" \u2192 \`leadbay_pull
5714
5716
  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.
5715
5717
 
5716
5718
  Examples that SHOULD invoke this tool:
5717
- - "Find me 10 gyms around Dallas that would buy our flooring, with someone I can call."
5719
+ - "Find me 10 gyms around Dallas that would buy our flooring, with a contact."
5718
5720
  - "Get me 20 new US SaaS companies, 50-2000 employees, with the VP People's email."
5719
5721
  - "We're launching in Lyon \u2014 find 15 hotels that fit our ICP."
5720
5722
 
@@ -5725,11 +5727,10 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
5725
5727
 
5726
5728
  ## RENDER (quick)
5727
5729
 
5728
- 3-col table of delivered leads in returned order: col 1 = 10-segment fit
5729
- bar + linked company \xB7 location \xB7 size; col 2 = why-fits \u226420 words; col 3
5730
- = contact + found channels. ALWAYS close with the honest funnel line
5731
- (matched/examined/delivered/stop reason) \u2014 especially on 0
5732
- delivered. Full algorithm below.
5730
+ 3-col table of delivered leads in returned order: col 1 = 10-segment fit bar
5731
+ + linked company \xB7 location \xB7 size; col 2 = why-fits \u226420 words; col 3 =
5732
+ contact + found channels. ALWAYS close with the honest funnel line
5733
+ (matched/examined/delivered/stop reason), especially on 0 delivered.
5733
5734
 
5734
5735
  ---
5735
5736
 
@@ -5738,27 +5739,25 @@ company universe, applies hard filters, skips what the org already knows
5738
5739
  (\`novelty: org\`), optionally qualifies against the org's own intelligence
5739
5740
  (questions, tags, ideal buyer profile \u2014 frozen at submit), and optionally reveals
5740
5741
  contact channels. Polls up to \`wait_seconds\` (default 45); a longer job returns
5741
- \`still_running\` + \`next_poll\` \u2014 hand off to \`leadbay_lead_job_status\`. Jobs run
5742
+ \`still_running\` + \`next_poll\` \u2014 hand to \`leadbay_lead_job_status\`. Jobs run
5742
5743
  \u226430 min, results kept 30 days.
5743
5744
 
5744
5745
  **Free vs usage quota \u2014 never use quota silently.** Default (\`qualify: false\`,
5745
5746
  \`channels: []\`) is FREE: company profile + fit score + cached research +
5746
5747
  contact identity. \`qualify: true\` (per candidate EXAMINED, capped by
5747
5748
  \`exploration_cap\`/\`max_cost\`) and \`channels\` (only when a value is found) draw
5748
- on the org's usage quota; nothing is invoiced. Enforced in code: such a call is WITHHELD unless it
5749
- carries \`confirm: true\` \u2014 nothing is submitted and you get
5749
+ on the org's usage quota; nothing is invoiced. Enforced in code: such a call is
5750
+ WITHHELD unless it carries \`confirm: true\` \u2014 nothing is submitted and you get
5750
5751
  \`mode: "needs_confirmation"\` with a real quote to show the user. Re-call with
5751
5752
  \`confirm: true\` on their go-ahead ("go ahead / get their emails" counts).
5752
- \`confirm: false\` vetoes. Free needs no consent. **Preview free first** \u2014
5753
- reshaping an off-profile seed is free, exploring it with \`qualify: true\` is
5754
- not.
5753
+ \`confirm: false\` vetoes. **Preview free first** \u2014 reshaping an off-profile seed
5754
+ is free, exploring it with \`qualify: true\` is not.
5755
5755
 
5756
5756
  **Ad-hoc exclusions ("no chains") are enforced by NO tier** \u2014 \`filters\` has no
5757
- exclusion key, and \`qualify\` scores against the org's FROZEN questions and IBP,
5758
- which need not mention chains; the seed's inverse only shifts ranking.
5759
- Violators can survive, use quota and be delivered \u2014 post-filter them yourself
5760
- and say the tier didn't enforce it. Durable enforcement \u2192
5761
- \`leadbay_refine_prompt\`.
5757
+ exclusion key, and \`qualify\` scores against the org's FROZEN questions and IBP.
5758
+ Violators survive, use quota and get delivered: post-filter them yourself and
5759
+ say the tier didn't enforce it. Durable enforcement \u2192
5760
+ \`leadbay_set_qualification_questions\` / \`leadbay_refine_prompt\`.
5762
5761
 
5763
5762
  ### Crafting the \`example_lead\` seed \u2014 the input that decides result quality
5764
5763
 
@@ -5778,13 +5777,13 @@ measured:
5778
5777
  model, what they sell or operate, who they serve, observable scale. Write
5779
5778
  it like the first paragraph of their About-Us page.
5780
5779
  - STRONG: "Operator of full-service fitness centers offering strength
5781
- areas, group classes and personal training to members across multiple
5782
- clubs."
5783
- - WEAK (generic): "A gym in Texas."
5784
- - WRONG (seller-side): "Supplier of durable modular flooring for gyms."
5785
- 4. **No event language.** "hiring", "expanding", "just raised" are not
5786
- filters \u2014 registry descriptions never contain them, so they dilute the
5787
- profile. Purchase triggers belong in the org's qualification questions.
5780
+ areas, group classes and personal training to members across clubs."
5781
+ - WEAK: "A gym in Texas." WRONG: "Supplier of gym flooring." (seller-side)
5782
+ 4. **NO event language \u2014 in \`description\` or \`query\`.** "recrute", "hiring",
5783
+ "expanding", "just raised" never appear in registry text, so they match
5784
+ nothing. Send the trigger to \`leadbay_set_qualification_questions\` or
5785
+ \`leadbay_refine_prompt\` and say so. "companies hiring a senior SDR" seeds as
5786
+ "B2B software company operating an in-house outbound sales team."
5788
5787
  5. **No meta-markers.** Never "(example)", "(fictional)", "(placeholder)".
5789
5788
  6. **Hard constraints go in \`filters\`, not prose \u2014 exact keys:**
5790
5789
  \`sectors: string[]\`, \`locations: string[]\`, \`employees_min: number\`,
@@ -5794,24 +5793,22 @@ measured:
5794
5793
  never a country: this workspace's own is dropped, any other is refused.
5795
5794
  7. **Prefer \`example_lead\` over \`query\`.** Query matches topic *vocabulary*:
5796
5795
  "gyms that need durable flooring" surfaced flooring VENDORS, 0 delivered.
5797
- Use \`query\` only for signal an example can't express.
5798
5796
  8. **One seed per buyer archetype.** An ask spanning two segments ("gyms and
5799
5797
  warehouses") needs one search each with its own description and
5800
- \`request_id\` \u2014 a blended seed lands between the clusters and matches
5801
- neither.
5798
+ \`request_id\` \u2014 a blended seed lands between the clusters, matching neither.
5802
5799
 
5803
5800
 
5804
5801
  **Parameter notes**
5805
5802
  - \`request_id\` (REQUIRED) is the retry contract: SAME value retrying the same
5806
5803
  ask (same live job, no double launch); NEW for a changed ask. Derive from ask
5807
5804
  + archetype + date: \`gyms-dallas-2026-07-28\`.
5808
- - Never lower \`min_ai_score\` together with \`channels\` \u2014 that reveals emails for
5809
- leads the AI just scored as junk.
5810
- - \`count\` \u2264 50; \u22643 active jobs/org; \u226410 submits/hour (429 + Retry-After \u2014
5811
- wait, don't hammer).
5805
+ - Never lower \`min_ai_score\` with \`channels\` \u2014 that reveals emails for leads
5806
+ the AI just scored as junk.
5807
+ - \`count\` \u2264 50; \u22643 active jobs/org; \u226410 submits/hour (429 + Retry-After \u2014 wait,
5808
+ don't hammer).
5812
5809
 
5813
5810
  **Read the result honestly** \u2014 \`funnel\` + \`explain.scope_notes\` tell the story;
5814
- zero delivered gets a cause and a next move (rules in RENDERING).
5811
+ zero delivered gets a cause and a next move (RENDERING).
5815
5812
 
5816
5813
  ---
5817
5814
 
@@ -6292,26 +6289,28 @@ WHEN NOT TO USE: when the lead summary's \`prospecting_actions_count\` is 0.
6292
6289
  `;
6293
6290
  var leadbay_get_qualification_questions = `## WHEN TO USE
6294
6291
 
6295
- 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".
6292
+ 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".
6296
6293
 
6297
- 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\`.
6294
+ 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\`.
6298
6295
 
6299
- Prefer when: user wants the ORG-level qualification questions catalog, no lead and no buyer profile
6296
+ 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
6300
6297
 
6301
6298
  Examples that SHOULD invoke this tool:
6302
6299
  - "What qualification questions does Leadbay use to score my leads?"
6303
6300
  - "Show me my org's qualification questions."
6301
+ - "Why do I keep getting these companies? What are my settings?"
6304
6302
 
6305
6303
  Examples that should NOT invoke this tool (sound similar, route elsewhere):
6306
6304
  - "How did Acme Corp answer the qualification questions?"
6307
- - "What's my ideal buyer profile?"
6305
+ - "Add a question about install crews."
6308
6306
 
6309
6307
  ## RENDER (quick)
6310
6308
 
6311
- Numbered list of the questions (chat-native markdown), each one line. When
6312
- \`is_admin\` is true, append the \`hint\` as a footnote (points at
6313
- leadbay_set_qualification_questions for editing). When the list is empty,
6314
- render the \`hint\` instead.
6309
+ Numbered list of the questions (chat-native markdown), each one line,
6310
+ verbatim. Below it, the ideal buyer profile summary + anti-patterns and the
6311
+ targeting prompt when set. When \`is_admin\` is true, append the \`hint\` as a
6312
+ footnote (points at leadbay_set_qualification_questions). When the question
6313
+ list is empty, say so explicitly and render the \`hint\` instead.
6315
6314
 
6316
6315
  ---
6317
6316
 
@@ -6324,7 +6323,15 @@ Returns:
6324
6323
 
6325
6324
  - **\`qualification_questions\`** \u2014 the catalog. Each: \`{question, created_at,
6326
6325
  lang}\`. Ordered as the backend returns them.
6327
- - **\`count\`** \u2014 number of configured questions.
6326
+ - **\`count\`** \u2014 number of configured questions. **Zero is a finding, not a
6327
+ blank** \u2014 an org with no questions scores every lead on firmographics alone.
6328
+ Say so and offer a starter set.
6329
+ - **\`ideal_buyer_profile\`** \u2014 \`{summary, key_characteristics, anti_patterns}\`
6330
+ or null. The questions score against THIS. A rule the user states is often
6331
+ already an \`anti_pattern\` here.
6332
+ - **\`targeting_prompt\`** \u2014 the org's free-text instruction to the AI agent, or
6333
+ null. Qualitative rules that no single yes/no can express live here; change
6334
+ it with \`leadbay_refine_prompt\`.
6328
6335
  - **\`is_admin\`** \u2014 whether the current user is an org admin. Modifying the
6329
6336
  questions (\`leadbay_set_qualification_questions\`) is an org-admin action; for
6330
6337
  admins a \`hint\` points there.
@@ -6332,25 +6339,132 @@ Returns:
6332
6339
  when no questions are configured.
6333
6340
 
6334
6341
  This tool only READS. To change the questions, use
6335
- **leadbay_set_qualification_questions** (add / remove / replace). The result is
6336
- cached on the client (it reuses the same taste-profile fetch as
6337
- \`leadbay_get_taste_profile\`), so repeated calls in a session are cheap.
6342
+ **leadbay_set_qualification_questions**; to change the targeting prompt, use
6343
+ **leadbay_refine_prompt**; for sector / size / territory, use
6344
+ **leadbay_adjust_audience**. **leadbay_research_lead_by_id** shows how a
6345
+ SPECIFIC lead answered these questions.
6346
+
6347
+ ### A stated fit rule is a SETTING \u2014 read, decide, propose, then write
6348
+
6349
+ **Never answer a stated rule from memory.** "C'est not\xE9", "already applied",
6350
+ "I'll keep that in mind", "the rule is now active" \u2014 every one of those is a
6351
+ claim about the user's ACCOUNT, and it is false unless a tool call made it
6352
+ true. The rule lives in the org's settings or it does not exist: your context
6353
+ window ends with this conversation, and the next session, the scheduled run and
6354
+ the user's colleague all read the account, not your memory. If you have not
6355
+ called a tool, do not say the rule is in place.
6356
+
6357
+ When the user says what makes a lead good or bad \u2014 *"\xE9carte les soci\xE9t\xE9s
6358
+ liquid\xE9es"*, *"je ne veux pas d'associations"*, *"our best customers run their
6359
+ own maintenance crews"*, *"les leads ne sont pas pertinents"* \u2014 they are
6360
+ describing their account, not just this batch. Filter the batch and they say
6361
+ it again next week; their questions, buyer profile and targeting prompt never
6362
+ move.
6363
+
6364
+ **1 \u2014 Read before you decide.** Call \`leadbay_get_qualification_questions\`
6365
+ first. It returns the question set PLUS the ideal buyer profile and the
6366
+ targeting prompt those questions sit beside. You cannot judge whether a rule is
6367
+ already covered without seeing them.
6368
+
6369
+ **2 \u2014 Decide WHERE the rule belongs.** One rule, one destination:
6370
+
6371
+ | What the user stated | Where it belongs |
6372
+ |---|---|
6373
+ | A sector, a headcount band, a territory | \`leadbay_adjust_audience\` / \`leadbay_new_lens\` filters \u2014 never a question |
6374
+ | 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 |
6375
+ | 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\` |
6376
+ | Named companies \u2014 "exclude Groupe Solidum, Dentego" | \`leadbay_dislike_lead\` / \`leadbay_set_lead_status\` on those leads. A question must NEVER name a company |
6377
+ | 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 |
6378
+ | 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 |
6379
+ | 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 |
6380
+
6381
+ **3 \u2014 Decide whether to change anything at all.** Touching a question
6382
+ re-scores every lead in the pipeline and draws on the org's quota, so a change
6383
+ that surfaces the same companies is a pure loss. Four reasons to write NOTHING
6384
+ and say why:
6385
+
6386
+ 1. **An existing question already covers the rule.** Quote that question back
6387
+ and stop. **Never reword a question that already means the same thing** \u2014
6388
+ even when the user asks you to "clarify" or "improve" the wording. A reword
6389
+ is a removal plus an addition: it re-scores every lead, spends quota, and
6390
+ surfaces exactly the same companies. Offer instead to find out whether any
6391
+ question is testing the WRONG thing.
6392
+ 2. **The audience filter already enforces it** \u2014 sector, headcount, territory.
6393
+ 3. **An existing question already tests that dimension.** Two questions on one
6394
+ dimension waste a slot and add no signal.
6395
+ 4. **The question the user asked for is not decisive.** See step 4: say so,
6396
+ offer the sharper version, and write only what they then choose. Adding a
6397
+ question you know separates nothing is worse than adding none.
6398
+
6399
+ **The ceiling is 5 questions.** Read the count before you answer an "add a
6400
+ question" request: at 5 the honest answer is not "sure, I'll add it". Say in
6401
+ that same turn that the set is full, list the five, and let the USER name which
6402
+ one goes. Never pre-pick the casualty.
6403
+
6404
+ **4 \u2014 Write a DECISIVE question.** Check all six before you propose the text:
6405
+
6406
+ 1. **The estimative marker is literal and mandatory.** English questions start
6407
+ \`Is the company likely to \u2026\`. French questions start
6408
+ \`L'entreprise est-elle susceptible de/d' \u2026\`. There is no third form. The
6409
+ scorer works from public text it cannot verify, so a verifiable question
6410
+ scores almost everything as no.
6411
+ - \u2705 \`Is the company likely to run its own in-house maintenance crew?\`
6412
+ - \u274C \`Does the company run its own maintenance crew?\`
6413
+ - \u274C \`Is the company a cold-storage plant?\` \u2014 no \`likely to\`
6414
+ - \u2705 \`L'entreprise est-elle susceptible d'\xEAtre en liquidation judiciaire ?\`
6415
+ - \u274C \`L'entreprise est-elle en liquidation ?\` \u2014 no \`susceptible\`
6416
+ 2. **It tests the lead as a BUYER.** Before you write a question, ask: would
6417
+ a company answering yes write a cheque to THIS user? A question that only
6418
+ describes what the lead's own business does \u2014 "Is the company likely to
6419
+ manufacture branded pharmaceuticals?" for a seller of advertising \u2014 is a
6420
+ category test, not a buying test, and it scores the user's competitors and
6421
+ suppliers as well as their prospects. Test the need the user's offering
6422
+ meets: "Is the company likely to run consumer campaigns that need paid media
6423
+ placement?"
6424
+ 3. **Estimable from public material** \u2014 the company's website, its about page,
6425
+ its job postings, its registry entry. Never its budget, its internal plans
6426
+ or its future intentions. When the user's rule is un-observable ("has budget
6427
+ for copywriting"), propose the observable proxy, and tell them you swapped
6428
+ it and why.
6429
+ 4. **One dimension each.** \`Is the company likely to operate a cold-storage
6430
+ plant AND run its own maintenance crew?\` is two questions. Split it into
6431
+ two, or pick the one that discriminates better.
6432
+ 5. **Decisive.** It should split companies in general roughly 30/70 while the
6433
+ user's own customers answer yes. A question nearly everyone answers yes to
6434
+ \u2014 "has a website", "uses email", "is a company" \u2014 separates nobody. Do NOT
6435
+ write it as asked: say it would add no signal, and offer the sharper version
6436
+ you would write instead.
6437
+ 6. **\u2264120 characters, in the user's language.**
6438
+
6439
+ An org with **zero** questions scores every lead on firmographics alone. That
6440
+ is a finding worth stating, and the fix is a starter set of **3** questions on
6441
+ three different dimensions \u2014 not one. Propose all three at once.
6442
+
6443
+ **5 \u2014 The change is the user's call, not yours.** This holds for the questions,
6444
+ the targeting prompt and the buyer profile alike. Show the exact text you
6445
+ propose and what it will change, then get an explicit yes before calling
6446
+ \`leadbay_set_qualification_questions\` or \`leadbay_refine_prompt\`. Ask through
6447
+ \`ask_user_input_v0\` when the host offers it. A removal or a swap additionally
6448
+ needs \`confirm:true\`. Do not write an org setting in the same turn the user
6449
+ first stated the rule \u2014 and once they have said yes, actually write it:
6450
+ describing the change is not making it.
6451
+
6452
+ **Answer the ask as well.** A rule stated in passing \u2014 *"sors-moi les leads du
6453
+ jour, et arr\xEAte de me remonter des h\xF4pitaux publics"* \u2014 does not replace the
6454
+ ask. Deliver the leads first, then raise the setting.
6338
6455
 
6339
- Companion tools: **leadbay_set_qualification_questions** to modify the questions;
6340
- **leadbay_get_taste_profile** when the user also wants the Ideal Buyer Profile +
6341
- purchase-intent tags; **leadbay_research_lead_by_id** for how a SPECIFIC lead
6342
- answered these questions; **leadbay_refine_prompt** to shape the AI agent's
6343
- behaviour.
6344
6456
 
6345
6457
  ### RENDERING
6346
6458
 
6347
6459
  Render \`qualification_questions\` as a numbered list \u2014 one question per line, in
6348
6460
  the order returned. Lead with a short heading like **"Qualification questions
6349
- (N)"**. When \`qualification_questions\` is empty, render the \`hint\` sentence
6350
- instead of an empty list. When \`is_admin\` is true and there are questions,
6351
- append the \`hint\` as a one-line footnote (points at
6352
- leadbay_set_qualification_questions). Do not invent questions or reword them \u2014
6353
- render verbatim.
6461
+ (N)"**. Then, when present, the \`ideal_buyer_profile\` summary with its
6462
+ \`anti_patterns\` as a short bulleted list, and the \`targeting_prompt\` as a
6463
+ blockquote. When \`qualification_questions\` is empty, say **"You have no
6464
+ qualification questions \u2014 leads are scored on firmographics alone"** and render
6465
+ the \`hint\`. When \`is_admin\` is true and there are questions, append the \`hint\`
6466
+ as a one-line footnote. Do not invent questions or reword them \u2014 render
6467
+ verbatim.
6354
6468
  `;
6355
6469
  var 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".
6356
6470
 
@@ -8438,20 +8552,22 @@ WHEN NOT TO USE: when you already know the exact titles you want to enrich.
8438
8552
  `;
8439
8553
  var leadbay_refine_prompt = `## WHEN TO USE
8440
8554
 
8441
- 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".
8555
+ 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".
8442
8556
 
8443
- 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\`.
8557
+ 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\`.
8444
8558
 
8445
- 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.
8559
+ 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.
8446
8560
 
8447
8561
  Examples that SHOULD invoke this tool:
8448
8562
  - "Focus on hospitals that run their own IT in-house."
8449
8563
  - "Prioritize companies that have recently expanded headcount."
8564
+ - "On ne vend qu'au priv\xE9 \u2014 arr\xEAte de me remonter des h\xF4pitaux publics."
8450
8565
 
8451
8566
  Examples that should NOT invoke this tool (sound similar, route elsewhere):
8452
8567
  - "Create a lens specialized in automobile."
8453
8568
  - "Add fintech to my Joinery lens."
8454
8569
  - "Show me my lenses."
8570
+ - "Exclude Groupe Solidum, we had an unpaid invoice with them."
8455
8571
 
8456
8572
  ## RENDER (quick)
8457
8573
 
@@ -8462,6 +8578,121 @@ clarification was raised, surface its question (route via ask_user_input_v0).
8462
8578
 
8463
8579
  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).
8464
8580
 
8581
+ **Read \`leadbay_get_qualification_questions\` first.** It returns the current
8582
+ targeting prompt alongside the questions and the buyer profile. Setting a
8583
+ prompt REPLACES the previous one \u2014 write the combined instruction, not just the
8584
+ new clause, or you silently drop rules the user set earlier.
8585
+
8586
+ ### A stated fit rule is a SETTING \u2014 read, decide, propose, then write
8587
+
8588
+ **Never answer a stated rule from memory.** "C'est not\xE9", "already applied",
8589
+ "I'll keep that in mind", "the rule is now active" \u2014 every one of those is a
8590
+ claim about the user's ACCOUNT, and it is false unless a tool call made it
8591
+ true. The rule lives in the org's settings or it does not exist: your context
8592
+ window ends with this conversation, and the next session, the scheduled run and
8593
+ the user's colleague all read the account, not your memory. If you have not
8594
+ called a tool, do not say the rule is in place.
8595
+
8596
+ When the user says what makes a lead good or bad \u2014 *"\xE9carte les soci\xE9t\xE9s
8597
+ liquid\xE9es"*, *"je ne veux pas d'associations"*, *"our best customers run their
8598
+ own maintenance crews"*, *"les leads ne sont pas pertinents"* \u2014 they are
8599
+ describing their account, not just this batch. Filter the batch and they say
8600
+ it again next week; their questions, buyer profile and targeting prompt never
8601
+ move.
8602
+
8603
+ **1 \u2014 Read before you decide.** Call \`leadbay_get_qualification_questions\`
8604
+ first. It returns the question set PLUS the ideal buyer profile and the
8605
+ targeting prompt those questions sit beside. You cannot judge whether a rule is
8606
+ already covered without seeing them.
8607
+
8608
+ **2 \u2014 Decide WHERE the rule belongs.** One rule, one destination:
8609
+
8610
+ | What the user stated | Where it belongs |
8611
+ |---|---|
8612
+ | A sector, a headcount band, a territory | \`leadbay_adjust_audience\` / \`leadbay_new_lens\` filters \u2014 never a question |
8613
+ | 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 |
8614
+ | 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\` |
8615
+ | Named companies \u2014 "exclude Groupe Solidum, Dentego" | \`leadbay_dislike_lead\` / \`leadbay_set_lead_status\` on those leads. A question must NEVER name a company |
8616
+ | 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 |
8617
+ | 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 |
8618
+ | 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 |
8619
+
8620
+ **3 \u2014 Decide whether to change anything at all.** Touching a question
8621
+ re-scores every lead in the pipeline and draws on the org's quota, so a change
8622
+ that surfaces the same companies is a pure loss. Four reasons to write NOTHING
8623
+ and say why:
8624
+
8625
+ 1. **An existing question already covers the rule.** Quote that question back
8626
+ and stop. **Never reword a question that already means the same thing** \u2014
8627
+ even when the user asks you to "clarify" or "improve" the wording. A reword
8628
+ is a removal plus an addition: it re-scores every lead, spends quota, and
8629
+ surfaces exactly the same companies. Offer instead to find out whether any
8630
+ question is testing the WRONG thing.
8631
+ 2. **The audience filter already enforces it** \u2014 sector, headcount, territory.
8632
+ 3. **An existing question already tests that dimension.** Two questions on one
8633
+ dimension waste a slot and add no signal.
8634
+ 4. **The question the user asked for is not decisive.** See step 4: say so,
8635
+ offer the sharper version, and write only what they then choose. Adding a
8636
+ question you know separates nothing is worse than adding none.
8637
+
8638
+ **The ceiling is 5 questions.** Read the count before you answer an "add a
8639
+ question" request: at 5 the honest answer is not "sure, I'll add it". Say in
8640
+ that same turn that the set is full, list the five, and let the USER name which
8641
+ one goes. Never pre-pick the casualty.
8642
+
8643
+ **4 \u2014 Write a DECISIVE question.** Check all six before you propose the text:
8644
+
8645
+ 1. **The estimative marker is literal and mandatory.** English questions start
8646
+ \`Is the company likely to \u2026\`. French questions start
8647
+ \`L'entreprise est-elle susceptible de/d' \u2026\`. There is no third form. The
8648
+ scorer works from public text it cannot verify, so a verifiable question
8649
+ scores almost everything as no.
8650
+ - \u2705 \`Is the company likely to run its own in-house maintenance crew?\`
8651
+ - \u274C \`Does the company run its own maintenance crew?\`
8652
+ - \u274C \`Is the company a cold-storage plant?\` \u2014 no \`likely to\`
8653
+ - \u2705 \`L'entreprise est-elle susceptible d'\xEAtre en liquidation judiciaire ?\`
8654
+ - \u274C \`L'entreprise est-elle en liquidation ?\` \u2014 no \`susceptible\`
8655
+ 2. **It tests the lead as a BUYER.** Before you write a question, ask: would
8656
+ a company answering yes write a cheque to THIS user? A question that only
8657
+ describes what the lead's own business does \u2014 "Is the company likely to
8658
+ manufacture branded pharmaceuticals?" for a seller of advertising \u2014 is a
8659
+ category test, not a buying test, and it scores the user's competitors and
8660
+ suppliers as well as their prospects. Test the need the user's offering
8661
+ meets: "Is the company likely to run consumer campaigns that need paid media
8662
+ placement?"
8663
+ 3. **Estimable from public material** \u2014 the company's website, its about page,
8664
+ its job postings, its registry entry. Never its budget, its internal plans
8665
+ or its future intentions. When the user's rule is un-observable ("has budget
8666
+ for copywriting"), propose the observable proxy, and tell them you swapped
8667
+ it and why.
8668
+ 4. **One dimension each.** \`Is the company likely to operate a cold-storage
8669
+ plant AND run its own maintenance crew?\` is two questions. Split it into
8670
+ two, or pick the one that discriminates better.
8671
+ 5. **Decisive.** It should split companies in general roughly 30/70 while the
8672
+ user's own customers answer yes. A question nearly everyone answers yes to
8673
+ \u2014 "has a website", "uses email", "is a company" \u2014 separates nobody. Do NOT
8674
+ write it as asked: say it would add no signal, and offer the sharper version
8675
+ you would write instead.
8676
+ 6. **\u2264120 characters, in the user's language.**
8677
+
8678
+ An org with **zero** questions scores every lead on firmographics alone. That
8679
+ is a finding worth stating, and the fix is a starter set of **3** questions on
8680
+ three different dimensions \u2014 not one. Propose all three at once.
8681
+
8682
+ **5 \u2014 The change is the user's call, not yours.** This holds for the questions,
8683
+ the targeting prompt and the buyer profile alike. Show the exact text you
8684
+ propose and what it will change, then get an explicit yes before calling
8685
+ \`leadbay_set_qualification_questions\` or \`leadbay_refine_prompt\`. Ask through
8686
+ \`ask_user_input_v0\` when the host offers it. A removal or a swap additionally
8687
+ needs \`confirm:true\`. Do not write an org setting in the same turn the user
8688
+ first stated the rule \u2014 and once they have said yes, actually write it:
8689
+ describing the change is not making it.
8690
+
8691
+ **Answer the ask as well.** A rule stated in passing \u2014 *"sors-moi les leads du
8692
+ jour, et arr\xEAte de me remonter des h\xF4pitaux publics"* \u2014 does not replace the
8693
+ ask. Deliver the leads first, then raise the setting.
8694
+
8695
+
8465
8696
  WHEN TO USE: when audience filters (leadbay_adjust_audience) aren't enough.
8466
8697
 
8467
8698
  WHEN NOT TO USE: to answer a pending clarification \u2014 that's leadbay_answer_clarification.
@@ -9513,7 +9744,35 @@ WHEN NOT TO USE: the user is just skipping ONE outreach attempt \u2014 that's a
9513
9744
 
9514
9745
  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\`.
9515
9746
  `;
9516
- var 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".
9747
+ var leadbay_set_qualification_questions = `## WHEN TO USE
9748
+
9749
+ 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".
9750
+
9751
+ 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\`.
9752
+
9753
+ 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.
9754
+
9755
+ Examples that SHOULD invoke this tool:
9756
+ - "Our best customers are cold-storage plants that run their own maintenance crews \u2014 update my qualification for that."
9757
+ - "\xC9carte les soci\xE9t\xE9s en liquidation, je ne veux plus les voir."
9758
+ - "Remove the flooring question and add one about install crews."
9759
+
9760
+ Examples that should NOT invoke this tool (sound similar, route elsewhere):
9761
+ - "What qualification questions does Leadbay use?"
9762
+ - "Add fintech to my Joinery lens."
9763
+ - "Exclude Groupe Solidum, we had an unpaid invoice with them."
9764
+
9765
+ ## RENDER (quick)
9766
+
9767
+ Before writing: show the exact question text you propose, say what it
9768
+ changes, and ask for a yes (route via ask_user_input_v0). After writing:
9769
+ one confirmation line ("Added 1 question \u2014 you now score leads against 4
9770
+ questions.") then the resulting questions as a numbered list. On a
9771
+ non-changing preview, surface the \`hint\` and ask \u2014 never auto-confirm.
9772
+
9773
+ ---
9774
+
9775
+ 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".
9517
9776
 
9518
9777
  The backend stores the list as a whole, so this tool reads the current questions and applies your change:
9519
9778
 
@@ -9525,11 +9784,142 @@ Leadbay allows **at most 5** qualification questions. If a change would exceed 5
9525
9784
 
9526
9785
  **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.
9527
9786
 
9528
- 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?").
9787
+ Returns the resulting \`{qualification_questions, count, previous_count, changed}\`.
9788
+
9789
+ ### A stated fit rule is a SETTING \u2014 read, decide, propose, then write
9790
+
9791
+ **Never answer a stated rule from memory.** "C'est not\xE9", "already applied",
9792
+ "I'll keep that in mind", "the rule is now active" \u2014 every one of those is a
9793
+ claim about the user's ACCOUNT, and it is false unless a tool call made it
9794
+ true. The rule lives in the org's settings or it does not exist: your context
9795
+ window ends with this conversation, and the next session, the scheduled run and
9796
+ the user's colleague all read the account, not your memory. If you have not
9797
+ called a tool, do not say the rule is in place.
9798
+
9799
+ When the user says what makes a lead good or bad \u2014 *"\xE9carte les soci\xE9t\xE9s
9800
+ liquid\xE9es"*, *"je ne veux pas d'associations"*, *"our best customers run their
9801
+ own maintenance crews"*, *"les leads ne sont pas pertinents"* \u2014 they are
9802
+ describing their account, not just this batch. Filter the batch and they say
9803
+ it again next week; their questions, buyer profile and targeting prompt never
9804
+ move.
9805
+
9806
+ **1 \u2014 Read before you decide.** Call \`leadbay_get_qualification_questions\`
9807
+ first. It returns the question set PLUS the ideal buyer profile and the
9808
+ targeting prompt those questions sit beside. You cannot judge whether a rule is
9809
+ already covered without seeing them.
9810
+
9811
+ **2 \u2014 Decide WHERE the rule belongs.** One rule, one destination:
9812
+
9813
+ | What the user stated | Where it belongs |
9814
+ |---|---|
9815
+ | A sector, a headcount band, a territory | \`leadbay_adjust_audience\` / \`leadbay_new_lens\` filters \u2014 never a question |
9816
+ | 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 |
9817
+ | 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\` |
9818
+ | Named companies \u2014 "exclude Groupe Solidum, Dentego" | \`leadbay_dislike_lead\` / \`leadbay_set_lead_status\` on those leads. A question must NEVER name a company |
9819
+ | 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 |
9820
+ | 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 |
9821
+ | 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 |
9822
+
9823
+ **3 \u2014 Decide whether to change anything at all.** Touching a question
9824
+ re-scores every lead in the pipeline and draws on the org's quota, so a change
9825
+ that surfaces the same companies is a pure loss. Four reasons to write NOTHING
9826
+ and say why:
9827
+
9828
+ 1. **An existing question already covers the rule.** Quote that question back
9829
+ and stop. **Never reword a question that already means the same thing** \u2014
9830
+ even when the user asks you to "clarify" or "improve" the wording. A reword
9831
+ is a removal plus an addition: it re-scores every lead, spends quota, and
9832
+ surfaces exactly the same companies. Offer instead to find out whether any
9833
+ question is testing the WRONG thing.
9834
+ 2. **The audience filter already enforces it** \u2014 sector, headcount, territory.
9835
+ 3. **An existing question already tests that dimension.** Two questions on one
9836
+ dimension waste a slot and add no signal.
9837
+ 4. **The question the user asked for is not decisive.** See step 4: say so,
9838
+ offer the sharper version, and write only what they then choose. Adding a
9839
+ question you know separates nothing is worse than adding none.
9840
+
9841
+ **The ceiling is 5 questions.** Read the count before you answer an "add a
9842
+ question" request: at 5 the honest answer is not "sure, I'll add it". Say in
9843
+ that same turn that the set is full, list the five, and let the USER name which
9844
+ one goes. Never pre-pick the casualty.
9845
+
9846
+ **4 \u2014 Write a DECISIVE question.** Check all six before you propose the text:
9847
+
9848
+ 1. **The estimative marker is literal and mandatory.** English questions start
9849
+ \`Is the company likely to \u2026\`. French questions start
9850
+ \`L'entreprise est-elle susceptible de/d' \u2026\`. There is no third form. The
9851
+ scorer works from public text it cannot verify, so a verifiable question
9852
+ scores almost everything as no.
9853
+ - \u2705 \`Is the company likely to run its own in-house maintenance crew?\`
9854
+ - \u274C \`Does the company run its own maintenance crew?\`
9855
+ - \u274C \`Is the company a cold-storage plant?\` \u2014 no \`likely to\`
9856
+ - \u2705 \`L'entreprise est-elle susceptible d'\xEAtre en liquidation judiciaire ?\`
9857
+ - \u274C \`L'entreprise est-elle en liquidation ?\` \u2014 no \`susceptible\`
9858
+ 2. **It tests the lead as a BUYER.** Before you write a question, ask: would
9859
+ a company answering yes write a cheque to THIS user? A question that only
9860
+ describes what the lead's own business does \u2014 "Is the company likely to
9861
+ manufacture branded pharmaceuticals?" for a seller of advertising \u2014 is a
9862
+ category test, not a buying test, and it scores the user's competitors and
9863
+ suppliers as well as their prospects. Test the need the user's offering
9864
+ meets: "Is the company likely to run consumer campaigns that need paid media
9865
+ placement?"
9866
+ 3. **Estimable from public material** \u2014 the company's website, its about page,
9867
+ its job postings, its registry entry. Never its budget, its internal plans
9868
+ or its future intentions. When the user's rule is un-observable ("has budget
9869
+ for copywriting"), propose the observable proxy, and tell them you swapped
9870
+ it and why.
9871
+ 4. **One dimension each.** \`Is the company likely to operate a cold-storage
9872
+ plant AND run its own maintenance crew?\` is two questions. Split it into
9873
+ two, or pick the one that discriminates better.
9874
+ 5. **Decisive.** It should split companies in general roughly 30/70 while the
9875
+ user's own customers answer yes. A question nearly everyone answers yes to
9876
+ \u2014 "has a website", "uses email", "is a company" \u2014 separates nobody. Do NOT
9877
+ write it as asked: say it would add no signal, and offer the sharper version
9878
+ you would write instead.
9879
+ 6. **\u2264120 characters, in the user's language.**
9880
+
9881
+ An org with **zero** questions scores every lead on firmographics alone. That
9882
+ is a finding worth stating, and the fix is a starter set of **3** questions on
9883
+ three different dimensions \u2014 not one. Propose all three at once.
9884
+
9885
+ **5 \u2014 The change is the user's call, not yours.** This holds for the questions,
9886
+ the targeting prompt and the buyer profile alike. Show the exact text you
9887
+ propose and what it will change, then get an explicit yes before calling
9888
+ \`leadbay_set_qualification_questions\` or \`leadbay_refine_prompt\`. Ask through
9889
+ \`ask_user_input_v0\` when the host offers it. A removal or a swap additionally
9890
+ needs \`confirm:true\`. Do not write an org setting in the same turn the user
9891
+ first stated the rule \u2014 and once they have said yes, actually write it:
9892
+ describing the change is not making it.
9893
+
9894
+ **Answer the ask as well.** A rule stated in passing \u2014 *"sors-moi les leads du
9895
+ jour, et arr\xEAte de me remonter des h\xF4pitaux publics"* \u2014 does not replace the
9896
+ ask. Deliver the leads first, then raise the setting.
9897
+
9898
+
9899
+ WHEN TO USE: the user stated a durable company trait no existing question covers, and they have agreed to the exact text you proposed.
9900
+
9901
+ 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.
9902
+
9903
+ ## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
9904
+
9905
+ 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.
9529
9906
 
9530
- WHEN TO USE: the user wants to change the org's qualification questions.
9907
+ **The Big Three** \u2014 when a tool result fits, route there:
9908
+
9909
+ | Host widget | Use when | Field map (from Leadbay payload) |
9910
+ |---|---|---|
9911
+ | \`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. |
9912
+ | \`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") |
9913
+ | \`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. |
9914
+
9915
+ 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.
9916
+
9917
+ **Rules:**
9918
+ - 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.
9919
+ - Pass identifiers (place_id, lead.id, contact_id) verbatim. Don't rewrite.
9920
+ - 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.
9921
+ - One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
9531
9922
 
9532
- 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.
9533
9923
 
9534
9924
  ### RENDERING
9535
9925
 
@@ -10123,7 +10513,7 @@ Do NOT use for: "show me today's leads / what's new today" \u2192 \`leadbay_pull
10123
10513
  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.
10124
10514
 
10125
10515
  Examples that SHOULD invoke this tool:
10126
- - "Find me 10 gyms around Dallas that would buy our flooring, with someone I can call."
10516
+ - "Find me 10 gyms around Dallas that would buy our flooring, with a contact."
10127
10517
  - "Get me 20 new US SaaS companies, 50-2000 employees, with the VP People's email."
10128
10518
  - "We're launching in Lyon \u2014 find 15 hotels that fit our ICP."
10129
10519
 
@@ -10134,11 +10524,10 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
10134
10524
 
10135
10525
  ## RENDER (quick)
10136
10526
 
10137
- 3-col table of delivered leads in returned order: col 1 = 10-segment fit
10138
- bar + linked company \xB7 location \xB7 size; col 2 = why-fits \u226420 words; col 3
10139
- = contact + found channels. ALWAYS close with the honest funnel line
10140
- (matched/examined/delivered/stop reason) \u2014 especially on 0
10141
- delivered. Full algorithm below.
10527
+ 3-col table of delivered leads in returned order: col 1 = 10-segment fit bar
10528
+ + linked company \xB7 location \xB7 size; col 2 = why-fits \u226420 words; col 3 =
10529
+ contact + found channels. ALWAYS close with the honest funnel line
10530
+ (matched/examined/delivered/stop reason), especially on 0 delivered.
10142
10531
 
10143
10532
  ---
10144
10533
 
@@ -10147,27 +10536,25 @@ company universe, applies hard filters, skips what the org already knows
10147
10536
  (\`novelty: org\`), optionally qualifies against the org's own intelligence
10148
10537
  (questions, tags, ideal buyer profile \u2014 frozen at submit), and optionally reveals
10149
10538
  contact channels. Polls up to \`wait_seconds\` (default 45); a longer job returns
10150
- \`still_running\` + \`next_poll\` \u2014 hand off to \`leadbay_lead_job_status\`. Jobs run
10539
+ \`still_running\` + \`next_poll\` \u2014 hand to \`leadbay_lead_job_status\`. Jobs run
10151
10540
  \u226430 min, results kept 30 days.
10152
10541
 
10153
10542
  **Free vs usage quota \u2014 never use quota silently.** Default (\`qualify: false\`,
10154
10543
  \`channels: []\`) is FREE: company profile + fit score + cached research +
10155
10544
  contact identity. \`qualify: true\` (per candidate EXAMINED, capped by
10156
10545
  \`exploration_cap\`/\`max_cost\`) and \`channels\` (only when a value is found) draw
10157
- on the org's usage quota; nothing is invoiced. Enforced in code: such a call is WITHHELD unless it
10158
- carries \`confirm: true\` \u2014 nothing is submitted and you get
10546
+ on the org's usage quota; nothing is invoiced. Enforced in code: such a call is
10547
+ WITHHELD unless it carries \`confirm: true\` \u2014 nothing is submitted and you get
10159
10548
  \`mode: "needs_confirmation"\` with a real quote to show the user. Re-call with
10160
10549
  \`confirm: true\` on their go-ahead ("go ahead / get their emails" counts).
10161
- \`confirm: false\` vetoes. Free needs no consent. **Preview free first** \u2014
10162
- reshaping an off-profile seed is free, exploring it with \`qualify: true\` is
10163
- not.
10550
+ \`confirm: false\` vetoes. **Preview free first** \u2014 reshaping an off-profile seed
10551
+ is free, exploring it with \`qualify: true\` is not.
10164
10552
 
10165
10553
  **Ad-hoc exclusions ("no chains") are enforced by NO tier** \u2014 \`filters\` has no
10166
- exclusion key, and \`qualify\` scores against the org's FROZEN questions and IBP,
10167
- which need not mention chains; the seed's inverse only shifts ranking.
10168
- Violators can survive, use quota and be delivered \u2014 post-filter them yourself
10169
- and say the tier didn't enforce it. Durable enforcement \u2192
10170
- \`leadbay_refine_prompt\`.
10554
+ exclusion key, and \`qualify\` scores against the org's FROZEN questions and IBP.
10555
+ Violators survive, use quota and get delivered: post-filter them yourself and
10556
+ say the tier didn't enforce it. Durable enforcement \u2192
10557
+ \`leadbay_set_qualification_questions\` / \`leadbay_refine_prompt\`.
10171
10558
 
10172
10559
  ### Crafting the \`example_lead\` seed \u2014 the input that decides result quality
10173
10560
 
@@ -10187,13 +10574,13 @@ measured:
10187
10574
  model, what they sell or operate, who they serve, observable scale. Write
10188
10575
  it like the first paragraph of their About-Us page.
10189
10576
  - STRONG: "Operator of full-service fitness centers offering strength
10190
- areas, group classes and personal training to members across multiple
10191
- clubs."
10192
- - WEAK (generic): "A gym in Texas."
10193
- - WRONG (seller-side): "Supplier of durable modular flooring for gyms."
10194
- 4. **No event language.** "hiring", "expanding", "just raised" are not
10195
- filters \u2014 registry descriptions never contain them, so they dilute the
10196
- profile. Purchase triggers belong in the org's qualification questions.
10577
+ areas, group classes and personal training to members across clubs."
10578
+ - WEAK: "A gym in Texas." WRONG: "Supplier of gym flooring." (seller-side)
10579
+ 4. **NO event language \u2014 in \`description\` or \`query\`.** "recrute", "hiring",
10580
+ "expanding", "just raised" never appear in registry text, so they match
10581
+ nothing. Send the trigger to \`leadbay_set_qualification_questions\` or
10582
+ \`leadbay_refine_prompt\` and say so. "companies hiring a senior SDR" seeds as
10583
+ "B2B software company operating an in-house outbound sales team."
10197
10584
  5. **No meta-markers.** Never "(example)", "(fictional)", "(placeholder)".
10198
10585
  6. **Hard constraints go in \`filters\`, not prose \u2014 exact keys:**
10199
10586
  \`sectors: string[]\`, \`locations: string[]\`, \`employees_min: number\`,
@@ -10203,24 +10590,22 @@ measured:
10203
10590
  never a country: this workspace's own is dropped, any other is refused.
10204
10591
  7. **Prefer \`example_lead\` over \`query\`.** Query matches topic *vocabulary*:
10205
10592
  "gyms that need durable flooring" surfaced flooring VENDORS, 0 delivered.
10206
- Use \`query\` only for signal an example can't express.
10207
10593
  8. **One seed per buyer archetype.** An ask spanning two segments ("gyms and
10208
10594
  warehouses") needs one search each with its own description and
10209
- \`request_id\` \u2014 a blended seed lands between the clusters and matches
10210
- neither.
10595
+ \`request_id\` \u2014 a blended seed lands between the clusters, matching neither.
10211
10596
 
10212
10597
 
10213
10598
  **Parameter notes**
10214
10599
  - \`request_id\` (REQUIRED) is the retry contract: SAME value retrying the same
10215
10600
  ask (same live job, no double launch); NEW for a changed ask. Derive from ask
10216
10601
  + archetype + date: \`gyms-dallas-2026-07-28\`.
10217
- - Never lower \`min_ai_score\` together with \`channels\` \u2014 that reveals emails for
10218
- leads the AI just scored as junk.
10219
- - \`count\` \u2264 50; \u22643 active jobs/org; \u226410 submits/hour (429 + Retry-After \u2014
10220
- wait, don't hammer).
10602
+ - Never lower \`min_ai_score\` with \`channels\` \u2014 that reveals emails for leads
10603
+ the AI just scored as junk.
10604
+ - \`count\` \u2264 50; \u22643 active jobs/org; \u226410 submits/hour (429 + Retry-After \u2014 wait,
10605
+ don't hammer).
10221
10606
 
10222
10607
  **Read the result honestly** \u2014 \`funnel\` + \`explain.scope_notes\` tell the story;
10223
- zero delivered gets a cause and a next move (rules in RENDERING).
10608
+ zero delivered gets a cause and a next move (RENDERING).
10224
10609
 
10225
10610
  ---
10226
10611
 
@@ -17382,8 +17767,16 @@ function townName(value) {
17382
17767
  }
17383
17768
  function cityHintCore(cityHint) {
17384
17769
  const head = cityHint.split(",")[0] ?? "";
17385
- const expanded = expandAlias(head);
17386
- return { name: townName(expanded), isCity: expanded !== head };
17770
+ const segments = cityHint.split(",").map((part) => part.trim()).filter(Boolean);
17771
+ for (let take = segments.length; take > 0; take -= 1) {
17772
+ const candidate = segments.slice(0, take).join(" ");
17773
+ const expanded = expandAlias(candidate);
17774
+ if (expanded !== candidate) {
17775
+ return { name: townName(expanded), isCity: true };
17776
+ }
17777
+ }
17778
+ const prefixed = ADMIN_PREFIX.test(normalizeGeo(head));
17779
+ return { name: townName(head), isCity: prefixed };
17387
17780
  }
17388
17781
  function stateFieldOf(lead) {
17389
17782
  const state = lead?.location?.state;
@@ -19167,6 +19560,174 @@ var researchLeadByNameFuzzy = {
19167
19560
  }
19168
19561
  };
19169
19562
 
19563
+ // ../core/dist/composite/set-qualification-questions.js
19564
+ var MAX_QUESTIONS = 5;
19565
+ var ESTIMATIVE_MARKERS = [
19566
+ "is the company likely to",
19567
+ "l'entreprise est-elle susceptible",
19568
+ "l\u2019entreprise est-elle susceptible"
19569
+ ];
19570
+ function formWarnings(questions) {
19571
+ const out = [];
19572
+ for (const q of questions) {
19573
+ const low = q.trim().toLowerCase();
19574
+ if (!ESTIMATIVE_MARKERS.some((m) => low.startsWith(m))) {
19575
+ 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 ...".`);
19576
+ }
19577
+ if (q.length > 120) {
19578
+ out.push(`"${q.slice(0, 60)}\u2026" is ${q.length} chars; keep a question under 120.`);
19579
+ }
19580
+ }
19581
+ return out;
19582
+ }
19583
+ var setQualificationQuestions = {
19584
+ name: "leadbay_set_qualification_questions",
19585
+ annotations: {
19586
+ title: "Modify the org's qualification questions",
19587
+ readOnlyHint: false,
19588
+ destructiveHint: true,
19589
+ idempotentHint: false,
19590
+ openWorldHint: true
19591
+ },
19592
+ description: leadbay_set_qualification_questions,
19593
+ write: true,
19594
+ inputSchema: {
19595
+ type: "object",
19596
+ properties: {
19597
+ questions: {
19598
+ type: "array",
19599
+ items: { type: "string", maxLength: 255 },
19600
+ 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.`
19601
+ },
19602
+ add: {
19603
+ type: "array",
19604
+ items: { type: "string", maxLength: 255 },
19605
+ 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.`
19606
+ },
19607
+ remove: {
19608
+ type: "array",
19609
+ items: { type: "string" },
19610
+ description: "Exact question strings to remove from the current list. Mutually exclusive with `questions`. A removal requires confirm:true."
19611
+ },
19612
+ confirm: {
19613
+ type: "boolean",
19614
+ 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."
19615
+ }
19616
+ },
19617
+ additionalProperties: false
19618
+ },
19619
+ outputSchema: {
19620
+ type: "object",
19621
+ properties: {
19622
+ qualification_questions: {
19623
+ type: "array",
19624
+ description: "The questions AFTER the change. Each: {question}.",
19625
+ items: { type: "object" }
19626
+ },
19627
+ count: { type: "number" },
19628
+ previous_count: { type: "number" },
19629
+ changed: {
19630
+ type: "boolean",
19631
+ description: "True when the list was actually written; false on a no-op or an unconfirmed shrink."
19632
+ },
19633
+ region: { type: "string" },
19634
+ form_warnings: {
19635
+ type: "array",
19636
+ items: { type: "string" },
19637
+ 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."
19638
+ },
19639
+ hint: {
19640
+ type: "string",
19641
+ description: "Operator note \u2014 confirm prompt on a shrink, or a no-op explanation."
19642
+ },
19643
+ _meta: { type: "object" }
19644
+ },
19645
+ required: ["qualification_questions", "count", "changed"]
19646
+ },
19647
+ execute: async (client, params, ctx) => {
19648
+ const hasSet = Array.isArray(params.questions);
19649
+ const hasAdd = Array.isArray(params.add) && params.add.length > 0;
19650
+ const hasRemove = Array.isArray(params.remove) && params.remove.length > 0;
19651
+ if (hasSet && (hasAdd || hasRemove)) {
19652
+ 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}");
19653
+ }
19654
+ if (!hasSet && !hasAdd && !hasRemove) {
19655
+ 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}");
19656
+ }
19657
+ const orgId = await client.resolveOrgId();
19658
+ const current = await client.request("GET", `/organizations/${orgId}/ai_agent_questions`);
19659
+ const currentQs = (current ?? []).map((q) => q.question);
19660
+ const norm = (s) => s.trim();
19661
+ let next;
19662
+ if (hasSet) {
19663
+ next = params.questions.map(norm).filter((s) => s.length > 0);
19664
+ } else {
19665
+ next = [...currentQs];
19666
+ if (hasRemove) {
19667
+ const drop = new Set(params.remove.map(norm));
19668
+ next = next.filter((q) => !drop.has(norm(q)));
19669
+ }
19670
+ if (hasAdd) {
19671
+ const seen2 = new Set(next.map(norm));
19672
+ for (const q of params.add.map(norm)) {
19673
+ if (q.length > 0 && !seen2.has(q)) {
19674
+ next.push(q);
19675
+ seen2.add(q);
19676
+ }
19677
+ }
19678
+ }
19679
+ }
19680
+ const seen = /* @__PURE__ */ new Set();
19681
+ next = next.filter((q) => {
19682
+ const k = norm(q);
19683
+ if (seen.has(k))
19684
+ return false;
19685
+ seen.add(k);
19686
+ return true;
19687
+ });
19688
+ if (next.length > MAX_QUESTIONS) {
19689
+ 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}");
19690
+ }
19691
+ const previousCount = currentQs.length;
19692
+ const noChange = next.length === currentQs.length && next.every((q, i) => norm(q) === norm(currentQs[i] ?? ""));
19693
+ if (noChange) {
19694
+ return {
19695
+ qualification_questions: currentQs.map((q) => ({ question: q })),
19696
+ count: currentQs.length,
19697
+ previous_count: previousCount,
19698
+ changed: false,
19699
+ region: client.region,
19700
+ 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."
19701
+ };
19702
+ }
19703
+ const removed = currentQs.filter((q) => !next.some((n) => norm(n) === norm(q)));
19704
+ if (removed.length > 0 && params.confirm !== true) {
19705
+ return {
19706
+ qualification_questions: currentQs.map((q) => ({ question: q })),
19707
+ count: currentQs.length,
19708
+ previous_count: previousCount,
19709
+ changed: false,
19710
+ region: client.region,
19711
+ 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.`
19712
+ };
19713
+ }
19714
+ await client.requestVoid("POST", `/organizations/${orgId}`, {
19715
+ ai_agent_lead_questions: next
19716
+ });
19717
+ client.invalidateTasteProfile();
19718
+ const written = hasSet ? next : (params.add ?? []).map(norm).filter((q) => next.includes(q));
19719
+ const warnings = formWarnings(written);
19720
+ return {
19721
+ qualification_questions: next.map((q) => ({ question: q })),
19722
+ count: next.length,
19723
+ previous_count: previousCount,
19724
+ changed: true,
19725
+ region: client.region,
19726
+ ...warnings.length > 0 ? { form_warnings: warnings } : {}
19727
+ };
19728
+ }
19729
+ };
19730
+
19170
19731
  // ../core/dist/composite/get-qualification-questions.js
19171
19732
  var getQualificationQuestions = {
19172
19733
  name: "leadbay_get_qualification_questions",
@@ -19195,6 +19756,13 @@ var getQualificationQuestions = {
19195
19756
  type: "number",
19196
19757
  description: "Number of qualification questions configured."
19197
19758
  },
19759
+ ideal_buyer_profile: {
19760
+ 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."
19761
+ },
19762
+ targeting_prompt: {
19763
+ type: ["string", "null"],
19764
+ 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."
19765
+ },
19198
19766
  is_admin: {
19199
19767
  type: "boolean",
19200
19768
  description: "Whether the current bearer-token holder is an org admin. Admins can modify the questions via leadbay_set_qualification_questions."
@@ -19213,11 +19781,21 @@ var getQualificationQuestions = {
19213
19781
  const isAdmin = me?.admin ?? false;
19214
19782
  const orgId = me?.organization?.id ?? await client.resolveOrgId();
19215
19783
  const questions = await client.request("GET", `/organizations/${orgId}/ai_agent_questions`) ?? [];
19784
+ const [ibpResult, promptResult] = await Promise.allSettled([
19785
+ client.request("GET", `/organizations/${orgId}/ideal_buyer_profile`),
19786
+ client.request("GET", `/organizations/${orgId}/user_prompt`)
19787
+ ]);
19788
+ const ibp = ibpResult.status === "fulfilled" ? ibpResult.value ?? null : null;
19789
+ const targetingPrompt = promptResult.status === "fulfilled" ? promptResult.value?.prompt ?? null : null;
19216
19790
  let hint;
19217
- if (questions.length === 0) {
19218
- hint = "No qualification questions configured yet. Use leadbay_set_qualification_questions to add some, or leadbay_refine_prompt to shape the AI agent.";
19791
+ if (questions.length >= MAX_QUESTIONS && isAdmin) {
19792
+ 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.`;
19793
+ } else if (questions.length >= MAX_QUESTIONS) {
19794
+ 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.`;
19795
+ } else if (questions.length === 0) {
19796
+ 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.`;
19219
19797
  } else if (isAdmin) {
19220
- hint = "You're an org admin \u2014 use leadbay_set_qualification_questions to add, remove, or replace these questions.";
19798
+ 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.`;
19221
19799
  }
19222
19800
  return {
19223
19801
  qualification_questions: questions.map((q) => ({
@@ -19226,6 +19804,12 @@ var getQualificationQuestions = {
19226
19804
  lang: q.lang
19227
19805
  })),
19228
19806
  count: questions.length,
19807
+ ideal_buyer_profile: ibp ? {
19808
+ summary: ibp.summary,
19809
+ key_characteristics: ibp.key_characteristics,
19810
+ anti_patterns: ibp.anti_patterns
19811
+ } : null,
19812
+ targeting_prompt: targetingPrompt,
19229
19813
  is_admin: isAdmin,
19230
19814
  region: client.region,
19231
19815
  ...hint ? { hint } : {}
@@ -19450,148 +20034,6 @@ var gettingStarted = {
19450
20034
  }
19451
20035
  };
19452
20036
 
19453
- // ../core/dist/composite/set-qualification-questions.js
19454
- var setQualificationQuestions = {
19455
- name: "leadbay_set_qualification_questions",
19456
- annotations: {
19457
- title: "Modify the org's qualification questions",
19458
- readOnlyHint: false,
19459
- destructiveHint: true,
19460
- idempotentHint: false,
19461
- openWorldHint: true
19462
- },
19463
- description: leadbay_set_qualification_questions,
19464
- write: true,
19465
- inputSchema: {
19466
- type: "object",
19467
- properties: {
19468
- questions: {
19469
- type: "array",
19470
- items: { type: "string", maxLength: 255 },
19471
- description: "Full replacement list of qualification questions (replaces ALL current questions). Mutually exclusive with add/remove."
19472
- },
19473
- add: {
19474
- type: "array",
19475
- items: { type: "string", maxLength: 255 },
19476
- description: "Questions to append to the current list (deduped). Mutually exclusive with `questions`."
19477
- },
19478
- remove: {
19479
- type: "array",
19480
- items: { type: "string" },
19481
- description: "Exact question strings to remove from the current list. Mutually exclusive with `questions`. A removal requires confirm:true."
19482
- },
19483
- confirm: {
19484
- type: "boolean",
19485
- 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."
19486
- }
19487
- },
19488
- additionalProperties: false
19489
- },
19490
- outputSchema: {
19491
- type: "object",
19492
- properties: {
19493
- qualification_questions: {
19494
- type: "array",
19495
- description: "The questions AFTER the change. Each: {question}.",
19496
- items: { type: "object" }
19497
- },
19498
- count: { type: "number" },
19499
- previous_count: { type: "number" },
19500
- changed: {
19501
- type: "boolean",
19502
- description: "True when the list was actually written; false on a no-op or an unconfirmed shrink."
19503
- },
19504
- region: { type: "string" },
19505
- hint: {
19506
- type: "string",
19507
- description: "Operator note \u2014 confirm prompt on a shrink, or a no-op explanation."
19508
- },
19509
- _meta: { type: "object" }
19510
- },
19511
- required: ["qualification_questions", "count", "changed"]
19512
- },
19513
- execute: async (client, params, ctx) => {
19514
- const hasSet = Array.isArray(params.questions);
19515
- const hasAdd = Array.isArray(params.add) && params.add.length > 0;
19516
- const hasRemove = Array.isArray(params.remove) && params.remove.length > 0;
19517
- if (hasSet && (hasAdd || hasRemove)) {
19518
- 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}");
19519
- }
19520
- if (!hasSet && !hasAdd && !hasRemove) {
19521
- 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}");
19522
- }
19523
- const orgId = await client.resolveOrgId();
19524
- const current = await client.request("GET", `/organizations/${orgId}/ai_agent_questions`);
19525
- const currentQs = (current ?? []).map((q) => q.question);
19526
- const norm = (s) => s.trim();
19527
- let next;
19528
- if (hasSet) {
19529
- next = params.questions.map(norm).filter((s) => s.length > 0);
19530
- } else {
19531
- next = [...currentQs];
19532
- if (hasRemove) {
19533
- const drop = new Set(params.remove.map(norm));
19534
- next = next.filter((q) => !drop.has(norm(q)));
19535
- }
19536
- if (hasAdd) {
19537
- const seen2 = new Set(next.map(norm));
19538
- for (const q of params.add.map(norm)) {
19539
- if (q.length > 0 && !seen2.has(q)) {
19540
- next.push(q);
19541
- seen2.add(q);
19542
- }
19543
- }
19544
- }
19545
- }
19546
- const seen = /* @__PURE__ */ new Set();
19547
- next = next.filter((q) => {
19548
- const k = norm(q);
19549
- if (seen.has(k))
19550
- return false;
19551
- seen.add(k);
19552
- return true;
19553
- });
19554
- const MAX_QUESTIONS = 5;
19555
- if (next.length > MAX_QUESTIONS) {
19556
- 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}");
19557
- }
19558
- const previousCount = currentQs.length;
19559
- const noChange = next.length === currentQs.length && next.every((q, i) => norm(q) === norm(currentQs[i] ?? ""));
19560
- if (noChange) {
19561
- return {
19562
- qualification_questions: currentQs.map((q) => ({ question: q })),
19563
- count: currentQs.length,
19564
- previous_count: previousCount,
19565
- changed: false,
19566
- region: client.region,
19567
- 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."
19568
- };
19569
- }
19570
- const removed = currentQs.filter((q) => !next.some((n) => norm(n) === norm(q)));
19571
- if (removed.length > 0 && params.confirm !== true) {
19572
- return {
19573
- qualification_questions: currentQs.map((q) => ({ question: q })),
19574
- count: currentQs.length,
19575
- previous_count: previousCount,
19576
- changed: false,
19577
- region: client.region,
19578
- 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.`
19579
- };
19580
- }
19581
- await client.requestVoid("POST", `/organizations/${orgId}`, {
19582
- ai_agent_lead_questions: next
19583
- });
19584
- client.invalidateTasteProfile();
19585
- return {
19586
- qualification_questions: next.map((q) => ({ question: q })),
19587
- count: next.length,
19588
- previous_count: previousCount,
19589
- changed: true,
19590
- region: client.region
19591
- };
19592
- }
19593
- };
19594
-
19595
20037
  // ../core/dist/composite/get-lead-custom-fields.js
19596
20038
  var getLeadCustomFields = {
19597
20039
  name: "leadbay_get_lead_custom_fields",
@@ -24546,11 +24988,11 @@ var findNewLeads = {
24546
24988
  properties: {
24547
24989
  query: {
24548
24990
  type: "string",
24549
- 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."
24991
+ 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."
24550
24992
  },
24551
24993
  example_lead: {
24552
24994
  type: "object",
24553
- 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).",
24995
+ 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).",
24554
24996
  properties: {
24555
24997
  name: { type: "string" },
24556
24998
  description: { type: "string" },
@@ -27764,6 +28206,7 @@ var FRICTION = `Problem reports: when the user asks you to report a Leadbay prob
27764
28206
  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.`;
27765
28207
  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.`;
27766
28208
  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.`;
28209
+ 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.`;
27767
28210
  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.`;
27768
28211
  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.`;
27769
28212
  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.`;
@@ -27932,6 +28375,9 @@ function buildServerInstructions(exposed) {
27932
28375
  }
27933
28376
  parts.push(TRIGGERED_BY);
27934
28377
  parts.push(MENTAL_MODEL);
28378
+ if (has("leadbay_get_qualification_questions")) {
28379
+ parts.push(STATED_RULES);
28380
+ }
27935
28381
  if (has("leadbay_create_topup_link")) {
27936
28382
  parts.push(QUOTA_TOPUP);
27937
28383
  }
@@ -28862,7 +29308,7 @@ function parseWriteEnv(env = process.env) {
28862
29308
  }
28863
29309
 
28864
29310
  // src/http-server.ts
28865
- var VERSION = true ? "0.39.3" : "0.0.0-dev";
29311
+ var VERSION = true ? "0.39.5" : "0.0.0-dev";
28866
29312
  var PORT = Number(process.env.PORT ?? 8080);
28867
29313
  var HOST = process.env.HOST ?? "0.0.0.0";
28868
29314
  var logger = {