← All reports

FUN-392 · verification record · 2026-07-24

NL visual-first routing — ambiguity & no-match open the surface, never chat

On iframe-capable hosts (Claude.ai, ChatGPT) an ambiguous or no-match entity lookup now stops asking in chat and opens the right dashboard surface with the search box pre-seeded. Text-only hosts (Cursor, Gemini CLI, MCP inspector) are unchanged. This record captures what changed, the evidence behind it, and — honestly — what is not yet verified.

PR #796 → main 77 files · +2610 / −751 2991 / 0 tests green 29 acceptance criteria 5 review findings resolved

Code & automated suite verified In-host round-trip pending

The change, in one line per surface

host-scoped behaviour delta

Every row is a real user path. Was is the behaviour before this PR; Now is what ships. Unless a row says all hosts, the change applies only to Claude / ChatGPT — text-only clients keep the Was column.

PathWasNow (iframe host)
open_crews / contacts
list_events
— ambiguous name
Chat asks “which one?” or prints a numbered list; nothing opens. One line Found more than one [name]. Opening your [surface]. then the tab opens with the search box seeded to the typed name. No chat question.
open_crew / contact / event
— name matches nothing
Emitted no routing — nothing opened at all. No [entity] found matching "[name]". Opening [list]... and the list opens with the term seeded. reverses a prior lock
create_event / _form
— crew_name matches 2+ · all hosts
A multi-match wrongly fired the no-match line. Found more than one [crew]. Opening the event builder. (combined with the parsed-fields echo), crew row left empty.
add_people_to_event_form
— ambiguous / no-match contact
Recency guess (ambiguous_assumed_first) pre-selected a contact. Locked line fires, the Shared Picker opens seeded with the name, nothing pre-selected.
list_events search
— all hosts
Filtered client-side over page 1 only (25/pg) — a match on a later page was missed; box not clearable. DB-side ilike on event_name (LIKE metachars escaped); the live box drives the query and clears to reveal all.
create_contact
— name + phone, no confirmed
Committed the contact immediately. Opens the pre-filled Add-manually sheet; the organizer’s Save commits. A call carrying email or crew_id is exempt and commits directly (the 3-field prefill can’t round-trip those).
update_profile
— standalone edit, no confirmed
Committed the edit immediately. Opens the Profile visual; the organizer’s Save commits Profile updated. A mid-create timezone capture still commits in one call via confirmed:true in its hint.
delete_crew · update_crew
revoke_crew_link
delete_contact · update_contact
— ambiguous identifier
Chat asked “I found N … Which one?”. Found more than one [name]. Opening your [surface]. directs the follow-up that opens the filtered list. Never commits a delete/update on a disambiguation turn.

Verification evidence

what is actually proven

Acceptance criteria — all 29

docs/urgent-2026-07/FUN-392-acceptance-criteria.md

The full list, verbatim from the repo doc. Host scopes each AC; Status folds in the method — verified = green, and the amber rows are unit-proven but their live-host half is a Track-2 check. Method per section is on the grey group row.

