ClientsFlow · Email-System Overhaul · W5 · EBO

EBO — W5 Recover Stranded Features

DRAFT for owner comments · 2026-07-12. W5 is a verification-only workstream: features that were built + QA'd in past sessions but ended up stranded, reverted, or only claimed done must now be settled live — clicked on the real board, not just found in the code. Every scenario below is the exact click-path that proves one stranded candidate works today (or records, in the open, that it doesn't and who owns the fix). W5 builds nothing, merges nothing, deploys nothing. Scenarios W5-1 … W5-8, covering findings F3 · F6 · F7(a/b/c) · F8 · F10.

What W5 owns
The live verdicts: for each stranded candidate, one clean click-through on the real (staging/prod) app that upgrades a code-grep verdict to a seen-it-work verdict — plus the recover-or-route call per item, and the ZZ test-data cleanup afterwards.
What W5 only routes (owned elsewhere)
Anything found missing is routed, never built here: the proposal workbench (Q27=A) → W2's one-page inline workbench; the tax-number field validation → W7 (named work item, sequenced after W3); a classifier retune (if the eval says so) → the Templates/prompt-iteration owner; Fireflies dispatch latency → its existing meet-recorder-autojoin-5s plan. W5 hands over verdicts with exact anchors; it writes zero product code.
You do → click/action/hover You should see → on-screen result Element changes → copy · look · where What changes underneath → data/state Must NOT happen → bug guard 🕓 Touchpoint history → impact on the card's history (or —) 💡 UX critique + suggestion → genuine critique + concrete improvement (owner: keep/omit)
Ground rules folded in (ANSWERS_ROUND3 > handoff, per style calibration)Q22=A: every verification that creates data runs as a live-fire test on staging with ZZ… sentinel deals, and everything created is purged afterwards (W5-8). Read-only checks (token GET, looking at a card) may touch prod. Q27=A overrides the handoff's "owner-decide-priority" on the proposal workbench: the owner has now ruled it IN — a one-page inline proposal workbench (edit any text/price inline, one button sends + schedules follow-ups). That build belongs to W2's scope; W5 only confirms live that nothing of it exists today, so W2 starts from a clean, honest baseline. Not stranded, no recovery: F8 (Fireflies auto-dispatch) already has an active owner (plans/meet-recorder-autojoin-5s/) — W5 runs one confirm-not-regress check and stays out of its way.
Invariants that hold everywhere — W5 changes no product code and deploys nothing; every created test artifact carries the ZZ sentinel and is purged at the end; Notion cleanup is archive-only, never permanent delete; nothing is ever sent to a real lead; each verdict lands as an explicit ✅ CONFIRMED LIVE / ❌ GAP CONFIRMED + owner, recorded as it happens (never batched in memory); all verification uses the current dash token — never a locally computed one (the local secret is stale and produces a token that 403s).

W5-1 — Precondition: live dash access with the current token (gate / must pass first)

Who: Verifier (agent or Mátyás)  ·  When: Before any other W5 scenario. The previous W5 pass was blocked exactly here: the dash token had been rotated and the locally computed one silently 403'd, so every verdict stayed grep-only. This gate makes sure that never repeats — no live access, no W5 run.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history💡 UX critique + suggestion
1 Open the live dash URL with the current known token (…/dash?token=57723d9da370c5ff3afcd52e) in a browser The Control Panel loads with a 200 and the pipeline board renders with real cards — proof the token is valid and live verification can proceed Copy: normal dash chrome + board columns · Look: full board paint, no error page · Where: production dash (read-only look, nothing clicked yet) Nothing — a read-only GET. The token's validity is recorded in the W5 findings log before proceeding Must NOT proceed to any other W5 scenario on a 403; must NOT fall back to a locally computed token (known stale — it produces the old 6e90… value and always 403s) Critique: A silent 403 was exactly what stranded the last pass — the failure looked like "feature can't be checked" instead of "door is locked".
Suggestion: Log the gate result (200/403 + timestamp) as the first line of the W5 findings file, so any future re-run sees instantly whether access — not the feature — was the blocker.
2 If step 1 returned 403: fetch the current dash secret from the clientsflow Modal secret store and retry with the freshly derived token The dash loads with 200 on the retry — the rotated token is now the recorded working token for the rest of the run Copy: same dash · Look: same board · Where: production dash The new token replaces the stale one in the findings log; all later scenarios use it Must NOT ask the owner for the token before trying the secret store; must NOT continue the run on the stale token "just in case"

