ClientsFlow · Live-test fixes · Signed-EBO acceptance oracle

EBO — 2026-07-15 live-test fixes (B1–B11)

One signed doc = the human review artifact AND the machine oracle for the ebo-live-build loop. 10 scenarios (S1–S10), 36 steps, grouped into 4 work items · owner decisions in DECISIONS.md folded in · root causes in notes/INV_*.md · lead of record: Kocsy Gergely (prod build 876f15b-dirty+v225).

You do → click / action the walker performs You should see → on-screen pixel expectation Element that changes → copy · look · where What changes underneath → data / state (the probe proves this) Must NOT happen → bug guard critical strong-tier judged pixel-only vision-judged, no probe
Owner decisions folded in (DECISIONS.md, 2026-07-15). B3 = the read-only page is fine; the fix is that the unique per-lead link is missing from most drafts (embed it in the AI reply + both nudges + neg-FUP rebook + proposal cover/chase — booking_context() returns lead_booking_link(deal), plus the three template additions). B4 = keep the 2h-before reminder (Q3=B); for a same-day booking drop any day-type reminder whose slot is already past at first observation. B9 (Q4=A) = show the "Waiting for transcript" overlay on Booked/appt_booked cards too, without marking prepped. B1 (Q5) = collapsible 🔔 notification bell + a one-line red summary pinned only on send failure, dismissible with an ✕. B6 (Q6=A) = lower .booked-card .flag to ~1rem (amend frontend.md). B8 = NO bot ever dispatched (native Fireflies join only); native-join reliability is owner-side Fireflies config (see the owner note under S8).
Probe surfaces — verified against the serializers (2026-07-15, INV re-trace). Every probe was confirmed to read a key the endpoint actually emits: /dash/api/deal/{lead_id} returns the RAW deal record — draft bodies live at ai_draft.body and armed-sequence step bodies at seq_steps[i].body; the deal's stage is stage_key (the board JS d.stage is a frontend projection — a probe grepping stage misses stage_key). · /dash/api/send_ledger?deal={lead_id} returns full ledger rows (category, status, step, frozen_subject/body, sent_subject/body) — reminders ride it as category="reminder", step∈{3d,1d,2h}; neg-FUP as category="neg_fup". · /dash/api/queue returns Review-needed rows (name, lead, draft) — nudge/chase drafts land here (their Notion "Missive account" is empty, so the cold-inbox account-gate keeps them). · /dash/api/history?lead=<lowercase ZZ email> returns touchpoint rows. · POST /dash/api/fireflies/dispatch?token=… with {"meeting_url":…}. The booking staging page is served by a DIFFERENT app (booking base, not $BASE) and needs a booking-minted token the walker can't forge — so the read-only-page steps (S1.7/S1.8) are pixel-only by necessity, not laziness.
Chunk A · Booking & Calendar  (work item W-A-booking)

S1 — Every booking draft carries the unique read-only link (B3)

seed: ZZ-S1 Booking Kft / zz-s1@zz-sentinel.test — one booking-eligible lead (booking_ok=true, no call booked yet). The seed drives ONLY the real product emitters, then reads what the product wrote — it never plants a link. (1) POST an inbound reply to /missive/incoming so process_missive generates the AI reply and stamps deal.ai_draft.body. (2) Arm the post-reply state (awaiting_reply, last_outbound_at back-dated) and run followup_nudge_sweep twice so _draft_nudge writes the "Follow-up #1" and "Follow-up #2" Review-needed rows (their booking link comes through booking_context). (3) Advance to the proposal step and call the post-call step-3 path so sequences.define("proposal_chase", …) stores the proposal cover (seq_steps[0]) + chase (seq_steps[1]) bodies on the deal. (4) Put it through a negative reply and click the +2-day rebook so send_negative_followup writes the neg_fup ledger row (send BLOCKED on the ZZ address, but the frozen body is recorded). Identity ZZ-S1 Booking Kft + zz-s1@zz-sentinel.test re-created verbatim on every reseed.