ACHostCriterionStatus
§1 · search_query + locked ambiguity line — unit + local render
AC-01iframeA crew name matching 2+ crews returns exactly Found more than one family. Opening your crews. and opens the Crews tab seeded family — no chat question, no numbered list.verified
AC-02iframeSame for contacts (Opening your contacts.) and events (Opening your events.).verified
AC-03iframeThe pre-filled search box is clearable — deleting the text reveals the full list.verified
AC-04iframeA second consecutive ambiguous lookup re-seeds the box to the new term (does not keep the old one).verified
AC-05text-onlyThe same lookups return the existing text output — no search_query, no behaviour change.verified
§2 · create_event ambiguous crew — unit
AC-06allA crew_name matching 2+ crews narrates Found more than one [crew_name]. Opening the event builder. (with the parsed-fields echo) and opens the builder with the crew row empty — not the no-match line.verified
AC-07allA crew_name matching zero crews narrates No crew found matching "[crew_name]". Opening the event builder. (unchanged Jul-9 behaviour).verified
§3 · add_people opener — unit
AC-08iframeA contact_name matching 2+ contacts narrates Found more than one [name]. Opening your contacts., opens the Shared Picker seeded with the name, and pre-selects nothing.verified
AC-09iframeMatching one contact narrates Adding [name] to event: [event]. and the picker opens (no search pre-fill).verified
AC-10iframeMatching zero contacts narrates No contact found matching "[name]". Opening contacts... and the picker still opens (never a silent generic opener).verified
§4 · no-match opens the list — unit reverses a prior lock
AC-11iframeopen_crew / _contact / _event with a name matching nothing opens the corresponding list seeded. Previously these emitted no routing and nothing opened.verified
AC-12text-onlyThe same lookups return the not-found text with no surface change.verified
§5 · UI search-box seeding — local Chrome (Track 1)
AC-13iframeCrews tab with a seeded search_query shows the term in “Search crews and members” and filters the list to matches.verified
AC-14iframeSame for the Contacts tab (“Search contacts”).verified
AC-15iframeEvents tab opens the search field automatically (seed non-empty) and filters the events list.verified
AC-16iframeThe Shared Picker opens seeded from form_context.search_query with nothing pre-checked.verified
§6 · events DB-side search — handler + engine unit
AC-17iframeA seeded event lookup returns the matching event(s) even when the match is on a later page — filtered DB-side (ilike on event_name), not client-side over page 1.unit ✓ · cross-page = Track 2
AC-18allsearch terms containing %, _ or \ match those characters literally (LIKE metacharacters escaped), not as wildcards.logic ✓ · direct test deferred (O-05)
§7 · contact-create reversal (L53) — unit
AC-19iframecreate_contact with a name + phone and no confirmed does not commit; it opens the pre-filled Add-manually sheet (Creating contact: [name].). The organizer’s Save commits.verified
AC-20iframeA create carrying an email or a crew_id commits directly (exempt — the 3-field prefill can’t round-trip those). No data dropped.verified
AC-21text-onlyThe two-step chat confirm (Add [name] with phone …?confirmed:true) is unchanged.verified
§8 · profile visual-first commit — unit
AC-22iframeA standalone chat profile edit does not commit from chat — it opens the Profile visual (funlet_open_profile); the organizer’s Save commits Profile updated. No chat confirm turn.verified
AC-23iframeA first-time timezone capture mid-create (TIMEZONE_REQUIRED) commits in one call — its hints pass confirmed:true so the builder flow proceeds and the builder opens on retry (not routed through the Profile visual).verified
AC-24text-onlyThe two-step confirm (confirmed:falseawaiting_confirmationconfirmed:true) is unchanged.verified
AC-25allA name-gate save (source:"name_gate") still commits single-step and the FUN-388 NAME_REQUIRED enforcement is intact.verified
§9 · write-tool disambiguation — unit
AC-26iframedelete_crew / update_crew / revoke_crew_link with an ambiguous crew_identifier returns Found more than one [name]. Opening your crews. and directs a follow-up that opens the filtered Crews list — no “Which one?”, no commit.unit ✓ · open-rate = Track 2
AC-27iframedelete_contact / update_contact with an ambiguous contact_identifier does the same for the Contacts list (Opening your contacts.).unit ✓ · open-rate = Track 2
AC-28text-onlyAll five tools keep the existing chat disambiguation question (I found N … Which one: …?) with candidates — unchanged.verified
AC-29allNone of the five tools commits a delete/update on a disambiguation turn (destructive-safety rule preserved).verified

Pre-merge review findings

Codex review · 2026-07-24
#FindingSevResolution
O-01The search param was dropped in the funlet_list_events handler — inert end-to-end.majorfixed 398042ea + handler test
O-02Events search box not clearable; didn’t drive the DB query.majorfixed 2326403c · Chrome-verified
O-03Write-tool redirect is model-dependent (error + agent_hint, no routing _meta): if the host model skips the follow-up, the line shows but the list doesn’t open.majoraccepted Q1a best-effort; open-rate is a Track-2 check
O-04Standalone profile edit committed from chat on iframe hosts.majorresolved 8b6a9434 — opens the Profile visual
O-05No repo-level test for LIKE-escaping; no UI tests for seed/clear.minornoted deferred

Locked-string conformance

nl-visual-first-routing-universal-lock-2026-07-21.md

Verified against the ticket’s authority: the description + comments, then the Notion link they cite (mirrored in funlet-docs). Older Notion docs are superseded. Two conventions QA checks byte-exact — the ambiguity name is not quoted; the no-match name is.

Not yet verified · deferred

read before sign-off
The one gate that remains: the in-host round-trip on Claude.ai and ChatGPT (Track 2) is not done. It needs a staging deploy of funlet-mcp first (no Supabase-MCP writes are permitted from here). Until then, the routing _meta and the write-tool redirect open-rate are proven only by unit tests and a local render — not by a live host.

Source of truth — FUN-392 ticket description + comments, then the Notion lock cited there (mirrored in funlet-docs as nl-visual-first-routing-universal-lock-2026-07-21.md).
Repo record — docs/urgent-2026-07/FUN-392-acceptance-criteria.md · FUN-392-verify-in-host.md · mcp-build-gap-log.md.
Verdict — code & automated suite verified against 8b6a9434; in-host round-trip pending a staging deploy. Generated 2026-07-24.