W5-2 — F7(c): "My signing link" button on the awaiting-signature card (recovered feature / existing flow, verify unchanged)

Who: Verifier, acting as Mátyás  ·  When: A deal has its proposal + contract out for signature and the contractor (Mátyás) must counter-sign too. The wiring for a rep-side signing button is confirmed on main in the code — this scenario proves it actually renders and opens the right document on a live card. Runs on staging with a ZZ sentinel deal advanced to the awaiting-signature stage.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history💡 UX critique + suggestion
1 On the staging board, create/advance a ZZ sentinel deal to the awaiting-signature (post-proposal) stage and open its card The card shows two distinct signing affordances: the client's signing link AND a separate "My signing link" button for the contractor's own signature Copy: "My signing link" (rep/contractor side), clearly distinct from the client link's label · Look: a button on the awaiting-signature card · Where: the ZZ card, signature-stage panel The card payload carries both signer URLs (client + rep) from the DocuSeal submission — existing shipped wiring, nothing new is created by looking Must NOT show only the client link (that was the original stranded-claim bug); must NOT label the two links so similarly they're mistaken for each other — (opening a card writes nothing) Critique: Two links on one card differ only by their label — a rushed click on the wrong one puts the owner into the client's signer session.
Suggestion: If a mis-click proves easy during this verification, note it for W3/W7 (Details-panel owners) as "visually separate the two signer links" — W5 records the observation, does not restyle anything.
2 Click "My signing link" A DocuSeal signer page opens scoped to the contractor's own signature fields on the ZZ deal's combined document — not the client's fields, not someone else's submission Copy: DocuSeal signer form showing the contractor party's fields · Look: the signing surface · Where: new tab, DocuSeal Nothing in the pipeline — the click only opens the already-issued signer URL. Verdict recorded: F7(c) ✅ CONFIRMED LIVE (or ❌ with the exact wrong behavior seen) Must NOT open the client's signer session; must NOT 404 / open an expired submission; must NOT sign anything for real during verification (do not submit the form)
3 While the signer page from step 2 is open: attempt to place a typed signature in a signature field (this is F7(a), verified opportunistically here — same open document, zero extra setup) The signature field only accepts a drawn signature — the typed-text option is rejected/absent on this submission Copy: DocuSeal's signature widget without a usable "type" mode · Look: draw-only signature input · Where: the open DocuSeal form Nothing — behavior check only. Verdict recorded: F7(a) ✅ CONFIRMED LIVE (the per-submission draw-only setting is already confirmed on main in code; this is the one live confirmation) Must NOT accept a typed signature; must NOT complete/submit the signature — abandon the form after checking Critique: None — piggybacking F7(a) onto the F7(c) click keeps the run cheap; a separate scenario would be ceremony.

W5-3 — F10: both-parties-signed → exactly ONE client email with signed doc + payment link (recovered feature / existing flow, verify unchanged)

