Skip to content

Green Wellness

Changelog

What’s new in each release of the scheduling platform

Show:
v2.97.VD0005current
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. 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. 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. 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). 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). 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. 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