ClientsFlow · Email-System Overhaul · W6 · EBO

EBO — W6 · Copy QA & Trust

FINAL — owner comments applied · 2026-07-12. The target state after W6 ships: the canonical sign-off is locked by a machine test that has teeth, every client-facing copy and every AI prompt is editable from the Templates panel (nothing code-only), the payment-details block stays locked inside the two money emails, and no raw {{variable}}, empty greeting, or double closing can ever reach a client. Scenarios S1 … S4.

What W6 owns
The proof layer for client-facing copy: the sign-off regression test (+ its owner-visible Health-tab stamp), migrating the signed-contract and payment-reminder bodies into the Templates panel with a locked payment-details block, making all remaining copies and AI prompts Templates-editable (owner ruling — nothing stays code-only), and the variable/greeting/closing guard. Three W6 deliverables are documents, not app behavior, so they live in the handoff and are not walked as EBO steps: the QA-trust gate page, the 3-step Codex second-model review plan (Q21=B), and the read-only live-truth audit of real sent mail.
What W6 only verifies (owned elsewhere)
W1 owns what sends and when (auto-send whitelist, the automatic daily payment-reminder ladder) — W6 never changes send behavior. W2 owns the touchpoint-history UI, freeze-at-Send, and the AI-reply-on-card feature itself. W3 owns the Details panel. W6 proves the text those surfaces produce is correct. The signed-contract email already exists and works — W6 changes only where its text LIVES, never what it says or when it fires.
You do → click/action/command 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 —)
Owner rulings folded in (comments 2026-07-12 — newest, win on conflict)The phone/bank leak-guard scenario is DELETED: "Don't overcomplicate things. I only asked just to simply remove two fucking lines from the sign off." There is no allowlist machinery, no site-by-site leak scan — the sign-off test's strict "every client-facing body ends with the canonical block" assertion already makes a phone/bank footer impossible, and that is the whole ask. Everything becomes editable: "Make all copies and prompts editable from the template panel instead" — so W6 does not ship a "code-only" marker for un-migrated bodies; it migrates them ALL (emails, SMS, AI prompts) into the Templates panel. The Codex-review scenario is removed from this EBO — the Q21=B 3-step operating plan is a document deliverable, specified in the handoff. The QA-trust gate and the live-truth prod audit move to the handoff too (owner: "I don't really understand this stuff so maybe you should put it into the handoff document") — they are process/reading tasks with no clickable surface. Standing: round-2 Q6 (money emails become Template-editable, bank block locked inside) · W1's comments (signed-contract email exists and auto-sends with its 4 parts — migrate its copy verbatim, never redesign; the payment chase is automatic daily reminders, no button; no failures list) · Q4/Q7 (the AI reply and every history row carry a resolved greeting + the canonical sign-off, 100% parity between shown and sent).
Invariants that hold everywhere — the canonical sign-off is exactly the 4-line SIGNATURE_HU block (name / role line / company + address / email): no phone line, no bank line, ever; bank + Stripe details appear only inside the payment-instruction bodies where a client actually has to pay; no client-facing text ever contains a raw {{token}}, an empty greeting ("Kedves ,") or a double closing; W6 sends nothing to a real lead — every live step runs on a ZZ-sentinel staging deal, purged after; all operator UI is English, Hungarian appears only inside client-facing copy; W6's regression test may never be disabled, skipped or weakened to make a run green — a red test is the feature working.
✅ Owner decisions — ALL SETTLED (2026-07-12, DECISIONS_FINAL.md). Nothing in this EBO is open; the run never guesses, never defaults, never flags amber.
✔ D1 The locked payment block is FIXED IN PLACE (S2.3). Its position in the body is not owner-movable — no drag handle, no "move up/down", no reordering as a unit. Its content stays locked as before. Editable text flows around it; the block itself does not move.
✔ D2 ⚠️ A template save is REFUSED — a hard stop — when the owner's edit removed a required placeholder (the amount, or the Stripe link) (S2.4). This reverses the earlier warn-and-allow spec: warn-and-allow is DEAD, do not build it. The Save is blocked, the template keeps its last valid version, and a plain-English message names what is missing ("The payment amount is missing from this template — the save was not applied"). A payment email with no way to pay is never a valid thing to save.
✔ D3 W0's daily watchdog does NOT spot-check real sent emails (not built). No recurring sign-off/no-token sampling of live mail, no extra tripwire on the alarm channel. The handoff's WD-3 one-off launch audit is the whole coverage — do not add a daily check, and do not ask W0 for one.