Who: System (signing completes) + Verifier watching the ZZ inbox  ·  When: The old complaint: after both parties signed, the client got two separate emails (one with the signed document, one with the payment link). The code on main says this was consolidated into one email long ago — this scenario proves it live, end to end, on a staging ZZ deal. Existing flow, verify unchanged.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history💡 UX critique + suggestion
1 Complete both signatures on the ZZ staging deal's document (or replay the "both signed" webhook / trigger the existing DocuSeal backup-poll reconciliation for it) Within the normal processing window, the ZZ card leaves the awaiting-signature state — the signed-contract flow has fired Copy: card's stage/status advances per the existing flow · Look: board reflects the post-signing state · Where: the ZZ card on staging The existing post-signing flow runs: signed document stored, payment link ensured, the client email composed and sent — all pre-existing behavior, W5 only triggers it on a sentinel deal Must NOT run this against a real lead's deal; must NOT run it on prod (staging + ZZ only) The signing event and the outgoing email land in the ZZ card's history per the existing rules — see step 2 for the exact check.
2 Open the ZZ deal's inbox (the sentinel email address) and count the client-facing emails from this signing event Exactly ONE client email, containing all three payment-relevant pieces in one body: the signed-contract download link, the Stripe payment link, and the manual bank-transfer details with the exact amount Copy: one email, three payloads (Hungarian client copy, e.g. the signed-doc link + fizetési link + átutalási adatok) · Look: a single message in the thread · Where: ZZ inbox Verdict recorded: F10 ✅ CONFIRMED LIVE if one-email-with-all-three; ❌ GAP if a second client email arrived or a payload is missing Must NOT find two separate client emails (the original bug); must NOT find the Stripe link or the amount missing from the single email; must NOT count the internal rep-copy notification as a client email (it is a separate, clearly-labeled internal message — expected, not a regression) EXISTING ENTRY — verify parity
The ZZ card's history shows the sent client email as one outbound touchpoint whose stored copy matches the email in the inbox word-for-word (the 100% history↔sent parity rule, Q7).
Critique: "One email" can silently regress again — this exact flow already drifted once from what the owner believed was live.
Suggestion: Hand W6 (QA gate) this scenario as a permanent regression check: any future change touching the post-signing flow must re-run this one-email count before shipping.
3 Check the internal notification side of the same event One clearly-labeled internal rep-copy notification exists, separate from the client email — so the owner knows the contract closed without the client seeing a second message Copy: the internal notification's label marks it internal · Look: internal channel/inbox, not the client thread · Where: owner-side surfaces Nothing new — confirming the shipped design (client email + separate internal copy) is intact as a pair Must NOT find the internal copy addressed to the client; must NOT find it missing entirely (the owner would be blind to closings)

W5-4 — F6: classifier false-negative check — run the frozen-set eval, then owner-decides (verification run / no board click)

Who: Verifier; Mátyás decides on the result  ·  When: The stranded claim: the reply classifier was retuned so positive leads stop being flagged negative (once called the highest business risk). The eval tooling is confirmed shipped; whether the retune itself happened was never confirmed. The check is not a click on the board — it is one measured run of the existing frozen-set eval against the live classifier prompt. No emails, no leads touched.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history💡 UX critique + suggestion
1 Run the existing frozen-set classifier eval against the current live classify prompt and record the false-negative rate (positive leads classified negative) A concrete number: "X of N frozen positive-lead replies were classified negative" — plus the comparison to whatever pre-fix baseline can be reconstructed from the June session logs Copy: the eval's printed result · Look: terminal/eval output, written into the W5 findings log · Where: offline eval run (Eval-Lab machinery) Read-only evaluation over the frozen labelled set; the live prompt is not modified; the result + timestamp is recorded as F6's evidence Must NOT modify or "quickly improve" the live prompt during the run (W5 is verify-only); must NOT let the eval send anything or touch lead data; must NOT report a vibes verdict without the number Critique: Without a declared "good enough" line, the number lands and nobody knows if it closes F6 or reopens it.
Suggestion: Owner sets the acceptance threshold when signing this EBO (see open decision D1) — e.g. "0 false negatives on the frozen set closes it; any >0 goes to the prompt-iteration loop." Then the verdict is mechanical, not a judgment call.
2 Apply the verdict per the recorded number One of two recorded outcomes: CLOSE ✅ false-negative rate within the accepted threshold — F6 closed as already-good; or ROUTE rate above threshold — F6 handed to the Templates/prompt-iteration owner as a normal measured iteration loop (frozen set + metric already exist), with the eval result attached Copy: the verdict line in the W5 findings log · Look: — · Where: findings log + the receiving workstream's queue if routed Nothing in the product — a routing decision with evidence attached Must NOT retune the classifier inside W5; must NOT close F6 without the eval number as evidence; must NOT let a ROUTE verdict silently die — it must land in a named owner's queue

W5-5 — F8: Fireflies auto-dispatch — confirm-not-regress, then hands off (not stranded / one confirmation only)

