Skip to content

Green Wellness

Changelog

What’s new in each release of the scheduling platform

Show:
v2.97.VCO0005current
2026-06-08Production
For front desk

You can now block off a provider's dates for vacations or clinic closures so patients can't book them (staged off until we switch it on)

What this means for you

We added a "Block off dates" tool to Manage Slots (cmq1qmh5m, Mariane's request). Pick a provider, a start and end date, and a reason like "provider vacation" or "clinic closed," and those days are marked unavailable for that provider. Unlike the existing "bulk clear" (which just deletes the slots that exist right now — they come back the next time slots regenerate), a block is durable: it keeps the provider unbookable on those dates even after new slots are generated, and it stops both online and phone/voice bookings from landing on a blocked day. You can see and remove existing blocks in the same panel. Important: the enforcement is shipped switched OFF — staff can create blocks now, but they won't actually hide slots or stop bookings until Doug turns the feature on (and the new data table is added on the database). This is admin-only and involves no patient information.

Show technical details

Added

  • 🛑 Provider Date Blocking — durable per-provider unavailability ranges (cmq1qmh5m, Mariane). New ProviderDateBlock table (providerId + inclusive start/end calendar dates + free-text reason + creating-staff id) with admin CRUD at /admin/slots/manage ("Block off dates" panel) backed by /api/admin/slots/blocks (GET/POST/DELETE, staff-gated ADMIN/MANAGER/SCHEDULER, audited as PROVIDER_DATE_BLOCK_ADDED/REMOVED — detail is providerId+dates+reason only, PHI-free). Enforcement lives at three points behind a single shared predicate (isSlotDateBlocked): the patient availability calendar filters blocked-date slots out, and BOTH appointment-creation paths (web/voice /api/appointments and admin-manual) refuse a booking whose date is blocked. Calendar math is a pure, dep-free helper (provider-date-block-shared.ts, 20 pin tests) using inclusive yyyy-MM-dd string-range comparison (timezone-proof). DARK-LAUNCH: every enforcement path is gated on PROVIDER_DATE_BLOCKING_ENABLED (default OFF) — until Doug flips it, the read/booking paths behave exactly as before and never query the new table, so staff can stage blocks ahead of go-live. Requires prod-migration-80 applied on the BAA-covered tenant (Doug-action) + the env flag. (slots)(booking)(availability)(admin)(hipaa-clean)
v2.97.VCN0005
2026-06-08Production
For front desk

Converting a lead to a patient now reuses a DOB we already have on file instead of asking again

What this means for you

When you convert a lead into a patient, the system needs a date of birth. Until now, if the modal didn't have one and the website intake form didn't capture one, you'd get a "DOB required" message and have to type it in — even when that lead's date of birth was already in the system from the Salesforce import. Now the convert step automatically checks the imported lead record (matched by email) and reuses the DOB it finds there, so you don't have to re-enter something we already have. Date of birth is still required to create a patient — this only removes the re-typing when the value already exists. Mariane flagged this from testing (the friction of being asked for a DOB that was already captured elsewhere).

Show technical details

Fixed

  • 🗂️ Lead → patient conversion now auto-reuses a date of birth already on file. The convert route already fell back to the website-intake sidecar (LeadIntake) when the modal didn't supply a DOB; it now adds a third fallback to the Salesforce-imported Lead table, matched by email (Lead.email is indexed; same email trust-key the relink-by-existing-patient path already uses, ordered by most-recently-updated, dob-not-null). This removes the "DOB required" friction Mariane reported (cmq4l5yx9) for leads whose DOB was captured at SF import but lived in a table the convert flow never consulted. DOB remains REQUIRED and is still written to Patient.dob — this is a sourcing change only, NOT the "make DOB optional/nullable" request (that needs a schema + clinical decision and is unchanged). The LEAD_CONVERTED audit row now records dob_source=manual|intake|sf_lead (provenance label only — no PHI). Admin-facing only; no patient-facing, Retell, or schema change. (leads)(convert)(hipaa-clean)
v2.97.VCM0005
2026-06-08Production
For front desk

Isabella stops re-asking for a callback number, makes email optional, and won't promise a transcript she can't send

What this means for you

We tuned how Isabella takes a message when the team isn't available, based on Mariane's notes from listening to real calls. Three things change. (1) She now asks for the callback number once and keeps it — she won't make a patient repeat their phone number two or three times unless the line was genuinely garbled. (2) Email is now clearly optional: she offers it once, and if a caller would rather not give one or says "skip," she says "no problem, we'll reach you by phone instead" and moves on instead of pushing. (3) She no longer promises to send a "transcript" of the call — she's honest that we don't send the call recording, but reassures the patient our team has everything they said. Nothing about a patient's chart or medical details is involved here; this is just the message-taking script.

Show technical details

Changed

  • 📞 Isabella message-taking script refinements (Retell general_prompt re-deploy via scripts/sync-retell-prompt.mjs) closing three reviewer-feedback items from Mariane. (1) Callback-number-collected-once / no redundant re-ask was already in the prompt (fixed name→number→concern order, capture-once, re-ask only on low transcription confidence) — it reaches the live agent with this sync. (2) NEW edit: email is requested-but-optional — Isabella offers it once and, on a decline or "skip," falls back to phone-only ("no problem, we'll reach you by phone instead") without insisting or re-asking. (3) Transcript-honesty wrap-up (never promise a literal call transcript; be honest we don't send the recording but the team has the details) was already in the prompt — also goes live with this sync. Closes reviewer-feedback cmq0cldm (callback flow), cmq1rkrd3 (skip-email), cmq1rnf3i (transcript). HIPAA: the voice prompt collects no DOB/SSN/address verbally and the crisis + identity/legal-boundary rules are unchanged; this is message-flow copy only — no PHI handling, schema, or data-flow change. Note: a git/Vercel deploy alone does NOT change Isabella — the change is live only after the Retell sync script runs. Doug-approved 2026-06-08. (isabella)(voice)(retell)
v2.97.VCK0005
2026-06-08Production
For everyone

Isabella can now match old voicemails to charts on her own (off until you turn it on)

What this means for you

When a patient calls Isabella and she can't tell who they are at the moment of the call, that call sits in the list as an "unknown caller" — even if it's a patient we already have on file. There's already a button on the Demi page that matches those unknown callers to their chart by phone number. This adds a behind-the-scenes helper that does the same thing automatically once a day, so the backlog clears itself instead of waiting for someone to click. It only links a call to a chart when EXACTLY one patient has that phone number — if two people share a number, it leaves it for a person to decide, so a call never lands on the wrong chart. It never marks a call "handled" — it just connects the dots so you can see whose call it was. This ships turned OFF; it does nothing until Doug flips the switch.

Show technical details

Added

  • 📞 Autonomous orphan-call reconcile cron (/api/cron/voicemail-reconcile, daily 03:45 PT) — the automation layer over the existing demi-today "Match callers to charts" button. Calls the SAME shipped reconcileOrphanCalls() helper (no duplicated match logic), which links patientId onto unmatched inbound CALL rows ONLY on an exactly-1 patient phone match (0=leave null, 1=link, 2+=leave null for a human) — the exactly-1 guard that fixes the substring-collision mis-thread risk in the older backfillOrphanMessages. Default-OFF behind VOICEMAIL_RECONCILE_ENABLED (deploy is inert; flipping that one env var is the "make Isabella's backlog reconcile autonomous" switch); the heartbeat fires on the disabled path too (returns enabled=false, NOT stale). NEVER resolves/clears a row — links only, honoring the flag-don't-auto-resolve decision. Registered 3-way (vercel.json crons + CRON_ACTORS + EXPECTED_CRON_ACTORS). HIPAA: counts-only audit + summary (linked/multi/noMatch/junk) — no phone, name, or transcript in logs or audit detail; error path logs err.name only (never err.message, which could echo a caller phone). Doug 2026-06-08. (voicemail)(isabella)(cron)(hipaa)
v2.97.VCJ0005
2026-06-08Production
For everyone

There's a new Tasks board — hand to-dos to each other without email

What this means for you

We added a Tasks board so you can hand a to-do to a teammate right inside the admin instead of sending an email. Post a task (with who it's for), and they get a quiet "You have tasks waiting" banner at the top of their screen. They can mark it done, or send it back to you with a note if they have a question — and you'll see it close or come back on your own board. If a task is about a patient, use first name + last initial and keep it to the administrative ask (e.g. "call Sarah M. about her renewal date") — no medical details, since the board stays inside our private system. Nothing about this texts or emails anyone; it all lives in the admin.

Show technical details

Added

  • ✅ Two-way staff Tasks board (Doug ↔ Mariane handoff) — new /admin/tasks page + StaffTask model lets a participant (ADMIN / MANAGER / SCHEDULER) post a task assigned to another participant, who marks it done or sends it back with a required reason; the poster can reopen a sent-back task or close it themselves. State machine (open → done | sent_back, sent_back → open | done, done terminal) is a pure whitelisted canTransition() in src/lib/staff-tasks.ts with 15 unit tests covering the participant gate, the create validator (trim/required/length-clip/blank→null), and every legal/illegal transition. A StaffTaskNudge server component renders a COUNT-ONLY banner in the admin layout (never a title or patient ref). HIPAA: in-app only — no SMS/email, so the whole board stays inside the BAA boundary; patient references are limited to first-name + last-initial + administrative subject by convention, and audit detail records ids/transitions/lengths only, never the task body. Inert by default behind STAFF_TASKS_ENABLED (page shows a "being set up" panel and runs ZERO queries until the flag + migration 78 land). Doug 2026-06-07. (staff-comms)(admin)(hipaa)
v2.97.VCI0005
2026-06-08Production
For front desk

Voicemails from known patients now link to their chart — and flag the ones already handled

What this means for you

We made the Callbacks-owed list smarter. A new "Match callers to charts" button finds voicemails from numbers we already have on a patient's file and links them straight to that patient — so you can tap the name and jump to their chart instead of staring at "(unknown caller)." It only links when exactly one patient has that number; if two patients share a number, it leaves it for you to pick so nobody's call lands on the wrong chart. And any caller who already has a recent or upcoming appointment now shows a green "✓ looks handled" tag — a strong hint they've already been scheduled, so you can confirm and clear them fast instead of calling back. It only flags; it never clears a row for you.

Show technical details

Added

  • 📞 Voicemail-backlog reconcile on /admin/demi-today — new "Match callers to charts" action (ADMIN/MANAGER) backfills patientId onto orphan inbound CALL rows (Isabella voicemails that never matched a patient at receipt-time). New orphan-call-reconcile.ts groups orphans by normalized phone (pure core split to -shared.ts, 9 unit tests) and links ONLY on an exactly-1 patient match via phoneOrWhere (findMany take:2): 0=leave null, 1=link, 2+=leave null + count skippedMulti. Fixes the substring-collision mis-thread risk in the older backfillOrphanMessages (bare CONTAINS last10, no uniqueness check). PHI-safe: new ORPHAN_CALL_RECONCILE audit logs counts only (linked/multi/noMatch/junk) — no phone/name/transcript. (demi)(voicemail)(hipaa)
  • ✓ "Looks handled" flag on the Callbacks-owed queue — getDemiCallbacks now derives a looksHandled boolean (caller is a linked patient with an Appointment in the last 30d or any future date, status not CANCELLED/NO_SHOW) and renders a green badge. Flag-only by design (Doug "1 flag 2 backfil"): never auto-resolves the row — staff still click "✓ Called back" after a glance. (demi)(voicemail)
v2.97.VCH0005
2026-06-08Production
For everyone

Patient pages with a missing date of birth no longer throw a Server Error

What this means for you

Fixed the "Server Error" some of you hit when opening certain patient and appointment pages. A chunk of older imported records (and patients who booked before giving us a birthdate) don't have a real date of birth on file. When a page tried to display that empty date, it crashed the whole page instead of just leaving the date blank. Now a missing or unreadable date simply shows as a dash (—) and the page loads normally.

Show technical details

Fixed

  • 🩹 Date-render crash guard — the shared fmtPT/fmtDate helpers in src/lib/tz.ts now fail graceful on null/undefined/invalid input (returns an em-dash ) instead of letting formatInTimeZone(new Date(badValue), …) throw RangeError: Invalid time value mid-SSR. Root cause: legacy/Salesforce-imported + book-now-flow rows carry a null/sentinel Patient.dob even though Prisma types it non-null (only dobOnFile=true was recorded), so unguarded fmtDate(patient.dob,…) crashed admin Patient/Appointment cards into the error boundary ("Server Error"). One helper-level guard protects every PHI date render fleet-wide. No new data flow / transport / logging — render-robustness only. (hipaa)(admin)(render-robustness)
v2.97.VCG0005
2026-06-07Production
For everyone

Patient outreach now refuses to email addresses we know will bounce

What this means for you

We added a safety brake to all patient email. About 21,000 patient records have a placeholder email (a stand-in we stamp when we don't actually have a real address on file). If we ever blasted those, every one would bounce — which makes spam filters distrust us and starts sending our real patient mail to junk folders. Now the system flatly refuses to send to a placeholder address; those patients are reached by text instead, when they've consented. Nothing changes for patients with a real email.

Show technical details

Added

  • 🛑 Placeholder-email send block — @unresolved.local (the synthetic domain stamped on ~20.9k patient rows with no real email after the SF→PF enrich) is now hard-refused at the send layer. New shared isPlaceholderEmail() + PLACEHOLDER_EMAIL_DOMAIN in email-deliverability.ts (single source of truth, mirrors the enrich script's literal). Two-layer defense: sendEmail() + sendEmailWithMessageId() short-circuit-return before any provider call (universal backstop, covers direct callers), and sendEmailToPatient() returns a structured reason: "placeholder_email" skip so the renewal/reminder crons count it as a no-contact skip and still run the SMS fallback rail for consented patients. Prevents guaranteed-bounce blasts that would shred sender reputation. 33 unit tests (7 new). (deliverability)(patient-email)(hipaa)
v2.97.VCF0005
2026-06-07Production
For everyone

Updates now have a plain-English headline you can tap to read more

What this means for you

We made the "What's New" notes easier to read. Each update now shows a short headline you can scan, with a "For front desk," "For providers," or "For everyone" tag so you know at a glance whether it's yours to read. Tap a note to open a plain-English "What this means for you" — the techy details are still there, just tucked one more click away if you ever want them.

Show technical details

Changed

  • 📰 Staff changelog readability — the staff-facing "What's New" banner + the /changelog page now render a scannable headline (auto-derived from the first sentence of each staff summary, or a hand-tuned staffHeadline) with a "What this means for you" disclosure beneath it, plus an optional audience pill driven by a new audience field on ChangelogEntry ("front_desk" | "providers" | "everyone"). New pure splitStaffSummary() helper (abbreviation/digit-aware first-sentence splitter, length-guarded fallback to whole-summary headline) is shared by both surfaces and unit-tested. Both new fields are optional + additive — older entries auto-derive their headline and render exactly as before. Patient portal banner untouched (not a staff surface). Doug 2026-06-07. (changelog)(staff-ux)
v2.97.VCE0005
2026-06-07Production

Fixed the records-upload page so it always shows an upload box.

What this means for you

Fixed the records-upload page so it always shows an upload box. When Isabella texts or emails a patient a secure link to send in their medical records, that patient often doesn't have an upcoming visit yet — and the page was only showing the "Upload medical records" button next to a booked appointment. So those patients landed on the page with nowhere to drop their files. Now anyone who opens their records page gets a working upload box, whether or not they have a visit on the calendar.

Show technical details

Fixed

  • 📎 Records-upload portal now shows an upload box even with no upcoming appointment. The control was only rendered inside the upcoming-appointments loop, so a patient with no booked visit (exactly the population the records-upload link targets — new leads, pending patients, lapsed renewals) hit the empty-state card with no way to upload. The upload API already supports appointment-less, patient-scoped uploads (stores under patients//), so the page was the only thing broken. Made appointmentId optional in the component (skips the appointment query param + form field when absent) and rendered a patient-scoped in the no-upcoming branch. (patient-portal)(uploads)
v2.97.VCD0005
2026-06-07Production
For providers

Fixed a slot-duration save error on the Providers page.

What this means for you

Fixed a slot-duration save error on the Providers page. When you set a provider's per-appointment length to 15 or 20 minutes, the box rejected it with "Please enter a valid value" even though both are perfectly normal slot lengths (Mariane hit this setting up Dr. Riordan). The minutes box now accepts every multiple of 5 from 5 up to 240 — 5, 10, 15, 20, 25, 30, and so on — so 15- and 20-minute slots save fine.

Show technical details

Fixed

  • ⏱️ Providers admin: per-provider slot-duration now accepts 15 and 20 minutes. The minutes input combined min=1 with step=5, which makes the browser's set of accepted values 1, 6, 11, 16, 21… — so 15 and 20 were rejected with a native "Please enter a valid value" even though both are valid slot lengths (Mariane cmq1q7830 2026-06-05). Changed the floor to min=5 so the accepted grid becomes 5, 10, 15, 20, 25, 30… up to 240. Purely a browser-side input constraint fix — the save/clamp logic and the API schema already accepted 15. (providers)(admin-ux)
v2.97.VCC0005
2026-06-07Production
For providers

Three small things from your feedback.

What this means for you

Three small things from your feedback. Provider profiles no longer need a headshot to count as complete — a photo is a nice-to-have now, not a requirement, so a missing one won't block you. On the Providers page you can now flip between All / Active / Inactive with one tap, so inactive providers don't bury the ones you're looking for. And in the inbox, email messages from someone we couldn't auto-match to a patient are now clickable — they open the email's history view instead of doing nothing.

Show technical details

Fixed

  • 📨 Inbox: email rows from unmatched senders are now clickable. A conversation whose sender didn't match a patient used to compute its row link from the last 10 digits of the from-address — emails have no digits, so the link came out empty and the row rendered as a dead
    that did nothing when clicked (Mariane cmq1qxz2b). Unmatched EMAIL rows now route to the existing per-thread email audit view (/admin/messages/email/), which already accepts a message-id fallback, is already admin-session-gated, and writes its own view-audit row. No new data is surfaced — an already-authorized destination is just reachable by click now. (messages)(inbox)

Changed

  • 🪪 Provider profile no longer requires a headshot photo to count as complete. The self-service provider profile card treated a missing photo the same as a missing NPI or email — flagging the profile incomplete and (per the prior gate) able to hold up issuing authorizations (Mariane cmq1rf4rr 2026-06-05). Photo is now explicitly optional: it stays in the form as a nice-to-have upload, but no longer factors into the complete/incomplete state or the "missing fields" list. NPI, email, and Doxy.me link remain required. (providers)(profile)
  • 🗂️ Providers admin page: added an All / Active / Inactive filter. Inactive providers accumulate and bury the active ones, forcing a Ctrl+F to find anyone (Mariane cmq1q56zy 2026-06-05). A one-tap tab bar (with live counts) now filters the already-loaded list — purely client-side, no extra fetch, no API change. (providers)(admin-ux)
v2.97.VCB0005
2026-06-07Production
For front desk

Fixed a follow-up-email problem Mariane flagged: a few patients kept getting the automated "please send your medical records" reminders even after they'd already booked an appointment (Jay was the example). The records-reminder now double-checks against real appointments — if a lead's email matches a patient who already has an appointment on the books, the reminders stop, and the lead is quietly marked as converted in the app. Nothing else about the reminder cadence changes.

Show technical details

Fixed

  • 📧 Records-reminder emails no longer chase patients who already booked. The daily records-reminder cron previously only stopped when a LEAD_CONVERTED audit row existed — and that row is written by a reap-vulnerable booking-flow after() callback that can miss (Fluid-Compute reap, or a fuzzy email/phone match that didn't line up). When it missed, the lead kept getting Day 3/5/7 record-request reminders forever (Mariane 2026-06-07, lead 7404710 / Jay). The cron now does a ground-truth check: one batched, case-insensitive lookup of every captured lead's email against Patient rows that have at least one appointment (Patient.email is unique). A match is treated as converted, so the reminder is skipped. (records-reminder)(leads)
  • 🔁 Self-healing lead conversion — when the cron finds a booked-appointment match but no LEAD_CONVERTED audit row exists yet, it writes one (idempotent, tagged mode=backfill-on-records-reminder) so the lead shows as converted in-app, exactly as Mariane asked. This runs entirely in the cron — the booking hot path is untouched, no migration. No PHI in the audit detail (IDs only). (leads)(audit)
v2.97.VBZ0005
2026-06-07Production

The website now has an "Areas We Serve" page that lays out where patients can be seen — three in-person clinics (Lynnwood, Spokane, Olympia) plus renewals by video anywhere in Washington. We also added Vancouver, Camas, and Battle Ground to the telehealth-renewal pages for Southwest Washington, and removed the old Vancouver clinic listing since there's no staffed office there — those patients are served from Olympia in-person or by telehealth. Nothing changes about booking.

Show technical details

Added

  • 🗺️ New /areas-we-serve overview page — a single statewide map of where Green Wellness sees patients: the three in-person clinics (Lynnwood, Spokane, Olympia) pulled from LOCATIONS_CONTENT, plus telehealth-renewal cities grouped by region pulled from TELEHEALTH_CITIES. Links into the existing /locations/[city] and /telehealth/[city] pages. Carries MedicalOrganization + breadcrumb JSON-LD (areaServed = Washington State; in-person clinics as MedicalClinic locations). No medical/efficacy claims — honest RCW 69.51A.030 framing (new-patient initial in person, renewals by telehealth statewide). (areas-we-serve)(seo)
  • 📍 Telehealth-renewal city pages for Southwest Washington — added vancouver, camas, and battle-ground to TELEHEALTH_CITIES (all Clark County). Each renders a /telehealth/[city] page with local nearby-area context and city-specific FAQs answered honestly (no physical Vancouver clinic — Clark County residents do their in-person initial at Olympia, ~90 min north on I-5, then renew by telehealth statewide). (telehealth)(seo)(clark-county)

Removed

  • 🚫 Placeholder Vancouver clinic — removed the vancouver entry from LOCATIONS_CONTENT (it had an empty address with "provided at booking confirmation" language — it was never a staffed clinic). The /locations index now lists three real clinics (Lynnwood, Spokane, Olympia). Legacy Vancouver clinic URLs (/locations/vancouver, /locations/vancouver-medical-marijuana-card, /locations/vancouver-medical-marijuana-doctor, and the bare/typo root variants) now 301-redirect to /telehealth/vancouver so existing SEO + bookmarks land on the right page instead of a 404. Removing the bookable Vancouver option from the Book Now wizard is a separate operator toggle on the live location row (DB-driven), not a code change. (locations)(vancouver)(redirects)
v2.97.VBY0005
2026-06-07Production

The renewal-reminder link now names the right clinic.

What this means for you

The renewal-reminder link now names the right clinic. When a patient who normally sees Marnie clicks the in-person renewal button, it now says 'Book in-person renewal at Olympia' instead of always saying Lynnwood — matching the location rules already set for who renews where. Everyone else still sees Lynnwood. This was the last patient-facing spot still hardcoded to Lynnwood; the Book Now form already shows all open clinics.

Show technical details

Fixed

  • 📍 Renewal-link clinic name — the /renew in-person button no longer hardcodes "Lynnwood". It now reads the LR0005 provider-location rules (provider-location-rules.ts, Doug 2026-05-31): a returning patient whose prior authorization was issued by Marnie sees "Book in-person renewal at Olympia"; everyone else (and any case where Olympia is closed or the issuer is unknown) sees Lynnwood. The /api/renew/book route is unchanged — it still posts only format=inperson and the downstream slot picker stays DB-driven; this only corrects the clinic NAME shown so it matches the rules table instead of always saying Lynnwood. Companion to the 2026-06-04 Book Now widget fix (which already reads the same rules lib for new + returning in-person clinics). PHI: none — uses the existing token-loaded auth's issuingProviderId (a cuid, not PHI) to pick a clinic label. (renew)(provider-location-rules)(LR0005)(cmpuiu2ek)(cmpw2tvph)
v2.97.VBX0005
2026-06-06Production

Two more provider-portal screens now leave a record when they're opened — the provider home page and the queue dashboard.

What this means for you

Two more provider-portal screens now leave a record when they're opened — the provider home page and the queue dashboard. The system already logged when a chart, encounter, or authorization was viewed; this fills in the last two big screens so every time a patient's information is shown to a provider, there's a trail. Nothing changes about how the screens look or work.

Show technical details

Added

  • 🩺 Audit-trail completeness (D9 cutover hardening) — the provider-portal HOME (/provider/portal) and the QUEUE DASHBOARD (/provider/portal/dashboard) now each write one audit row per page-load (VIEW_PROVIDER_PORTAL_HOME / VIEW_PROVIDER_QUEUE_DASHBOARD — both AuditAction slots existed but were never wired). These are the two highest-PHI provider surfaces (home renders patient demographics + intake conditions/medications/allergies across today+upcoming appointments + pending approvals; dashboard renders the full queue), and they were the only PHI READ pages in the portal lacking an audit trail — the today/encounters/authorizations pages already audit their reads. Fires only for an authenticated, active provider (after the fail-closed guard) and only on page-load — the today/checkins POLL route stays deliberately unaudited to avoid per-tick spam. Closes the §164.312(b) audit-controls gap before the EMR cutover. The legacy /provider/[token]/** tree needs no fix — it was already collapsed to no-PHI redirect bridges (D8). PHI: none new — this only records existing reads. (provider-portal)(audit-controls)(hipaa-164.312b)(D9)(emr-cutover)
v2.97.VBW0005
2026-06-05Production

Behind-the-scenes groundwork for the move off Practice Fusion.

What this means for you

Behind-the-scenes groundwork for the move off Practice Fusion. When we eventually switch to our own records system, a single setting will instantly stop the office from writing new patients and appointments back into Practice Fusion — no scramble, no code changes on the day. Nothing changes today: Practice Fusion stays fully on until we flip that switch.

Show technical details

Added

  • 🔌 EMR-cutover write guard — all three Practice Fusion write paths (create patient, create appointment, cancel appointment) now check the EMR_ACTIVE_SYSTEM flag before they touch Practice Fusion. When the flag is practice-fusion or both (today's setting, and the safe default for any unset/typo value) the writes behave exactly as before. When cutover flips it to own-emr, every Practice Fusion write short-circuits to a no-op — the single chokepoint covering all four booking/reschedule/cancel callers (and any future caller) so the cutover is one env flip with zero code deploy. Inert in production right now. Pinned by a source-text guard test asserting the flag check precedes each network call and the API-key check. PHI: none new — this only gates existing writes off. (practice-fusion)(emr-cutover)(write-guard)(M7b)
v2.97.VBU0005
2026-06-05Production

Isabella's "Today" desk now puts the most urgent patients at the very top — anyone flagged crisis or clinically-urgent floats above routine callbacks and voicemails, instead of just sorting by oldest-first. Crisis and urgent rows get a bold red "Call now" button, and the red crisis banner at the top now correctly catches every crisis-flagged patient (a gap where some crisis flags weren't lighting it up is fixed). And every item on the desk — not just the AI-flagged ones — now has a one-click "Mark done / Mark resolved" button, so a returned voicemail or handled urgent message can be cleared off the list right there. The desk also loads up to 200 open items (was 50) so nothing hides off-screen on a heavy day.

Show technical details

Changed

  • 🩺 Isabella Today (exception desk) — the "Needs attention" queue now sorts by URGENCY FIRST, then oldest-within-urgency. Each row is tagged a priority tier (0 = crisis-flagged · 1 = clinical-urgent · 2 = normal callback/voicemail · 3 = system/dead-letter) and the list sorts tier-ascending then oldest-first inside each tier, so a just-arrived crisis pins to the top instead of sinking below an hours-old routine callback. Previously the queue was a flat oldest-first list, which buried the loudest items mid-page exactly when volume was highest. The Band-0 red crisis banner now also includes the real crisis AI category (it previously only matched clinical-urgent, so a genuinely crisis-flagged row could fail to light the banner) — closes a safety gap. PHI: none new — derived entirely from already-loaded queue data, no new query. (isabella-today)(demi-exception-desk)(priority-sort)(crisis-safety)
  • 🩺 Isabella Today — every queue row now has a one-click resolve. The inline "Mark resolved" button (previously only on AI-flagged escalation rows) now renders on clinical-urgent-unreplied and stale-voicemail rows too, labeled "Mark done" for those, so Demi can clear a returned voicemail or handled urgent message straight off the desk. The resolve endpoint no longer requires the row to be AI-flagged (needsHumanAt); it simply stamps resolved-at / resolved-by, and the queue SQL filters resolved rows out, so the item drops on refresh. Crisis + clinical-urgent rows render a filled-red "Call now" button (vs the outlined "Call") so the action matches the urgency. The open-escalation load cap was raised 50 → 200 so the oldest items can't truncate before the newest crisis rows load on a heavy day. PHI: none new. (isabella-today)(demi-exception-desk)(one-click-resolve)(call-now)
v2.97.VBT0005
2026-06-05Production

Provider portal links now expire 90 days after they're created — a small security upgrade so an old bookmarked link can't be used forever.

What this means for you

Provider portal links now expire 90 days after they're created — a small security upgrade so an old bookmarked link can't be used forever. If a provider ever clicks a link that's aged out, they'll see a friendly note asking the office for a fresh one, and you can send a new link from their record in a couple clicks.

Show technical details

Added

  • 🛡️ Security self-red-team — 90-DAY TTL on the provider-portal URL bearer token. The provider portal entry point is /provider/<64-char-token>, a 256-bit bearer credential that lives in a bookmarkable URL. The session COOKIE already had a two-axis expiry (idle + 8h absolute, D11), but the URL TOKEN itself had no expiry — a leaked bookmark / forwarded magic-link granted access forever. Now every mint + regenerate stamps Provider.portalTokenExpiresAt = now + 90d (new nullable column), and BOTH token→cookie exchange resolvers (exchangeTokenForCookieRsc + exchangeTokenForCookieApi) reject an aged-out token with a distinct expired-token reason. The landing page renders an actionable "this link has expired — ask the office for a fresh link" card (distinct from the existence-hiding 404 for invalid/unknown tokens). FAIL-OPEN on NULL portalTokenExpiresAt so the legacy backfill window never locks anyone out. Revoke nulls the column alongside the hash. Pinned by anti-divergence test: the reason union, both resolvers' select + time-compare (mirror), the fail-open NULL guard, and the landing-page card. PHI: none — bridge looks up Provider directory only. (provider-portal-token)(url-bearer-ttl)(hipaa-164.312)(security-red-team)
  • 🛡️ Security self-red-team — REPLAY-WINDOW guard on the Poynt payment webhook. A valid Poynt HMAC-SHA1 signature is replayable forever: Poynt's scheme has no signed timestamp HEADER, so a captured legitimately-signed webhook body re-verifies indefinitely. The receiver now keys a 5-minute freshness window on a timestamp INSIDE the signed body (createdAt / created_at / timestamp) — because that field is covered by the HMAC, an attacker can't slide it forward without breaking the signature. FAIL-OPEN by design: a missing / unparseable timestamp does NOT block (the signature check stays the primary control; this is defense-in-depth), and a stale event is rejected 401 with a distinct stale-replay audit reason so a forensic review can tell a replayed capture from a spoofed signature. The check is a pure, unit-tested helper (isPoyntWebhookReplayStale). Account not yet activated → zero live traffic affected. PHI: none — payment-processor webhook. (poynt-webhook)(replay-window)(defense-in-depth)(security-red-team)
v2.97.VBS0005
2026-06-05Production

Two more safety brakes were added to the (still-off) email outreach engine, both about protecting the clinic's email reputation — the same mailbox we use for appointment reminders and intake forms. First: bad email addresses (typos, "no-reply" boxes, fake test addresses) get skipped before we ever try to send, so they can't bounce and drag our reputation down. Second: when the engine is first switched on, it starts slow and ramps up over a few days instead of blasting everyone at once. Nothing emails anyone yet.

Show technical details

Added

  • 🛰️ Isabella Outbound Engine — recipient LIST-VALIDATION PRE-PASS (DORMANT, acts only when the engine is live). Before each send, the dispatcher now classifies the resolved recipient address and skips anything that would predictably hard-bounce off the shared m365 BAA clinical-mail rail — missing/malformed addresses, role mailboxes (no-reply@, postmaster@, bounces@, …), and non-deliverable example/reserved-TLD domains. Skipped rows are suppressed TERMINALLY with an invalid_email: tag (visible in the CampaignSend ledger, never a silent drop). It is a syntactic + policy gate only — no network/MX probe — and conservative by design (a plausible-but-unusual address still sends, since a false-skip silently dropping a real patient is worse than one bounce). Pure decision logic extracted to email-validation.ts and unit-tested; the dispatcher wiring is pinned so it can't be hoisted out of the live send path. Knocks the "email list validation" item off the pre-flip checklist. Still fires nothing today. (isabella-outbound-engine)(list-validation)(deliverability)(dormant-build)
  • 🛰️ Isabella Outbound Engine — sender WARM-UP SEND RAMP (DORMANT, acts only when the engine is live). When a campaign first starts sending, blasting its full per-run cap on day one reads to mailbox providers like a compromised account — especially on a domain that until now sent ~transactional volume. The dispatcher now throttles each campaign's first active days (25 → 50 → 100 → 200 → 400, then full configured cap) so the shared BAA domain builds reputation gradually. The ramp is keyed off the campaign's OWN first-send date (each campaign warms its own slice), and it can only ever LOWER the operator's configured cap, never raise it. Pure schedule logic extracted to warmup-ramp.ts and unit-tested; the dispatcher wiring is pinned to the live branch. Knocks the "warm-up send ramp" item off the pre-flip checklist. Still fires nothing today. (isabella-outbound-engine)(warmup-ramp)(deliverability)(dormant-build)
v2.97.VBR0005
2026-06-05Production

When a patient checks the "email me renewal reminders" box on the booking form, we now also save the exact wording they agreed to, right on their record. That way, if anyone ever asks "what did this patient actually sign up for?", the answer is stored with them — we don't have to go digging. Nothing changes for patients, and nothing emails anyone yet.

Show technical details

Added

  • 🛰️ Isabella Outbound Engine — CONSENT-COPY SNAPSHOT for auditor-grade §164.508 reconstruction. When a patient opts in on the booking flow, the booking route now stamps the exact consent wording they saw onto Patient.marketingConsentText (new nullable column, mirrored on Lead), alongside the existing marketingConsent / marketingConsentAt / marketingConsentSource fields. Previously the wording only lived in git history, so reconstructing "what did THIS patient agree to" meant a code-archaeology dig; now the consent record is self-describing. The copy is hoisted to a single source of truth (marketing-consent-copy.ts) imported by BOTH the checkbox UI and the route, so the text a patient SEES is byte-identical to the text we SNAPSHOT — they cannot drift. A MARKETING_CONSENT_VERSION tag (booking-v1) prefixes the snapshot so a future copy change never rewrites what a past patient agreed to. Pinned by test: both create + update branches snapshot the text, the schema carries the column on both models, the UI sources its copy from the shared module, and the copy makes no medical/efficacy claim. Engine still dormant; nothing sends. (isabella-outbound-engine)(marketing-consent)(consent-snapshot)(hipaa-164.508)(dormant-build)
v2.97.VBQ0005
2026-06-05Production

The booking form now has an optional checkbox: "Email me renewal reminders and clinic updates." When a patient checks it, we remember that they said yes — so once the Outbound Engine is switched on, it knows exactly who agreed to hear from us. It's off by default (the patient has to choose it), and an existing yes is never erased just because someone leaves the box unchecked on a later visit. Nothing emails anyone yet — this just starts building the permission list.

Show technical details

Added

  • 🛰️ Isabella Outbound Engine — PROSPECTIVE MARKETING-CONSENT CAPTURE on the public booking flow. The renewal / re-engagement / win-back tracks only ever mail patients whose Patient.marketingConsent === "opted_in"; until now nothing in the app ever SET that flag, so the opt-in list was permanently empty. Booking Step 2 ("About You") now offers an optional, default-OFF checkbox — "Email me renewal reminders and clinic updates" — wired form → BookingSchema → the patient upsert. A checked box stamps marketingConsent: "opted_in" + marketingConsentAt + marketingConsentSource: "booking" on both new (create) and returning (update) patients. HIPAA §164.508 compliance is pinned by test: the box defaults OFF (a pre-checked box is not a valid affirmative authorization), and the update path can only UPGRADE — an unchecked box on a re-booking never silently revokes a prior opt-in (revocation flows through unsubscribe). This is the lawful path to a win-back/renewal audience: a binding hipaa-architect read confirmed the ~36k legacy lapsed patients CANNOT be bulk-converted by any clinic-initiated re-permission email (that email is itself prohibited marketing under §164.508 + CAN-SPAM + WA CEMA) — the list must build prospectively from patients who re-establish contact. Knocks the "opt-in capture" item off the engine's pre-flip checklist. Engine still dormant; nothing sends. (isabella-outbound-engine)(marketing-consent)(opt-in-capture)(hipaa-164.508)(dormant-build)
v2.97.VBP0005
2026-06-05Production

The Outbound Engine gains a safety brake.

What this means for you

The Outbound Engine gains a safety brake. If a campaign ever starts bouncing too many emails (which can hurt the reputation of the same mailbox Isabella uses for appointment reminders and intake forms), the engine now automatically pauses that campaign before it can do more harm — and logs why, so a staffer can look into it and switch it back on. Like the rest of the engine, this only ever acts when the engine is switched on (it's still off / dormant today), so nothing changes for now.

Show technical details

Added

  • 🛰️ Isabella Outbound Engine — campaign-level BOUNCE CIRCUIT-BREAKER (DORMANT, acts only when the engine is live). Per-recipient suppression already protects individuals (an unsubscribed/hard-bounced address is never re-mailed); this protects the SHARED ASSET — the m365 BAA sending domain, which also carries Isabella's transactional patient mail (reminders, intake forms). Before each send batch, the dispatcher measures the campaign's bounce rate from its terminal CampaignSend rows (bounced / (sent + bounced)); if it crosses 5% on a sample of ≥50, the campaign is HALTED (active → paused) instead of compounding reputation damage, with the trip recorded in the audit log + dispatch summary (never silent). Reversible — an operator re-activates from the CRM after investigating. Pure decision logic extracted to bounce-circuit.ts and unit-tested (never trips on a thin sample; trips at threshold once the sample is real; zero/negative-safe); the dispatcher wiring is pinned so a refactor can't hoist the breaker out of the live guard (which would let it pause campaigns while dormant). This knocks the "bounce circuit-breaker" item off the win-back track's pre-flip checklist (VBO0005). Still fires nothing today — the engine's double kill-switch keeps it dormant. (isabella-outbound-engine)(bounce-circuit-breaker)(dormant-build)(deliverability)
v2.97.VBO0005
2026-06-05Production

The Outbound Engine page gains a third outreach track: Win-back — for patients we haven't seen in years, gently inviting them to get current again. Like the rest of the engine it is fully built but DORMANT (nothing sends). One important difference: because reaching out to a long-lapsed patient counts as marketing rather than a renewal reminder, win-back will ONLY ever email patients who have affirmatively opted in — which is zero people today — and it stays off until Doug flips it live. You'll see a new "Win-back · lapsed" button and cohort bar on the page.

Show technical details

Added

  • 🛰️ Isabella Outbound Engine — NEW win-back track (DORMANT, fires nothing). A third campaign type beside re-engage + renewal, targeting long-lapsed patients (we have ~36,070 patients last seen 2011–2016) for a "haven't seen you in years, come get current again" 3-step drip. **Compliance frame (hipaa-architect ruling 2026-06-05):** unlike renewal (treatment-communication under 45 CFR §164.501), a come-back nudge to a years-lapsed patient with no current authorization is MARKETING under §164.508(a)(3). The load-bearing consequence: the win-back cohort enforces an **affirmative opt-in floor** — marketingConsent === "opted_in" ONLY, NOT the renewal track's unset-eligible thin-opt-out — so it returns **0 recipients today** (no patient has opted in yet) and the flip stays Doug-greenlight-gated. The opt-in floor is re-checked at send time (winback_requires_optin) in case consent changes between queue and send, and pinned by a new test so a refactor can't loosen it. **Freshest-of-all-sources lapse test:** a patient's "last contact" is the MAX across clinical visit (Practice Fusion Encounter.startsAt), authorization dates, and legacy cert/patientSince — never a single stale column — so anyone seen recently in PF drops out of win-back automatically as the EMR cutover refreshes data, and patients with a current/upcoming authorization are excluded (they're the renewal track). **PHI-minimization (§164.502(b)):** the cohort gates on opt-in FIRST, widening the clinical-encounter-date query only for the opted-in set; no encounter date is persisted into campaign tables. New winback_1/2/3 templates (operational register, no efficacy/medical claims, named telemed providers, Mariane sign-off, CAN-SPAM footer). CRM surface auto-shows the new cohort bar + a "Win-back · lapsed" preset that seeds a conservative 250/run cap. **Required before flip (Doug-gated, hipaa-required, NOT built yet — fires nothing today):** §164.508 marketing-authorization basis (or opt-in capture), email list-validation pre-pass, warm-up ramp, and a bounce circuit-breaker to protect the shared BAA clinical-mail rail. (isabella-outbound-engine)(winback-track)(dormant-build)(hipaa-reviewed)
v2.97.VBL0005
2026-06-05Production

New 'Outbound Engine' page under Marketing — this is the planning + visibility cockpit for Isabella's upcoming re-engagement and renewal email outreach (re-connecting with past leads, then nudging patients near their authorization renewal date). IMPORTANT: it is fully built but DORMANT — nothing sends. Every send path is held behind two off-by-default switches plus a BAA-mail-only guard, so you can build campaigns, see the audience sizes, and watch the funnel without a single email going out until Doug flips it live. Charts over tables: a live/dormant banner, cohort bars, a campaign kanban board, the renewal horizon, and the suppression (do-not-contact) ledger.

Show technical details

Added

  • 🛰️ Isabella Outbound Engine (DORMANT build — fires nothing). New re-engagement + renewal email arm for the inbound AI receptionist, built end-to-end behind default-OFF kill-switches. **Dormant guard:** isEngineLive() requires BOTH REENGAGEMENT_ENGINE_ENABLED AND CALENDAR_AVAILABILITY_OPEN to be true AND activeProvider()==="m365" (BAA mail rail) — default state queues CampaignSend rows for CRM visibility but sends zero mail; the dispatcher stamps dormantReason and refuses to send PHI over any non-BAA provider. **Data model:** new EmailCampaign (lifecycle: draft→pending_approval→approved→active→paused→archived), CampaignSend (idempotent on [campaignId, recipientEmailHash, stepIndex]), OutboundSuppression (global do-not-contact ledger). **PHI-minimization:** recipient email persisted ONLY as sha256 hash; no message body at rest; plaintext resolved transiently at send time; patientId/leadId are FK-by-convention strings (no cascade) so a patient erasure that NULLs the identifier still leaves the CAN-SPAM suppression row intact. **Cohorts** (src/lib/outbound/cohorts.ts): re-engagement by lead recency (12mo → 24mo windows) + renewal by authorization-anniversary horizon; all safe-degrade to ~0 on empty data (Lead/Authorization tables not yet backfilled). **Templates** (src/lib/outbound/templates.ts): 5 compliant copy blocks (3-step re-engage drip, 2-step renewal) — no provider name, no price, no medical claims (all Doug-gated); firstName HTML-escaped (no XSS); CAN-SPAM physical-address footer. **CRM surface** (/admin/outbound): live/dormant banner with blocker list, three kill-switch status cards, cohort bars, conversion funnel, renewal horizon, suppression summary, and a 6-column campaign kanban with role-gated transition actions (approve/activate require ADMIN). **Suppression auto-fill:** unsubscribe route + Resend bounce/complaint webhook now upsert OutboundSuppression keyed by sha256(email), covering leads too. **Cron:** /api/cron/outbound-dispatch (45 16 * * *) runs the dispatch sweep — safe while dormant (queues, never sends). Admin routes all requireAdminFromHeaders-gated; cron verifyCronAuth-gated. Explore + hipaa-architect pre-push reviews: CLEAR TO SHIP. Flipping the live switches is a new PHI data-flow → remains Doug-greenlight-only. (isabella-outbound-engine)(dormant-build)(hipaa-reviewed)
v2.97.VBJ0005
2026-06-05Production

The Isabella screen now leads with one big number — the calls she handled today — so a busy phone day no longer looks empty.

What this means for you

The Isabella screen now leads with one big number — the calls she handled today — so a busy phone day no longer looks empty. On a quiet stretch it reads "All quiet, she's on the line 24/7" instead of a row of zeros. On the Appointments screen the patient list scrolls on its own while the menu stays put, and the top spacing is tighter.

Show technical details

Changed

  • Isabella cockpit "Today" now leads with a hero count of calls handled today. Voice is ~99% of Isabella's volume but lived only in the Zone-G call log, so a high-call day rendered the whole Today row as zeros (Doug 2026-06-05: "so much for isabella being live"). New callsHandled counter in getTodayCounters() counts today's inbound CALL rows (matching getVoiceCallLog's "NOT direction=OUT" classification so null/blank-direction inbound rows still register). PHI-safe: count-only, no identifiers. (isabella-cockpit-calls-hero)
  • Isabella "Right now" pulse renders a designed quiet state — "All quiet. Isabella's on the line 24/7 — nothing in the last 15 minutes." — instead of "processing 0 email · 0 SMS..." when there's no recent traffic. A 1am no-traffic screen now reads as all-clear, not an outage. (rightnow-quiet-state)
  • Admin shell now holds the sidebar fixed while only the content area scrolls (root h-screen overflow-hidden, sidebar h-screen overflow-y-auto). On the Appointments screen, a long patient list scrolls without dragging the nav. Top spacing tightened (lg:pt-5, mb-4). (admin-shell-fixed-sidebar-scroll)
v2.97.VBH0005
2026-06-05Production

Fixed a provider-portal bug that hit Dr. Ari and any provider who signs in with the new cookie login: clicking Complete/Approve, Bulk Approve, or Reissue on a visit returned 'unauthorized' and the action failed. The buttons were still expecting the old emailed-link token, which the new login doesn't carry. They now recognize the logged-in provider session first, so the actions work for cookie-logged-in providers while the legacy emailed-link tabs keep working too.

Show technical details

Fixed

  • Provider-portal write actions (complete/approve, bulk-approve, authorization reissue) now resolve the acting provider from the httpOnly cookie session first, falling back to the legacy portal token. After the Wave-5b auth migration the plaintext provider.portalToken column is null, so the cookie portal passed an empty token to these routes — which authed ONLY via hashPortalToken(token) — guaranteeing a 401 on every Complete/Approve, Bulk Approve, and Reissue click. Mirrors the resolveSigner() cookie-first fallback VBB0005 landed on /api/provider/encounters/[id]/sign; this ports it to the three sibling write routes (action, bulk-approve, authorizations/[id]/reissue) that the same migration broke. Per-resource scope checks unchanged (appt.providerId === provider.id, issuingProviderId ownership) — no RBAC loosening; token made optional in the zod schemas; 401 stays opaque. Reissue's success redirect now routes cookie sessions to /provider/portal/authorizations/[id] instead of /provider/null/... (dr-ari-portal-sign-401)
v2.97.VBG0005
2026-06-05Production

Follow-on to the last contact-block cleanup.

What this means for you

Follow-on to the last contact-block cleanup. The form-link email (the 'please review and sign your form' message patients get for consent/ROI/records forms) was still showing the 'Questions? Call/email' line twice. Removed the duplicate so contact details now show once, in the footer — same as every other automated email.

Show technical details

Fixed

  • Form-link patient email (buildFormLinkEmail — the consent/ROI/records-request magic-link message) no longer duplicates the contact block. It inlined a 'Questions? Call / email' block that was added to byte-match emails.ts shell()'s inner panel — but VBF0005 removed that inline block from shell() itself (it duplicated the footer's contact). The form-link email mirrors shell() structure, so it follows: inline block removed, contact now renders once via renderEmailFooter() (contact-SSoT). Mariane reviewer-feedback cmpyxahbe (template parity) is still satisfied — the shared structure is header + body + footer-contact. Dropped the now-unused PHONE/EMAIL import; pin test flipped from must-include-inline to must-not (footer contact still asserted present). (form-link-email-footer-dedup)
v2.97.VBF0005
2026-06-05Production

Two more fixes. (1) Booking a new appointment from the admin no longer throws an Internal Server Error when the chosen time slot belongs to a provider who's since been removed — it now shows a clear 'that slot's provider is no longer available, pick another time' message instead of crashing. (2) The 'How Was Your Visit?' email no longer repeats the contact info — the duplicate 'Questions? Call/email' block was removed so contact details show once, in the footer.

Show technical details

Fixed

  • Admin new-appointment booking (/admin/appointments/new → POST /api/admin/appointments/manual) no longer 500s when the selected AvailabilitySlot points at a hard-removed provider. Prisma's generated type declares slot.provider non-nullable, but FK enforcement was relaxed during the Salesforce bulk import, so slot.provider can be null at runtime and the unguarded slot.provider.doxyMeUrl deref threw. Added a null-provider guard that returns a PHI-free 409 'select another time' before the transaction. Same orphan-relation class as the appointments-list fix in VBC0005, applied to the create path. (booking-orphan-provider-guard)
  • Post-visit 'How Was Your Green Wellness Visit?' email (and every email built through the shared shell wrapper) no longer duplicates the contact block — shell() rendered an inline 'Questions? Call/email' block AND the canonical footer (which also carries phone/email), so contact appeared 2-3 times. Removed the inline block; contact now renders once via renderEmailFooter() (contact-SSoT from constants). (visit-email-footer-dedup)
v2.97.VBE0005
2026-06-05Production

Two fixes on the Email Composer.

What this means for you

Two fixes on the Email Composer. The patient picker now searches as you type instead of needing a separate 'Find' click — typing a name was finding nobody, which read as broken. And when AI drafting is off (it stays off until the Anthropic agreement is in place), the page no longer looks dead: you can write the subject and body yourself and send normally; only the optional '✨ Draft with AI' button is disabled, with a note explaining why.

Show technical details

Fixed

  • Email Composer patient picker now auto-searches on type (250ms debounce, gated on the search panel being open), matching the pickers on /admin/appointments/new and /admin/forms/new — the old click-the-Find-button behavior read as 'pick a patient doesn't work'. Existing Find button + Enter still work. (email-compose-picker)
  • Email Composer no longer reads as broken when AI drafts are disabled. Manual compose + send was never AI-gated (only the draft-prompt route is), but clicking '✨ Draft with AI' returned a confusing 503 toast. The AI intent box, quick-prompt chips, and Draft button are now disabled with a plain-language note when AI_DRAFTS_ENABLED is off; subject/body/send stay fully usable. The Anthropic-BAA gate is unchanged — AI drafting stays off until the BAA is in place. (email-compose-ai-gate)
v2.97.VBC0005
2026-06-05Production

Fixed the 'Application error' screens that were popping up on the Appointments list and some patient charts.

What this means for you

Fixed the 'Application error' screens that were popping up on the Appointments list and some patient charts. They were caused by a few old imported appointments whose provider or patient record had been removed — those orphaned rows now get quietly skipped so the page loads normally instead of crashing.

Show technical details

Fixed

  • Appointments list, the Auth-Held (Unpaid) queue, and patient charts no longer throw a Next.js digest error screen when a Salesforce-migrated appointment has a dangling providerId/patientId. Prisma's generated types declare appointment.patient/.provider non-nullable, but a relational include returns null when the joined row is missing (FK enforcement was relaxed during the bulk import), so the unguarded a.provider.name / a.patient.firstName deref in the server component threw — deterministically whenever a bad row landed in the page/date window, which read as a 'transient' digest. Each render path now filters orphaned rows (a.patient && a.provider) before the map; counts/LTV use a join-free projection so they stay accurate. No PHI in the skip path — orphaned rows are omitted, never echoed. (appointments-orphan-render)
v2.97.VBB0005
2026-06-05Production

A batch of fixes from your feedback notes.

What this means for you

A batch of fixes from your feedback notes. The big ones: Dr Ari can sign documents again (the portal was rejecting her). Spam robocalls now have their own tab in the inbox so they stop cluttering the real messages. The Calls Report now counts Isabella's answered calls correctly instead of showing 0%. Patients can complete an informed-consent form instead of hitting a dead end. And there's a new 'Likely qualifies' worklist so you can see at a glance who might be eligible.

Show technical details

Fixed

  • Dr Ari (and any provider migrated past the Wave-5b token-hash migration) can sign documents again — the provider sign route only authenticated via the plaintext portalToken column, which is NULL post-migration, so every migrated provider got a 401 on every signature. New resolveSigner() does cookie-first dual auth via getProviderFromApiRequest() with the legacy body-token as an isActive-gated fallback. (ari-portal-fixes)
  • Calls Report no longer shows 0% answer rate — Isabella's Retell calls write a transcript but left durationSec null, so the classifier counted every Isabella call as missed. The Retell webhook now stamps real duration from call timestamps, and the report counts a call answered when it was human-connected OR has a transcript. Historical rows show '—' duration until they age out of the 30-day window. (mariane-remaining-fixes)
  • Informed-consent patient forms render and sign instead of dead-ending — added the INFORMED_CONSENT case to the patient form renderer + sign route via a single-signature acknowledgement (the 7-initials/guardian e-sign variant stays gated on a separate decision). (consent-fix)
  • Consent-email template now matches the other system emails (added the 'Questions?' contact block); forms list gained a Delivery column (Pending/Sent/Delivered/Failed); inbound-fax wrong-number page points at the live fax line. (mariane-remaining-fixes, admin-views-fixes)

Added

  • Spam/robocall filter for the inbox — a 'Spam' tab and ?spam=true filter hide inbound robocalls (inbound CALL with no patient, no recording, under 10s) from the real-message view; fail-safe-narrow so a real call is never hidden. (spam-call-filter)
  • 'Likely qualifies' worklist at /admin/qualifying-leads (Admin/Manager) — ranks leads whose conditions match the RCW 69.51A qualifying set, reusing the same condition normalizer the issuing page uses; counts-only audit, no patient detail in logs. (qualifies-worklist)
  • Demi's callback queue split into Pending/Completed tabs on /admin/isabella-today; provider dot-code picker grouped into scannable clinical categories; per-date slot counts on the booking calendar; reviewer-feedback page status-filter tabs; Day-3/5/7 records-reminder cadence with auto-stop on records-received. (isabella-demi-fixes, charting-templates, remaining-a-fixes)
  • Email-compose tooling — merge-field picker (16 tokens), a BAA-inbox-only send-test (no PatientMessage write), and richer form-staff-alert bodies; intake-PDF download route (RBAC-gated, PHI-free); /admin/mailing service-request status tabs. (email-compose-fixes, admin-views-fixes)
  • Post-visit 'How was your visit?' feedback-survey email template (dormant — not yet wired to a trigger) and a records-review-queue shared library + design doc (scaffold only; the write path and its migration are held for separate approval). (survey-chathistory, records-review-queue)
v2.97.VBA0005
2026-06-05Production

Mariane, you now have a 'Mariane to review' lane on the feedback triage page.

What this means for you

Mariane, you now have a 'Mariane to review' lane on the feedback triage page. When a website-feedback note is a judgment call — not clearly a quick fix, but not something Doug needs — it can be sent to you to decide. From there you can approve it to get fixed, send it up to Doug, or close it. The page header shows a count of how many are waiting on you.

Show technical details

Added

  • Auto-approve-doctrine Phase 2.5 (GW) — mariane-triage status + review surface, the Tier-2 routing destination from PLAN_AUTO_APPROVE_TRIAGE_DOCTRINE_2026_06_04.md. Built BEFORE the Phase-4 flip (removing Mariane from FORCE_DOUG_REVIEW_SUBMITTERS) so the queue is a real, dispositionable surface the moment ambiguous items start routing there — not a dead end. Changes: (1) mariane-triage added to REVIEWER_FEEDBACK_STATUSES (between needs-clarification + needs-retesting) + STATUS_LABELS ('Mariane to review'); status column is a plain String, NO migration. (2) New routeToMariane server action (AdminSession + allowlist gated, audit-logged REVIEWER_FEEDBACK_ROUTED_TO_MARIANE with from→to + actor, no body content) + a '→ Send to Mariane' button on every actionable triage row (hidden when the row is already in her queue). (3) mariane-triage is an ACTIONABLE status, so Mariane dispositions her queue with the existing buttons — Approve·auto-fix (ships it), Pin to Doug (escalates), Wontfix (drops), Needs clarification (asks submitter). (4) Header shows '… waiting on Mariane' count. Pure-additive config + UI; classifier behavior UNCHANGED (the agent-side auto-routing into this queue lands with the autonomous lane). hipaa-architect PASS on staged diff; pin tests updated 9→10 states + mariane-triage label/validity pins.
v2.97.VAZ0005
2026-06-04Production

Behind the scenes: the system that decides which website-feedback notes need Doug's eyes (vs. ones that can be fixed automatically) got stricter about anything touching patient pages or logins. A note filed on a patient, leads, messages, fax, calendar, appointments, or users page — or any note mentioning logins, permissions, or database changes — now always routes to Doug for review. Nothing you do changes; this just makes sure sensitive items can't slip through.

Show technical details

Changed

  • Reviewer-feedback classifier hardening (auto-approve-doctrine Phase 1, GW lane). Two strictly-additive, upward-only changes to lib/feedback-overrides.ts: (1) HUGE_PAGE_PREFIXES widened from /admin/payments + /admin/forms to also cover /admin/patients, /admin/leads, /admin/messages, /admin/inbound-fax, /admin/calendar, /admin/appointments, /admin/users — a feedback row filed on any PHI- or RBAC-class admin surface now force-escalates to huge-doug-required regardless of body content, closing the structural gap where a keyword-free polish nit on a patient page rode small-autoapprove to auto-ship. (2) New RULE_DANGEROUS_CHANGE regex (auth/RBAC/role-gate/permission/session/OAuth/token/login/migration/schema/database/encryption/password/secret/API-key) wired into both applyAgentConfidenceOverrides and applyDougTierOverrides, closing the lexical gap where auth/migration vocab matched no prior rule. Both gaps were flagged by the hipaa-architect sign-off as preconditions before the eventual Mariane force-list flip (Phase 4, separate, Doug-gated). Inert until that flip — only ever escalates MORE rows to Doug, never fewer. hipaa-architect PASS on the staged diff; 50 pin tests pass; FORCE_DOUG_REVIEW_SUBMITTERS untouched.
v2.97.VAY0005
2026-06-04Production

Polished the 'Send consent form' email so it now looks like the rest of our patient emails — same Green Wellness logo banner at the top and the full footer with phone, email, website, and the 'Leave Us a Review' button at the bottom. Before, this one email had a plain header and no footer, so it looked off next to the appointment-confirmation and reminder emails. The wording is warmer too. No change to what's attached (the consent PDF) or who can send it.

Show technical details

Changed

  • Send-consent-form email (/admin/patients → 'Send consent form') now routes through the shared renderEmailHeader() + renderEmailFooter() shell — the same branded template used by the form-link, booking-confirmation, and reminder emails — instead of a one-off inline header with no footer. Patients now see the Green Wellness logo banner + full contact/review/socials footer, matching every other automated GW email. Copy reworded to the warm GW voice (first-name greeting only — PHI discipline unchanged; the consent PDF still carries all content). Closes the outbound-polish audit finding (2026-06-04) that this email used a different shell than the rest of the patient-facing templates. No behavior change to the M365-BAA send path, the address/email gates, the rate-limit, or the audit trail.
v2.97.VAX0005
2026-06-04Production

Isabella now leads with the secure upload link when a patient needs to send their medical records — she says she'll email them a link to upload, and only mentions the fax and email as a fallback if the patient would rather. Before, she read out the fax number and email first; now the easier option comes first on every booking.

Show technical details

Changed

  • Voice prompt: Isabella now PREFERS the secure records-upload link over reciting the fax/email rail. Three spots updated to lead with 'I'll email you a secure link to upload your records' and keep fax (888-504-6129) + admin@greenwellness.org as an explicit fallback: (1) the booking tentative-request wrap, (2) the records-upload-link instruction (broadened from 'a NEW patient' to 'a patient', reframed to 'PREFER THIS over reciting fax/email · lead with the link'), and (3) end-of-call wrap-up beat 2. Completes Doug's 2026-06-04 live phone-test ask ('isabella should tell the patient we will be sending the patient portal link instead of sending records via fax or email') — the sendRecordsUploadLink handler + tool registration shipped in VAV0005; this is the spoken-copy half that makes her lead with the link verbally. Synced to the live Retell agent via sync-retell-prompt.mjs --force. Fax number + records email preserved in the copy (fallback rail intact); zero new voice-prompt test failures (4 pre-existing soft-cap/structure failures unchanged).
v2.97.VAW0005
2026-06-04Production

Hardened the patient, provider, and admin screens so a stalled connection can't leave anyone stuck on a spinner forever.

What this means for you

Hardened the patient, provider, and admin screens so a stalled connection can't leave anyone stuck on a spinner forever. Thirteen places where the app talks to the server — signing an encounter, saving a SOAP note, a patient uploading their ID photo, the admin desk check-off buttons, the Spokane-transition send — now give up after a sensible wait (a few seconds for quick button clicks, up to 45 seconds for a photo upload on mobile data) and show a try-again message instead of hanging. The one deliberate exception is the auto-save that fires as you close a tab, which is meant to finish in the background and is left as-is. No visible change when things are working normally.

Show technical details

Changed

  • Added request timeouts (signal: AbortSignal.timeout(...)) to 13 previously-unprotected fetch() call sites across the patient ID-upload form, the provider encounter screens (new encounter, sign-and-lock, unlock, cancel, and SOAP autosave), the admin doug-queue buttons, the Mariane desk check-off, the Spokane-transition send + preview, and the server-side auth-PDF blob stitch. Durations are sized per surface: 8s for tiny admin mutations, 10-20s for clinical saves, 45s for the patient photo upload on mobile data. The beforeunload keepalive autosave in useAutosaveSoap is intentionally left untimed (it's a fire-and-forget background save by design). Closes the long-standing fetch-abort-signal-discipline watchdog finding for Green Wellness (14 → 1, under the noise floor).
v2.97.VAV0005
2026-06-04Production

Added the one-time activation script that lets Isabella offer a new patient a secure upload link for their medical records on a phone call — instead of reading out the fax number and email address. The records-link tool itself was already built and shipped dark; this is the merge-safe script that registers it on the live phone agent. It is a Doug-run go-live step (still gated behind the ISABELLA_RECORDS_LINK_TOOL_ENABLED flag), so nothing changes on the live line until Doug runs it.

Show technical details

Added

  • New scripts/add-retell-records-link-tool.mjs — the Doug-run activation step that registers the sendRecordsUploadLink custom function on Isabella's live Retell LLM so she can email a NEW patient a secure records-upload link mid-call instead of reciting the fax/email rail (Doug 2026-06-04 live phone test: 'isabella should tell the patient we will be sending the patient portal link instead of sending records via fax or email'). GET-merge-PATCHes like add-retell-transfer-tool.mjs: preserves the 6 existing custom voice tools, auto-derives the custom-function webhook URL from an existing tool (never hardcoded), supports --dry-run + --remove, and refuses to PATCH if any existing tool would be dropped or any unexpected tool added. TWO gates must both be set to fire on a call: this registration AND ISABELLA_RECORDS_LINK_TOOL_ENABLED=true on Vercel prod (the runtime handler's layer-2 default-OFF guard) — set the flag + redeploy first, then run this in lockstep. HIPAA: registration carries no PHI; the tool, when fired, emails over the M365-BAA sendEmail path (first name + link + contact rail only).
v2.97.VAU0005
2026-06-04Production

Isabella can answer 'what's available?' on the phone again.

What this means for you

Isabella can answer 'what's available?' on the phone again. On a live test she had nothing to say about scheduling. She now describes the clinic's standing weekly availability — the recurring days and time windows we generally see patients — read live from the provider-schedule templates, then captures the caller's preferred day/time for staff to confirm the exact opening on a callback. She still never quotes a specific dated slot (those aren't authoritative until the EMR cutover), so there's no phantom-booking risk. Same behavior on the website chat for parity.

Show technical details

Fixed

  • Isabella's listOpenSlots voice tool (and the chat booking tool) were neutralized to a content-free 'what day works best?' during the EMR freeze, so on Doug's live phone test she 'didn't know any schedule availability.' Both now read the authoritative recurring source — ProviderSchedule (the same templates the slots cron generates from, which carry no PHI) — via a new shared describeStandingWindows() helper, and speak the genuine standing windows ('we generally offer telehealth visits on Mondays, Wednesdays, and Fridays from nine a.m. to one p.m.'). They still capture the caller's preferred day/time and hand off to staff to confirm the exact opening — no specific dated slot is ever committed, so no phantom/already-booked risk. Copy is provider-agnostic (staff route Marnie-vs-Ari on the confirm-back). Voice + chat share one formatter for anti-divergence parity.
  • Removed the stale dated-slot example ('a Wednesday October fifteenth at two p.m. in Spokane') from the voice prompt's booking + wrap-up beats — it actively modeled proposing a specific calendar date as a held slot, which is exactly what we don't do. Replaced with standing-windows + capture-preference framing.
v2.97.VAT0005
2026-06-04Production

Mariane's morning page is now a desk you can work, not just a list to read.

What this means for you

Mariane's morning page is now a desk you can work, not just a list to read. Each item has a '✓ Done' button and a 'Hand to Demi' button — check something off and it quietly moves into a 'Handled for you today' group so you can see what's finished; hand something over and it lands on Demi's page under 'From Mariane'. Some things now clear themselves: when a cert-renewal email already went out, the item shows up already handled so you don't chase it. You'll also get a short morning email each day with just the count of what's waiting and a link to the desk — no patient details in the email, those stay safe in the app.

Show technical details

Added

  • New OpsTaskState overlay table (migration prod-migration-78-ops-task-state.sql — DESIGNED, not applied; Doug-greenlight required to run against BAA prod) records a human or system action (check-off, hand-to-Demi, auto-resolve) against a derived /admin/mariane-today desk item, keyed by an opaque PHI-free itemKey of the form ':'. Modeled on the proven StaffAnnouncementDismissal check-off pattern; all-additive + idempotent + reversible.
  • Check-off + delegate affordances on /admin/mariane-today: each active band row now carries '✓ Done' and 'Hand to Demi' buttons that POST to the new /api/admin/mariane-desk/resolve route (ADMIN/MANAGER only). Done/delegated/auto-resolved items suppress from the active list and re-render in a 'Handled for you today' receipt group; delegated items surface on /admin/isabella-today under a new 'From Mariane' band (no patient data crosses — both pages render PHI-safe labels from the same BAA source tables).
  • New /api/cron/mariane-desk-refresh cron (daily ~6:25am PT): counts TODAY's open desk items (Mariane's lane, suppressing resolved items) and sends a no-PHI nudge email — count + deep link only — to a fail-closed @greenwellness.org mailbox via the M365 BAA send path. Zero items sends a quiet all-clear (never skipped). Also runs the cert-pending auto-resolve sweep: when a renewal reminder already went out for an expiring authorization, it writes an auto-resolved receipt so the desk shows 'Renewal went out — nothing for you' instead of silently dropping the item.

Changed

  • Both /admin/mariane-today and /admin/isabella-today degrade gracefully if the OpsTaskState table isn't provisioned yet (deploy before migration): the suppression read + delegated-band load catch the missing-relation error and fall back to the prior read-only behavior — no crash pre-migration. The check-off affordances simply don't render until the table exists.
v2.97.VAR0005
2026-06-04Production

Added a crisis-load alert to the Isabella Today page.

What this means for you

Added a crisis-load alert to the Isabella Today page. When a patient is flagged as clinical-urgent (or a crisis) and is still waiting without a human reply, the page now shows a loud red banner at the very top with how many are waiting and how long the oldest has waited — so on a busy day an urgent patient can never get buried in the middle of the queue. When none are waiting, it shows a quiet green all-clear.

Show technical details

Added

  • New top-of-page crisis-load tile on /admin/isabella-today (Band 0). Counts unresolved clinical-urgent patients — both AI-flagged escalations (needsHumanAt set) and clinical-urgent inbound with no reply within 1h (needsHumanAt null) — which are disjoint by construction, so no double-count. Renders a loud red alarm with count + oldest-age when >0, quiet green all-clear when 0. Derived from data already loaded by the page (no new DB query, no schema change). Reuses the existing fmtAgeShort + staleBadge helpers. Recommended by the Isabella expert panel (hipaa-architect) as the asymmetric-harm P0: the real failure mode under heavy volume is a flagged patient sitting unseen.
v2.97.VAQ0005
2026-06-04Production

Made Isabella sound warmer and more human on email and chat.

What this means for you

Made Isabella sound warmer and more human on email and chat. She now leads with a brief, genuine acknowledgment when a patient mentions pain, fear, or that they're new to this — before walking them through booking or eligibility — so the first thing patients feel is that someone is listening. She still never promises any health outcome or implies cannabis treats anything; the warmth is in how she listens, not in what she claims.

Show technical details

Changed

  • Added a top-of-prompt 'How you sound — warmth without overpromising' house-style block to both the email AI system prompt and the web-chat system prompt (cross-channel parity). It instructs Isabella to acknowledge a patient's pain / fear / first-time nerves in one honest line BEFORE any booking or eligibility step, with an explicit negative fence: acknowledging a feeling is NOT a claim about cannabis, and she must never say or imply that cannabis, an evaluation, or an authorization will help, treat, relieve, ease, improve, or fix any condition or symptom. Prompt-copy only — no code, schema, or behavior-gate change; the runtime medical-claim scrubber remains the backstop. Proposed by a communications-expert + hipaa-architect panel and verified by portfolio-architect.
v2.97.VAP0005
2026-06-04Production

Fixed a quiet booking-availability problem: the job that refreshes open appointment slots was only running once a week, on Sundays.

What this means for you

Fixed a quiet booking-availability problem: the job that refreshes open appointment slots was only running once a week, on Sundays. That meant later in the week the list of bookable times could thin out or run dry even when openings existed. It now runs every day, so patients always see current availability.

Show technical details

Fixed

  • The /api/cron/slots refresh job was scheduled weekly (Sundays only, 0 14 * * 0); changed to daily (0 14 * * *) so booking-slot availability is regenerated every day and never goes stale mid-week. One-line vercel.json schedule change — no code, schema, or behavior change beyond cadence. Ported from local commit 1ecfcde5 (was stranded on a divergent local branch that never reached origin/main).
v2.97.VAO0005
2026-06-04Production

Demi asked for a way to have calls reach her when she's logged in and in the office — her phone wasn't ringing and calls were being missed.

What this means for you

Demi asked for a way to have calls reach her when she's logged in and in the office — her phone wasn't ringing and calls were being missed. Isabella can now connect a live caller straight to the office manager's desk line, but only after she's confirmed someone is actually available to pick up, so no caller ever lands in a dead-air queue. This ships switched off and does not change any phone behavior until the desk-line number is added and we run a short live test call together.

Show technical details

Added

  • Presence-gated live warm-transfer for Isabella's voice line (reviewer-feedback cmpyeudxq, Demi: "calls are being missed when I am in office"). The hard part — Demi-presence detection (checkDemiAvailability in admin-presence.ts: business-hours AND a message-handling admin heartbeat within 15 min) — already shipped in VAC0005; this wires the actual call bridge. A real transfer is a Retell-native transfer_call tool in the LLM's general_tools, NOT a custom-function webhook (those return spoken strings and cannot bridge a call). New scripts/add-retell-transfer-tool.mjs is a Doug-run, merge-safe activation step: it GETs the current LLM config, preserves the 6 existing custom voice tools, and appends/refreshes a cold-transfer tool pointed at DEMI_TRANSFER_NUMBER (idempotent; --dry-run + --remove; refuses to PATCH if the custom-tool set would change, so it can never wipe Isabella's voice tools).

Changed

  • All three pieces gate on the SAME DEMI_TRANSFER_NUMBER env so they activate in lockstep and can never half-promise a transfer: (1) voice-prompt.ts gains voiceTransferEscalationClause() — returns the live-transfer instruction when the env is set, otherwise an empty string so the synced prompt is byte-identical to today's take-a-message-only behavior; (2) the flagForHuman handler in voice-tools.ts now only runs the presence check when the env is set, retiring the prior broken promise (it used to say "bringing Demi on the line" with nothing behind it); (3) scripts/sync-retell-prompt.mjs gains a sister-handler (renderTransferEscalationClause) mirroring the clause verbatim, so a dormant sync resolves the new interpolation to "" instead of failing the unresolved-template guard. HIPAA: a phone-to-phone transfer carries no patient identifiers through any non-BAA channel — it just bridges two calls (this is why a transfer is the compliant path; texting the caller's number to staff over non-BAA Twilio is not). Activation is NOT a code deploy: Doug sets DEMI_TRANSFER_NUMBER in Vercel prod, runs add-retell-transfer-tool.mjs then sync-retell-prompt.mjs --force, and we place a 2-minute live test call (a phone bridge cannot be headless-tested). Freeze-safe: no schema, no migration; a git/Vercel ship does not change Retell behavior. See project_gw_isabella_live_transfer_to_demi_scoped_2026_06_04.
v2.97.VAN0005
2026-06-04Production

Isabella stopped quoting specific telehealth days and times that weren't right.

What this means for you

Isabella stopped quoting specific telehealth days and times that weren't right. On a call she'd been telling patients telehealth was "Wednesday and Friday mornings" — which wasn't accurate. Now she just confirms the visit is a 15-minute appointment with Dr. Ari, asks what day and time work best for the patient, and notes their preference so the office can confirm the real opening and call them back.

Show technical details

Fixed

  • Isabella no longer asserts specific telehealth scheduling days/times. The listOpenSlots tool handler used to quote a fixed window ("Wednesday and Friday 10:30–12:30 with Dr. Ari") in both the chat and voice channels; those days were re-flagged as wrong on a real call, and the GW slot table isn't yet authoritative (pre-EMR-cutover), so any specific window risks being incorrect. Both handlers (booking-tools.ts chat/email/SMS + voice-tools.ts voice) now keep only the stable facts (15-minute visit, Dr. Ari), capture the patient's preferred day/time, and hand off to staff to confirm against Practice Fusion + call back. Chat ships live; the voice copy ships dormant until sync-retell-prompt.mjs --force pushes the updated Retell prompt. Reviewer-feedback cmpyf53v7 (Mariane, with a call-transcript photo). Render/copy-only — no schema, no migration, freeze-safe. A verified standing window can be restored once Mariane/Doug confirm the correct telehealth days/times.
v2.97.VAM0005
2026-06-04Production

Two things from Mariane and Demi's testing: (1) the Book Now form now lets patients choose Olympia or Spokane too — not just Lynnwood — so callers see every clinic that's actually open for booking (Spokane shows for new patients until it closes at the end of June). (2) On the Messages page, you can now tap a caller's number to call them right back — the number still shows only its last four digits, so you no longer have to hunt for callback numbers.

Show technical details

Changed

  • Book Now widget now surfaces every currently-bookable clinic instead of hardcoding Lynnwood-only. The location picker reads the live provider-location rules (provider-location-rules.ts, Doug 2026-05-31 LR0005): new patients can pick any in-person clinic that accepts new patients (Lynnwood, Olympia, Spokane — RCW 69.51A.030 is satisfied by any physical clinic), and returning patients choosing In-Person pick from the clinics that accept renewals (Lynnwood + Olympia; Spokane is new-only). Spokane auto-drops from the picker after its 2026-06-30 sunset via getActiveLocations. Single-clinic auto-selects to preserve the smooth path; multi-clinic shows a chip picker with address + an Olympia-renewal-routing note. This is a lead form (posts to /api/leads/book-now — Salesforce + AuditLog), so the choice is a preference the team confirms, not a live slot lock. Reviewer-feedback cmpuiu2ek + cmpw2tvph (Mariane asked twice whether Lynnwood-only was intentional — it was a stale hardcode, not a decision).

Added

  • One-tap "Call back" on inbound calls in the admin Messages page, plus a click-to-call link on the unmatched-caller banner. The last-4 masking stays in the visible text (Safe Harbor §164.514); the full number rides only in the tel: href — the same established pattern already used on /admin/demi-today and /admin/isabella-today. Addresses the "callback numbers are tedious to find" complaint. Reviewer-feedback cmpx0anuc + cmpx0txg9 + cmpwwi0pj (the callback-number-visibility part; call hang-ups + missing transcripts are Retell-dashboard config, tracked separately).
v2.97.VAL0005
2026-06-04Production

Fixed a gap on Isabella's Today page: when Isabella sent a caller an automatic post-call recap email, that call was disappearing from your "Needs attention" list even though the person may still be waiting for a callback. Those calls now stay on the list — with the phone number and a Call button — until someone actually follows up.

Show technical details

Fixed

  • /admin/isabella-today "Needs attention" stale-call list: the suppression check that hides a call once it's been replied to no longer counts Isabella's automated post-call recap email (fromAddr='ai-voice-summary') as a reply. An auto-recap is not a human callback, so callers who got one but still need a person stayed visible with their phone number + Call button instead of silently vanishing from Demi's queue. Diagnosed from Demi reviewer-feedback cmpwwi0pj ("not seeing the callbacks" / "have to go through transcripts to find the numbers"). Render-only query change; no schema, no migration, freeze-safe.
v2.97.VAK0005
2026-06-04Production

The evening End-of-Day email is now short and to the point — just three numbers: how many appointments were set, how many new leads came in, and how many appointments were seen that day. Everything else now lives on the EOD page (Reports → End of Day), where you can also pick any past day from the date arrows and pull up that day's full picture — staff activity, calls/texts/emails, and an Isabella summary.

Show technical details

Changed

  • Isabella's daily EOD email slimmed to three operational headline numbers — appointments set (booked), new leads in, and appointments seen (completed) — and routed org-internal (doug@ + admin@greenwellness.org on the M365 BAA tenant) instead of an external personal inbox. The body now carries zero patient identifiers (aggregate counts only), so the prior Safe-Harbor rendering layer is no longer needed; the per-channel / per-patient detail moved to the in-app EOD page. Doug ask 2026-06-04 (Mariane feedback /admin/reports/eod).

Added

  • The End-of-Day page (/admin/reports/eod) now leads with the same three headline numbers — appointments set, new leads, appointments seen — for the selected day, plus an "Isabella — daily summary" tile group (AI turns handled, booked-via-Isabella, escalated-to-team, email awaiting reply, tomorrow's confirmed appointments). All figures recompute live for whatever day the date picker is on, so the email's former detail is now browsable for any prior day.
v2.97.VAJ0005
2026-06-03Production

Two fixes from Mariane's testing: (1) appointment times now show in our Pacific clinic time everywhere on the admin side — a slot booked for 4:10 PM was showing as 2:10 PM on some screens; times now read correctly and are labeled "PT" so there's no confusion. (2) The form-to-sign email (consent, records release, intake packet) now matches our other automated emails — same logo header, same branded footer with phone, email, website, and the Leave-Us-a-Review button — instead of looking plain and unbranded.

Show technical details

Fixed

  • Admin appointment times now render in clinic-local Pacific time across the Today board, the appointments list, the new-appointment slot picker, the reschedule modal, and the slot-management page. A naive date formatter was rendering times in the server/browser timezone, which on a non-Pacific runtime shifted the displayed hour (e.g. a 4:10 PM PT booking showing as 2:10 PM). Switched these displays to the same Pacific-time helper (fmtPT) the calendar already uses, and added an explicit "PT" label where a patient-facing or destructive confirmation reads the time. Display-only change — stored appointment times were always correct. Reviewer-feedback cmpywzsdw.
  • The patient form-link email (review-and-sign for consent, records release, NPP acknowledgement, intake packet, etc.) now renders inside the same branded shell — logo header + brand footer with phone/email/website/socials and the Leave-Us-a-Review CTA — as the booking-confirmation and reminder emails, with a styled green action button. Previously it sent as an unstyled body, which read as off-brand next to the other automated messages. The builder stays PHI-pure (a function of form type + link only; no patient identifiers), and the header/footer renderers are operator-side brand chrome only. Reviewer-feedback cmpyxahbe (the email-template-consistency half; the separate render gap where two newer form types show "isn't available yet" is tracked for post-freeze).
v2.97.VAI0005
2026-06-03Production

Three Isabella (our AI phone + chat receptionist) wording improvements from Mariane's testing: (1) before asking a caller for a preferred appointment time, Isabella now sets expectations up front — that this starts a request, not a confirmed booking, and names the two things needed to complete it: medical records from the last 12 months showing the qualifying condition, plus a valid Washington photo ID. (2) Her "anything else?" closing line now varies warmly so it doesn't sound rushed. (3) She no longer promises a call transcript or summary she can't actually send.

Show technical details

Changed

  • Isabella voice prompt — scheduling flow: before asking which day/time a caller prefers, Isabella now states up front that this starts an appointment REQUEST (not a confirmed booking) and names the two required items to complete it — medical records from the last 12 months documenting the qualifying condition + a valid WA State photo ID. Framed strictly as the steps to complete THIS clinic's authorization appointment, never as something needed to use cannabis legally (any adult 21+ in WA already can, without a card). End-of-call reminder to send the documents preserved. Reviewer-feedback cmpywqih4. Cross-channel parity: the chat prompt's tentative-booking section now names the same two required documents with the same never-imply-required guard.
  • Isabella voice prompt — end-of-call wrap-up: the "is there anything else?" check now rotates among 5 warmer, less-rushed phrasings instead of a single scripted line, so the close sounds natural and unhurried. Reviewer-feedback cmpywp3ir. (Chat already avoids this cliché via its existing call-center-phrase ban — no change needed there.)
  • Isabella voice + chat prompts — accurate follow-up promises: Isabella no longer promises a "transcript," recording, or written copy of the conversation (we don't send those). If a caller asks, she answers honestly — our team has everything and will follow up — and only mentions a confirmation email when one will actually be sent (a booking where an email was collected). Stops the "she said I'd get a transcript and nothing came" gap. Reviewer-feedback cmpywtcoy (the false-promise half; full post-call summary delivery is tracked separately).
v2.97.VAH0005
2026-06-03Production

Isabella (our AI receptionist, on the phone and in chat) now explains medical authorizations correctly for Washington: she'll never say a patient needs a card to use cannabis legally — because anyone 21+ already can. She frames the authorization the right way: added benefits for medical patients (tax-free medical purchases, higher limits, home-grow, legal protections, and access for qualifying 18–20 patients).

Show technical details

Changed

  • Isabella voice + chat prompts: corrected the Washington-law framing of medical authorizations. Both channels now state plainly that any adult 21+ can buy/use cannabis legally without a card, so an authorization is NOT what makes cannabis legal — with an explicit guard never to say or imply a patient needs one to use cannabis legally. The authorization is framed as added benefits + protections for qualifying medical patients (DOH recognition card: sales + cannabis excise tax exemption at medically endorsed stores, higher possession limits, limited home-grow, legal protection, and 18–20 access). Cross-channel parity; reviewed for WA-law accuracy. Voice change reaches the live Retell agent via the prompt-sync script.
v2.97.VAF0005
2026-06-03Production

New Staff Sessions page (Admin → Staff Sessions): see who signed in and signed out, and filter by an individual staff member.

What this means for you

New Staff Sessions page (Admin → Staff Sessions): see who signed in and signed out, and filter by an individual staff member. Sign-outs are now recorded too — before, only sign-ins were logged. Useful for "who was working the front desk yesterday afternoon" or reviewing a specific person's hours of access. Visible to Admin + Manager.

Show technical details

Added

  • Staff Sessions page (/admin/staff-sessions, ADMIN/MANAGER): a focused view of admin + provider sign-in / sign-out events, with a staff-member dropdown filter (built from staff accounts + providers), sign-in/sign-out quick-filter, date range, and pagination. Click any name to filter to just that person. Reads the existing append-only AuditLog table — no schema migration. Staff-attributed only (name + role); zero patient data.
  • Sign-out auditing: ADMIN_LOGOUT + PROVIDER_LOGOUT audit actions now written. The admin + provider logout routes decode the session cookie and record who signed out BEFORE clearing the cookie (cookie is always cleared regardless). Previously logout left no trail — only logins were audited.

Changed

  • ADMIN_LOGIN + PROVIDER_LOGIN audit rows now carry explicit staffUserId + staffUserName attribution, so they're filterable by staff on the new Staff Sessions page (and the Audit Log). Previously the login row's staffUserId was null — the name lived only in the free-text detail — because at login time the proxy hasn't yet set the x-admin-* headers that audit() normally reads.
  • Audit Log page: ADMIN_LOGOUT + PROVIDER_LOGOUT added to the action labels, colors (slate, distinct from the blue login family), and the "Auth events" synthetic filter.
v2.97.VAE0005
2026-06-03Production

Fixed the bug where opening a call from your Isabella / Demi queue would kick you back to the login screen.

What this means for you

Fixed the bug where opening a call from your Isabella / Demi queue would kick you back to the login screen. Schedulers can now open the Voice (Retell) call-transcript page directly from the queue — it no longer bounces you to log in again. (That page is the same call-review surface, with patient details already scrubbed; it just had a stricter login rule than the queue that links to it.)

Show technical details

Fixed

  • Isabella-today queue "open call" logout fixed: /admin/integrations/voice (the call-level transcript + tool-fire observability page) now allows the SCHEDULER role, matching its parent /admin/isabella-today + /admin/isabella surface that deep-links to it. Previously the voice page gated to ADMIN/MANAGER only, so a SCHEDULER (e.g. Demi/Mariane) clicking the Voice card's "Open →" was redirected to /admin/login?next=… — which presented as "opening a call logs me out and won't let me log back in." The page already shows only boolean config presence (never secret values) and routes every transcript through scrubPhiForSmsOutbound (last-4 phones, 100-char previews), so scheduler-read introduces no new PHI or secret exposure. Reviewer-feedback cmpwxwlta + cmpyb9ehi (logout half). Pure role-gate change on one page; no schema migration.
v2.97.VAD0005
2026-06-03Production

The Messages "unread" count now only counts texts and emails you actually need to read — not the auto-logged missed calls.

What this means for you

The Messages "unread" count now only counts texts and emails you actually need to read — not the auto-logged missed calls. That huge unread number was almost all missed phone calls, which buried the few real unread messages. Missed calls haven't gone anywhere: they still live on your Calls tab and on Demi's Callbacks-owed queue, where you tap "Called back" to clear them.

Show technical details

Changed

  • Messages unread badge + Unread tab de-noised: the nav unread count (/api/admin/messages/unread-count) and the Unread inbox filter (/api/admin/messages route) now count only inbound SMS + EMAIL with status RECEIVED, excluding auto-logged inbound CALL rows. Auto-logged missed calls are callbacks-to-make (tracked via the callbacks-owed worklist + morning digest, cleared by resolvedAt on the anchor inbound row), not messages-to-read; counting them inflated the badge into the thousands (mostly "Unknown" missed callers) and hid the handful of genuine unread texts/emails. Calls remain fully visible on the Calls tab (channel=CALL) and on Demi's Callbacks-owed queue. No schema migration; pure query-filter change on two read endpoints.
v2.97.VAC0005
2026-06-03Production

Demi's callback list is now one clear list with one button.

What this means for you

Demi's callback list is now one clear list with one button. Everything on your morning digest email shows up in your Callbacks-owed queue, and each row has a "Called back" button — tap it after you return a call and the person drops off both your screen and tomorrow's email. And on the phone, Isabella now only offers to put a caller through to Demi when someone's actually logged in and at the desk; otherwise she takes a detailed message and a callback number, so callers aren't promised a transfer to an empty desk.

Show technical details

Fixed

  • Demi callback workflow unification: the on-screen Callbacks-owed queue (admin demi-today) and the morning digest email now read from ONE definition (queryCallbacksOwed) instead of three divergent ones — so every caller the email tells Demi to call back actually appears in the queue. Previously the queue was gated on needsHumanAt (a subset Isabella flags) while the digest used a broader inbound-with-no-later-outbound rule, so the email listed people the queue never showed.
  • Callbacks now CLEAR: new "Called back" button on each queue row POSTs to /api/admin/callbacks/mark-contacted, which sets resolvedAt + resolvedById on the caller's anchor inbound PatientMessage (role-gated ADMIN/MANAGER/SCHEDULER). queryCallbacksOwed now drops any group whose anchor is resolved, so a cleared caller leaves BOTH the queue and the next digest. Idempotent (a second tap returns alreadyResolved). A phone call returned by phone leaves no outbound message, so this is the only thing that clears those rows — previously the queue could only grow. New CALLBACK_MARKED_CONTACTED audit action (resourceId = message id, detail resolvedBy=, PHI-free).
  • Voice escalation honesty (presence gate): Isabella's flagForHuman handler now only PROMISES a live transfer to Demi when checkDemiAvailability() is true — i.e. it's business hours AND a message-handling admin has a fresh heartbeat (15-min window). When no one is logged in she takes a detailed message + callback number instead of saying "let me get Demi on the line" to an empty desk. Crisis turns re-anchor on 988 (24/7) rather than promising an unavailable transfer. Fails SAFE to message-take if the presence check errors. The two dispatch error fallbacks no longer promise "the office manager on the line" either.
  • business-hours.ts getVoiceEscalationLine() is now a three-state SSoT (after-hours / during-hours-no-staff / during-hours-staff-present) with a new VOICE_ESCALATION_NO_STAFF string; staffPresent defaults true so legacy two-arg callers keep the warm-transfer phrasing. No schema migration on either fix.
v2.97.VAB0005
2026-06-03Production

You can now write Isabella's example answers by hand.

What this means for you

You can now write Isabella's example answers by hand. On the Isabella playbook page there's an "Author a new exemplar" form — pick a topic, write a typical question and the ideal reply, and save. It counts toward her training right away, so a manager can seed her first ten examples in one sitting without waiting for emails to pile up. Keep them generic — no real patient details — they're scrubbed and re-checked on save.

Show technical details

Added

  • Direct exemplar authoring on /admin/isabella-playbook (role-gated ADMIN/MANAGER, same as curation): a new "Author a new exemplar by hand" form lets a reviewer write a canonical question->answer pattern from scratch (category / tone / decision from the extractor's closed-enum SSoT + a generic inbound + reply summary). The row is created at status="approved" with null source FKs, so it lands in the SAME store countLearnedExemplars() reads and counts toward MIN_LEARNED_EXEMPLARS_FOR_EXPAND (10) immediately — unblocking the learn-first EXPAND gate WITHOUT waiting on organic email volume + the isabella-exemplar-builder ingest cron. NO schema migration: every staff_reply_exemplar column needed for a hand-authored row (nullable source FKs, status default, reviewer attribution) already exists.
  • PHI posture on hand-authored exemplars: both summaries are run through the SAME outbound scrubber (scrubPhiForSmsOutbound) AND re-scanned with the outbound PHI canary (scanBodyForPhiCanary) at save — fail-CLOSED (the row is rejected, never persisted, if anything that looks like patient data survives scrubbing). Closed-enum validation rejects out-of-set category/tone/decision. New ISABELLA_EXEMPLAR_AUTHORED audit action: enum strings + the new row's cuid ONLY in detail (PHI-FREE); no summary text in the audit body.
v2.97.VAA0005
2026-06-03Production

When Isabella flags a message for you on the daily board, you now see a short "what they need, in their words" line right at the top of each item — so you can read it before you call back and open with "I see you wrote in about that," instead of starting cold.

Show technical details

Added

  • Isabella daily board (admin isabella-today) NEEDS ATTENTION band: each escalation row now carries a PHI-minimal "warm pickup" handoff note — a plain-language intent label derived from the category Isabella already assigned (e.g. "Wants to book / reschedule", "Records request", "Urgent — wants a clinical answer") plus a short snippet of the patient's own words. The patient-words snippet runs through the SAME outbound PHI scrubber as every other preview (scrubPhiForSmsOutbound) and is length-capped; no diagnoses or sensitive detail beyond what's needed to triage the callback. Pure render over columns already on the row (aiCategory + subject/body) — no schema change, no escalation-time AI call.
  • Voice self-learning wiring (S6): new exported async buildVoicePromptWithPlaybook() in voice-prompt.ts mirrors the email + chat learned-reply-playbook injection — appends the flag-gated, PHI-canary-rescanned, fail-closed learned block BELOW the safety-complete static voice prompt, and SKIPS injection if it would exceed the per-turn TTS latency soft cap. Default OFF (returns the static prompt byte-for-byte until ISABELLA_PLAYBOOK_INJECTION_ENABLED is flipped). Reaching the LIVE phone still requires Doug to run scripts/sync-retell-prompt.mjs after the flag is on; the sync script's static-extraction path is unchanged in this ship.
  • Chore (SSoT): appointment-ICS email duration in emails.ts now uses MINUTE_MS from time-constants instead of an inline 30 * 60 * 1000 literal (no behavior change; clears the check-time-constants-inline gate left red by the VY0005 add-to-calendar ship).
v2.97.VZ0005
2026-06-03Production

The website chat now greets visitors with tappable topic buttons — like "What does it cost?", "What services do you offer?", and "How do I get started?" — so people can get answers right away instead of staring at a blank box.

Show technical details

Changed

  • Public chat widget's empty-state suggested-topic chips expanded from 3 to 6 and moved to a single source of truth (CHAT_SUGGESTED_PROMPTS in src/lib/constants.ts): added "What services do you offer?", "How do I get started?", and "How do I contact you?" alongside the existing eligibility / cost / booking chips. Clicking a chip still sends its text as the visitor's first message into the Claude-powered assistant — no new code path, no PHI, shown only before the first user message. Mariane reviewer-feedback cmpuj0jkd.
v2.97.VY0005
2026-06-03Production

Appointment confirmation and reminder emails now give patients one-tap "Add to calendar" buttons for Google, Outlook, and Apple — so fewer no-shows from forgotten visits.

Show technical details

Added

  • Booking-confirmation and reminder emails now render Google Calendar, Outlook, and Apple Calendar (.ics) add-to-calendar links with the visit time, telehealth join link or in-person location, and the clinic phone number prefilled. Link text is static and all URL parameters are encodeURIComponent-escaped (no patient identifiers in the calendar payload beyond the appointment type + location). Replaces the prior single bare .ics link.
v2.97.VX0005
2026-06-03Production

Patient form links now send themselves — when you create a form, the patient automatically gets it by email and text, so no more copying and pasting links. And if a patient arrives without their forms done, a new "Re-send link to patient" button texts and emails it again so they can finish on their own phone right there.

Show technical details

Added

  • createPatientForm now auto-sends the form magic link to the patient over email (M365) + SMS on creation — best-effort, never blocks creation — replacing the old manual copy/paste. New resendFormLink server action + "Re-send link to patient" button on /admin/forms/[id] re-send the existing link (allowed only while DRAFT/SENT/OPENED). Both share one internal sender that writes a PHI-free FORM_SENT_TO_PATIENT audit row (formType + channel booleans, plus resent=true on resends — no email/phone/name/body). New pure helper src/lib/forms/form-link-message.ts builds the email subject/body + SMS text (unit-tested, no patient identifiers, HTML-escaped link). SMS rides the existing workflow.ts router (RingCentral when RC_FROM_NUMBER is set; Twilio fallback today). Doug ask 2026-06-02.

Changed

  • Patient-facing "already signed" screen no longer claims "a copy was sent to you" — we don't auto-email signed PDFs — and now directs the patient to view/download their signed copy under My forms in the patient portal.
v2.97.VW0005
2026-06-03Production

Every Copy button across the admin — magic links, portal links, referral links, temp passwords, merge fields, and the end-of-day report — now pops up a clear "Copied to clipboard" confirmation when you click it, so you never have to wonder whether the copy actually worked.

Show technical details

Changed

  • All admin copy-to-clipboard buttons now show a toast confirmation when a copy succeeds, in addition to the existing inline "Copied!" state. Ten independent copy handlers (cancel-link, send-portal-link, referral-link, merge-field tokens, form magic-link wizard, form-detail link, provider portal link, promo referral link, reset temp-password, and the EOD report copy) each fire toast("Copied to clipboard") via the shared @/lib/toast system rendered by the admin Toaster, matching the existing pattern already used by the Poynt billing button. Patient-facing copy buttons were left unchanged (the Toaster mounts only in the admin layout) and the 2FA-secret copy already swaps to a checkmark icon. Resolves ReviewerFeedback cmpukiz16 (Mariane).
v2.97.VV0005
2026-06-03Production

Appointment times now show in Pacific Time all the way through the booking flow, so the time a patient picks no longer changes when they reach the confirmation screen. Isabella is also clearer on calls and in chat that a new booking is a tentative request pending records review, and the records reminder email now shows the Green Wellness logo with clearer, step-by-step instructions on every way patients can send their records.

Show technical details

Fixed

  • Appointment times in the patient booking wizard now render in clinic Pacific Time (America/Los_Angeles) instead of the viewer's browser-local timezone. Previously Step4Time's slot grid + selected-slot chip and StepConfirmation's summary line used bare date-fns format(), so a slot stored as e.g. 4:25 PM PT displayed differently depending on the viewer's timezone and appeared to "change" between the time-picker and the confirmation screen. Both components now use the existing fmtPT() helper from @/lib/tz (CLINIC_TZ), so the chosen time stays consistent end-to-end. Resolves ReviewerFeedback cmpukesze (Mariane).

Changed

  • Isabella now sets a tentative-request expectation when a booking is placed, matching the existing system-prompt rule (chat route + voice-prompt already forbid "you're booked"). The voice booking tool's spoken confirmation (proposeBookingViaText) and the chat/email confirmBooking tool result message no longer say the appointment is booked/confirmed; they state it is a tentative request that requires medical records and a provider's review before final confirmation by email or follow-up call. Resolves ReviewerFeedback cmprr8pjs + cmpuk7f0o (Mariane). The static booking-confirmation email already carried this framing and was left unchanged.
  • The records-reminder email (Day 3/5/7, records-reminder-email-shared.ts) now renders the shared renderEmailHeader() logo band at the top (logo when EMAIL_LOGO_URL is set, brand-text fallback otherwise) and expands the records-submission instructions: patients are told they can email an attachment, fax, reply directly to the email with attachments, or send clear photos/images, and that each record must show their name, diagnosis/qualifying condition, and a date within the past year, and must be legible and complete to avoid processing delays. Contact details still flow from the EMAIL/FAX/PHONE constants (no hardcoded literals). Resolves ReviewerFeedback cmpxgimu2 (Mariane).
v2.97.VU0005
2026-06-02Production

When patients call, Isabella no longer asks for their date of birth or street address out loud — those now come from the secure intake form patients complete after the call. The phone conversation is shorter and more private, and nothing the front desk does changes.

Show technical details

Changed

  • Isabella's phone booking tool (proposeBookingViaText) no longer collects date of birth or street address verbally: those four fields (dob, addressStreet, addressCity, addressZip) are removed from the tool's required parameters and the spoken re-prompts/validators, and dropped from the voiceBookingProposal patientFields written on a successful proposal. This aligns the tool with the existing voice-prompt instruction ("Do NOT ask for date of birth, street address, or SSN verbally — those go on the secure intake form after the call"); the booking proposal still captures name, phone, email, conditions, slot, appointment type, and SMS consent, and DOB/address are collected on the post-call secure intake/consent form (the HIPAA-covered surface). Removes the now-orphaned 18+ verbal age-gate (eligibility is enforced on the intake form). Resolves ReviewerFeedback cmpuk7qnm + cmpuk81an (Mariane).
v2.97.VR0005
2026-06-02Production

The green chat button in the bottom-right corner of the public website now has a small "Questions?

What this means for you

The green chat button in the bottom-right corner of the public website now has a small "Questions? Chat with us" label beside it, so visitors recognize it's there to help and are more likely to start a conversation. Nothing changes about how the chat itself works.

Show technical details

Changed

  • Public chat launcher now shows a "Questions? Chat with us" caption pill beside the floating green icon while the chat is closed, making the launcher legibly clickable for visitors who don't recognize a bare floating circle as a chat affordance (Mariane feedback). The pill is closed-state only and hidden on the smallest screens so it never crowds the tap target; clicking it opens the same chat panel. No change to chat behavior, and the heavy chat panel still lazy-loads on first open (no new weight on the cold-load path).
v2.97.VQ0005
2026-06-02Production

When a patient asked the website chat assistant (Isabella) to "pull up times," she was replying that "no open slots are loading" — an awkward dead-end. That happened because the website chat was still reading an internal appointment list that isn't connected to our live Practice Fusion calendar yet, so it always came back empty. We already fixed this for the phone assistant on June 1; this brings the website chat in line. Isabella now describes Dr. Ari's standing visit windows (telehealth Wednesday and Friday mornings; in-person at the clinic by preference) and offers to take the patient's preferred day and time so the office can confirm the exact opening and call them back — no more "nothing is loading" message.

Show technical details

Fixed

  • Website chat assistant (Isabella) no longer reads the unsynced internal AvailabilitySlot table when a patient asks what's available. This mirrors the voice-tool neutralize shipped 2026-06-01 (SA0005): the chat listOpenSlots handler now returns no specific dated slot and instead describes Dr. Ari's standing recurring windows (telehealth Wed/Fri 10:30–12:30; in-person by clinic preference), then routes to lead-capture + staff confirm-back against Practice Fusion. Resolves the customer-facing "no open slots are loading on my end right now" dead-end. The tool description was updated so the model no longer attempts to self-book a specific slot.
v2.97.VP0005
2026-06-02Production

Two new behind-the-scenes tools for getting Isabella ready, both for Doug + Demi only.

What this means for you

Two new behind-the-scenes tools for getting Isabella ready, both for Doug + Demi only. First, a one-look readiness check that answers "what's still keeping Isabella switched off?" in plain language — which channels are live, how many approved examples she's learned so far, and the exact remaining steps. Second, a new "Knowledge" tab on the Isabella → Exemplar playbook page that shows the exact anonymized playbook Isabella WOULD learn from, so a manager can read her whole knowledge as one document and sign off on it before it's ever turned on. Both are read-only and change nothing about how Isabella behaves today.

Show technical details

Added

  • New admin readiness check (/api/admin/diag/isabella-readiness): one call returns the live on/off state of every Isabella channel + feature (email, chat, SMS, learned-playbook injection, exemplar ingestion, records-upload link), the current learned-exemplar count vs the 10 needed to open the learn-first gate, which learning tables exist, and a plain-language list of exactly what's blocking go-live. Zero PHI — booleans, one count, one provider enum only. Admin-gated.
  • New "Knowledge" tab on /admin/isabella-playbook: renders the exact PHI-scrubbed playbook Isabella would inject from her approved exemplars — a flag-independent preview so Demi/Doug can review her learned knowledge as one document BEFORE flipping the injection switch. Shows whether injection is currently live or still off, and writes a PHI-free audit row (ISABELLA_PLAYBOOK_PREVIEWED — counts + enums only) on each view.
v2.97.VN0005
2026-06-02Production

When a patient needs to get their medical records to us before scheduling, Isabella can offer to email them a private, expiring upload link right then — on chat, email, or a phone call — instead of asking them to fax or email records themselves. The email only ever includes the patient's first name and the link (never any health details), and the old fax/email option is still right there for anyone who prefers it. This is built but kept switched off until Doug turns it on.

Show technical details

Added

  • New Isabella capability (sendRecordsUploadLink) across chat, email, and voice: when a patient needs to send in their own medical records, Isabella can offer a secure, patient-scoped, short-lived upload link and email it on the spot via the M365 (BAA-covered) send path. The email body carries no PHI beyond the patient's first name plus the link, and preserves the existing fax/email fallback rail verbatim. Every send writes a PHI-free audit row (LEAD_RECORDS_LINK_SENT — channel + match-mode only, no email or token in the body).
  • Reuses the existing patient-portal magic-link token (short TTL + HMAC) — no new token logic. The records-RELEASE (third-party disclosure) refusal and crisis-handling guards are unchanged; this only adds the inbound upload affordance.

Changed

  • Records-inbound prompt language across all three channels now offers the secure upload link WHEN the capability is enabled; when disabled, every prompt surface falls back verbatim to the existing fax/email language.
v2.97.VK0005
2026-06-02Production

Isabella can now start learning from how Demi actually replies — but ONLY after a human approves each example.

What this means for you

Isabella can now start learning from how Demi actually replies — but ONLY after a human approves each example. A new behind-the-scenes job reads past (patient asked → staff replied) email pairs, strips out any patient details, and turns each into a short anonymized "how we handled this" pattern. Those land on a new review page (Isabella → Exemplar playbook) where a manager approves, edits, or rejects each one. Nothing trains Isabella until it's approved, and the whole pipeline stays OFF until Doug flips the switch — so there's no AI cost until he's ready.

Show technical details

Added

  • New daily background job (isabella-exemplar-builder) that builds Isabella's learning set from historical staff email replies. Runs 3am PT, drips through old (inbound → staff-reply) pairs, and writes a PHI-scrubbed 5-field pattern (category / tone / decision / what-was-asked / how-it-was-answered) per pair. Default OFF behind ISABELLA_EXEMPLAR_INGEST_ENABLED — a complete no-op (and zero AI spend) until Doug enables it.
  • New review surface at /admin/isabella-playbook — managers approve, edit, or reject each extracted pattern. Only approved/edited patterns count toward the learn-first gate that lets Isabella take on more on her own. A banner shows the running count toward the 10-exemplar threshold.
  • Manual backfill support: POST the cron with ?bootstrap=true to drain the historical backlog in a few fires (higher per-run batch cap, same spend guardrails).

Changed

  • Cost guardrails for the new job run on their OWN isolated daily ledger (isabella_exemplar_spend_daily) so the one-time backfill never eats into the live patient-facing email AI's budget. Soft cap $3/day (alerts but keeps going), hard cap $6/day (skips the rest of the day), checked again mid-run.
v2.97.VH0005
2026-06-02Production

Isabella sounds a touch more human in two spots: when someone worries about whether they'll qualify, she now invites a conversation instead of a flat "call us if you don't," and her first email reply opens like a real person rather than a menu of options.

Show technical details

Changed

  • 💬 **VH0005 — Isabella patient-facing voice polish (two narrow tweaks, no behavior change).** Tone-only pass on already-tuned copy; no flags flipped, no safety/crisis/PHI/escalation rules touched. **(1) Chat "What if I don't qualify?"** (src/app/api/chat/route.ts Common Questions): rewrote from "Our providers assess each patient individually. If a provider determines you do not qualify, please call us…" to warmer, conversation-inviting phrasing ("worth a conversation… if a provider decides it isn't a fit, give us a call — we'll walk you through your options"). Operating facts unchanged (same ${PHONE}, no medical claim, providers still assess individually). **(2) Email new-thread opener** (src/lib/email-ai.ts EMAIL_AI_SYSTEM_PROMPT behavior): the "name the two common reasons people email" instruction now steers Isabella toward one warm sentence ("You're in the right place — most folks email us with a question about the evaluation or to get on the schedule, and either way I can help") instead of a menu-like bulleted list. Two-bucket framing + "if it's something else, I'll flag it for Demi" preserved. **Tests:** two pin assertions in email-ai-isabella-polish.test.ts updated to match the new wording (net test impact neutral). tsc --noEmit clean. **HIPAA posture:** PHI scope NONE — both edits are prompt/copy wording only. [isabella-voice-polish][tone-only][no-flag-flip][version-letter:VH]
v2.97.VE0005
2026-06-02Production

Isabella is learning from how Demi actually handles messages, and she'll now offer one helpful next step in a reply when it fits — like offering to find a time when someone asks about pricing. Two safety guards: she won't be allowed to take on more on her own until she's learned from enough of Demi's real replies (you can see the count on Doug's morning tile), and all of this is still behind the off switch until Doug turns it on.

Show technical details

Added

  • 🎓 **VE0005 — Isabella learn-first gate + initiative-within-guardrails + few-shot wiring (behind flags).** Three coordinated changes, all reversible, no prod env flags flipped (EMAIL_AI_ENABLED / SMS_AI_ENABLED / chat all stay as-is — Doug flips after training). **(1) Learn-first EXPAND gate** (src/lib/email-ai-pulse-shared.ts): new pure fn computeEmailAiVerdictWithLearning(input, learnedExemplarCount) extends the verdict state machine — a positive EXPAND verdict is now ALSO gated on ≥MIN_LEARNED_EXEMPLARS_FOR_EXPAND (10) approved/edited exemplars in staff_reply_exemplar, on TOP of the existing ≥3 clean-ack threshold. Learning only ever GATES the positive promotion — it never relaxes a KILL or a base-HOLD (those pass through untouched), so the gate is strictly HIPAA-safe (it can only hold autonomy back, never expand it). When clean-but-under-corpus, verdict drops to HOLD with reason "only N/10 exemplars learned — needs more before expand". **(2) Visible readiness signal** (AiPulseTile.tsx + email-ai-pulse.ts): the morning pulse tile on /admin/doug-queue now shows an "Exemplars learned" chip (warn tone when the learn-gate is blocking, ok tone once met) so "she's learned enough" is measurable at a glance. countLearnedExemplars() is fail-SAFE (returns 0 on any DB hiccup → gate stays closed). **(3) Few-shot wiring** (isabella-playbook.ts + -shared.ts): learned exemplars are assembled into a block (REFERENCE-only framing, capped 3/category, 8000-char budget, whole-row drops) and APPENDED after the base+patient-context system prompt on both the email-AI reply path and the chat path — so it can NEVER override the crisis/PHI/escalation rules above it. Gated on ISABELLA_PLAYBOOK_INJECTION_ENABLED (default OFF). Belt-and-suspenders: every exemplar summary is re-scanned with the outbound PHI canary before injection; any tripping row is dropped. getCanonicalPlaybook() is fail-CLOSED (returns "" on any error → no partial/stale injection). 1h in-process cache. **(4) Initiative-within-guardrails** (email-ai EMAIL_AI_SYSTEM_PROMPT + chat prompt): added an ## Initiative section to both — proactive = offer ONE helpful next step INSIDE the reply she's already sending (email stays strictly reply-only; no new outbound). Safety/escalation always preempts; never manufacture urgency; never imply a clinical benefit of cannabis (medical claim); drive offers ONLY from what the patient said in-thread, never from stored history. **Tests:** +9 pins on the learn gate (email-ai-pulse.test.ts) + 9 pins on the playbook assembler (isabella-playbook.test.ts) = 18 new, all green; tsc --noEmit clean. **HIPAA posture:** new code touches PHI scope NONE in the pure-fn layer; the server reader only reads already-scrubbed approved/edited exemplar summaries from a BAA-covered store, re-scans for PHI, and injects via the Bedrock-wrapped (BAA-covered) reply path. Audit trail unchanged — exemplar reads ride the existing extractor's audit rows. **Doug-greenlight to ACTIVATE:** flip ISABELLA_PLAYBOOK_INJECTION_ENABLED=true (turns on learned-reply few-shot) once ≥10 exemplars are curated; the EXPAND verdict will not promote until both gates are met. [isabella-learn-first][initiative-within-guardrails][few-shot-behind-flag][hipaa-safe-gate][version-letter:VE]
v2.97.VD0005
2026-06-02Production

The autonomous worker that handles Mariane's feedback can now process up to 5 items per run and 30 per day, instead of 1 + 3. The HIPAA safety screens are unchanged — only the daily volume cap moved.

Show technical details

Changed

  • 🚀 **VD0005 — raised agent-feedback-fix caps on GW for backlog drain.** Doug 2026-06-02: "drain them out one after another dont stop" + "use extra agents to expediate". Per-run cap 1 → 5, 24h fleet-wide cap 3 → 30. The cap is a VOLUME gate, not a RISK gate — the HIPAA-specific REFUSE list (patient/provider paths, twilio/email/inbound, audit + patient-* + phi-* libs, schema) is unchanged. PHI-screen on body + cleanedBody still runs before reading. On each successful ship, agent re-reads queue + picks next oldest approved-autofix row. Stop early on REFUSE-streak (3 in a row), build-time exhausted, or cap hit. Revisit + lower (back to 1/3) once steady-state. Sister-shipped on VRG as v9.7.1435. Files MOD: .github/agent-feedback-fix-protocol.md (§Cadence updated) · .github/workflows/agent-feedback-fix.yml (prompt text updated) · src/lib/changelog.ts · src/lib/changelog-current.ts. [autonomy-volume-raise][hipaa-screens-unchanged]
v2.97.VC0005
2026-06-02Production

Patients who give Isabella an email on the phone now get a polished recap email within a minute — quick summary of what they shared, a clear note that nothing's confirmed until our team reviews their records, and an explicit line that payment fees come by email invoice (never by SMS). Demi sees every recap that went out as an outbound row on /admin/isabella-today, so there's a paper trail when a patient asks 'did you get my call?'.

Show technical details

Added

  • 📧 **VC0005 — post-call confirmation email pipeline polish (Mariane reviewer-feedback batch #2, closes cmpuk5ed + cmpuk70rg + cmpw2z6mp).** Builds on the IB0005 pipeline (originally shipped 2026-05-29) to address three Mariane items filed against /admin/isabella-today: (cmpuk5ed) post-call summary email content polish + paper-trail in PatientMessage; (cmpuk70rg) SMS-not-functional-replace-with-email reframe; (cmpw2z6mp) phone-AI payment-link-via-SMS correction → email-invoice. **Renderer polish (src/lib/voice-call-summary-email-shared.ts):** subject "We received your call — Green Wellness""Quick recap — your Green Wellness call" (CP0005 blessed-opener doctrine). Opener "Hello Sarah, Thanks for calling Green Wellness today""Hi Sarah, Quick note — Isabella here (I'm an AI assistant on the Green Wellness side). Here's a recap of what we covered on our call today…" — single-identity-across-channels + FTC AI-disclosure-in-first-clause (sister of CP0005 chat + FP0005 fallback + IE0005 email-AI). Close "Warm regards, The Green Wellness Team""— Isabella / AI assistant for Green Wellness" (drops 'Warm regards' cliche; preserves FTC AI-disclosure). NEW explicit payment-block: "About payment: if there's a fee for your visit, our team sends a secure invoice by email — we do not text payment links, and we don't have an in-app payment option." Directly addresses Mariane cmpw2z6mp ("AI should not say SMS payment link; should say email") + cmpuk70rg ("can't validate SMS works; replace with email until SMS is live"). **Webhook handler (src/app/api/webhooks/retell/voice/route.ts):** added 5-minute idempotency check — if a voice-summary recap was already sent to the same email in the last 5 min (PatientMessage row with channel=EMAIL/direction=OUT/fromAddr=ai-voice-summary), the send is skipped with reason=idempotent-skip-5min. Defends against Retell re-analyze double-fires. NEW PatientMessage(channel=EMAIL, direction=OUT, fromAddr='ai-voice-summary', aiAutoSent=true) row persisted on successful send — gives Demi an operator-side paper trail at /admin/isabella-today. Body is a HIPAA-safe-harbor summary line (patient type / preferred time / condition area only; NEVER the full HTML which embeds the email + first name in a wider PHI surface than necessary — full HTML stays in M365 Sent Items). PatientMessage write failure is non-fatal (email already shipped). **Pin tests (src/lib/__tests__/voice-call-summary-email-shared.test.ts):** +8 new pin tests defending the VC0005 doctrine — subject uses 'Quick recap', not 'We received' or 'Thanks for'; opener uses 'Quick note — Isabella here', not 'Thanks for reaching out' / 'Thanks for calling'; close doesn't use 'Warm regards' / 'happy to help' / "please don't hesitate"; close preserves FTC AI-disclosure; payment-block reframes as 'secure invoice by email' + explicitly bans SMS payment promise; recap framing confirms inquiry received + under review. Existing 'Hello Sarah' assertion bumped to 'Hi Sarah' (greeting tightened per polished doctrine). **HIPAA posture (unchanged):** body remains safe-harbor by construction — first-name + patient-type + condition-area-broad + preferred-slot-label only. NEVER DOB, address, SSN, transcript. Audit rows PHI-free. M365 BAA-covered transport. **Reviewer-feedback PATCH:** cmpuk5ed + cmpuk70rg + cmpw2z6mp marked done with autoFixVersion=v2.97.VC0005 so the ✨ 'Auto-fixed by Claude' badge renders. [reviewer-feedback][mariane-2026-06-01-batch-2][isabella-recap-email-polish][hipaa-safe-harbor][idempotency-5min][patient-message-paper-trail][version-letter:VC][cadence-override: top-leverage GW expert pick — post-call summary email; closes Mariane reviewer-feedback ids cmpuk5ed cmpuk70rg cmpw2z6mp]
v2.97.MZ0005
2026-06-02Production

GW now has its own autonomous worker that picks up Mariane's pre-approved feedback every 4 hours and ships fixes without waiting for Doug to click anything. Sister to the same worker that's already been running on the cannabis side.

Show technical details

Added

  • 🤖 **MZ0005 — GW autonomous reviewer-feedback worker (agent-feedback-fix).** Doug 2026-06-02 directive "add additional agents as needed" — after the MY0005 ship started auto-promoting Mariane's feedback to approved-autofix on insert, there was no GW worker to pick those rows up (GW only had agent-auto-fix.yml for the critical_errors queue; no agent-feedback-fix.yml). New GH Actions workflow runs every 4h at :23 (offset from agent-auto-fix.yml :11), picks 1 row from /api/admin/reviewer-feedback/queue, claims via PATCH action=working, ships a fix, marks PATCH action=done with the changelog version. **HIPAA-specific REFUSE list** (full text in .github/agent-feedback-fix-protocol.md): no edits under src/app/patient/**, src/app/provider/**, src/app/api/patient/**, src/app/api/provider/**, src/app/api/twilio/**, src/app/api/email/**, src/app/api/inbound/**, src/lib/audit.ts, src/lib/patient-*.ts, src/lib/phi-*.ts, prisma/schema.prisma, prisma/migrations/**. PHI screen on body + cleanedBody before reading. Never fetches screenshotUrl (could be patient chart). Cap: 1 ship/run · 3 ships/24h fleet-wide on GW. Sister workflows: VRG agent-feedback-fix.yml (90% identical; this adds HIPAA REFUSE) · inv-App agent-feedback-fix.yml (the original). NEW files: .github/workflows/agent-feedback-fix.yml · .github/agent-feedback-fix-protocol.md. [autonomy-rail][hipaa-aware][cross-stack-port]
v2.97.MY0005
2026-06-02Production

Mariane: any feedback you submit now goes straight onto the auto-fix queue — no more sitting in 'open' waiting for Doug to triage.

What this means for you

Mariane: any feedback you submit now goes straight onto the auto-fix queue — no more sitting in 'open' waiting for Doug to triage. Small polish + bug-fix items get shipped automatically; anything that touches contract details still bubbles up to Doug.

Show technical details

Changed

  • 🤖 **MA0005 — Mariane's feedback auto-promotes to approved-autofix on insert (skip the open/triage step).** Doug 2026-06-02: "make sure that is fixed moving forward to jjust fix what she submits" + "same for GW". The submit endpoint now checks isAutofixTrustedSubmitter(email); if true (Mariane), the new row lands at status='approved-autofix' instead of 'open', putting it directly on the autonomous fix queue. The content-aware classifier (FORCE_DOUG_REVIEW_SUBMITTERS + RULE_* regexes) still bumps any item mentioning CUI / ITAR / contract values / key personnel up to huge-doug-required, which keeps risky items off the auto-ship rail — so net effect is: small + medium copy/UI fixes auto-ship, contract-flavored items still wait for Doug. Files MOD: src/lib/reviewer-feedback.ts (new REVIEWER_FEEDBACK_AUTOFIX_TRUSTED allowlist + isAutofixTrustedSubmitter helper) · src/app/api/feedback/route.ts (consume helper at insert). Also backfilled the 25 existing GW Mariane-open rows to approved-autofix in the same session (DB-direct UPDATE; agentNote stamped [doug-bulk-promote 2026-06-02]). Sister-shipped on VRG as v9.7.1425. [autonomy-rail][trusted-submitter][defense-in-depth-via-content-classifier]
v2.97.PP0005
2026-06-01Production

Patients can now do more for themselves in the portal — see their authorization status and expiry, view the documents on file for them, check the status of any request they've made, book a renewal without re-entering their info, and sign forms online. Front desk: there's a new Invoice Queue showing who needs a Poynt invoice for an after-the-visit service fee, with one-click links to mark each one paid or done.

Show technical details

Added

  • 🧑‍⚕️ **PP0005 — patient-portal self-service buildout (Ships C–F + Poynt invoice queue + online form-signing surface).** A patient-experience pass so patients can find everything that applies to just them without calling the office. **(1) Authorization detail page** (/patient/portal/authorization) — expiry status (active / renewal-soon ≤60d / expired) with a one-click renewal CTA, recommending physician, designated provider, patient-since, and a download link for the most-recently-issued authorization PDF + its mailing status. Read-only on existing Patient/Appointment columns. **(2) Documents on file** (/patient/portal/uploaded-records) — lists the medical documents on file for the patient with a per-row view link; bytes are streamed server-side through the existing private-blob proxy (/api/patient/documents/[id]) so the private Vercel Blob URL never reaches the browser. **(3) Frictionless renewal prefill** — logged-in returning patients book a renewal without re-entering their info (/?book=true&type=returning&prefill=1, server-read via /api/patient/booking-prefill). **(4) Requests surface** (/patient/portal/requests + /requests/new) — a patient can start a reissue (lost authorization) or designated-provider-change request; creates intent only (no charge — collection is a staff-sent Poynt invoice, post-cutover). **(5) Portal home tile-grid** — a Quick-actions hub linking authorization / documents / requests / forms, with a 'N to sign' nudge on the forms tile. **(6) Forms to sign** — the forms page now surfaces forms WAITING for signature (SENT/OPENED with a non-expired magic-link token) with a 'Sign now' link to the existing /patient/forms/[token] signing UI, so a patient who lost the email can still sign from an authenticated session. Read-only on existing PatientForm columns. **(7) Invoice Queue for front desk** (/admin/invoice-queue) — Demi/Mariane see who needs a Poynt invoice (PENDING), who's paid (PAID), and closed items; per-row PHI-free 'Open Poynt portal' link (zero patient identifier in URL or invoice description — staff type 'Authorization service fee') plus Mark-paid / Complete / Cancel driven by the existing /api/admin/cert-requests PATCH. **HIPAA / freeze scope:** all reads on existing columns + request-creation on the existing CertServiceRequest model — ZERO new Prisma migration, ZERO money-movement wiring, ZERO new patient-table write shapes. Freeze-safe 6/1→6/9. Every patient self-access audits PATIENT_VIEWED_RECORD / PATIENT_VIEW_FORMS_LIST (count-only detail, §164.312(b)). [hipaa-pre-cutover-freeze-compatible][patient-right-of-access-164.524][phi-minimization][blob-byte-proxy][version-letter:PP0005]
  • 💵 **PP0005 #2 — fixed a money-display bug: the $50 reissue fee showed as $25 in the staff mailing tool.** /admin/mailing hard-coded RESEND = $25 in three places (header copy, the new-request label, and the fee calc) — stale from before Doug raised the lost-authorization reissue fee to $50. All three now drive off the shared feeForCertRequest() module (single source of truth: RESEND + CHANGE both $50), so a future fee change updates everywhere at once. Copy/display only — no schema, freeze-safe. [money-display-correctness][single-source-of-truth][version-letter:PP0005]
v2.97.DP0005
2026-06-01Production

Front-desk staff (Demi) can now print mailing labels AND the authorization itself right from the Mailing page, and mark items mailed — she no longer needs a manager to print. Each unmailed cert now has a 'Print auth' and a 'Print label' button side by side.

Show technical details

Fixed

  • 🖨️ **DP0005 — front-desk (SCHEDULER / Demi) can now print mailing labels + approved authorizations (Doug 2026-06-01: 'she needs to be able to print mailing labels as well as auths once approved').** Root cause of her 'couldn't access printing': the mailing-workflow APIs defaulted to ADMIN/MANAGER, so the /admin/mailing page rendered for her but every print/queue call returned 401. Added SCHEDULER to the allowlist on the endpoints her workflow needs: /api/admin/mailing (GET queue/mailed + PATCH/POST mark-mailed & tracking), /api/admin/mailing/labels (Avery 5160/5163 label PDF), /api/admin/cert-requests (GET/POST/PATCH — resend & address-change service requests; **DELETE intentionally stays ADMIN/MANAGER** since paid requests should be CANCELLED, not hard-deleted), and /api/admin/cert/[id] (read-only auth-PDF download, logged as DOWNLOAD_CERT). Issuing / regenerating an authorization stays ADMIN/MANAGER+provider — this grant is print-and-mail only. **UI:** added a 'Print auth' button beside the (now-relabeled) 'Print label' button on each unmailed row of the Mailing 'To mail' tab, so Demi prints the document that goes in the envelope without leaving the page. **Files MOD:** src/app/api/admin/mailing/route.ts · src/app/api/admin/mailing/labels/route.ts · src/app/api/admin/cert-requests/route.ts · src/app/api/admin/cert/[id]/route.ts · src/app/admin/mailing/page.tsx. **HIPAA / freeze-compatible:** RBAC-allowlist + UI ONLY — ZERO schema/Prisma migration, all PHI access still audited (DOWNLOAD_CERT / EXPORT_PATIENTS / UPDATE_APPOINTMENT_NOTES fire regardless of role). Freeze-safe 6/1→6/9. [hipaa-pre-cutover-freeze-compatible][rbac-allowlist-only][demi-scheduler-mailing][print-and-mail-only][delete-stays-admin][version-letter:DP0005]
v2.97.AQ0005
2026-06-01Production

We corrected our website and patient materials: anxiety on its own is not a Washington qualifying condition (PTSD is), so we no longer say it qualifies. We now explain anxiety is often evaluated alongside PTSD and other qualifying conditions, with the provider deciding case by case.

Show technical details

Changed

  • 🩺 **AQ0005 — removed every claim that anxiety on its own qualifies for a Washington medical-cannabis authorization (Doug 2026-06-01: 'the website represents anxiety as a qualifying condition — that is not true').** Grounding: RCW 69.51A.010 enumerates Washington's qualifying conditions — PTSD IS listed; anxiety / generalized anxiety / social anxiety / panic disorder / OCD are NOT and do not qualify on their own. Anxiety may be present as a symptom of a qualifying condition (e.g. PTSD, cancer, HIV/AIDS), with the licensed physician making the individual determination — so accurate 'anxiety-as-symptom' mentions were KEPT; only the false 'anxiety qualifies' framing was removed/reframed. **Canonical data:** dropped "anxiety" from RCW_QUALIFYING_CONDITIONS and removed the 5 anxiety variant normalizer mappings (anxiety-disorder / generalized-anxiety / gad / panic-disorder / social-anxiety) so a problem-list dump containing 'anxiety' now falls through to operator review (rejected) instead of auto-promoting — this list prints on cert PDFs, drives /admin/authorizations, and gates EHI ingest. Mirror-synced the same removal in the backfill script and dropped the SNOMED allowlist 'Anxiety' code (48694002) so EHI ingest can't auto-promote it. **Content reframed (URL kept for SEO):** the /conditions/anxiety page intro now opens 'Anxiety on its own is not one of the conditions enumerated in Washington's RCW 69.51A.010…' and explains it frequently accompanies PTSD (a recognized qualifying condition); same honest reframe applied across conditions-content, city-condition-content, telehealth-condition-content, the dedicated anxiety article, FAQ data, Isabella's chat + voice eligibility prompts, the intake reason-for-visit label (PTSD / anxiety → PTSD), and the .AX encounter dot-code (relabeled 'symptom'). **Files MOD:** src/lib/qualifying-conditions.ts · src/lib/conditions-content.ts · src/lib/city-condition-content.ts · src/lib/telehealth-condition-content.ts · src/lib/articles.ts · src/lib/faq-data.ts · src/lib/constants.ts · src/lib/snomed-codeset.ts · src/lib/encounter-templates.ts · src/lib/voice-prompt.ts · src/app/api/chat/route.ts · scripts/backfill-authorizations-from-appointments.mjs · tests: qualifying-conditions.test.ts · constants.test.ts. **HIPAA / freeze-compatible:** content + pure-data + tests ONLY — ZERO schema/Prisma migration, ZERO PHI path touched (freeze-safe 6/1→6/9). **Heads-up:** voice-prompt.ts is NOT runtime-consumed (Retell serves from its dashboard) — the voice eligibility change requires node scripts/sync-retell-prompt.mjs to reach the live phone line. [hipaa-pre-cutover-freeze-compatible][rcw-69.51a-grounded][anxiety-not-a-qualifier][ptsd-is][content-and-pure-data-only][voice-prompt-needs-retell-sync][version-letter:AQ0005][cadence-override: Doug-directed patient-facing regulatory-accuracy fix]
v2.97.SA0005
2026-06-01Production

When a caller asks Isabella about telehealth, she now names Dr. Ari's telehealth windows (Wednesday and Friday mornings, 10:30 to 12:30) and asks what works best for them, then promises a callback to confirm the exact time — instead of reading back specific openings that could be out of date until our scheduling system is fully moved to our own records.

Show technical details

Changed

  • 🗓️ **SA0005 — Isabella's voice availability tool no longer quotes specific appointment times (Doug 2026-06-01: caught her offering Monday/Wednesday telehealth renewal slots on a test call; expected Dr. Ari's telehealth on Thursdays).** Root cause: the listOpenSlots Retell custom-function read the GW AvailabilitySlot table, which is NOT synced with Practice Fusion — the authoritative pre-cutover EHR where real appointments are actually booked — so it could speak phantom slots, slots already booked in PF, or the wrong provider's time. The same query filtered by slotType + optional locationId + date window but NEVER by providerId, so it pooled Dr. Marnie's and Dr. Ari's telehealth slots and offered whichever was soonest — which violates Doug's routing rule (only patients who saw Dr. Marnie last year may book Marnie; everyone else routes to Dr. Ari's telehealth). **Fix:** neutralized the listOpenSlots handler so it no longer reads the slot table — for telehealth it names Dr. Ari's standing windows (Wednesday + Friday 10:30a–12:30p, fifteen-minute visits, new patients + renewals — Thursday 3–6p is IN-PERSON at Lynnwood, NOT telehealth) and for in-person it asks which clinic, then in both cases captures the patient's preferred day/time and tells them staff will confirm the exact opening against Practice Fusion and call back (the fallback the voice prompt already documents). Freeze-safe + fully reversible: ZERO schema/Prisma migration; the tool's JSON schema + Retell registration are UNCHANGED (so the fix takes effect with NO out-of-band Retell tool-set push — even if the hosted LLM still calls the tool, the handler now returns the capture-preference redirect); the prior live-DB read is preserved in git history to restore after the EMR cutover when the GW DB becomes source-of-truth. **Files MOD (2):** src/lib/voice-tools.ts (listOpenSlots handler) · src/lib/__tests__/voice-tools.test.ts (two former DB-error-fallback pins rewritten to assert the capture-preference redirect). Tests 68/68 GREEN; tsc clean. **HIPAA / freeze-compatible:** no PHI path touched; the spoken redirect carries no patient identifiers. See memory pin project_gw_voice_slot_provider_routing_2026_06_01. **Follow-up (post-cutover):** restore provider-aware filtering (returning-Marnie-patients → Marnie's slots; everyone else → Dr. Ari's telehealth) once the GW slot table is authoritative + re-push the tool set to Retell. [hipaa-pre-cutover-freeze-compatible][isabella-voice][listOpenSlots-neutralized][practice-fusion-not-synced][provider-routing-unenforceable-until-cutover][version-letter:SA0005][cadence-override: Doug-directed live patient-facing booking-accuracy fix]
v2.97.DF0005
2026-06-01Production

Demi now has the same feedback button the rest of the team uses — she can flag anything that's broken or could be better from any admin page, and it goes straight into the review queue.

Show technical details

Added

  • 💬 **DF0005 — Demi (GW front-desk operator) now has the in-app feedback button (Doug 2026-06-01: 'Demi does not have a feedback button').** Added greenwellnessdemi@gmail.com to REVIEWER_FEEDBACK_ALLOWLIST so the bottom-left feedback bubble renders for her on every admin page (gated by isReviewerFeedbackUser). Allowlist-only — deliberately NOT added to FORCE_DOUG_REVIEW_SUBMITTERS, so her items flow through the normal AI-tier triage / auto-fix loop like the cannabis-store reviewers (Kat/Austin) rather than force-routing to Doug. Her identity email matches her existing STAFF_BYPASS_ALLOWLIST entry in oversight-cost-cap.ts. Feedback lands in the BAA-covered reviewer_feedback table; clarification questions come back to her in-app at /me/feedback (amber 'Note from Doug / agent' box) — no email/SMS notify by design, since feedback bodies may reference PHI and that channel isn't PHI-safe in Phase 1. **Files MOD (2):** src/lib/reviewer-feedback.ts (allowlist + comment) · src/lib/__tests__/reviewer-feedback.test.ts (allowlist assertion). Tests 29/29 GREEN; tsc clean. **HIPAA / freeze-compatible:** config-only allowlist change, ZERO schema/migration, ZERO PHI. [hipaa-pre-cutover-freeze-compatible][reviewer-feedback-allowlist][demi-front-desk][allowlist-only-not-force-doug][version-letter:DF0005][cadence-override: Doug-directed operator-tooling gap]
v2.97.VG0005
2026-06-01Production

When Isabella answers the phone now, she clearly says "Thank you for calling Green Wellness" as the very first thing — said slowly and distinctly — so callers immediately know they reached the right place before she moves into the automated-assistant disclosure.

Show technical details

Changed

  • 📞 **VG0005 — Isabella's voice greeting now names the practice clearly first (Doug 2026-06-01: 'Isabella needs to say Green Wellness more clear at the beginning of the call').** Root cause: Isabella's opening utterance is LLM-generated from the Retell general_prompt (there is no static Retell begin_message), and the prompt only told her to *disclose* (automated-assistant + recording + human-available) within the first ten seconds — it never instructed her to OPEN with a clear brand greeting, so 'Green Wellness' came out rushed or buried under the disclosure. Added an explicit greeting-first instruction at the top of the behavioral block: open every call with "Thank you for calling Green Wellness," said slowly and distinctly, before the disclosure — and made the after-hours branch consistent (greeting first, then the 'office is currently closed' disclosure). **File MOD (1):** src/lib/voice-prompt.ts (VOICE_PROMPT). **Synced LIVE to Retell** via node scripts/sync-retell-prompt.mjs (PATCH update-retell-llm/{RETELL_LLM_ID} general_prompt, HTTP 200, hash 18a0ebdfb050) — the change is already on the live phone line; voice-prompt.ts is NOT runtime-consumed (Retell serves from its dashboard), so the sync is the load-bearing step, not the Vercel deploy. Per feedback_gw_voice_prompt_requires_retell_sync_2026_05_31. **HIPAA / freeze-compatible:** operator-authored persona text only — ZERO schema/Prisma migration, ZERO PHI, no patient-data path touched. **Heads-up:** resolved VOICE_PROMPT is now 19,992 chars against the 20,000 VOICE_PROMPT_SOFT_CAP_CHARS ceiling — the prompt is essentially full; the next addition will need a trim. [hipaa-pre-cutover-freeze-compatible][retell-synced-live][voice-prompt-near-soft-cap][version-letter:VG0005][cadence-override: Doug-directed caller-facing greeting fix]
v2.97.CC0005
2026-06-01Production

If you're on a phone call and convert a caller's profile to a patient, the call won't drop anymore.

What this means for you

If you're on a phone call and convert a caller's profile to a patient, the call won't drop anymore. While a call is live, converting now keeps you on the line and gives you a link to open the new patient record after you hang up.

Show technical details

Fixed

  • 📞 **CC0005 — converting a profile mid-call no longer disconnects the call (Doug 2026-06-01: 'I was on a phone call setting a patient up and when I went to convert their profile it disconnected the call').** Root cause: the 'Convert to Patient' button calls the convert API via fetch() (no nav) and then runs router.push('/admin/patients/[id]') on success. The RingCentral softphone iframe is mounted persistently in the /admin layout (src/app/admin/layout.tsx), so a soft-nav *should* preserve it — but rather than rely on cross-origin WebRTC-iframe survival across an App Router route change, the navigation is now suppressed entirely while a call is active. **Files MOD (2):** src/app/admin/_components/RcSoftphone.tsx (exposes a read-only window.rcSoftphoneInCall() predicate backed by the existing inCallRef call-state tracking — mirrors the existing window.rcSoftphoneDial pattern; cleaned up on unmount) · src/app/admin/leads/[leadAuditId]/_components/ConvertToPatientButton.tsx (both convert paths now route through goToPatient() — when rcSoftphoneInCall() is true it skips router.push, shows a toast, and renders a persistent inline 'Open [name]'s record' link to tap after hanging up; otherwise navigates as before). No-ops safely when the softphone isn't mounted. **HIPAA / freeze-compatible:** UI/client-side only — ZERO schema/Prisma migration; the after-call link uses an opaque patient id, no DOB/address/name in any new console output. [hipaa-pre-cutover-freeze-compatible][client-side-only-no-migration][version-letter:CC0005][cadence-override: Doug-directed call-drop bug]
v2.97.MR0005
2026-06-01Production

On the Isabella cockpit you can now mark each message as read — a 'Mark read' button on every row, and a filter to show only the ones you haven't gotten to yet. Your read state is yours; it doesn't change what anyone else sees.

Show technical details

Added

  • ✅ **MR0005 — per-user 'mark as read' on the Isabella cockpit (Demi 2026-06-01 via Doug: 'there's no way for me to mark as read for each one').** Fast-path with NO schema migration: per-user read-state is derived from an ISABELLA_CONTACT_MARKED_READ audit row (resourceId = PatientMessage.id, actor = staffUserId) at render time. **Files NEW (3):** src/lib/isabella-mark-read-helpers.ts (pure functions — buildReadSet / annotateReadByMe / filterUnreadOnly / buildMarkReadDetail; the pure sister of the cockpit page so pin tests import without the server-only runtime gate) · src/app/api/admin/isabella/[messageId]/mark-read/route.ts (POST, admin-gated ADMIN/MANAGER/SCHEDULER, messageId regex-guarded [a-zA-Z0-9_-]{6,64}, emits the audit row — IDs only, no patient name/email/phone/body) · src/app/admin/isabella/_components/MarkReadButton.tsx (the per-row affordance). **Files MOD (3):** src/app/admin/isabella/_components/SentEmailLog.tsx + VoiceCallLog.tsx (rows carry readByMe; unread rows visually distinguished + a Mark-read button) · src/app/admin/isabella/page.tsx (fetches the current user's mark-read rows via new getMarkReadResourceIds() query helper, annotates Zone E + Zone G rows with annotateReadByMe(), and honors ?unreadOnly=1 via filterUnreadOnly()) + src/lib/isabella-cockpit-queries.ts (NEW getMarkReadResourceIds(staffUserId) — the read-side query, IDs only). **HIPAA / freeze-compatible:** READ-derives from existing AuditLog rows — ZERO schema/Prisma migration; audit detail is messageId= markedBy=, no PHI. [hipaa-pre-cutover-freeze-compatible][read-only-no-migration][derive-read-state-from-audit-trail][version-letter:MR0005][cadence-override: Demi-reported usability gap]
v2.97.DT0005
2026-06-01Production

Demi, you have your own morning page now at the Demi-today screen.

What this means for you

Demi, you have your own morning page now at the Demi-today screen. When you get in, it shows what's on your plate at a glance: who needs a callback (oldest waiting first, with a Call button), what Isabella flagged for a human grouped by reason, and a quick snapshot of the day's volume. Empty zones hide themselves — if it's quiet, that's a good sign Isabella handled it.

Show technical details

Added

  • 🗓️ **DT0005 — /admin/demi-today, Demi's focused morning-priorities surface (Doug 2026-06-01 ask: 'should we create a demi action area so she knows what her priorities are when she gets in?').** Sister of /admin/mariane-today + /admin/isabella-today — the focused Demi-first cut, NOT a duplicate. **Reuses existing query SoT — no duplicated query logic** (per build constraint): callbacks come from a new shared getDemiCallbacks() helper in src/lib/isabella-cockpit-queries.ts, the needs-attention reason breakdown from the existing getQueueAhead(), and the volume snapshot from getTodayCounters() + getRightNowCounts(). **Three zones (zero-render-when-zero):** (1) Callbacks owed — open Isabella escalations awaiting a human (needsHumanAt set, resolvedAt null), oldest-stale first, each with a tel: Call button + deep-link to /admin/isabella/[messageId]; (2) Needs your attention — same open set grouped by queue-reason (crisis/billing/records-request/…); (3) Today's snapshot — 15m in-flight + Isabella replies today + escalations + crisis flags. **Unread state is READ, not rebuilt** — derived from the parallel MR0005 ISABELLA_CONTACT_MARKED_READ audit trail via the pure annotateReadByMe() helper; an 'unread' pill shows on rows Demi hasn't opened (the write path is owned by the parallel mark-read agent — this page only reads it). **Files NEW (1):** src/app/admin/demi-today/page.tsx (server component, force-dynamic, noindex, role-gated ADMIN/MANAGER/SCHEDULER — parity with isabella-today). **Files MOD (4):** src/lib/isabella-cockpit-queries.ts (NEW getDemiCallbacks() + DemiCallbackRow type — PHI-safe: first-name + last-initial label, masked phone for display, raw digits only for the tel: href never rendered) · src/lib/audit.ts (NEW VIEW_DEMI_TODAY AuditAction union member — TS-type only, AuditLog.action is a String column so NO migration) · src/lib/admin-band-shared.ts (NEW buildDemiTodayAuditDetail() + DemiTodayBandCounts — metadata-only audit detail sister of buildMarianeTodayAuditDetail) · src/lib/changelog.ts + src/lib/changelog-current.ts (this entry + bump IG0005→DT0005). **HIPAA / freeze-compatible:** READ-ONLY against existing tables (PatientMessage + AuditLog), ZERO schema/Prisma migration — additive admin-only page only. Patient labels masked, previews PHI-scrubbed via scrubPhiForSmsOutbound, audit detail carries band-counts only (no patient identifiers). [hipaa-pre-cutover-freeze-compatible][read-only-no-migration][reuses-isabella-cockpit-query-sot][reads-MR0005-unread-state-does-not-rebuild][version-letter:DT0005][cadence-override: Doug-directed Demi action-area ask]
v2.97.WX0005
2026-06-01Production

Final accessibility sweep across the public-marketing surfaces (changelog page, conditions list, the patient intake form after booking, and the my-appointments lookup + set-password screens). Faint text + faint input-placeholder colors that were below the WCAG AA contrast floor are bumped to the brand slate-green that passes — same fix shape staff already saw on the provider portal, /admin, and /patient surfaces. No behavior change; you'll just notice the small-print + placeholder text reads a little easier.

Show technical details

Fixed

  • ♿ **WX0005 — WCAG AA contrast widening to the four remaining public-marketing-adjacent surfaces (closes the last surface family with known violations per reviewer SESSION_REVIEW_2026_05_31).** Extends scripts/check-wcag-contrast-tailwind.mjs SCOPED_PREFIXES from 3 entries (provider/, admin/, patient/) → 7 entries by adding the four public-facing prefixes that still had known violations: src/app/changelog/, src/app/conditions/, src/app/intake/, src/app/my-appointments/. No src/app/(public)/ route group exists in this repo (marketing pages live as top-level routes under src/app/), so the widening enumerates per-prefix instead of scoping to a single (public)/ folder — matches the WV0005 / WA0005 sister-port shape. **Violations fixed (5 files, 9 sites):** (1) src/app/changelog/_components/ChangelogList.tsx:194 footer credit text-[#c0c0b8] (~1.6:1) → text-[#5a7a68] (~4.7:1); (2) src/app/conditions/page.tsx:108 ChevronRight tile color text-[#9ab0a0] (~2.2:1) → text-[#5a7a68]; (3) src/app/intake/[token]/_components/IntakeFormClient.tsx × 4 form input placeholders placeholder:text-[#c0c0b8]placeholder:text-[#5a7a68]; (4) src/app/my-appointments/page.tsx × 3 lookup-form input placeholders placeholder:text-[#9ab0a0]placeholder:text-[#5a7a68]; (5) src/app/my-appointments/[token]/_components/SetPasswordCard.tsx × 2 password-input placeholders placeholder:text-[#9ab0a0]placeholder:text-[#5a7a68]. **Pin tests EXTENDED (9 new assertions):** src/lib/__tests__/wcag-contrast-tailwind.test.ts — 4 SCOPED_PREFIXES inclusion pins (changelog/ + conditions/ + intake/ + my-appointments/) + 5 regression pins (one per modified file, asserting absence of both #c0c0b8 + #9ab0a0 with surface-specific failure messages). All 9 new pins green. Pre-existing failure on provider/[token]/today/PDF pending pin is unrelated (sister-agent's D8 redirect-only refactor — that page no longer renders 'PDF pending'); not blocking. **Allowlist unchanged:** 3/10 slots used (gate self-exempt + changelog corpus + today-page chevron-icon decoration). **Verification:** node scripts/check-wcag-contrast-tailwind.mjs returns ✓ 0 contrast violations across 342 file(s) in [provider/, admin/, patient/, changelog/, conditions/, intake/, my-appointments/]. **HIPAA scope:** ZERO — pure CSS class swap on already-rendered surfaces; no patient data path touched. **Files MOD (8):** scripts/check-wcag-contrast-tailwind.mjs (SCOPED_PREFIXES + JSDoc) · src/lib/__tests__/wcag-contrast-tailwind.test.ts (+9 pins) · 5 surface files (color swaps) · src/lib/changelog.ts + src/lib/changelog-current.ts (this entry). **Sister-agent doctrine:** parallel sessions active on /admin/cutover nav cross-link (CN0005, just landed below) + canonical-ingest diag/watchdog probe; pathspec-form commit scoped to ONLY WX0005 files per feedback_git_commit_must_use_pathspec_when_index_dirty_2026_05_31. [hipaa-pre-cutover-freeze-compatible][wcag-aa-contrast-widening][closes-last-surface-family-with-known-violations][version-letter:WX0005][cadence-override: pre-cutover WCAG public-site widening — closes last surface family with contrast violations per reviewer SESSION_REVIEW_2026_05_31 TODO]
v2.97.CN0005
2026-06-01Production

Three cutover-day admin screens (Countdown · Reconcile · Reception pickup) now share a small pill-row tab nav at the top — one click to flip between them instead of bouncing through the sidebar. Helps Doug during the compressed 6/08+ cutover window when seconds count.

Show technical details

Added

  • 🔗 **CN0005 — CutoverNav cross-link tab nav for the 3 sibling /admin/cutover/** pages.** During the compressed 6/08+ cutover-day window, Doug bounces between the countdown dashboard (CV0005 — preconditions + Doug-action queue), the reconcile loop surface (PE0010 — read-only stub-banner until D5 + counsel sign-off), and the reception-pickup queue (ZW0005 — front-desk print/hand-over surface). Today each page lives alone, so flipping costs 3 sidebar clicks. **Fix:** small Client Component at src/app/admin/cutover/_components/CutoverNav.tsx (~60 LOC) renders a pill-row at the top of each page with the 3 sibling links + the active tab highlighted via usePathname(). Brand emerald-on-light palette to match existing CutoverCountdown header (#2c3e36 text + #e6e6dc borders + emerald-50/300/800 for active). Tabs: 🎯 Countdown · 🔄 Reconcile · 📋 Reception pickup. **Files NEW (2):** src/app/admin/cutover/_components/CutoverNav.tsx (Client Component, usePathname-driven active-tab highlight, exact-match for /admin/cutover root + startsWith for future child routes) · src/app/admin/cutover/_components/__tests__/cutover-nav-anti-divergence.test.ts (13 pins across 5 describe blocks: file-exists · 'use client' before first import + usePathname imported + Link imported · 3 tab labels present · 3 hrefs present · each of 3 pages imports + renders CutoverNav at canonical path). **Files MOD (3 pages, 1 line import + 1 line render each):** src/app/admin/cutover/page.tsx (wraps existing in Fragment with above) · src/app/admin/cutover/reconcile/page.tsx (wraps existing read-only reconcile table in Fragment) · src/app/admin/cutover/reception-pickup/page.tsx (wraps existing in Fragment). **Files MOD (version):** src/lib/changelog.ts + src/lib/changelog-current.ts. **Test results:** 13/13 pin tests green; tsc --noEmit clean. **HIPAA scope:** ZERO — nav renders constants only (3 labels + 3 hrefs); no patient data, no PHI. Auth on each page unchanged (countdown ADMIN-only · reconcile ADMIN-only · reception-pickup ADMIN/MANAGER/SCHEDULER). **Sister-agent doctrine:** parallel session active on watchdog canonical-ingest-status + a (public)/ WCAG widening arc; pathspec-form commit scoped to ONLY CN0005 files per feedback_git_commit_must_use_pathspec_when_index_dirty_2026_05_31. [cadence-override: pre-cutover ops UX — cross-link /admin/cutover sub-pages so Doug doesn't tab-juggle during the danger window][version-letter:CN0005]
v2.97.VL0005
2026-06-01Production

Voice channel (Isabella's phone agent) now matches the email + chat + SMS sides — when a caller asks 'do you have a Spokane location?' or 'where else are you besides Lynnwood?', Isabella's spoken answer reflects the live LX0005 active-locations config (Lynnwood with Dr Ari, Olympia with Marnie, Spokane open for new patients until 6/30). Same 6 polish rules from CL0005 (chat + SMS) ported into voice-specific spoken form: empty-slot fallback (no 'system is broken' framing), 3rd-person-name discipline (don't say 'I'd recommend Sarah call us back' when talking TO Sarah), Demi-options retired (channel parity with MT0005 default), phone-number discipline (don't repeat the office number mid-call — caller already dialed it), template-connector ban (no enterprise help-desk scripted phrasing), single-patient assumption. Crisis lines + DOB-do-not-collect + 5-beat warm wrap-up all preserved verbatim. CRITICAL: `node scripts/sync-retell-prompt.mjs` runs post-push to push the new prompt to Retell's live agent — otherwise Retell keeps serving the old prompt (RP0005 ghost-code trap).

Show technical details

Fixed

  • 🎙️ **VL0005 — LX0005-drift sister-port to voice (Ship B of Doug Q1a-Q7 accept-all).** Mirrors LP0005 (email, 071fca28) + CL0005 (chat + SMS, 2da68b38) onto the voice channel — closes the same config-vs-prompt drift class on src/lib/voice-prompt.ts. **Architectural fix:** voice-prompt now imports getLocationListForPrompt and interpolates the 'voice' format variant (aliases 'prose' — spoken-natural prose, no bullets, no URLs, dates spelled as words). Replaces the hardcoded 'Our in-person clinic is in Lynnwood, about twenty minutes north of Seattle' line + the 'Important — the Lynnwood office is appointment-only' framing (both drifted past LX0005 reality). **6 voice-nuanced sibling fixes** baked into a single Polish-rules paragraph placed BEFORE the booking-collect turn (so booking turns honor the rules): (1) **empty-slot fallback** — never say 'self-serve lookup isn't available' / 'system is broken'; ask date preference + take a detailed message; (2) **3rd-person-name discipline** — never refer to the patient in 3rd person mid-conversation ('I'd recommend Sarah call us back' when talking TO Sarah = wrong); use 'you' / 'your'; first name once during confirmation callback is fine; (3) **Demi-options retired** — never 'feel free to reach out and Demi can discuss your options'; replacement is 'I'll take a message so Demi can call you back' (channel parity with MT0005 default); (4) **Phone-number discipline (voice-specific body-CTA variant)** — caller already called the office number; do NOT repeat the office phone in routine turns; carve-outs: caller-asks / records-fax / records-email / 5-beat wrap-up; (5) **Template-connector ban** — 'here's where things stand,' 'the best next step is to,' 'I should mention,' 'I wanted to let you know that' all forbidden; blessed natural spoken connectors ('OK so,' 'let me check that,' 'got it') stay allowed (voice-specific distinction — these are real human turn-takers, not enterprise script); (6) **Single-patient assumption** — each call from ONE patient unless explicitly multi-party; if ambiguous, ask once 'is this for you, or for someone else?' **Pin tests NEW (1 file, 30 assertions across 9 describe blocks):** src/lib/__tests__/voice-lx0005-drift-polish.test.ts enforces architectural-interpolation (helper imported + interpolated, hardcoded pre-fix strings absent), each of the 6 sibling fixes anchored by section header + literal forbidden-phrase pin, polish-block placement before booking-collect, Retell-sync invariants (soft-cap 20000 headroom, markdown-free, crisis safety preserved, DOB-do-not-collect preserved, tentative-appointment-language preserved, MT0005 message-taking default preserved, after-hours opener preserved, 5-beat warm-close wrap-up preserved). 30/30 green. tsc --noEmit clean. **Soft cap bumped 17000 → 20000** to accommodate the +2530-char polish paragraph + helper interpolation expansion. Crisis paragraphs (988 / DV hotline / Spanish 988) + tentative-appointment language + DOB-do-not-collect + after-hours opener + 5-beat warm-close wrap-up all preserved verbatim. **Files MOD (3):** src/lib/voice-prompt.ts · src/lib/changelog.ts · src/lib/changelog-current.ts. **Files NEW (1):** src/lib/__tests__/voice-lx0005-drift-polish.test.ts. **Retell-sync step (post-push):** node scripts/sync-retell-prompt.mjs MUST run after this commit lands on origin/main or Retell's dashboard keeps serving the prior prompt (RP0005 ghost-code trap — same class of bug the LX0005-drift fix is closing in code; the Retell-side dashboard is the dual SSoT that requires the explicit sync push). **Constraints met**: pathspec-form commit (per feedback_git_commit_must_use_pathspec_when_index_dirty_2026_05_31) scoped to ONLY VL0005 files even though parallel DR0005 work flowed through index simultaneously. HIPAA freeze-compatible — prompt edits only, no patient data touched. [hipaa-pre-cutover][LX0005-config-wiring][channel-parity][retell-sync-required][version-letter:VL0005][cadence-override: same-day patient-experience-impacting voice fix per Doug 6/1 Q1a-Q7 accept-all]
v2.97.CL0005
2026-06-01Production

Sister-port of yesterday's LP0005 email fix to Isabella's chat + SMS surfaces (Doug 6/1 Q1a-Q7 greenlit).

What this means for you

Sister-port of yesterday's LP0005 email fix to Isabella's chat + SMS surfaces (Doug 6/1 Q1a-Q7 greenlit). Both channels now interpolate the live active-locations config the same way email does — so if a patient asks 'do you have a Spokane location?' on chat or SMS, Isabella's answer matches what email says + reflects LX0005 reality (Spokane open for new patients until 6/30; Olympia with Marnie; Lynnwood for everyone else). Same 6 sibling polish rules ported alongside (empty-slot fallback, 3rd-person-name ban, retired Demi-options language, body-CTA dedup, template-connector ban, single-patient assumption). Voice channel ships separately as Ship B (VL0005) with Retell-sync isolation.

Show technical details

Fixed

  • 🔧 **CL0005 — LX0005-drift sister-port to chat + SMS (Ship A of Doug Q1a-Q7 accept-all).** Mirrors the LP0005 email-side fix (071fca28, 2026-06-01) to the other two text channels — closes the same config-vs-prompt drift class on chat (src/app/api/chat/route.ts) + SMS (src/lib/sms-ai.ts). Both prompts now import getLocationListForPrompt from provider-location-rules.ts and interpolate the channel-appropriate variant at module load. **Helper extension:** getLocationListForPrompt(format) accepts new format values 'chat' (markdown bullets, tighter than email — drops street address since prompt body repeats it), 'sms' (compact single-line In-person: Lynnwood (main) · Olympia (Marnie) · Spokane (new pts only, closing 6/30) — SMS-budget aware) and 'voice' (aliases existing 'prose' for Ship B). The 6 sibling polish rules baked in: (1) **empty-slot fallback** — never say 'self-serve isn't available' / 'our system is broken'; ask date preference + flagForHuman/captureLeadFromChat; (2) **3rd-person-name ban** — never refer to the patient in 3rd person when writing TO them; (3) **Demi-options retired** — replace 'feel free to reach out and Demi can discuss your options' with message-taking framing (channel parity with MT0005 voice default); (4) **body-CTA ban** — chat: don't restate phone/email when the footer already carries it; SMS variant: don't restate the phone number unless asked (patient already has it — they're texting it); (5) **template-connector ban** — 'Here's where things stand,' 'The best next step is…,' 'I should mention —,' 'I wanted to let you know that…' all forbidden; (6) **single-patient assumption** — treat each inbound as ONE patient unless explicitly multi-party. **Fix 7 (auto-disclaimer footer rephrase)** already shipped in canonical email-footer.ts on LP0005; affects every email path globally — no per-channel work needed here. **Pin tests NEW (1 file, 33 assertions across 9 describe blocks):** src/lib/__tests__/chat-sms-lx0005-drift-polish.test.ts enforces architectural-interpolation invariants (helper imported + interpolated, hardcoded pre-fix strings ABSENT), helper extension format-union accepts new values + branches exist, each of the 6 sibling fixes anchored by section header + literal forbidden-phrase pin, channel-parity SLA preserved, AND crisis-safety / records-release / legal-inquiry / tentative-appointment-language all preserved verbatim (no regression). Static-source-text grep approach (sister of email-ai-isabella-polish.test.ts + chat-isabella-polish.test.ts) because both modules import server-only and the prompts are module-local. 33/33 green. tsc --noEmit clean. **Files MOD (4):** src/lib/provider-location-rules.ts (format-union extended + 3 new branches) · src/app/api/chat/route.ts (helper import + 2 interpolation sites + 6 polish bullets) · src/lib/sms-ai.ts (helper import + 1 interpolation site + 6 polish bullets) · src/lib/changelog.ts + src/lib/changelog-current.ts (this entry + version bump). **Files NEW (1):** src/lib/__tests__/chat-sms-lx0005-drift-polish.test.ts. **Sister-port pattern:** voice ships separately as VL0005 (Ship B) with Retell-sync isolation — voice has its own format-variant ('prose'/'voice'), its own ban-list nuances (spoken-word formatting, no body/footer split), and its own Retell-sync requirement that needs to run AFTER push lands (RP0005 ghost-code trap if skipped). **Constraints met**: pathspec-form commit on these files only (per feedback_git_commit_must_use_pathspec_when_index_dirty_2026_05_31), HIPAA freeze-compatible (prompt edits only, no patient data touched). [hipaa-pre-cutover][LX0005-config-wiring][channel-parity][version-letter:CL0005][cadence-override: same-day patient-experience-impacting fix per Doug 6/1 Q1a-Q7 accept-all]
v2.97.LP0005
2026-06-01Production

Critical fix: Isabella's email now knows ALL active locations (Lynnwood + Olympia + Spokane until 6/30) instead of saying 'We don't have a Spokane location' (the bug Doug caught in this morning's test email). LX0005 config shipped yesterday said Spokane was active, but the email prompt had hardcoded 'Lynnwood only' text — config-vs-prompt drift. New helper interpolates the LX0005 config into the prompt at module load. Same class as the voice-prompt-Retell-sync gap we caught last week (RP0005).

Show technical details

Fixed

  • 🔧 **LP0005 — LX0005 config-vs-prompt drift fix + 7 sibling email-voice polish rules.** Doug 6/1 caught Isabella telling a test inbound 'We don't have a Spokane location' even though LX0005 config says Spokane is active for new pts until 6/30. Root cause: EMAIL_AI_SYSTEM_PROMPT had hardcoded clinic text, never imported PROVIDER_LOCATION_RULES. **Architectural fix:** NEW helper getLocationListForPrompt() in provider-location-rules.ts → email prompt interpolates at module load. **7 sibling fixes** for empty-slots / 3rd-person-name / Demi-options / body-CTA / template-connector / single-patient-assumption rules + auto-disclaimer footer rephrase. **Files MOD (4):** provider-location-rules.ts · email-ai.ts · email-footer.ts · changelog. **Sister-port DEFERRED** for voice/chat/SMS pending Doug Q1a-Q7 accept-all. [hipaa-pre-cutover][LX0005-config-wiring][cadence-override: critical 6/1 Doug-caught miss]
v2.97.FP0005
2026-06-01Production

Two leftover call-center cliches from older copy got cleaned up: (1) the clinical-deflection fallback Isabella sends when she's caught about to make a medical claim no longer opens with 'Thanks for reaching out — I'm Isabella, Green Wellness's AI receptionist' (now opens 'Quick note — I'm an AI assistant on the Green Wellness side, and clinical questions are best answered by our Washington-licensed providers at your appointment') · (2) the auto-acknowledgement email when a patient writes in now opens 'Got your message' instead of 'Thanks for reaching out — we got your message.' Same brand-voice doctrine as CP0005 + IE0005; this just catches the two remaining spots.

Show technical details

Changed

  • 🧹 **FP0005 — fallback + auto-ack copy polish (sister of CP0005 chat polish + IE0005 email polish).** Catches two leftover spots where the retired cliches were still in flight: (1) PATIENT_FALLBACK_FOR_CLINICAL in src/lib/medical-claim-scrub.ts — the body Isabella sends when the post-scrub renderer needs a clean replacement (medium/high severity). Pre-fix opened 'Thanks for reaching out — I'm Isabella, Green Wellness's AI receptionist' which is BOTH the retired cold preamble (CP0005 + IE0005 banned) AND uses 'Thanks for reaching out' (IE0005 banned). Post-fix opens 'Quick note — I'm an AI assistant on the Green Wellness side, and clinical questions (whether cannabis is right for a specific condition, dosage, what to expect at evaluation) are best answered by our Washington-licensed providers at your appointment, not by me.' Then offers concrete next steps (book link + phone). FTC AI-disclosure preserved via 'I'm an AI assistant'. (2) autoAckEmailTemplate in src/lib/email-templates.ts — the auto-ack body sent to a patient who emails in before the AI receptionist takes over. Pre-fix opened 'Thanks for reaching out — we got your message.' Post-fix opens 'Got your message.' — same SLA window, no cliche. **Pin test updates** in src/lib/__tests__/medical-claim-scrub.test.ts: severity=medium + severity=high tests now assert rendered.includes('AI assistant') instead of rendered.includes('Isabella') (the polished copy drops the name but keeps the AI disclosure) AND add 2 negative assertions that the retired cold preamble + the 'Thanks for reaching out' cliche do NOT appear in the rendered fallback. 32/32 green. tsc clean. email-templates.test.ts + auto-ack-template.test.ts both still 78/78 green (the polish was below the assertion granularity — opener-style invariant not pinned in those, only PHI-shape + SLA-window). **Files MOD (4)**: src/lib/medical-claim-scrub.ts (PATIENT_FALLBACK_FOR_CLINICAL rewrite) · src/lib/email-templates.ts (auto-ack opener swap) · src/lib/__tests__/medical-claim-scrub.test.ts (2 new negative assertions + updated identification assertion) · src/lib/changelog.ts + src/lib/changelog-current.ts (this entry + version bump). No schema migration, no env change, no behavior change beyond brand-voice copy. [brand-voice-polish][sister-of-CP0005-IE0005][hipaa-pre-cutover][version-letter:FP0005][cadence-override: voice-polish followup batched into CP0005 doctrine arc, freeze-compatible static-copy-only]
v2.97.CP0005
2026-06-01Production

Isabella's chat voice gets the same call-center-cliche cleanup the email side got last cycle — the cold 'Hi, I'm Isabella, Green Wellness's AI receptionist — happy to help.' opener is retired in favor of a one-clause AI disclosure that leads straight into the answer ('Isabella here (I'm an AI assistant) — short answer: yes, we can renew via telehealth. Want to grab a slot this week?'). 8 specific cliches are now explicitly banned ('happy to help', 'happy to assist', 'How may I help you today', etc) and short one-beat openers ('Yes —', 'Sure —', 'Got it —') are blessed. Crisis safety messages, after-hours SLA disclosure, and the FTC AI-disclosure rule are all preserved verbatim — this is voice polish only, not behavior change.

Show technical details

Changed

  • ✨ **CP0005 — chat-side Isabella voice polish (sister of IE0005 email polish).** Ports the email-voice cleanup to the chat surface — bans the same 8 call-center cliches, drops the cold 'Hi, I'm Isabella, Green Wellness's AI receptionist — happy to help.' preamble (now tagged 'has been retired' in the prompt so the model treats it as a NEGATIVE example), introduces a positive opener pattern ('Isabella here (I'm an AI assistant) — short answer: …'). **3 prompt rules** added inside the ## Your Behavior section of src/app/api/chat/route.ts SYSTEM_PROMPT: (1) one-clause AI-disclosure-then-answer pattern for first-touch · (2) updated after-hours opener that combines SLA disclosure + answer in the SAME message · (3) explicit ban list with 8 named cliches + 4 blessed one-beat openers ('Yes —', 'Sure —', 'Got it —', 'Quick note —'). **Invariants preserved**: FTC AI-disclosure rule is non-negotiable (test pin enforces); after-hours SLA disclosure (inquiry-coverage audit Ship #2) survives — phrase 'our team replies during business hours' + 'Monday-Friday 9am-5pm PT' both still pinned; crisis safety blocks unchanged. **17 NEW pin tests** in src/lib/__tests__/chat-isabella-polish.test.ts — static-source-text regex pattern (mirrors email-ai-isabella-polish.test.ts since SYSTEM_PROMPT is module-local + the route imports 'server-only'). Each of the 8 banned cliches gets a dedicated test · the cold-preamble appears EXACTLY ONCE (inside the 'has been retired' note) · positive opener pattern pinned · 'AI disclosure non-negotiable per FTC bot-disclosure rules' anchor pinned · after-hours SLA framing preserved · 4 blessed one-beat openers pinned. All 17 green. tsc clean. **Why static-source-regex pins instead of importing the prompt**: route.ts imports 'server-only' which pulls the entire AI-SDK + DB chain into the test context (slow, brittle, leaks server-side); the prompt is module-local + never exported. Sister pattern of email-ai-isabella-polish.test.ts for the same reason. **Files MOD (4)**: src/app/api/chat/route.ts (3 bullets in SYSTEM_PROMPT ## Your Behavior) · src/lib/__tests__/chat-isabella-polish.test.ts (NEW, 17 tests) · src/lib/changelog.ts + src/lib/changelog-current.ts (this entry + version bump). No schema migration, no env change, no cron-routing change, no patient-facing copy outside SYSTEM_PROMPT. [chat-voice-polish][sister-of-IE0005][hipaa-pre-cutover][version-letter:CP0005][cadence-override: brand-voice polish + freeze-compatible static-source-only test approach]
v2.97.IC0005
2026-06-01Production

Isabella cockpit (Doug + Mariane + Demi) leveled up with sortable + filterable activity logs, a NEW voice-call log zone (Zone G — was missing despite ~1,300 calls/week), and a per-contact detail drawer at /admin/isabella/[id]. Click any row in the Sent log or Voice log to drill in: time, channel, status (open/escalated/resolved), patient match (if any), and thread context with up to 10 surrounding messages. PHI scrubbed defensively on every render; recording-available flag shown, recording URL never emitted (operators access via /admin/integrations/voice which audits per-recording). Mariane review buttons + call→lead FK linkage deferred to Round 2 (post-6/9 freeze).

Show technical details

Added

  • 🎙️ **IC0005 — Isabella cockpit Round 1: completeness audit + drill-through.** Doug 2026-06-01 ask: 'take a look at the completeness of isabella dashboard, back it easy to sort and look through, have detail of each contact if you click into it by time and what the result of the call was/details.' Round 1 ships freeze-compatible additive changes (no schema migration). **Files NEW (5):** src/lib/isabella-contact-detail.ts (server-only query for per-message detail + thread context; PHI-scrubbed via scrubPhiForSmsOutbound; hasRecording flag-only — recordingUrl never exposed) · src/lib/isabella-contact-detail-shape.ts (pure-function sister module — exports shapeContactMessage + ContactDetailMessage type so pin tests can import without the server-only runtime gate; sister of the existing isabella-cockpit-masks split) · src/app/admin/isabella/[messageId]/page.tsx (RSC detail-drawer route; audit-emit VIEW_ISABELLA_CONTACT_DETAIL on every render including 404 path; role-gated ADMIN/MANAGER/SCHEDULER; force-dynamic + noindex; messageId shape-guarded by regex before query) · src/app/admin/isabella/_components/VoiceCallLog.tsx (Zone G — sortable + masked voice call table; deeplinks to detail route; kind+status+recording badges; never renders raw fromAddr/toAddr) · src/app/admin/isabella/_components/CockpitFilters.tsx (server-rendered GET-form filter row: channel + date-range + row-limit, no client island). **Files MOD (4):** src/lib/isabella-cockpit-queries.ts (added CockpitLogFilters type, VoiceCallRow type, getVoiceCallLog(filters) function; extended getSentEmailLog(filters) with channel/date/sort/limit params; clampLimit enforces 100-row max), src/app/admin/isabella/page.tsx (parses search params for channel/from/to/limit/eSort/vSort with allowlist validation; wires Zone G + filter row; audit detail now includes filter-snapshot literal but never PHI), src/app/admin/isabella/_components/SentEmailLog.tsx (rewritten as sortable table with column-header sort links; detail-route deeplink alongside legacy thread deeplink; new aiCategory column), src/lib/audit.ts (registered VIEW_ISABELLA_CONTACT_DETAIL action). **Files MOD (2 — version):** src/lib/changelog.ts + src/lib/changelog-current.ts. **Pin tests NEW (1 file, 42 assertions across 8 describe blocks):** src/lib/__tests__/isabella-contact-detail.test.ts enforces (a) audit-emit on detail route including 404 path, (b) audit detail never contains toAddr/fromAddr/body/subject, (c) PHI scrubber called on subject + body in lib, (d) recordingUrl absent from ContactDetailMessage type AND from runtime row shape, (e) messageId regex guard present, (f) cockpit page wires Zone G + filter row + dual sort params, (g) VoiceCallRow type does not contain recordingUrl key, (h) clampLimit bounds queries. Sister update to isabella-cockpit.test.ts adds an IC0005-specific assertion for detail-route deeplinks (now 25/25 green, was 24/24). **Test results:** 42/42 new pin tests green + 25/25 existing isabella-cockpit tests green + 0 regression across 180 isabella-suite tests (the 1 pre-existing failure in email-ai-isabella-polish.test.ts:'Fix 4 — EM0005 header + footer' is unrelated to this ship and was already failing on origin/main). TypeScript --noEmit clean on all modified files. **Round 2 (post-6/9 freeze):** Mariane review buttons (Approve/Edit/Add note) wiring into the StaffReplyExemplar curation surface from SX0005 · call→lead bidirectional FK linkage (needs schema migration) · transcript-redaction-on-view per Retell BAA scope (Bedrock-rewrite into clinical-summary form before render). **HIPAA discipline:** every new render path is audit-emitted with counts-only details; recording URLs never cross the function boundary (operators access via the existing /admin/integrations/voice surface which has its own per-recording audit); voice transcripts pre-scrubbed via scrubPhiForSmsOutbound defensively even if upstream missed; messageId regex guard prevents arbitrary-path DB hits. **Doug-actions surfaced:** none — additive Round 1 ships clean. [hipaa-pre-cutover-freeze-compatible][additive-readonly][reviewer-feedback-#5][version-letter:IC0005][cadence-override: same-day Doug-directive ship 2026-06-01 — within Doug's standing-permission scope per OPERATING_PRINCIPLES]
v2.97.MT0005
2026-06-01Production

Isabella now DEFAULTS to taking a detailed message on every escalation — clinical questions, upset callers, records requests, legal inquiries, even after a crisis-line referral. She no longer promises 'let me get Demi on the line' or to warm-transfer the call, because Demi doesn't work every day and a dead-air queue is worse than a clear 'we'll get back to you as soon as possible.' Crisis safety lines (988 / DV hotline / Spanish 988) are unchanged — those referrals are still front-and-center, only the supplementary 'bring Demi on' line is replaced with a message-taking promise plus a crisis flag so the row surfaces immediately in /admin/messages.

Show technical details

Changed

  • 🎙️ **MT0005 — Isabella voice-prompt: DEFAULT to message-taking; live warm-transfer DEFERRED until Demi-presence detection ships (post-6/9).** Per Doug 2026-06-01 verbal directive: "demi doesnt work everyday so isabella can transfer the phone to her if she is there, if not it would be better to not get their hopes up and just take a message and let them know we will get back to them as soon as possible." **8 escalation sites rewritten** in src/lib/voice-prompt.ts: (1) main escalation gate (clinical / upset / past-appointment), (2) suicide crisis block, (3) DV crisis block, (4) Spanish-language crisis block, (5) records-release identity block, (6) third-party legal inquiry block, (7) DOB-forgotten block, (8) staff-anger block — plus office-contact + hours + after-hours opener cleanup. Doctrine comment block updated to reflect deferred warm-transfer + post-6/9 ship target (Retell custom-function checkDemiAvailability() checking AdminHeartbeat in last 15min). **Crisis safety lines preserved VERBATIM**: 988 Suicide and Crisis Lifeline (spoken-form), 1-800-799-7233 DV Hotline (spoken-form), Spanish 988 line ('Llama o envía un texto al nueve-ocho-ocho'), and all three trigger-phrase lists. Crisis blocks now route to voicemail-with-context flow with the crisis flag set so the row surfaces immediately in /admin/messages. **6 new pin tests** in voice-prompt.test.ts enforce: body does NOT promise warm-transfer outside the DO-NOT-SAY instruction list · body does NOT contain 'let me get Demi on the line' outside that list · 'as soon as possible' SLA phrase appears · 988 / DV / Spanish 988 verbatim preservation · Demi name still present in prompt body. Soft-cap bumped 16000 → 17000 (net adds ~950 chars). **Sync step**: node scripts/sync-retell-prompt.mjs MUST run post-push or the Retell agent dashboard keeps serving the old IH0005 prompt (RP0005 ghost-code trap). **Files MOD (4):** src/lib/voice-prompt.ts · src/lib/__tests__/voice-prompt.test.ts · src/lib/changelog.ts · src/lib/changelog-current.ts. **Doug-action surfaced**: post-6/9 ship for Demi-presence detection — Retell custom-function checkDemiAvailability() querying AdminHeartbeat (last 15min) + conditional transfer-vs-message branch in prompt. [hipaa-pre-cutover][voice-channel-doctrine][crisis-safety-preserved][message-taking-default][version-letter:MT0005][cadence-override: patient-experience-impacting voice rule change per Doug 6/1 directive]
v2.97.ST0005
2026-06-01Production

Isabella's email sign-off now reads 'Regards, Support Team @ Green Wellness' (Doug brand directive 6/1).

What this means for you

Isabella's email sign-off now reads 'Regards, Support Team @ Green Wellness' (Doug brand directive 6/1). The patient sees one consistent sign-off block from a team identity, not a named AI assistant. Mariane will see the change on the next patient email Isabella sends — body still warm + personalized (Hi {firstName}); just the closing line shifts to the team brand.

Show technical details

Changed

  • 🖋️ **ST0005 — Email sign-off rebranded to 'Regards, Support Team @ Green Wellness' (Doug 2026-06-01 directive).** Replaces the IE0005-canonical inline sign-off ('— Isabella, Green Wellness AI Receptionist') across all 3 patient-facing email paths: (1) main renderer src/lib/email-ai-render.ts (the canonical footer), (2) medical-claim scrub fallback src/lib/medical-claim-scrub.ts (the PATIENT_FALLBACK_FOR_CLINICAL body — also caught + closed a re-introduced duplicate-sign-off bug from RS0005 where the fallback body still ended with the old Isabella sign-off, then the renderer appended another), (3) admin dry-run-test prompt src/app/api/admin/test/email-ai-dry-run/route.ts (drifted sister of EMAIL_AI_SYSTEM_PROMPT — updated to match IE0005's no-sign-off-in-body rule + new brand line). **Files MOD (4):** src/lib/email-ai-render.ts (3 lines) · src/lib/medical-claim-scrub.ts (sign-off block removed + doc-comment added) · src/app/api/admin/test/email-ai-dry-run/route.ts (prompt rule rewritten) · src/lib/changelog.ts + src/lib/changelog-current.ts. No new pin tests — existing IE0005 pin at email-ai-isabella-polish.test.ts already enforces 'no sign-off in body'; sign-off TEXT is brand copy + low regression risk. Follow-on doctrine: the dry-run route has a STALE copy of EMAIL_AI_SYSTEM_PROMPT that drifted from email-ai.ts since IE0005 — should be refactored to import the prompt SoT directly to prevent future drift (deferred, not blocking). [hipaa-pre-cutover][brand-rebrand][duplicate-sign-off-prevention][version-letter:ST0005][cadence-override: same-day brand-voice ship per Doug verbal directive — 6/1]
v2.97.SX0005
2026-06-01Production

Lays the groundwork for Isabella to learn from the way Demi and Mariane actually reply to patients — a new admin-only table starts collecting de-identified examples of common requests (booking, records, billing, etc.) and how the team handles each one. Nothing changes for patients yet; the playbook surface where you'll approve or edit examples comes in a follow-up.

Show technical details

Added

  • 🧪 **SX0005 — Phase 1 substrate for Isabella learns from staff replies (from PLAN_ISABELLA_LEARN_FROM_STAFF_REPLIES_2026_05_31.md, Doug Q1-Q8 accept-all 2026-05-31).** Substrate-only ship — Phase 2 (extraction cron), Phase 3 (Demi curation UI at /admin/isabella-playbook), and Phase 4 (Isabella system-prompt injection across email/chat/SMS/voice) are SEPARATE follow-ups; this ship lands the table + the extractor lib + tests, nothing else. **NEW Prisma model StaffReplyExemplar** mapped to staff_reply_exemplar (snake_case, sister of email_ai_daily_rollup / doug_oversight_acks) — 14 columns capturing (a) lineage FKs into PatientMessage (sourceInbound/sourceReply via onDelete: SetNull named relations, so the scrubbed exemplar survives retention purge of the source rows), (b) extractor outputs (inboundCategoryEstimate + inboundSummaryScrubbed + replySummaryScrubbed + replyTone + decisionType), (c) curation lifecycle (status defaulting pending-reviewapproved | edited | rejected + reviewedByUserId + reviewedAt + editedSummary + notesByReviewer). Two compound indexes: (status, createdAt) for the Phase 3 admin queue surface + (inboundCategoryEstimate, status) for the Phase 4 per-category playbook pull. **NEW migration prod-migration-76-isabella-exemplar-corpus.sql** — additive-only, idempotent (IF NOT EXISTS on table + both indexes), reversible (DROP TABLE IF EXISTS staff_reply_exemplar CASCADE;), applied autonomously to prod (SELECT to_regclass('public.staff_reply_exemplar') returned non-null post-apply, 14 columns verified). **NEW pure-fn lib src/lib/isabella-exemplar-extractor.ts** — exports extractExemplar({inbound, reply, model}) async fn returning the 5-field ExemplarShape. **TRIPLE-PHI DEFENSE** (HIPAA-load-bearing): (1) **PRE-SCRUB** — scrubPhiForSmsOutbound runs on raw inbound.body + reply.body BEFORE the Bedrock call (Bedrock NEVER sees raw PHI); (2) **PROMPT-REDACT** — the extractor system prompt explicitly forbids patient names / DOB / MRN / phone / email / address / SSN / condition / medication echoes in the model's summaries + instructs summarize-not-quote; (3) **POST-SCRUB** — scrubPhiForSmsOutbound runs AGAIN on the model's response summaries before return (double-net catches any identifier-shape the model snuck through despite the prompt). **Bedrock model:** anthropic/claude-haiku-4-5 — Haiku (not Sonnet) per the Haiku-vs-Sonnet cost-discipline pattern: extraction is a structured-output classification task, not reasoning, so Haiku handles it for ~$0.001-0.005/pair vs ~$0.02-0.05 on Sonnet (10× cost discipline on ~1,000 historical pairs). Both Haiku + Sonnet ride the AWS BAA umbrella via Bedrock — no HIPAA delta. **Wrapper discipline:** lib does NOT import @anthropic-ai/sdk or @ai-sdk/amazon-bedrock directly; all Bedrock calls route through the LanguageModel handle the caller passes (built via getReceptionistModelWithFallback() or a circuit-wrapped variant). The check-ai-provider-baa-isolation.mjs gate stays clean — EXTRACTOR_MODEL_ID uses the anthropic/ prefix. **Defensive fallback:** any extractor failure (Bedrock error, malformed JSON, invalid enum, non-string fields) returns SAFE_DEFAULT_EXEMPLAR (category=other, tone=informational, decision=other, empty summaries) — fallback rows never accidentally influence Phase 4 prompt injection. **46 NEW pin tests** across 2 files: src/lib/__tests__/isabella-exemplar-extractor.test.ts (28 tests: type-shape pin × 2, function-export pin × 2, **triple-PHI defense pin × 5** asserting source-code grep for pre-scrub + post-scrub call-sites ordered before/after the generateText call + system-prompt redaction instruction, Haiku model-id pin × 2, parse-fallback × 8 across malformed/empty/wrong-enum/truncated/fenced JSON, closed-enum × 4, prompt-builder × 2) + src/lib/__tests__/staff-reply-exemplar-schema.test.ts (18 tests: schema model shape × 13 incl. all 14 columns + indexes + @@map + back-relations on PatientMessage × 2, migration shape × 5 incl. idempotency-counter assert). All 46 green. **Phase 2/3/4 deferred:** no extraction cron yet, no admin UI yet, no system-prompt injection — Phase 1 ships ONLY the substrate. **HIPAA posture:** zero new PHI surfaces. The exemplar table is PHI-FREE by construction (scrub at extract-time × 3 layers). notesByReviewer is operator-controlled (Mariane/Demi) and Phase 3 admin route will bound at 4KB at the gate. **Pre-cutover freeze (6/1-6/9) compatible:** additive-only schema + admin-only table + pure-fn lib with no consumer wiring + reversible migration — Wave-D additive precedent. **Files NEW (3):** src/lib/isabella-exemplar-extractor.ts · src/lib/__tests__/isabella-exemplar-extractor.test.ts · src/lib/__tests__/staff-reply-exemplar-schema.test.ts. **Files NEW (migration):** prod-migration-76-isabella-exemplar-corpus.sql. **Files MOD (3):** prisma/schema.prisma (NEW StaffReplyExemplar model + 2 back-relation fields on PatientMessage) · src/lib/changelog.ts + src/lib/changelog-current.ts (this entry + version bump). **No env change, no cron-routing change, no patient-facing copy. Migration applied autonomously.** [hipaa-substrate][isabella-learn-from-staff-replies][phase-1-substrate-only][freeze-compatible][triple-phi-defense][haiku-cost-discipline][migration-76-applied-autonomously][46-new-pin-tests][version-letter:SX0005][cadence-override: substrate ship for Doug Q1-Q8 accept-all]
v2.97.IE0005
2026-05-31Production

Isabella's after-hours email replies sound less template-y now — no more duplicate Isabella sign-off, no robotic 'I'm Isabella, Green Wellness's AI receptionist' intro, no two phone numbers crammed into one email, no call-center cliches like 'I'm happy to assist!' The opener now greets the patient by first name when we have it on file ('Hi Sarah —' / 'Hi there —' otherwise), names Demi by name in the footer when a human follow-up is implied, and carries the same Green Wellness brand header + footer (logo + socials + Leave Us a Review) that booking confirmations use. The two safety-net auto-acks (when Isabella's AI loop fails or the cap kicks in) also now carry the real '11am next business day' SLA instead of hedging with 'shortly'. Closes the Doug 2026-06-01 specimen review.

Show technical details

Changed

  • ✨ **IE0005 — Isabella email-voice polish (6 fixes + Doug intro-drop) from RECOMMENDATIONS_ISABELLA_EMAIL_VOICE_POLISH_2026_05_31.md.** Closes the voice-polish arc Doug greenlit via Q1-Q8 accept-all on 2026-05-31. Six prompt-only / template-only changes; no schema, no migration, no env, no cron-routing — freeze-compatible. **Fix 1 — strip the model's sign-off (the single biggest fix).** Pre-fix EMAIL_AI_SYSTEM_PROMPT line 109 instructed Always sign off as: "— Isabella, Green Wellness AI Receptionist". The renderer ALSO appended the same sign-off line, producing the duplicate-signature beat that read auto-generated. Post-fix the prompt explicitly forbids sign-offs (Do NOT sign off. The email footer adds the sign-off automatically; if you add one too, the patient sees a duplicate signature and the reply reads auto-generated.); the renderer's inline — Isabella, Green Wellness AI Receptionist line is now the canonical (and only) sign-off. **Fix 1.5 — firstName personalization.** Threaded patient first-name into the system prompt as a block appended at the END (so it can't override the load-bearing crisis-safety / PHI-minimization / identity-boundary rules above it). New lookupPatientFirstName(patientId) helper does one Prisma read (db.patient.findUnique({ where:{id}, select:{firstName:true} })); new buildEffectiveSystemPromptForEmail(firstName) helper wraps the base prompt + the context block. Sanitizes firstName against prompt-injection bytes (unicode-letter/digit/space/apostrophe/hyphen allowlist) + caps at 64 chars before render. New ## Patient name prompt section instructs Hi {firstName} — when known, Hi there — when not; explicitly forbids bare Hi,. **HIPAA:** firstName ALONE (with no chart context in body) is Safe Harbor §164.514(b)(2) low-risk. Audit forensic-trace via existing EMAIL_AGENT_REPLY_SENT detail string — appended firstNameKnown= token (boolean ONLY, never the firstName itself — belt-and-suspenders PHI partition). **Fix 2 — voice/tone polish.** Added new Tone discipline clause banning call-center stock phrases (I'm happy to assist, How may I help you today, Please don't hesitate, It's my pleasure). Added new clause for NEW-thread opens: name the two common reasons people email (questions about evaluations, or wanting to get on the schedule) + if it's something else I'll flag it for Demi escalation framing. **Fix 3 — phone-CTA dedup + footer soften.** Pre-fix line 110 said Always remind the patient they can reply at any time or call ${PHONE}… — that body-level phone CTA stitched together visibly with the renderer footer's call ${PHONE} any time line. Post-fix the prompt instructs Don't mention the office phone number in the body — the email footer carries it automatically. Body-level 'or call us at…' duplicates the footer. Encourages DO mention Demi by name when the topic needs a human (relational, not redundant). Renderer footer line softened from Need to reach a real person? Reply and someone from our team will pick this up when they're backWant a real person? Reply here and Demi will pick this up when she's back, or call anytime. 'Want' is more permissive than 'Need' (doesn't imply the email reply was inadequate); naming Demi explicitly converts the bot from 'the system that responds' into one half of a relationship the patient already has. **Fix 4 — EM0005 header + footer integrated into Isabella's render path.** src/lib/email-ai-render.ts now imports renderEmailHeader from @/lib/email-header + renderEmailFooter from @/lib/email-footer (Mariane reviewer-feedback cmpudy6vg + cmpufz6ch consolidated as EM0005 on 2026-05-31). The reply HTML is now a properly composed shell with brand header at top, Isabella's words inside a white card, Isabella's inline sign-off, soft footer line, and the Green Wellness brand footer (logo + phone + email + website + social pills + Leave Us a Review button). Two-tier hierarchy: Isabella's words → Isabella's name (inline) → Green Wellness brand chrome. Reads like a letter from a person who works at a place, not like a system notification. **Fix 5 — AUTO_ACK_BODY + FALLBACK_BODY copy.** Both deterministic safety-net strings rewritten from the pre-fix hedge Thanks for emailing Green Wellness — we've got your message. Our team will follow up shortly. → AUTO: Got your email — we've got it on file. Demi will pick this up by 11am next business day. If it's urgent before then, give us a call. FALLBACK: Got your email — something glitched on my end before I could read it properly. Demi will pick this up by 11am next business day. If it's urgent before then, give us a call. 'Shortly' was the laziest SLA word in English when business-hours.ts already exports the 11am-next-business-day commitment. FALLBACK owns the miss in one sentence without performing apology. **Doug 2026-05-31 directive — drop the intro off the top.** Pre-fix prompt line 108 said Open the reply with a brief identity line on the FIRST email in a thread — e.g. "Hi, I'm Isabella, Green Wellness's AI receptionist covering email after hours." Doug 2026-06-01 evening: drop entirely — the footer signature already names her, the intro reads as template-y self-introduction every time. Post-fix the rule is Open the reply directly — greet the patient by first name when known (per the Patient name rule below), then acknowledge their specific ask. Don't preamble with "I'm Isabella, Green Wellness's AI receptionist covering email after hours". **Files MOD (4):** src/lib/email-ai.ts (EMAIL_AI_SYSTEM_PROMPT — 6 rule replacements in Your Behavior section + new ## Patient name section + intro-rule replacement; AUTO_ACK_BODY + FALLBACK_BODY constants rewritten; NEW lookupPatientFirstName helper + NEW EXPORTED buildEffectiveSystemPromptForEmail helper; system: arg swapped from EMAIL_AI_SYSTEM_PROMPT → effectiveSystemPrompt at the generateText callsite; firstNameKnown= appended to EMAIL_AGENT_REPLY_SENT audit detail) · src/lib/email-ai-render.ts (NEW imports + composed shell w/ header + footer + softened footer line) · src/lib/__tests__/email-ai-isabella-polish.test.ts NEW (43 pin tests across 8 describe blocks: Fix 1 sign-off-strip × 2 / Doug intro-drop × 3 / Fix 1.5 firstName × 9 / Fix 2 voice-polish × 6 / Fix 3 phone-dedup + footer-soften × 6 / Fix 4 EM0005 integration × 5 / Fix 5 SLA copy × 4 / crisis + identity preservation × 5) · src/lib/__tests__/check-email-ai-render.test.ts (1 test updated: pre-fix 'reply and someone from our team' → post-fix 'Want a real person? Reply here and Demi') · src/lib/changelog.ts + src/lib/changelog-current.ts (this entry + version bump). **Test impact:** 249/249 across email-ai siblings green (email-ai-no-outbound-gate.test.ts + check-email-ai-render.test.ts + auto-ack-template.test.ts + email-ai-pulse.test.ts + email-ai-pulse-anti-divergence.test.ts + email-ai-isabella-polish.test.ts). Email header + footer pin tests 28/28 green. TypeScript --noEmit clean. **HIPAA scope:** firstName lookup is Safe Harbor low-risk (no chart context in body). Audit row carries firstNameKnown= only — never the raw firstName. ZERO new PHI fields, ZERO new patient-context surfaces. Crisis-safety + records-release + legal-inquiry + reply-only + tentative-appointment rules preserved verbatim (5 pin tests defend each). **Pre-cutover freeze (6/1-6/9):** explicitly compatible — prompt + template + helper-fn only, additive, reversible, NO schema migration, NO patient-facing copy outside the Isabella reply path, NO env rotation, NO cron-routing change. **Cadence-override:** reviewer-feedback-derived polish ship (Doug Q1-Q8 accept-all + 2026-06-01 specimen review). [hipaa-pre-cutover][isabella-email-voice-polish][reviewer-feedback-derived][doug-greenlit-accept-all][q1-q8-yes][intro-drop-doug-2026-05-31][6-fixes-batched-single-commit][43-new-pin-tests][249-existing-tests-green][freeze-compatible][no-no-verify][version-letter:IE0005][cadence-override: reviewer-feedback / patient-facing voice fix]
v2.97.RS0005
2026-06-01Production

Fixes a bug Doug caught in his Isabella test reply where a redaction marker ("[SCRUB-MEDICAL-ADVICE]") leaked into the patient-facing email — Isabella now sends a clean fallback that points clinical questions to the provider visit instead of a broken sentence. Also tightens Isabella's chat behavior with a concrete deflection example so she's less likely to get tricked into making medical claims in the first place.

Show technical details

Fixed

  • 🛡️ **RS0005 — patient-safe rendering for the medical-claim scrubber (closes the [SCRUB-MEDICAL-*] leak Doug caught on 2026-06-01).** Doug tested the LIVE Isabella email auto-reply and got back: "How can I help you today? Whether [SCRUB-MEDICAL-ADVICE]evaluations or want to book an appointment, I'm happy to assist!" — the inline audit-marker leaked verbatim into a patient-facing email because the email dispatcher passed claimScrubbed.text straight through to the M365 send. The scrub itself was working (model tried to emit diagnostic-pattern language, regex caught it, inline tag inserted) but the marker was designed for grep-ability, not patient eyes. **Fix:** new renderClaimScrubForPatient(result) helper in src/lib/medical-claim-scrub.ts that wraps the scrub result with patient-safe output — severity=clean returns text as-is, severity=medium|high replaces the entire reply with an on-brand fallback ("I'm Isabella, Green Wellness's AI receptionist — for clinical questions, our Washington-licensed providers handle those at your appointment" + booking link + phone), severity=low (conspiracy-only, rare) inline-strips the tag + collapses whitespace because surrounding text reads cleanly without it. Wired into src/lib/email-ai.ts:dispatchEmailAi between scrub + M365 send. **Why fallback instead of inline-replace for medium/high:** the scrub matched a mid-sentence diagnostic-pattern (e.g. "you have evaluations" or "you might have anxiety") — even if we strip the tag, the surrounding sentence is broken or wrong on its own; salvageable text is rare. The fallback preserves Isabella's voice + the booking CTA + the phone number, which is what the patient actually needs anyway. **5 NEW pin tests** in src/lib/__tests__/medical-claim-scrub.test.ts (now 32 total, was 27): severity=clean returns text as-is · severity=medium returns fallback (regression test for the exact Doug 2026-06-01 bug shape) · severity=high returns fallback · severity=low inline-strips tag + collapses whitespace · **invariant test across 6 mixed-severity samples that no SCRUB tag ever ships to a patient** — the whole-point of the helper, codified as a contract. All 32 green. **Sister tighten in chat (src/app/api/chat/route.ts ## Your Behavior section):** added a concrete deflection example with the exact deflection phrase Isabella should use for clinical questions ("Our Washington-licensed providers are the best people to answer that — they assess each patient individually at the appointment.") so the model has a positive script instead of just a negative "don't make medical claims" rule. Chat is post-stream audit-only (Phase 1.6 design — can't undo what the patient already saw without killing the streaming UX), so the system-prompt tighten reduces upstream emissions; the runtime render helper now exists in the lib if chat ever moves to pre-stream blocking. **Files MOD (5):** src/lib/medical-claim-scrub.ts (+30 LOC for fallback constant + helper, no breaking-change to scrub fn) · src/lib/email-ai.ts (1-line import + 1-line call-site swap + 3-line comment) · src/lib/__tests__/medical-claim-scrub.test.ts (+50 LOC, 5 new tests) · src/app/api/chat/route.ts (+1 line in SYSTEM_PROMPT) · src/lib/changelog.ts + src/lib/changelog-current.ts (this entry + version bump). **No schema migration, no env change, no cron-routing change, no patient-facing copy outside the Isabella fallback path.** [hipaa-safe-harbor][isabella-email-ai][post-phase-1-bugfix][doug-caught-in-prod-test][version-letter:RS0005]
v2.97.LX0005
2026-05-31Production

Corrects the provider-location rules shipped in LR0005 — initial ship had Dawn at Olympia (wrong); Doug clarified later that Marnie is at Olympia (her existing renewal patients + new pts) and Dr Ari (Dawn) is at Lynnwood. This catches the booking-rules config up to the actual schedule. Ruth at Spokane (new pts) until 6/30 stays unchanged.

Show technical details

Fixed

  • 🔧 **LX0005 — provider-location-rules CORRECTION on LR0005 (RE-SHIPPED after revert).** First LX0005 attempt (commit 0b81b54b) was reverted (85a49974) because it accidentally swept 2542 parallel-session staged deletions into the commit — 1748 files deleted from HEAD including src/proxy.ts + vercel.json + tsconfig.json + voice-prompt.ts. Root cause: git commit -m "..." (without pathspec) included the FULL index, not just my 3 staged files. This re-ship uses pathspec form git commit -- per the new doctrine pin feedback_git_commit_must_use_pathspec_when_index_dirty_2026_05_31. **Rule correction:** Olympia → Marnie (her renewal patients + new pts), Lynnwood → Dawn (Dr. Ari) + Ruth (her renewal patients) + Roy + new pts, Spokane → Ruth (new pts only, sunsets 6/30 per SC0005, unchanged). Renewal-routing nuance documented inline: a renewal patient's existing Authorization.issuingProviderId decides location (Marnie's prior → Olympia; everyone else → Lynnwood). **Files MOD (3):** src/lib/provider-location-rules.ts (config + comment block updated, no shape change) · src/lib/changelog.ts (this entry) · src/lib/changelog-current.ts (→ LX0005). No new pin tests — LR0005's 39 existing tests still cover rule-shape invariants. [hipaa-pre-cutover][doug-clarification-applied][LR0005-correction][re-ship-after-revert][cadence-override: 2nd-attempt correction after disaster-revert recovery]
v2.97.LR0005
2026-05-31Production

Provider-location rules now in code, per Doug's 2026-05-31 verbal directive: Olympia renewals + new patients go to Dawn; Lynnwood handles new patients and all other renewals (Ruth, Marnie); Spokane takes new patients only until the existing 6/30 closure cutoff. Mariane reviewer-feedback cmpuiu2ek closed. The booking UI + slot-gen pipeline still need to consume the new helpers in a follow-on ship — this drop is the substrate (config table + helpers + 39 pin tests) so the rules have a single source of truth. ProviderSchedule rows must still be backfilled for Olympia + Spokane before slots actually surface; surfacing as a Doug-action.

Show technical details

Added

  • 📋 **LR0005 — provider-location-appointment-type rules substrate (Doug 2026-05-31 verbal directive, closes Mariane reviewer-feedback cmpuiu2ek000004jvhk781wlf).** Doug's verbatim directive (parsed): "Oly renewals hours get scheduled with her and she can see new pts. Lynnwood new pts and all other renewals. Spv new pts the next couple weeks." Translated to enforceable rules: Olympia → Dawn only (NEW + RENEWAL); Lynnwood → Ruth + Marnie + Roy non-Dawn (NEW + RENEWAL); Spokane → Ruth (NEW only, auto-sunsets via existing isSpokaneClosedAt from SC0005 closure-cutoffs.ts). **Why Option B (pure-fn config) over Option A (ProviderLocation join table) or Option C (per-Provider field):** pre-cutover freeze (6/1 → 6/9) discourages schema-additive changes. The mapping evolves weekly (Doug said "next couple weeks" for Spokane — already an env-driven hard sunset). Operator edits via src/lib/provider-location-rules.ts + redeploy. If churn justifies an admin UI post-cutover, we promote to a ProviderLocation join table with acceptsNewPatients / acceptsRenewals flags per row. **Files NEW (2):** src/lib/provider-location-rules.ts (~225 LOC — PROVIDER_SLUGS slug→id map, LOCATION_IDS slug→id map, PROVIDER_LOCATION_RULES config table, helpers: getActiveLocations / isLocationActive / getAllowedProvidersAt / isCombinationAllowed / resolveProviderId / slugFromProviderId / slugFromLocationId. Reuses RUTH_PROVIDER_ID + SPOKANE_LOCATION_ID + isSpokaneClosedAt + isRuthDepartedAt from closure-cutoffs.ts — single SoT for the sunset arithmetic) · src/lib/__tests__/provider-location-rules.test.ts (~265 LOC, 39 pin tests across 9 describe blocks: stable id constants × 7 / Doug rules verbatim × 4 / isLocationActive × 4 / getActiveLocations × 2 / getAllowedProvidersAt × 9 / isCombinationAllowed × 6 / slug↔id round-trip × 5 / Mariane bug regression × 2). **Files MOD (2):** src/lib/changelog.ts (this entry) · src/lib/changelog-current.ts (→ LR0005). **Helper contract:** getAllowedProvidersAt(location, 'NEW' | 'RENEWAL', when?) returns the provider slugs allowed at a (location, appointmentClass) pair AT a given moment. Filters out departed providers (Ruth post-2026-06-30) without baking provider-specific sunset logic into the rule table. isCombinationAllowed({providerSlug, location, appointmentClass, when?}) is the API-gate use that refuses forged POSTs which bypass the UI picker. **HIPAA scope:** PHI-clean by construction. Zero patient context anywhere in the helpers (only provider slugs + location ids + appointment-class enum). **Cross-session edit-war defense:** parallel session has 1,747 deleted files staged (likely mid-cherry-pick) — this ship deliberately avoids any git add -A / pathspec-free commit and operates only on the 4 NEW/MOD files via pathspec-form to preserve the parallel session's working state. **Sister-modules:** src/lib/closure-cutoffs.ts (SC0005 — single SoT for SPOKANE_LOCATION_ID + Ruth's sunset) · src/lib/constants.ts (getAppointmentDurationMinutes() — orthogonal per-location duration helper, UN0005/DZ0005 arc). **Doug-action follow-on (NOT in this ship — substrate-only):** (1) backfill ProviderSchedule rows for Dawn @ Olympia (renewal hours) + Ruth @ Spokane (new-pt windows) so the slot-gen cron actually emits bookable slots — the rule layer is unblocked but DB-empty per project_gw_slot_source_sot_discovered_2026_05_30; today the slots are kept alive by manual /api/admin/slots/quick-generate clicks. (2) Wire getAllowedProvidersAt into BookNowFormModal.tsx + /api/renew/book/route.ts (currently hardcoded Lynnwood-only at line 194 of src/app/renew/page.tsx) + /api/cron/slots/route.ts provider+location filter — a follow-on ship lands the UI/API consumers (deferred per pre-cutover freeze to keep this substrate small + reversible). (3) Confirm with Doug: "her" at Olympia = Dawn Reardon ND? Confirm Spokane provider is Ruth? **Pre-cutover freeze (6/1-6/9):** explicitly compatible — pure-fn config + tests only, additive, reversible, NO schema migration, NO patient-facing copy change, NO env rotation, NO cron-routing change. Wave-D compatible. **Reviewer-feedback close:** PATCH https://greenwellness.org/api/admin/reviewer-feedback/cmpuiu2ek000004jvhk781wlf/agent with {action:'done', sha:'', autoFixVersion:'v2.97.LR0005'} after push lands. **Version-letter pick:** LR (Location Rules) — verified unique against full changelog corpus. [hipaa-pre-cutover][doug-verbal-directive-codified][reviewer-feedback-close-cmpuiu2ek][provider-location-rules-substrate][option-b-pure-fn-config][39-pin-tests][freeze-compatible][no-no-verify][version-letter:LR0005][cadence-override: reviewer-feedback close + Doug-verbal-directive — substrate ships now so the rules have a single SoT before UI/API consumers wire in next pass]
v2.97.EM0005
2026-05-31Production

Patient-facing emails (booking confirmation + appointment reminder + every other patient email that uses our shared template shell) now carry a centered brand header at the top and a professional footer at the bottom — brand name + phone + email + website + social-media icons + a 'Leave Us a Review' button that points at our Google review URL. The icons hide automatically when their Vercel env-vars aren't set, so they appear once Doug pastes the Facebook/Instagram/Google Business URLs. The header is text-only today (matches what patients see now); it auto-upgrades to a logo image as soon as the EMAIL_LOGO_URL env is set. Closes Mariane reviewer feedback cmpudy6vg + cmpufz6ch.

Show technical details

Added

  • 📧 **EM0005 — patient-facing email logo header + professional footer (Mariane reviewer-feedback consolidated close cmpudy6vg + cmpufz6ch, 2026-05-31).** Both rows ask for the same thing — logo prominently at top center + professional footer w/ brand + phone + email + website + social icons + 'Leave Us a Review' button on patient-facing email templates (appointment reminder + booking confirmation). Shipped as TWO new pure-fn helpers + minimal-touch wiring across the existing template shell so all ~25 templates that flow through emails.ts shell() (reminderEmail, bookingConfirmationEmail, noShowEmail, rescheduleEmail, renewalReminderEmail, postAppointmentEmail, etc.) inherit the upgrade uniformly. **Files NEW (4):** src/lib/email-header.ts (~70 LOC — renderEmailHeader() pure-fn; env-gated EMAIL_LOGO_URL with HTTPS-only validation + graceful text-only fallback when unset, matching the legacy 'Green Wellness' navy-bar shape exactly so pre-asset emails are byte-identical to current production) · src/lib/email-footer.ts (~110 LOC — renderEmailFooter() pure-fn; brand name + tel:/mailto: links to PHONE+EMAIL constants + canonical website link + 'Leave Us a Review' CTA pointing at getGoogleReviewUrl() (env GOOGLE_REVIEW_URL || ${APP_URL}/leave-a-review fallback, mirrors cron/review-request resolution chain) + 3 social-media icon pills (Facebook 'f' + Instagram 'IG' + Google Business 'G') each env-gated via NEXT_PUBLIC_FACEBOOK_URL / NEXT_PUBLIC_INSTAGRAM_URL / NEXT_PUBLIC_GOOGLE_BUSINESS_URL — hidden when unset, visible when Doug pastes them) · src/lib/__tests__/email-header.test.ts (~110 LOC, 16 pin tests across 4 describe blocks: env-gated logo or text fallback × 8 / HIPAA PHI hygiene × 2 / XSS attribute-injection defense × 1 + nested env management) · src/lib/__tests__/email-footer.test.ts (~190 LOC, 12 pin tests across 7 describe blocks: SSoT contact info pulls × 5 / 'Leave Us a Review' CTA × 4 / social icons env-gated × 2 / HIPAA PHI hygiene × 2 / brand palette × 1 / unsubscribe gating × 3). **Files MOD (3):** src/lib/constants.ts (+SOCIAL_URLS const + getEmailLogoUrl() + getGoogleReviewUrl() SSoT accessors, env reads at call-time so pin tests can flip env in-test without module-cache poisoning) · src/lib/emails.ts (shell() swapped inline navy-bar header for renderEmailHeader() + appended renderEmailFooter({unsubscribeUrl}) after the existing inner contact paragraph — affects every template that uses shell() including bookingConfirmationEmail line 140, reminderEmail line 256, noShowEmail, etc.) · src/lib/booking-confirmation-email-shared.ts (bookingConfirmationEmail() — the standalone auto-confirm fired AFTER booking-form submit — swapped its inline header div + footer div for the shared helpers). **HIPAA scope:** PHI-clean by construction. Both helpers take zero patient context (header takes no args, footer takes only an optional unsubscribeUrl which is an operator-side token URL). Pin tests defend the contract — any future signature drift that tries to thread patient identifiers into either helper would fail the Function.length checks + PHI-leak regex assertions. **Brand palette:** matches existing — navy #0f2744 header bg + slate-green #5a7a68 secondary text + brand-green #2d6a4f links/buttons + cream #f5f5f0 footer bg + soft border #dde6e0 + fine-print #aab8b0. WCAG-AA contrast: 5a7a68/f5f5f0 = 4.83:1 (body text), 2d6a4f/f5f5f0 = 6.42:1 (links + buttons). **Test impact:** 28/28 new pins GREEN + 18/18 existing booking-confirmation-email-shared.test.ts GREEN (no regression) + check-emails-firstname-xss + booking-confirmation-email-anti-divergence GREEN. tsc clean. **Doug-action items (3, none blocking):** (1) UPLOAD /public/email-logo.png asset (240×60 @ 2x retina — current site logo bumped through Squoosh or similar) then set Vercel env EMAIL_LOGO_URL=https://flow.greenwellness.org/email-logo.png to flip from text-only to image header. (2) PASTE Vercel env vars NEXT_PUBLIC_FACEBOOK_URL + NEXT_PUBLIC_INSTAGRAM_URL + NEXT_PUBLIC_GOOGLE_BUSINESS_URL once social URLs confirmed — pills auto-appear in footer. (3) OPTIONAL — set GOOGLE_REVIEW_URL to the direct Google review URL if the existing /leave-a-review per-location landing isn't preferred (current default already renders per-location cards for Lynnwood + Olympia + Spokane-post-task-#220 so the fallback is robust). **Pre-cutover freeze (6/1-6/9):** explicitly allowed — additive, reversible, no schema/env-rotation/cron-routing change, no PHI flow change, no patient-facing copy change (chrome only). Wave-D compatible. **Cross-session coordination:** parallel-session edit-war partially observed (constants.ts edit reverted once mid-session by a sister-session); recovered via re-apply + immediate pathspec-form git add. Pin tests authored as suite-level + describe-block-scoped env-restore beforeEach/afterEach so they don't leak global env mutations into sibling test files. **Reviewer-feedback close:** PATCH https://greenwellness.org/api/admin/reviewer-feedback/cmpudy6vg000604l4pwcsknhj/agent + .../cmpufz6ch000004ju38ptj84a/agent with {action:'done', sha:'', autoFixVersion:'v2.97.EM0005'} after push lands. [hipaa-pre-cutover][reviewer-feedback-close-2-rows-consolidated][email-header-logo-or-text-fallback][email-footer-brand-contact-socials-review-cta][shared-pure-fn-helpers][28-new-pin-tests][no-regression-existing-tests][version-letter:EM0005][cadence-override: reviewer-feedback agent-actionable consolidated 2-row close per Doug greenlight in Mariane reviewer-feedback marathon]
v2.97.SR0005
2026-05-31Production

Channel-parity backport: the same tentative-appointment language we just shipped for voice/chat/email (IH0005) is now also in Isabella's SMS prompt. When SMS_AI_ENABLED eventually flips on, SMS bookings will frame as 'tentative request pending records review' just like the other channels — so we don't get one channel telling patients they're booked while the others say tentative. No customer-visible change today (SMS_AI still flag-off); this just closes the regression risk for the flip day.

Show technical details

Fixed

  • 📱 **SR0005 — SMS channel parity for IH0005 tentative-appointment language.** Records-audit 2026-05-31 found Agent 1's IH0005 ship (a3bc8d7f, voice/chat/email tentative-appointment language) MISSED src/lib/sms-ai.ts — channel divergence regression risk for the day SMS_AI_ENABLED=true flips. Backported the same NEVER-SAY / REQUIRED-phrase contract to SMS_AI_SYSTEM_PROMPT immediately after the Booking section. Added concrete SMS-budget-aware template (compact phrasing for 160-char segments). No customer impact today (SMS still flag-off); closes the cross-channel-invariant gap before SMS flip. **Files MOD (3):** src/lib/sms-ai.ts (+19 LOC inserted before ## Your Behavior — SMS-specific) · src/lib/changelog.ts (this entry) · src/lib/changelog-current.ts (→ SR0005). No new pin tests — check-receptionist-invariants.test.ts cross-channel invariant 2 covers the after-hours handoff-voice gate already; adding tentative-language pin is follow-on polish. [hipaa-pre-cutover][channel-parity-backport][reviewer-feedback-side-effect][sms-flag-off-no-customer-impact][no-no-verify][version-letter:SR0005][cadence-override: records-audit-side-effect-fix — IH0005 missed SMS, closing the regression risk before SMS_AI_ENABLED flips]
v2.97.RF0005
2026-05-31Production

On the feedback triage page, every fix now has clickable shortcuts to see what changed (the commit on GitHub) and a 'Needs retesting' button — when an agent says it shipped, you can flag the row for re-verification before closing it for good. You can also leave follow-up comments on any row instead of filing duplicate feedback to add context. Closes Mariane's feedback id cmprrauv3 + cmprrd7ty.

Show technical details

Added

  • 📝 **RF0005 — reviewer-feedback Ship #6 + #7: comments thread + retest loop + sha → GitHub link (2026-05-31).** Closes Mariane reviewer-feedback ids cmprrauv300000bhy5gicazpl (lightweight tracking UI) + cmprrd7ty00000agkkmlpil3w (comments thread). **Surface changes on /admin/reviewer-feedback:** (1) sha → clickable GitHub commit link (Mariane can see exactly what changed); (2) NEW "↻ Needs retesting" button on agent-shipped rows (done / approved-autofix / approved-manual / agent-working) — flips status to needs-retesting so the row stays visible until reviewer re-verifies; (3) NEW collapsible comments thread under every row — reviewers can ask follow-up questions without filing duplicate feedback rows. **Schema:** NEW ReviewerFeedbackComment model (id · feedbackId FK with ON DELETE CASCADE · authorUserId · authorName · authorEmail · body · createdAt + @@index([feedbackId, createdAt])). NEW status enum value needs-retesting (TEXT column, application-layer enum). **API:** NEW POST /api/admin/reviewer-feedback/[id]/comments (4KB body cap · AdminSession + REVIEWER_FEEDBACK_ALLOWLIST gate · NO bearer write-path) · NEW GET ... (same gate · oldest-first · take=200). **Server actions:** NEW markNeedsRetesting in _actions.ts with NEEDS_RETESTING_VALID_FROM gate. **Audit:** NEW REVIEWER_FEEDBACK_COMMENT_ADDED + REVIEWER_FEEDBACK_NEEDS_RETESTING actions in AuditAction union. Comment audit detail = commentId=X bodyLen=N actor=email — NEVER body content (sister of EMAIL_AGENT_REPLY_SENT discipline). **Migration:** NEW prod-migration-75-reviewer-feedback-comments-and-retesting.sql (idempotent CREATE TABLE IF NOT EXISTS + index). **Pin tests:** NEW src/lib/__tests__/reviewer-feedback-comments.test.ts (21 pins across 5 describe blocks: enum extension × 6 / route shape + HIPAA × 6 / body cap × 2 / migration parity × 5 / audit union × 2). UPDATED reviewer-feedback.test.ts enum-array pin from 8 → 9 statuses. **Files (10):** NEW prisma/schema.prisma (ReviewerFeedbackComment model + back-relation on ReviewerFeedback) · NEW prod-migration-75-...sql · NEW src/app/api/admin/reviewer-feedback/[id]/comments/route.ts (~184 LOC) · NEW src/app/admin/reviewer-feedback/_components/CommentsThread.tsx (~181 LOC, "use client") · NEW src/lib/__tests__/reviewer-feedback-comments.test.ts (~250 LOC) · MOD src/lib/reviewer-feedback.ts (+needs-retesting status + REVIEWER_FEEDBACK_COMMENT_MAX_BYTES = 4096) · MOD src/lib/audit.ts (+2 AuditAction union members) · MOD src/app/admin/reviewer-feedback/_actions.ts (+markNeedsRetesting server action + audit emit) · MOD src/app/admin/reviewer-feedback/page.tsx (CommentsThread render + GitHub link on doneSha + Needs-retesting button gate + NEEDS_RETESTING_AVAILABLE_ON set + ACTIONABLE_STATUSES extension) · MOD src/lib/__tests__/reviewer-feedback.test.ts (status-enum pin extended from 8 → 9). **HIPAA scope:** comments may carry PHI (operator-controlled free-text), same BAA-covered Neon umbrella as ReviewerFeedback.body. Bounded to 4KB at gate. Audit detail NEVER echoes body content. Error logs use err.name only (D10 PHI-in-logs doctrine). **Pre-cutover freeze (6/1-6/9):** explicitly allowed — additive ADMIN-only surface, no patient-facing change, reversible, no PHI flow change. **Cross-session edit-war:** experienced 3-strikes-class peak during this ship (parallel sessions IH0005 + EN0005 + ts-rescue commits actively reverting my page.tsx + comments dir + pin test file). Recovery recipe: pathspec-form git add after every edit + recreate-then-immediately-stage for deleted directories + stash-pop conflict resolution on STATUS_PILL color. Final commit assembled in one atomic batch via pathspec-form git commit filtering to RF0005-only paths. **Reviewer-feedback close:** PATCH https://greenwellness.org/api/admin/reviewer-feedback/cmprrauv300000bhy5gicazpl/agent + .../cmprrd7ty00000agkkmlpil3w/agent with {action:'done', sha:'', autoFixVersion:'v2.97.RF0005'} after push lands. [hipaa-pre-cutover][reviewer-feedback-close-2-rows][ship-6-tracking-ui][ship-7-comments-thread][version-letter:RF0005][cadence-override: reviewer-feedback agent-actionable batched 2-row close per Doug greenlight in RECOMMENDATIONS_DOUG_JUDGMENT_REVIEWER_FEEDBACK_2026_05_31.md]
v2.97.IH0005
2026-05-31Production

Two Isabella voice prompt tweaks based on Mariane's feedback. (1) After-hours calls no longer attempt a live transfer to Demi when the office is closed — patients now get a clean callback promise instead of waiting in a dead-air queue with nobody to pick up. (2) When Isabella confirms a booking on a call, she frames it explicitly as a 'tentative appointment request, not yet confirmed' — she always tells the patient that a provider has to review their medical records first and that confirmation will follow within 1-2 business days. Same wording mirrored to the email and chat receptionist so all three channels (voice, chat, email) speak the same way about pending bookings. Closes reviewer feedback cmprr882y000304l5ggo8u8mb + cmprr8pjs000004ihohni3te1.

Show technical details

Fixed

  • 📞 **IH0005 — Isabella receptionist: after-hours transfer gate hardened + tentative-appointment language across voice/chat/email (Mariane reviewer-feedback close cmprr882y + cmprr8pjs).** Two patient-facing prompt tweaks shipped in a single commit to minimize cross-session edit-war on the high-contention voice-prompt.ts file. **Item 1 — after-hours transfer gate (cmprr882y000304l5ggo8u8mb):** Mariane reported Isabella attempted live-transfer-to-Demi on an after-hours test call; caller hung in dead-air queue. Pre-fix the voice prompt's after-hours branch said 'Demi is offline — do not promise a live transfer' but did NOT enumerate the specific NEVER-SAY phrases, leaving the model room to improvise something like 'let me grab someone for you' that maps to the transfer tool. Post-fix the after-hours branch now lists explicit DO-NOT-SAY phrases ('let me get Demi on the line' / 'I'll transfer you now' / 'please hold while I connect you' / 'let me grab someone for you') with the rationale 'those promises put the caller in a dead-air queue with nobody to pick up, which is worse than no transfer at all' + an explicit DO-SAY callback-promise shape ('Demi is offline right now — our office is closed — but I can take a detailed message and she'll call you back by eleven a.m. the next business day. What's the best number to reach you?') + an unconditional-after-hours hold: even if the caller insists on speaking to a live person, the model is told to repeat that the office is closed and offer the callback rather than improvise a transfer. Backed by the existing src/lib/business-hours.ts SSoT (Mon-Fri 9-5 PT, isAfterHours() boolean, VOICE_ESCALATION_DURING_HOURS + VOICE_ESCALATION_AFTER_HOURS exports). **Item 2 — tentative-appointment language (cmprr8pjs000004ihohni3te1):** Mariane reported Isabella says 'your appointment is scheduled' when the booking is actually a tentative request pending medical-records review. Pre-fix the voice prompt's booking-close paragraph framed the appointment as 'a preference, not a confirmed booking yet' but the wording was thin — no NEVER-SAY list, no required-phrase list, no explicit telehealth-AND-in-person scope. Post-fix the booking-close paragraph now lists explicit NEVER-SAY phrases ('your appointment is scheduled' / 'you're booked' / 'you're confirmed' / 'you're all set for…') with the rationale 'that wording sets the wrong expectation and creates frustration when records-review denies the request' + a required-phrase list ('tentative appointment request' / 'not yet confirmed' / 'medical records required' / 'provider must review' / 'confirmation will follow') that MUST all appear in any booking wrap + an explicit 'both telehealth AND in-person paths — don't omit it on telehealth' scope clarifier. The required phrases come verbatim from Mariane's suggested wording in the reviewer-feedback row. Sister updates landed in src/app/api/chat/route.ts SYSTEM_PROMPT (new ## Tentative-appointment language section after the Booking-tools flow priority, post-confirmBooking reply guidance) and src/lib/email-ai.ts EMAIL_AI_SYSTEM_PROMPT (new ## Tentative-appointment language section after the Booking flow section, booking-confirmation reply guidance) so all 3 patient-AI channels (voice + chat + email) speak the same way about pending bookings. **Files MOD (5):** src/lib/voice-prompt.ts (after-hours transfer gate paragraph hardened + booking-close paragraph hardened + VOICE_PROMPT_SOFT_CAP_CHARS bumped 15000 → 16000 with full rationale in the soft-cap history comment block · final char count 15446 ≤ new cap 16000) · src/lib/email-ai.ts (new tentative-appointment-language section in EMAIL_AI_SYSTEM_PROMPT between Booking flow and Your Behavior — email-specific) · src/app/api/chat/route.ts (new tentative-appointment-language section in SYSTEM_PROMPT after Booking tools — flow priority) · src/lib/changelog.ts + src/lib/changelog-current.ts (this entry). **No code logic change** — pure prompt-tune across 3 patient-AI channels. **Test impact:** src/lib/__tests__/voice-prompt.test.ts 38/39 GREEN (same as baseline pre-edit; the 1 failure is pre-existing 'mentions all 4 clinics by name' looking for 'Olympia' which has not been in the prompt since the IL0005 trim — not introduced by this ship). src/lib/__tests__/check-receptionist-invariants.test.ts 21/21 GREEN. The Ship IB0005 invariant 'booking is framed as a preference, not a confirmed booking' still GREEN because the new wording preserves 'preference, not a confirmed booking yet' alongside the new 'tentative appointment request' phrasing. **Cost impact:** minor (~15-20 extra tokens per call/chat/email on booking-confirm + after-hours-transfer turns). Acceptable per Doug's reviewer-feedback close greenlight. **HIPAA scope:** prompt text contains no patient identifiers by construction. **Pre-cutover freeze (6/1-6/9):** explicitly allowed — pure prompt edits, reversible, no schema/env/route changes. **Cross-session coordination:** ship landed during peak high-contention edit-war (3+ concurrent agents on src/lib/changelog.ts racing to prepend entries; parallel sessions wrote EN0005 → NX0005/WX0005 → RT0005 within minutes of each other; another agent on Ship #6+#7 actively touching reviewer-feedback admin UI + Prisma schema enum + src/app/admin/reviewer-feedback/_actions.ts — zero source-file overlap with my prompt files per task-prompt collision check). Recipe: stashed parallel WX0005 changelog WIP via git stash push -m parking src/lib/changelog.ts src/lib/changelog-current.ts to dodge the syntax-error worktree state · prepended my entry on top of the clean HEAD via Python atomic prepend (dodges concurrent Edit-tool races per feedback_changelog_entry_stomped_twice_recovery_2026_05_29) · pathspec-form commit git commit -- src/lib/voice-prompt.ts src/lib/email-ai.ts src/app/api/chat/route.ts src/lib/changelog.ts src/lib/changelog-current.ts so commit content filters to ONLY my paths even if sister WIP lingers in index · post-commit git show --stat HEAD | tail -10 sanity check to catch empty-tree shape. **Reviewer-feedback close:** PATCH agent endpoint with {action:'done', sha:'', autoFixVersion:'v2.97.IH0005'} after push lands for BOTH cmprr882y000304l5ggo8u8mb AND cmprr8pjs000004ihohni3te1. **NO migration. NO new audit literals. NO new cron registrations. NO new API routes. NO --no-verify.** [hipaa-pre-cutover][reviewer-feedback-close-2-rows][isabella-after-hours-transfer-gate-hardened][isabella-tentative-appointment-language-across-3-channels][voice+chat+email-prompt-mirror][soft-cap-bump-15000-to-16000-with-rationale][version-letter:IH0005][cadence-override: reviewer-feedback agent-actionable fix — Mariane after-hours-dead-air + scheduled-vs-tentative language complaints, batched 2-row close per Doug greenlight]
v2.97.RT0005
2026-05-31Production

Voice activity page (Integrations → Voice) now shows the last 50 Isabella/Retell calls instead of just 10, so a recent test call won't slide off the bottom. We also added a short note explaining that calls appear here after Retell's call-ended webhook fires (usually within 30 seconds of hang-up), with a link to Reports → Calls for the full 30-day cross-channel log. Fixes reviewer feedback cmprrm38g000g04ju6rvi2uga (Mariane: 'I completed a test call with Isabella today, but I am unable to locate the call').

Show technical details

Fixed

  • 📞 **RT0005 — Isabella voice recent-calls list: bump take cap from 10 → 50 + add call-persistence hint + deep-link to /admin/reports/calls.** Reviewer-feedback cmprrm38g000g04ju6rvi2uga reported: Mariane placed a test call with Isabella, then couldn't find it on the Voice integration page (~70 items aggregated in tile counts but the recent-calls list only renders 10). Root cause: src/app/admin/integrations/voice/page.tsx capped recentCalls at take: 10 while the surface's other tiles aggregate over 24h/7d windows; a test call placed mid-day could slide below the 10-row visibility ceiling if other inbound activity (Retell + RC both write channel='CALL' rows) bumped it down. Two-part fix: (a) bumped take: 10take: 50 on the channel='CALL' findMany (keeps the page render cheap — 50 rows × tabular data is well under the original budget), (b) added help text in the section header explaining when a call appears (Retell call_ended/call_analyzed webhook, ~30s post-hangup) and linking to /admin/reports/calls for the full 30-day cross-channel log (RC + Retell). The Reports → Calls surface already renders up to 200 rows in a 30-day window with filter chips (inbound/outbound/missed/voicemail), so the 'where do I see ALL the calls' question now has a clear surface answer. **Files MOD (2):** src/app/admin/integrations/voice/page.tsx (Prisma take cap 10 → 50 with inline reviewer-feedback comment · header copy bumped from 'Recent calls (last 10)' → 'Recent calls (last N of last 50)' · added 4-sentence help-text addendum citing Retell webhook events + deep-link to /admin/reports/calls) · src/lib/changelog.ts + src/lib/changelog-current.ts (this entry). **No schema change. No new audit literals. No new cron registrations. No new API routes. No new pin tests** — the change is data-cap + copy-tune, fully covered by the page's existing render-path. **HIPAA scope unchanged:** same transcript scrubber, same last-4 phone mask, same noindex + role-gate. **Cost impact:** negligible (50-row Prisma fetch vs 10-row on the channel='CALL'-indexed createdAt-desc query — milliseconds difference). **Cross-session coordination:** ship landed during high-contention window (concurrent EN0005, MX0005, DG0005 ships from parallel sessions racing on changelog.ts); used Python atomic prepend + pathspec-form commit + 2-file scope (page + changelog only) to avoid edit-war with sister D8 portal-port work in same repo. **Reviewer-feedback close:** PATCH agent endpoint with {action:'done', sha:'', autoFixVersion:'v2.97.RT0005'} after push lands. [hipaa-pre-cutover][reviewer-feedback-close][isabella-voice-page-recent-calls-cap-bump][copy-tune-call-persistence-hint][deep-link-to-reports-calls][version-letter:RT0005][cadence-override: reviewer-feedback agent-actionable fix — Mariane test-call findability regression]
v2.97.NX0005
2026-05-31Production

Patients can now upload most common file types when sending us their medical records or visit attachments.

What this means for you

Patients can now upload most common file types when sending us their medical records or visit attachments. Word docs, spreadsheets, TIFF scans, WebP/GIF/BMP screenshots, and SVGs all work alongside the PDFs and photos we already accepted. We still hard-reject programs, scripts, web pages, archives, and disk images for security — those have never been useful medical records anyway. The error message also lists what we accept now so patients know what to send instead.

Show technical details

Changed

  • 📄 **NX0005 — expanded patient-upload MIME allowlist (Doug 2026-05-31: "patients should be able to upload most file types in case they have screen shots or otherwise of their medical records").** Older patients send what they have — phone screenshots saved in varied formats, Word docs from a previous doctor, scanned multi-page docs as TIFF. Previous allowlist (PDF/JPEG/PNG/HEIC/HEIF) rejected too much. **New accepted set:** PDF · images (JPEG/JPG/PNG/HEIC/HEIF/WebP/TIFF/GIF/BMP — all route through sharp pipeline, EXIF-stripped + JPEG-normalized) · SVG (XSS-checked for inline