S1 — Canonical sign-off is locked by a regression test that actually has teeth (happy path / core build)

Who: Any agent/CI running the suite; Mátyás reads the outcome on the Health tab  ·  When: The sign-off already regressed once — the 2026-07-11 rogue-decoy incident sent a leaked phone/bank footer to 5 real leads, and nothing failed. After W6, any change that breaks the sign-off on ANY outbound email turns the suite red before it can ship, and the owner can see the lock is on without reading a terminal.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history
1 Run the email-copy test (python3 -m pytest tests/test_signoff_footer_regression.py -v), then open the dashboard's Health tab The test passes green and names every outbound email it checked (signed-contract delivery, payment reminders, booking confirmation, pre-call reminders, proposal, no-show ladder, first reply, negative follow-ups) — and the Health tab shows one plain line: "Sign-off check: 15/15 emails canonical — 2026-07-12", so the lock is visible without a terminal Copy: Health-tab line "Sign-off check: N/N emails canonical — <date>" · Look: a normal Health-tab status line, green · Where: Health tab (dash) + new file tests/test_signoff_footer_regression.py Every client-facing outbound-email-composing function is rendered with sample lead data and asserted to END with SIGNATURE_HU (a leading "Üdv,\n" immediately before it is allowed); the run writes its dated one-line result where the Health tab reads it; rep-only internal notification bodies are explicitly excluded by name, never silently skipped Must NOT skip any client-facing send site (a body not in the walk is a hole — exactly how the incident escaped); must NOT pass if even one body ends with anything other than the canonical block; must NOT need network/Notion to run; must NOT show a stale Health-tab stamp from an older run — (a test run touches no lead)
2 Perform the documented "teeth check": temporarily swap the sign-off constant to the OLD leaked footer (the one with the phone line + "Bank: 1260…"), re-run the test, revert, and record the check The suite goes RED and names each affected email function and quotes the offending leaked line — the exact 2026-07-11 incident is now impossible to ship silently. After reverting, the test file's docstring carries a dated stamp: "Teeth check last proven: 2026-07-12 — RED as expected", so "when was the lock last proven?" is answerable at a glance Copy: failure text = function name + the leaked string found; docstring line "Teeth check last proven: <date> — RED as expected" · Look: terminal, red · Where: tests/test_signoff_footer_regression.py (swap recipe + dated stamp in its docstring, never a permanent fixture) Nothing persists in the app — the swap is reverted immediately; the only lasting artifact is the dated teeth-check stamp in the docstring Must NOT leave the leaked footer in the working tree after the check; must NOT encode the teeth check as an always-on expected-failure that could mask a real regression; must NOT claim the check without updating the dated stamp
3 Later, any workstream edits an email body and (accidentally) drops or mangles its sign-off, then runs the suite The suite fails loudly, naming exactly which email lost its sign-off and quoting its actual last lines — the fix is obvious from the message alone Copy: "email_signed_document body does not end with SIGNATURE_HU — got: '…'" · Look: red test, precise message · Where: CI / local suite Nothing merges through the Ship pipeline with this test red; the failing function is fixed, never the test loosened. If a body one day legitimately needs a line after the signature (a P.S.), that is a single explicit, commented allowlist entry in the test — a visible decision, never a silent loosening of the "ends with" rule Must NOT be "fixed" by disabling, skipping, weakening or xfail-ing the test (the run report must state "the sign-off regression test was NOT disabled or weakened"); must NOT pass because a body merely CONTAINS the sign-off somewhere — it must END with it

S2 — Signed-contract + payment-reminder emails become Template-editable, payment details locked inside (happy path / the money-path copy migration)