Who: System (draft generation) · When: any draft offering a time-booking is generated. The unique link = a booking URL carrying ?deal=<id>&token=<hmac> (resolves to the read-only page); the WRONG link is the generic public BOOKING_LINK (same host, NO deal=/token= → empty editable form). Each probe asserts the presence of BOTH deal= and token= in the specific draft body (a generic link has neither); the pixel guards only against a raw unrendered {{booking_link}}.

#You doYou should seeElement that changes
copy · look · where
What changes underneathMust NOT happen🕓 Touchpoint history
1 Open the AI reply draft the app generated for the inbound reply The draft body offers a booking CTA whose link contains the unique per-lead ?deal&token Copy: booking CTA (foglalás/időpont) · Where: AI reply draft body PROBE (api_deal): ai_draft.body contains both deal= and token= (from lead_booking_link, after booking_context stops returning the generic link) Must NOT leak a raw {{booking_link}}; must NOT carry the generic (token-less) public link No new entry — draft only, not sent.
2 Open the AI nudge #1 draft (Review queue) Nudge #1 body carries the same unique per-lead booking link Copy: "foglalj időpontot" CTA · Where: Follow-up #1 queue draft PROBE (api_queue): the "Follow-up #1" row for zz-s1@zz-sentinel.test has deal=+token= in its draft Must NOT ship nudge #1 with the generic link or no link No new entry — draft only.
3 Open the AI nudge #2 draft (Review queue) Nudge #2 body carries the same unique per-lead booking link Copy: booking CTA · Where: Follow-up #2 queue draft PROBE (api_queue): the "Follow-up #2" row for zz-s1@zz-sentinel.test has deal=+token= in its draft Must NOT ship nudge #2 with the generic link or no link No new entry — draft only.
4 Open the negative-FUP rebook nudge (its recorded body) The "want to rebook?" nudge now carries the unique per-lead booking link Copy: rebook CTA · Where: neg-FUP body PROBE (api_send_ledger): the neg_fup row's frozen_body/sent_body contains deal=+token= (NEG_FUP_BODY_HU now renders {booking_link}) Must NOT be a rebook nudge with no booking link at all No new entry (send blocked — ZZ address).
5 Open the proposal cover draft (post-call step 3) The proposal cover now includes the unique per-lead booking link Copy: booking CTA line · Where: proposal cover (seq_steps[0]) PROBE (api_deal): seq_steps[0].body contains deal=+token= Must NOT ship a proposal cover with no booking link No new entry — draft only.
6 Open the proposal chase draft (post-call step 3) The proposal chase also includes the unique per-lead booking link Copy: booking CTA line · Where: proposal chase (seq_steps[1]) PROBE (api_deal): seq_steps[1].body contains deal=+token= Must NOT ship a proposal chase with no booking link No new entry — draft only.
7 Open the unique link itself (booking staging page) A read-only page showing only the confirmation email — no editable fields, no name field Copy: "Foglalás e-mail-címe: zz-s1@zz-sentinel.test" · Look: read-only, date/time picker only · Where: booking page PIXEL-ONLY — the booking page is a separate app on the booking base (not $BASE) and needs a booking-minted token the walker can't forge; judged from the screenshot. Must NOT render the editable public form's "Neved" name input No new entry — page view.
8 Confirm the same link does NOT render the empty editable public form Still the read-only email page — none of the public form's editable inputs Look: no "Neved"/"Telefonszám" inputs · Where: booking page PIXEL-ONLY — negative guard on the same booking-app page (off $BASE). Must NOT show the "Telefonszám (opcionális)" editable field of the public form No new entry — page view.

S2 — Same-day booking fires only the 2-hour reminder (B4)

seed: TWO leads. (1) ZZ-S2 SameDay Kft / zz-s2@zz-sentinel.test — books a call TODAY ~90 min out through the real booking flow (so the 2h "hour" reminder is due and the 1d/3d "day" reminders are already in the past), then runs reminder_sweep once (the product emitter) so the ledger reflects exactly what fired. (2) ZZ-S2b Normal Kft / zz-s2b@zz-sentinel.test — books a call several DAYS out (REGRESSION control) through the same flow, so log_reminder_schedule records the full 3d/1d/2h schedule but nothing has fired yet. Never plant a ledger row or a reminder marker; both identities re-created verbatim on reseed.