Who: Verifier (starts a test Meet)  ·  When: F8 turned out NOT to be stranded — every related branch is merged, newer tightening landed on main on 07-08, and an active plan bundle owns the remaining latency work. W5's only job: confirm the current live behavior hasn't regressed, then point anyone chasing latency to the existing owner instead of "recovering" anything here.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history💡 UX critique + suggestion
1 Start a live test Google Meet (ZZ-tagged test event, no real lead invited) and wait through one poll cycle The recorder is dispatched to the Meet automatically, within the ~10-second polling cadence of the live-call signal — behaving exactly like clicking the manual dispatch button would (same dispatch, no title) Copy: the recorder/notetaker joins the Meet · Look: notetaker participant appears in the call · Where: the test Meet + the dash's call surfaces The existing merged auto-dispatch path fires on the live-call signal; nothing is configured or changed — pure observation. Verdict recorded: F8 behavior intact / regressed Must NOT treat a missed signal outside the poll window as a new bug to fix here (that is the documented architectural ceiling, owned by the existing recorder plan); must NOT invite a real lead to the test call The call's transcript/archive entries follow the existing call-layer rules once the recorder joins — W5 adds nothing to history itself. Critique: One test call is a thin sample for a timing-sensitive feature — a lucky pass can mask a flaky window.
Suggestion: If the single confirmation passes, still attach the dispatch timestamp delta (signal → join) to the findings log, so the recorder plan's owner accumulates real latency datapoints instead of pass/fail bits.
2 Record the hand-off in the findings log F8 marked NOT STRANDED with a pointer: all further latency/reliability work belongs to the existing recorder plan bundle + its QA checks — not to this overhaul's workstreams Copy: the routing line in the findings log · Look: — · Where: findings log A recorded routing decision; prevents duplicate ownership of the same feature area Must NOT leave F8 listed as an open recovery item after this run; must NOT scope any recorder work inside W5 or the overhaul

W5-6 — F3: proposal workbench — confirm the gap live, route the build to W2 (Q27=A) (confirmed-missing / routing)

Who: Verifier  ·  When: The stranded claim was a proposal-regeneration workbench with editable prompts. The code audit found it was never built — a deliberate exclusion, with no branch holding it. Round 3 supersedes the handoff's "owner-decide-priority": Q27=A rules the workbench IN (one-page inline proposal workbench — edit any text/price inline, one button sends + schedules follow-ups), owned by W2's scope. W5's job here: one honest live look confirming nothing of it exists today, so W2 scopes from truth, not from a stale "reported shipped" claim.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history💡 UX critique + suggestion
1 On the live dash, walk the current proposal path for the ZZ deal up to the proposal preview step and inspect every control on it The current proposal preview: whatever inline editing exists today is noted exactly — and specifically no editable system/user prompt pair, no regenerate-with-edited-prompt control is found anywhere on the proposal surface Copy: today's proposal-step controls, listed as found · Look: the existing preview screen · Where: proposal step on the dash Nothing — observation only. Verdict recorded: F3 ❌ CONFIRMED NEVER BUILT (live look matches the code audit) Must NOT "find" the workbench by generously interpreting an existing edit box as one (the claim is specific: editable prompts + regenerate + one send-and-schedule button); must NOT start building any part of it here Critique: A screenshot beats a sentence for a baseline that another workstream will build against.
Suggestion: Attach one screenshot of today's proposal preview to the findings log as W2's before-picture — the Q27 workbench acceptance can then be judged against a concrete baseline.
2 Record the routing: F3's rebuild = the Q27=A one-page inline workbench, owned by W2 The findings log carries the supersede note explicitly: handoff said "owner-decide-priority"; round 3 decided — build it (Q27=A), in W2; W5 contributes the confirmed-clean baseline and nothing else Copy: routing + supersede note · Look: — · Where: findings log + W2's queue A routing decision with the round-3 anchor attached Must NOT leave two contradictory verdicts alive (the handoff's "if still wanted" and Q27's "wanted") — the log must state round 3 wins; must NOT scope the workbench's design here

W5-7 — F7(b): tax-number field accepts anything — confirm the gap live, route to W7 (confirmed-missing / routing)