Who: Mátyás  ·  When: Today these two highest-stakes emails are hardcoded — if he edits them in the Templates tab, the edit silently does nothing and nothing tells him so. Round-2 Q6 (standing): make them editable, keep the bank-details block locked inside. The signed-contract email's content is already right and it already auto-sends on the client's signature — this scenario changes where its text LIVES, never what it says or when it fires. Staging deal: ZZ-S2-SIGN Kft.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history
1 Mátyás opens the Templates panel on the dashboard The signed-contract delivery email and each step of the payment-reminder ladder now appear in the template list as editable entries, alongside the existing templates — previously they were simply absent, with no hint they were code-only Copy: new list entries "Signed contract — delivery email" · "Payment reminder — day 1…4" · Look: same list style as the existing template entries · Where: Templates panel The two bodies move from hardcoded Python literals into the owner-editable copy layer; their send timing and triggers stay exactly as they are (W1's automatic flow — auto-send on the client's signature, then one reminder a day for 3–4 days until payment or a logged phone call) Must NOT change what these emails currently say (migration is verbatim — the current live text becomes the initial template text); must NOT alter when/whether they send (W1's, untouched); must NOT list them as editable while edits still silently do nothing (the whole point)
2 Mátyás edits the signed-contract email's opening sentence, saves, then a ZZ-sentinel staging contract-signing fires the email for ZZ-S2-SIGN Kft The staging email arrives with his edited opening — proof the edit is LIVE, not decorative — and still carries everything the flow promises: the confirmation, the signed-contract download link, the Stripe link and the transfer details, ending with the canonical sign-off Copy: his edited Hungarian opening, then the unchanged 4 parts · Look: a real received email (staging, ZZ lead) · Where: the ZZ test inbox + the ZZ-S2-SIGN Kft card's history The send site now reads its body through the owner-editable copy layer (live override → baked default), interpolating the deal's Stripe link/amount into the edited text; S1's regression test still passes against the edited copy Must NOT send the old hardcoded text after a save succeeded (the silent-no-op bug this exists to kill); must NOT let an edit strip the sign-off or the payment block; must NOT run against a real lead — ZZ sentinel only, purged after The ZZ-S2-SIGN Kft card's history logs the sent email with the edited text — word-for-word identical to what was received (W2's parity bar, verified here on W6's migrated copy).
3 In the template editor, Mátyás tries to change, delete or move the payment-details part (including a select-all-delete + save, and an attempt to drag it above his closing paragraph) The bank-transfer + Stripe details render as a visibly locked block — readable in place so he sees the whole email, marked "Locked — payment details", not editable, not deletable and not movable: it is fixed in place (D1). His edits flow around it; the saved template still renders the block, in the same position Copy: locked-block label "Locked — payment details" (English UI); the block's content stays the Hungarian bank/Stripe lines · Look: muted background + lock icon, visually set off from the editable text; no drag handle, no move-up/move-down control · Where: template editor for the two money emails The payment-details block stays sourced from the single code-side constant, at a fixed position in the body; owner edits can only change the text around it, never the block itself and never its place in the email Must NOT allow the locked block to be edited, emptied or removed through ANY editor path (select-all-delete included — the saved template must still render it); must NOT allow the block to be repositioned/reordered as a unit (D1: fixed in place — no drag handle, no move control, and a save that relocated it is not applied); must NOT hide the block from the editor view (invisible-but-sent text is a parity violation)
4 Mátyás edits the signed-contract (or a payment-reminder) template so that a required placeholder is gone — he deletes the amount placeholder, or the Stripe-link placeholder — and clicks Save The save is REFUSED — a hard stop, not a warning he can click past. A plain-English message names exactly what is missing: "The payment amount is missing from this template — the save was not applied." The editor keeps his text on screen so he can fix it, and the stored template is still the last valid version. A payment email with no way to pay never becomes a saved template Copy: refusal message naming the missing placeholder ("The payment amount is missing from this template — the save was not applied." / "The Stripe payment link is missing…") · Look: the Save is blocked, the message sits by the Save button; no "saved" confirmation appears · Where: template editor for the money emails Nothing is written — the copy layer still holds the previous valid body; the send site therefore still renders a payable email. The refusal is server-side too, not only a UI check Must NOT allow the save (D2 — this is a hard stop; the old "warn on save but allow it" behavior is DEAD and must not be built); must NOT save the placeholder-less body and merely display a warning; must NOT let a later, unrelated save silently persist the broken body; must NOT silently re-insert the placeholder for him (the refusal must be visible, not auto-corrected); must NOT block a save that only touched text around the required placeholders

S3 — Every copy and every AI prompt is editable from the Templates panel — nothing stays code-only (happy path / owner ruling)

Who: Mátyás (edits) + any agent (runs the completeness check)  ·  When: Owner ruling, verbatim: "Make all copies and prompts editable from the template panel instead." The "guide says editable, code says hardcoded" drift was rediscovered 3+ times before W6. The fix is not a report about which bodies are hardcoded — it is that none are. The drift detector survives only as the guard that keeps this true after every future change.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history
1 Mátyás opens the Templates panel and scrolls the full list of client-facing copies EVERY client-facing email and SMS the app can send is there as an editable entry — booking confirmation, pre-call reminders, first reply, sequence steps, no-show ladder, negative follow-ups, proposal, signed contract, payment reminders, welcome kit, review request — with none missing and nothing marked code-only Copy: one editable entry per send site, named in plain English · Look: the existing Templates list, longer · Where: Templates panel Every remaining hardcoded body is wired through the owner-editable copy layer (verbatim migration — the current live text becomes the initial template text); the copy layer, not the Python literal, is now what the send site reads Must NOT change any body's wording during migration; must NOT leave a single client-facing body unlisted; must NOT show an entry whose edits are silently ignored at send time
2 Mátyás opens the Prompts section of the same panel, edits an AI prompt (e.g. the reply-drafting prompt), saves, then triggers that AI feature on the staging card ZZ-S3-COPY Kft Every AI prompt the app uses is listed and editable in the same panel — and the next generation visibly follows his edited prompt, so a prompt change is a thing he can make himself, not a code change he has to ask for Copy: prompt entries (reply drafting, sequence generation, enrichment/personalization) with an editor + Save · Look: same panel, Prompts section · Where: Templates panel → Prompts Prompt text is read at generation time from the editable layer (live override → baked default); no prompt string is reachable only from code Must NOT keep any prompt code-only; must NOT apply an edited prompt only after a redeploy (the edit must take effect on the next generation); must NOT let a prompt edit bypass the sign-off/greeting rules (S4 still governs the output) — (a draft generation is not a send)
3 Run the drift guard: python3 scripts/detect_copy_drift.py — then, as its self-test, have it scan a copy of the tree with one email body deliberately hardcoded On the real tree it exits clean with 0 code-only bodies; on the seeded tree it fails and names the offending file:line — proof the guard actually bites, so a future workstream that adds a raw email literal is caught immediately instead of months later when an owner edit does nothing Copy: "0 code-only bodies — all N copies + M prompts editable" / on the seeded tree: "HARDCODED: flows_x.py:123" · Look: terminal, one summary line · Where: new file scripts/detect_copy_drift.py The script derives the fact from the live code on every run (never a saved snapshot): it walks every email/SMS body and prompt string and cross-checks each against the editable copy layer Must NOT miss inline body literals that never got a named constant (the strongest form of hardcoded); must NOT auto-rewrite code (report + exit code only); must NOT be satisfied by an entry that merely EXISTS in Templates while the send site still reads the literal

S4 — No raw {{variable}}, no empty greeting, no double closing ever reaches a client (happy path + guard / cross-surface proof)

Who: Mátyás (reading previews) + the test suite (proving it)  ·  When: A lead receiving "{{greeting}}" instead of "Kedves Anna," is the single most trust-destroying copy bug possible — and "Kedves ," (missing name) is worse, because it reads as carelessness in fluent Hungarian and no scanner keyed on "{{" catches it. W6 proves resolution on every surface where generated copy is shown or sent. The surfaces belong to W1/W2 — existing/parallel flows, verify, don't redesign. Staging deal: ZZ-S4-VAR Kft.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history
1 Mátyás clicks Send Emails on the staging card ZZ-S4-VAR Kft and reads the generated sequence in the modal Every email in the modal opens with the lead's real resolved greeting — "Kedves Anna," — correct name, correct Hungarian form; nowhere in any preview does a raw {{greeting}}, {{name}} or any other {{…}} token show (existing flow, verify unchanged) Copy: resolved Hungarian greeting per email · Look: the Send-Emails modal previews · Where: lead card → Send Emails modal (W1/W2's surface) Copy is generated at Send-click from the templates + touchpoint history; variable resolution happens at generation, so what he reads IS what will be sent and frozen Must NOT show a resolved preview but send an unresolved body (preview↔send parity); must NOT fall back to an empty greeting ("Kedves ,") when a name is missing — a missing input surfaces as a visible problem in the modal, never as broken Hungarian in a sent mail On Send, the frozen scheduled rows carry the same resolved text the modal showed (W2's parity bar — W6 checks the copy half of it).
2 On a card with an unreplied lead email (ZZ-S4-VAR Kft), Mátyás clicks the compose-AI-reply button and reads the AI draft (W2's feature) The draft opens with the proper resolved Hungarian greeting, its body draws on this lead's actual history, and it ends with the exact canonical sign-off — and with ONE closing only: no home-grown "Üdvözlettel, Mátyás" sitting on top of the canonical block, no double goodbye Copy: "Kedves Anna," + contextual Hungarian body + the single 4-line canonical sign-off · Look: the editable AI-reply field on the card · Where: lead card (W2's surface) The AI path runs the same greeting-resolution, sign-off and closing rules as template emails — one copy-rule set, not a separate AI-path approximation; a duplicate closing is stripped before the draft is shown Must NOT show a raw token in an AI draft; must NOT let the model improvise its own sign-off variant (extra titles, different address, added phone) — the canonical block is appended by rule, not by the model's memory; must NOT render two closings — (a draft is not a send; the send that may follow is W1/W2's, and its history row must match what left)
3 Run the copy guard across every rendered body: python3 -m pytest tests/test_copy_guard.py -v Green — zero {{ occurrences in any RENDERED body (template path and AI path), zero empty-greeting shapes ("Kedves ,", doubled spaces, lowercase name), zero duplicate closings Copy: test output, one line per checked body · Look: terminal, green · Where: new file tests/test_copy_guard.py The suite renders every body with sample data and inspects the OUTPUT, never the template source (tokens legitimately live in templates pre-resolution; the crime is a token in rendered output) Must NOT grep template source and call it a pass; must NOT check only the "{{" syntax and miss the Hungarian shape failures (empty greeting, double closing); must NOT send anything as part of the check
🕓 Touchpoint-history note for W6:
W6 is a proof workstream — it never authors a touchpoint. Its tests render bodies in memory, its guard reads code, its migrations move text between layers. History matters in exactly two places, both as verification targets: the ZZ-S2-SIGN Kft signed-contract send must log with the owner-edited text, word-for-word identical to the received email (S2.2), and the frozen scheduled rows must carry the same resolved copy the Send-Emails modal showed (S4.1). Parity itself is W2's bar; W6 supplies the copy-correctness half of the evidence.

Work-item → scenario-step mapping

Each build work item, and the scenario steps that prove it works when run/clicked through. The three W6 document deliverables (QA-trust gate · Codex 3-step review plan · live-truth prod audit) have no clickable surface — they are specified in the handoff (handoffs/W6_COPY_QA_TRUST.md) with their own definitions of done, and are not walked as EBO steps.

Work itemWhat it deliversProven by (scenario · step)
WI-1 · Sign-off regression test + Health stampHermetic test walking every client-facing send site: each rendered body ends with the canonical SIGNATURE_HU block; a dated teeth-check stamp in its docstring proves the test catches the 2026-07-11 incident footer; a one-line dated result on the Health tab makes the lock owner-visible.S1.1 · S1.2 · S1.3
WI-2 · Money-email Templates migration + locked payment blockSigned-contract delivery + payment-reminder ladder bodies become owner-editable in the Templates panel (verbatim migration, send behavior untouched — W1's); the payment-details block renders as a locked, non-editable, fixed-in-place unit inside the editor (D1); and a save that removed a required placeholder (amount / Stripe link) is REFUSED outright (D2).S2.1 · S2.2 · S2.3 · S2.4
WI-3 · All copies + prompts editable (nothing code-only)Every remaining client-facing email/SMS body AND every AI prompt migrates into the Templates panel; scripts/detect_copy_drift.py is the standing guard (0 code-only bodies, self-tested to actually bite).S3.1 · S3.2 · S3.3
WI-4 · Variable / greeting / closing guardtests/test_copy_guard.py: no rendered body (template or AI path) contains a raw {{token}}, an empty greeting, or a duplicate closing; verified on the Send-Emails previews and the on-card AI reply.S4.1 · S4.2 · S4.3
Sign-off — acceptance oracle. By signing, Sarudi Mátyás locks scenarios S1 … S4 and the invariants above as the acceptance answer key for W6 Copy QA & Trust. All owner decisions are settled (D1 · D2 · D3, DECISIONS_FINAL.md) — nothing in this EBO is open, and no step may be flagged AMBER. The W6 run's own final report must include the explicit statement "the sign-off regression test was NOT disabled or weakened to make this run green." No W6 code is built or deployed before this signature.
Sarudi Mátyás  ✔ FINAL · 2026-07-12 · awaiting sign-off