Who: System (reminder_sweep) · When: a last-minute / same-day call is booked. Correct behavior (Q3=B): only the 2h "hour" reminder fires; every "day"-type reminder whose slot is already past is dropped — but the NORMAL multi-day-out path must still schedule the day-before. Ledger step ids: 3d/1d/2h; the fired-reminder touchpoint reads "Reminder sent (<label>)".

#You doYou should seeElement that changes
copy · look · where
What changes underneathMust NOT happen🕓 Touchpoint history
1 Open ZZ-S2 SameDay Kft's history after booking Exactly one booking-confirmation email was sent for the same-day booking Copy: booking-confirmation touchpoint · Where: card history PROBE (api_send_ledger): exactly ONE booking_confirmation row exists Must NOT send the confirmation twice critical One booking-confirmation touchpoint.
2 Read the fired reminders in the history right after the confirmation The 2-hours-before reminder fired; NO day-before reminder fired minutes after the confirmation Copy: "Reminder sent (2 hours before the call)" · Where: card history PROBE (api_send_ledger): NO reminder row with step=="1d" exists (the day-before was dropped, not sent) Must NOT show "Reminder sent (1 day before the call)" critical A "2 hours before" reminder — no "1 day before" reminder.
3 Confirm the 2-hour reminder is the one that actually fired The imminent (2h) reminder is present in the send record Copy: "Reminder sent (2 hours before the call)" · Where: card history PROBE (api_send_ledger): a reminder row with step=="2h" exists Must NOT show "Reminder sent (1 day before the call)" The single fired "2 hours before" reminder.
4 Confirm the 3-days-before reminder was also dropped No 3-days-before reminder fired for the same-day booking Copy: absence of "3 days before" · Where: card history PROBE (api_send_ledger): NO reminder row with step=="3d" exists Must NOT show "Reminder sent (3 days before the call)" No "3 days before" reminder.
5 Open ZZ-S2b Normal Kft (booked several days out) — REGRESSION guard The normal multi-day booking STILL schedules the day-before reminder Copy: "Reminders scheduled: … 1 day before … Holnapi megbeszélés" · Where: card history PROBE (api_history, zz-s2b): the scheduled-reminders touchpoint contains "Holnapi megbeszélés" AND no "Reminder sent (1 day before the call)" has fired yet Must NOT drop the day-before reminder for a normal (multi-day-out) booking A "Reminders scheduled" touchpoint listing all three.

S3 — Reschedule: fresh email, old event gone, one live event, lands on the lead's calendar (B5)

seed: ZZ-S3 Reschedule Kft / zz-s3@zz-sentinel.test — a lead with an existing booked call. The seed books once, then reschedules TWICE through the real booking-link flow (the product emitter chain: handle_own_booking_finalize_own_booking_create_booking_meet_event(intent="reschedule")), so the send-ledger + calendar state are whatever the product produced. Never plant a confirmation row or an event id; ZZ-S3 re-created verbatim on reseed.

Who: Lead (self-reschedule) + System · When: the lead reschedules via their unique link (twice). Each reschedule must send a FRESH confirmation (unique ledger key per reschedule — bumped step or a booking_reschedule category, so no booking_confirmation:0 collision), delete the prior tracked event, and deliver the new Meet. The email + event-count + new-time are separate asserts; the lead's OWN Google Calendar cannot be read by the walker, so that leg is owner-verified.