Who: Verifier  ·  When: The stranded claim was digits-only validation on the client tax-number (adószám) field in the card's Details panel. The code audit found the field is plain free text with zero validation — a malformed tax number sails through to the contract unnoticed until the client spots it on the signed PDF. One live save proves the gap; the ~10-line fix is already a named W7 work item (sequenced after W3's Details-panel rework). W5 proves, W7 fixes.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history💡 UX critique + suggestion
1 Open the ZZ staging deal's Details panel, type an obviously malformed value into the tax-number field (e.g. not-a-tax-number) and save Today's expected (broken) behavior: the save is accepted silently — no format hint, no rejection, no flag. Seeing exactly this confirms the gap is real on the live app, not just in the code Copy: the malformed value sits saved in the field · Look: no error state anywhere · Where: Details panel, tax-number field on the ZZ card The malformed value is stored on the ZZ deal (sentinel data — purged in W5-8). Verdict recorded: F7(b) ❌ GAP CONFIRMED LIVE Must NOT do this on a real lead's card; must NOT patch the validation in as part of this run (it is W7's named work item, sequenced after W3 — fixing it early would race W3's Details-panel rewrite) Critique: The failure this guards is downstream and embarrassing — a client noticing their own tax number is garbled on a signed contract.
Suggestion: W5's finding should state the acceptance for W7 in one line — "malformed tax number on save → rejected with a format hint (client-side) AND refused server-side" — so W7 inherits a test, not just a bug report.
2 Record the routing in the findings log F7(b) marked GAP — ROUTED TO W7, with the note that it runs after W3 settles the Details-panel field schema Copy: routing line · Look: — · Where: findings log A routing decision; the malformed ZZ value stays flagged for the W5-8 purge Must NOT leave the malformed test value on any deal after the run; must NOT let this gap end up owned by nobody (it was orphaned once already)

W5-8 — Cleanup: purge every ZZ artifact the verification created (guardrail / mandatory close-out, Q22)

Who: Verifier  ·  When: After all verdicts are recorded. Q22=A: live-fire ZZ tests, purge after. Everything W5-2…W5-7 created — the ZZ deal(s), their emails, the signed test document, the test Meet event, the malformed tax-number value — is removed, and the board is shown clean.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history💡 UX critique + suggestion
1 Purge all ZZ sentinel data created by this run: the staging deal(s), their scheduled/sent test emails, the test calendar event, and any DocuSeal test submission Each purge step reports done; Notion-side removals are archive operations (reversible), never permanent deletes Copy: — · Look: ZZ artifacts gone from their surfaces · Where: staging store, Notion (archived), inbox, calendar Sentinel records removed/archived across every system the run touched; the findings log lists what was purged, item by item Must NOT permanently delete anything in Notion (archive only — standing invariant); must NOT purge anything without the ZZ sentinel; must NOT skip a surface (a leftover ZZ email or event is a failed cleanup) The ZZ card's history disappears with the archived card — no real lead's history is touched at any point.
2 Reload the staging board and sweep for leftovers No ZZ card in any column; the findings log closes with the full verdict table (W5-1…W5-7 outcomes + owners) — the deliverable the next workstreams build on Copy: clean board · Look: no sentinel cards · Where: staging board Run closed; verdict table final Must NOT close the run with any verdict still marked "pending"; must NOT report clean without the visual board sweep Critique: Cleanup is where verification runs historically get sloppy — a leftover ZZ deal later pollutes metrics and once even got auto-emailed.
Suggestion: Make the board-sweep screenshot (no ZZ cards visible) the last attachment of the findings log — cleanup proven the same way the features were: by pixels, not by a "purge succeeded" line.
🕓 Touchpoint-history note for W5:
W5 is a verification workstream — it never authors a touchpoint on any real lead. The only history it touches belongs to ZZ sentinel deals, and even there it only triggers existing flows (the post-signing email in W5-3) and then reads the resulting entries to check the history↔sent parity rule (Q7). All sentinel history is archived away in W5-8. Every real lead's history is untouched by design — a W5 run that wrote to a real card's history would itself be a red-verdict finding.
💡 UX themes across all W5 scenarios:
(1) A grep is not a verdict — every "shipped" claim here gets one real click/run on the live app; the previous pass stalled exactly where it trusted code-reading over clicking. (2) Gaps get owners, not fixes — W5's discipline is to prove and route (workbench → W2 per Q27=A; tax-number validation → W7 after W3; classifier retune → the prompt-iteration owner; recorder latency → its existing plan), never to sneak a build into a verify-only run. (3) Record as you go — each verdict lands in the findings log the moment it's decided, with its evidence (number, screenshot, timestamp), so a crashed or restarted run resumes instead of re-deriving. (4) Leave no trace — sentinel-only test data, archive-only removal, and a pixel-proven clean board at the end.