#You doYou should seeElement that changes
copy · look · where
What changes underneathMust NOT happen🕓 Touchpoint history
1 Open the send-ledger after reschedule #1 A fresh confirmation email was sent for the first reschedule (beyond the original booking) Copy: confirmation row w/ new time · Where: send-ledger drawer PROBE (api_send_ledger): ≥2 rows whose category ∈ {booking_confirmation, booking_reschedule} — the reschedule did NOT collide on the original key Must NOT skip the email as a "duplicate"; must NOT leak {{time}}/{{meet_link}} critical A reschedule-confirmation touchpoint for reschedule #1.
2 Check the tracked calendar event after reschedule #1 A single tracked live event; the prior event was deleted Copy: "áthelyezve" reschedule note · Where: card + owner calendar PROBE (api_deal): a non-empty gcal_event_id is tracked. OWNER-VERIFIED: that exactly one event exists and the prior was deleted (Google Calendar state — not readable by the walker). Must NOT leave the first-rescheduled event lingering pixel-only A reschedule touchpoint.
3 Open the send-ledger after reschedule #2 A second fresh confirmation email was sent for the second reschedule Copy: confirmation row w/ newest time · Where: send-ledger drawer PROBE (api_send_ledger): ≥3 rows whose category ∈ {booking_confirmation, booking_reschedule} (original + two fresh reschedule confirmations) Must NOT skip the second reschedule's email; must NOT leak {{time}} critical A reschedule-confirmation touchpoint for reschedule #2.
4 Check the card's meeting time + live event count after reschedule #2 The card shows the new time; exactly one live event remains Copy: new date-time + "Google Meet" · Where: card PROBE (api_deal): sales_call_date is a real ISO date (updated to the newest slot). OWNER-VERIFIED: exactly one live event remains (calendar state). Must NOT leave the lead on the old time; must NOT leak {{time}} A reschedule touchpoint with the newest time.
5 Confirm the new Meet reaches the lead (delivery) The fresh confirmation email carries the real Google Meet link (the lead's delivery channel) Copy: "meet.google.com/…" · Where: confirmation email body PROBE (api_send_ledger): a confirmation/reschedule row's body contains "meet.google.com". OWNER-VERIFIED: the invite actually lands on the LEAD's own calendar (their calendar can't be read by the walker). Must NOT ship a reschedule confirmation with no Meet link; must NOT leak {{meet_link}} Confirmation touchpoint carrying the Meet link.
Chunk B · Board & Dashboard UI  (work item W-B-boardui)

S4 — Notification bell + dismissible red send-fail summary (B1)

seed: ZZ-S4 Notif Kft / zz-s4@zz-sentinel.test — a lead with ONE failed outbound send. The seed drives send_ledger to a real FAILED row via the product's send path with a forced send failure (never a hand-written failed row), so the pinned red summary + the bell's failed-send item appear for real. ZZ-S4 re-created verbatim on reseed.