Work-item → scenario-step mapping

Each W5 verification work item (one per stranded-feature candidate), and the scenario steps that settle it live.

Work itemWhat it deliversProven by (scenario · step)
WI-1 · Live-access gateA validated dash token (retrying via the Modal secret store on 403) recorded before any verification — the blocker that stranded the previous pass can never silently repeat.W5-1·1 · W5-1·2
WI-2 · F7(c) rep sign-link, live"My signing link" renders on the awaiting-signature card and opens the contractor-scoped DocuSeal signer — upgraded from wiring-confirmed to seen-live.W5-2·1 · W5-2·2
WI-3 · F7(a) draw-only signature, liveTyped signatures rejected on a live DocuSeal submission (piggybacked on the same open document — zero extra setup).W5-2·3
WI-4 · F10 one-email consolidation, liveBoth-signed event → exactly ONE client email carrying signed-doc link + Stripe link + bank details, internal rep-copy separate, history↔sent parity verified.W5-3·1 · W5-3·2 · W5-3·3
WI-5 · F6 classifier eval + verdictA measured false-negative rate on the frozen set vs baseline, then a mechanical CLOSE/ROUTE verdict against the owner-set threshold (D1).W5-4·1 · W5-4·2
WI-6 · F8 confirm-not-regress + hand-offOne live Meet confirms auto-dispatch within the ~10s cadence; all further recorder work routed to its existing plan owner.W5-5·1 · W5-5·2
WI-7 · F3 clean-baseline + Q27 routingLive confirmation the proposal workbench was never built + the recorded supersede (round-3 Q27=A rules it IN, built by W2) with a before-screenshot baseline.W5-6·1 · W5-6·2
WI-8 · F7(b) validation gap + W7 routingLive proof the tax-number field saves garbage silently, plus the one-line acceptance handed to W7 (sequenced after W3).W5-7·1 · W5-7·2
WI-9 · ZZ purge + closed verdict tableEvery sentinel artifact archived/removed across all surfaces, a pixel-proven clean board, and the final F3/F6/F7/F8/F10 verdict table as W5's deliverable.W5-8·1 · W5-8·2
❓ Open decisions I made myself — please comment (keep / change) in this round:
❓ D1 F6 acceptance threshold (W5-4): I left the "good enough" false-negative rate for you to set at sign-off. My proposal: 0 false negatives on the frozen set closes F6; any >0 routes it to the prompt-iteration loop. Confirm or set a different line?
❓ D2 F8 test-call depth (W5-5): I scoped ONE live test Meet (confirm-not-regress) since the recorder plan owns the feature — not a multi-call latency campaign. Enough, or do you want 3 calls for a less lucky sample?
❓ D3 Signing-flow test realism (W5-2/W5-3): to prove F10 live, the ZZ document gets actually signed by both parties (test signatures on a sentinel deal, purged after). OK to fully sign a ZZ test contract, or prefer webhook-replay only (cheaper, slightly less end-to-end)?
Sign-off — acceptance oracle. By signing, Sarudi Mátyás locks scenarios W5-1 … W5-8 and the invariants above as the acceptance answer key for the W5 Recover Stranded Features verification pass, and records his keep/change verdict on the ❓ open decisions D1–D3. The pass runs verify-only: gaps found are routed to their named owners (W2 · W7 · prompt-iteration · recorder plan), never built inside W5.
Sarudi Mátyás  ✔ Draft · 2026-07-12 · awaiting sign-off