Who: Mátyás (Pipeline tab) · When: viewing the board. The old full-width red strips (the .sendfail-board board banner + the #healthbanner health strip) move INTO a collapsible 🔔 bell; a one-line red summary pins at top ONLY on send failure and is ✕-dismissible. The failed-send banner copy is "⛔ N email(s) failed to send".

#You doYou should seeElement that changes
copy · look · where
What changes underneathMust NOT happen🕓 Touchpoint history
1 Open the Pipeline tab A collapsible 🔔 bell (top-right) holding the failed-send + health items — no full-width red bar across the board Look: bell icon w/ count · Where: top-right, above the columns PIXEL-ONLY — .sendfail-board + #healthbanner no longer prepend the board; they live in the panel. Must NOT render a red horizontal bar spanning the full page width above the board columns pixel-only n/a.
2 Observe the top of the board with the failed send present A single one-line red summary pinned at top, with an ✕ dismiss icon Copy: "failed to send" one-liner + ✕ · Look: one line, red · Where: pinned top strip PROBE (api_send_ledger): a real row with status=="failed" drives the summary Must NOT render a multi-line health-alert strip pinned above the tab row critical n/a — UI surface.
3 Click the ✕ on the red summary The Pipeline board with no pinned red summary Look: summary gone · Where: top strip PIXEL-ONLY — the pinned summary is dismissed client-side; the bell still holds the item. Must NOT keep showing "failed to send" at the top after dismiss pixel-only n/a.
4 Look at the 🔔 bell after dismissing the summary The bell still carries the failed-send item (a count badge) Look: bell w/ count · Where: top-right PIXEL-ONLY — dismiss clears only the pinned strip, not the panel item. Must NOT show a notification bell with no count badge pixel-only n/a.
5 Open the 🔔 bell and look for health alerts Health alerts live inside the bell — not on a strip above the tabs Look: health items in panel · Where: bell panel PIXEL-ONLY — renderHealthBanner's content moved into the panel. Must NOT render a colored alert strip pinned above the tab row pixel-only n/a.

S5 — Inbound reply shows on the card + touchpoint history loads (B2 ⭐)

seed: ZZ-S5 Reply Kft / zz-s5@zz-sentinel.test — an EXISTING logged lead (Status "Logged", NOT a "Review needed" cold-inbox lead card). The seed POSTs an inbound reply to /missive/incoming (the product emitter) FROM a MIXED-CASE address (e.g. ZZ-S5@ZZ-Sentinel.Test) whose body contains the marker phrase ZZ-REPLY-BODY, so the .lower() fix on the stored "Lead email" is exercised. Never plant last_msg.row_id or the history row — the product must create them (the fix stamps the created Notion row id onto last_msg.row_id). ZZ-S5 + the marker re-created verbatim on reseed.

Who: Lead (reply) + Mátyás · When: an inbound reply arrives from a non-lead-card lead. The reply must render on the card face AND the 🕘 history must load (not "No prior events."), and the mixed-case sender must resolve after the .lower() fix.

#You doYou should seeElement that changes
copy · look · where
What changes underneathMust NOT happen🕓 Touchpoint history
1 Open the ZZ-S5 Reply Kft card after the inbound reply lands The incoming reply text (ZZ-REPLY-BODY) shows on the card face Copy: reply preview on card · Where: ZZ-S5 Reply Kft card PROBE (api_deal): last_msg.row_id is a non-empty id (the skip_notion reply now carries a stamped row id) Must NOT show a card face with no inbound-reply block (reply only as a purple side notification) critical An "Incoming reply" touchpoint on the card.
2 Confirm the card is not blank (the purple side-notification is fine) The card shows the reply block with ZZ-REPLY-BODY — the side notification is additive, not the only signal Look: reply block on card · Where: ZZ-S5 Reply Kft card PIXEL-ONLY — the purple side banner may also be present; the card face must carry the reply. Must NOT show an empty card face for this reply pixel-only n/a.
3 Open the card's 🕘 Full history panel The history panel loads with rows (including the reply) — not "No prior events." Copy: "Full history" + ZZ-REPLY-BODY row · Where: history panel PROBE (api_history, zz-s5): returns ≥1 row (the lowercased "Lead email" makes the equals-filter match) Must NOT render "No prior events." critical The history list renders for zz-s5@zz-sentinel.test.
4 Confirm the mixed-case sender resolved (the .lower() fix) The inbound reply is present in the history despite the mixed-case From-address Copy: ZZ-REPLY-BODY inbound row · Where: history panel PROBE (api_history, zz-s5): at least one row has dir=="in" (the mixed-case reply resolved through the lowercased store) Must NOT render "No prior events." for the lowercased address The inbound reply row resolves.
5 Confirm prior touchpoints are present too Earlier touchpoints (the outbound cold email / prior sends) render alongside the reply Copy: prior outbound row · Where: history panel PROBE (api_history, zz-s5): at least one row has dir=="out" (prior touchpoints render, not just the reply) Must NOT render "No prior events." The full timeline renders.

S6 — Booked / Prep tag sized ~1rem (B6)

seed: ZZ-S6 Tag Kft / zz-s6@zz-sentinel.test — a lead sitting in the Booked / Sales-Call-Prep column carrying at least one flag tag, so the tag size is visible for the crop. Product state only; ZZ-S6 re-created verbatim on reseed.

Who: Mátyás · When: viewing a card in the Booked / Sales-Call-Prep column. The tag drops from 1.32rem (2×) to ~1rem (Q6=A; frontend.md amended). Judged from a native-res crop so the relative size reads.

#You doYou should seeElement that changes
copy · look · where
What changes underneathMust NOT happen🕓 Touchpoint history
1 Look at the ZZ-S6 Tag Kft card tag in the Booked / Prep column A compact tag roughly the height of the card's body text — clearly smaller than the old 2× size Look: ~1rem tag · Where: Booked / Sales-Call-Prep card (crop) PIXEL-ONLY — .booked-card .flag font-size lowered to ~1rem. Must NOT render a flag tag taller than the card's company-name heading pixel-only n/a.

S7 — Scheduled-email dashed ring: slower + more dashed (B11)

seed: ZZ-S7 Dashed Kft / zz-s7@zz-sentinel.test — a lead with an ARMED scheduled email sequence so the card wears the animated dashed ring (.anim-sequence-ring). Product state only; ZZ-S7 re-created verbatim on reseed.

Who: Mátyás · When: viewing a card with scheduled emails. Owner taste: raise --anim-ring-dur (slower) and widen the conic-gradient dash gaps (more/bigger dashes). NOTE: rotation SPEED is temporal — a single screenshot can only judge the DASH PATTERN (gap presence); the slower rotation itself is owner-verified live.

#You doYou should seeElement that changes
copy · look · where
What changes underneathMust NOT happen🕓 Touchpoint history
1 Look at the ZZ-S7 Dashed Kft card's ring (crop) A widely-spaced dashed ring with clear gaps between segments Look: bigger gaps between dashes · Where: scheduled-email card border (crop) PIXEL-ONLY — wider transparent stops in the conic-gradient; higher --anim-ring-dur (rotation speed owner-verified live). Must NOT render a continuous solid ring with no gaps pixel-only n/a.
Chunk C · Call capture  (work item W-C-callcapture)

S8 — No Fireflies bot is ever dispatched (B8)

seed: ZZ-S8 Nobot Kft / zz-s8@zz-sentinel.test — a lead with a booked Meet call. The scenario drives the manual 🎙 dispatch endpoint (POST /dash/api/fireflies/dispatch) — the ONLY bot-dispatch path observable on staging — and asserts it no longer dispatches a bot, then calls it a SECOND time to prove no second bot. No planted state; ZZ-S8 re-created verbatim on reseed.

Who: Mátyás (🎙 button) + System · When: any capture trigger. All four bot-dispatch sites (auto _meet_autodispatch, manual 🎙, Slack force-start, participant-joined "bot" route) are neutered so addToLiveMeeting never fires. Native (non-bot) Fireflies join is the only capture path. Only the manual path is reachable on staging (see the owner note).

#You doYou should seeElement that changes
copy · look · where
What changes underneathMust NOT happen🕓 Touchpoint history
1 Trigger the manual 🎙 "Send Fireflies to a meeting" action A response/toast saying native Fireflies join only — no bot dispatched Copy: "Native Fireflies join only — no bot dispatched" · Where: 🎙 action result PROBE (POST fireflies/dispatch): the response marks native-only / not dispatched; addToLiveMeeting is never called Must NOT report "Fireflies bot elindítva" or dispatch a visible bot critical No "bot dispatched" touchpoint.
2 Trigger the manual 🎙 action a SECOND time on the same Meet Still native-only — a second call does not dispatch a second bot Copy: native-only again · Where: 🎙 action result PROBE (POST fireflies/dispatch ×2): both responses mark native-only / not dispatched (no bot on either call) Must NOT report "Fireflies bot elindítva" on the repeat No second "bot dispatched" event.
Owner note — the AUTO paths + single-flight are NOT walkable on staging. The double-bot the owner hit came from TWO paths, not one: the automatic _meet_autodispatch (fired by a Meet participant-join ping), the Slack #meetings force-start, and the participant-joined "bot" route — none of which is reachable on staging (the Apps-Script Meet ping / Workspace subscription is DORMANT, and no automated agent can join a Google Meet to trigger a join event). The fix neuters dispatch_notetaker at ALL four call sites and keeps the shared single-flight key so even a revived path can't double-fire — but only the manual 🎙 endpoint above is observable in this run. Native-join reliability is 100% owner-side Fireflies + Google-Calendar config, with no API to force it (see INV_fireflies.md): confirm the connected calendar is matyas@clientsflow.hu; set Auto-join = "All meetings with a web-conf link"; keep bot-free Meet capture enabled; check Recording Rules aren't excluding sales Meets; accept the ~5–10 min sync lead-time (admit the notetaker manually for truly last-minute calls). This paragraph is a note, not a walkable step.

S9 — "Waiting for transcript" overlay on a Booked card without marking prepped (B9)

seed: ZZ-S9 Overlay Kft / zz-s9@zz-sentinel.test — a lead in the Booked / appt_booked column whose sales_call_date is a few minutes in the PAST, and which was NOT marked prepped (so it never moved to sales_call). The seed sets the booked call time via the real booking path; it must NOT set sales_call / prep state. ZZ-S9 re-created verbatim on reseed.

Who: System (time projection) · When: a booked call's time has just passed and prep was not marked. The purple "Waiting for transcript" overlay must show column-agnostically (Q4=A) — no prep-semantics change. The deal's stage lives at stage_key (NOT stage — that's a frontend projection), so the probe asserts stage_key != "sales_call".

#You doYou should seeElement that changes
copy · look · where
What changes underneathMust NOT happen🕓 Touchpoint history
1 Look at the ZZ-S9 Overlay Kft card just after the call time passed The purple "Waiting for transcript" overlay on the card, still in the Booked column Copy: "Waiting for transcript" · Look: pulsing purple overlay · Where: Booked / appt_booked card PROBE (api_deal): sales_call_date is a real ISO date AND stage_key != "sales_call" (card stayed Booked, prep not marked) Must NOT require the card to be moved into the Sales Call column first critical The card's call-lifecycle shows transcript_pending.
Chunk D · Proposal  (work item W-D-proposal)

S10 — Proposal sign-off is last; no raw meet-link token on any send path (B10)

seed: ZZ-S10 Proposal Kft / zz-s10@zz-sentinel.test — a lead advanced to the post-call proposal step with a booked Meet. The seed drives the real post-call step-3 path WITH "Google Calendar event" ticked, so a real Meet event is created and sequences.define("proposal_chase", …) stores the finalized proposal cover (seq_steps[0]) on the deal — the pending-link token swapped for the real Meet link. It ALSO drives the /review (non-do_calendar) route rebuild, so every stored/sent body is covered. Never hand-edit a body; ZZ-S10 re-created verbatim on reseed.

Who: System (proposal draft) · When: the post-call proposal draft is generated. The Meet block must sit BEFORE the sign-off, the sign-off ("… ClientsFlow Digital …") must be the very last content, and no raw {{PENDING_MEET_LINK}} may survive on ANY stored/sent body (INV flagged the token shipping raw on the non-do_calendar route rebuild). Sign-off-order and token-resolution are separate asserts.

#You doYou should seeElement that changes
copy · look · where
What changes underneathMust NOT happen🕓 Touchpoint history
1 Open the post-call proposal cover body (do_calendar path) The canonical sign-off ("… ClientsFlow Digital …") is the LAST content; the Meet block appears above it Copy: "ClientsFlow Digital" close at the end · Where: proposal cover tail (seq_steps[0]) PROBE (api_deal): in seq_steps[0].body, index_of("ClientsFlow Digital") > index_of("egyeztet") — Meet block injected BEFORE the sign-off Must NOT append the "online egyeztetésünk" Meet block AFTER the signature critical No new entry — draft.
2 Scan the proposal cover body for the meet link (do_calendar path) A real Google Meet link is present; no raw token Copy: "meet.google.com/…" resolved link · Where: Meet block (seq_steps[0]) PROBE (api_deal): seq_steps[0].body contains "meet.google.com" and NOT "{{PENDING_MEET_LINK}}" (finalize swapped the token) Must NOT contain the literal {{PENDING_MEET_LINK}} critical No new entry — draft.
3 Confirm the token is stripped across EVERY stored/sent body (route + finalize) No raw token survives on any proposal step or ledger send record Copy: no raw token anywhere · Where: all seq_steps + send-ledger rows PROBE (api_deal + api_send_ledger): NO seq_steps[*].body and NO ledger frozen_body/sent_body contains "{{PENDING_MEET_LINK}}" Must NOT leak {{PENDING_MEET_LINK}} on the /review route or finalize path critical No new entry — draft.
Sign-off — acceptance oracle. By signing, Sarudi Mátyás locks scenarios S1–S10 (36 steps) and the owner decisions above as the acceptance answer key for the 2026-07-15 live-test fixes. No code is built, deployed, or merged before this signature; any edit to this file (including the machine block) invalidates it and requires a re-sign.
Sarudi Mátyás Signed · 2026-07-__