Skip to main content

MVP Launch Checklist

Last updated: 2026-06-04 — Structural re-org: domain-first, priority as inline badge (closes #1638). Content unchanged from the 2026-06-04 audit pass; only the grouping changed. Prior phase-first organization (P0/P1/P2 as top-level sections) is preserved verbatim in docs/_archive/mvp-launch-checklist-phase-first-2026-06-04.md. 2026-06-04 audit pass: §9 picked up promotion-flow CI guard (#1559), terraform-apply manual-only (#1577), and DEMO_MODE prod-guard CI check (#1510); §9 ECS cutover refs now also cite #1101 / #1303 / #1529; new RAG/KB section for Haystack (ADR-024 Accepted 2026-06-02, supersedes ADR-004 + ADR-009 RAGflow). 2026-06-01 baseline preserved: billing rail switched Orb → Lago — self-hosted (Railway for dev; AWS for staging + prod — see #1287) per #702 / #766 / #768; Resend domain verification closed (#146 resolved 2026-06-01 by E); identity bug closed (#677); priority shuffles per Eusden 2026-06-01 — Slack Official App submission (#1159) P1 → P0, Specialist prompt guardrails (#1194) P0 → P2, Veena mobile layout (#1210) P0 → P2.

Previously: 2026-05-25 — doc-hygiene pass: dropped obsolete WhatsApp-pool P1 item; added "Shipped since 2026-05-23" stanza below capturing #589, #621, #636, #642, #634, #623, #624, #604, #605, #600, #647.

Audit reconciled 2026-05-23. The 2026-05-22 PM-persona audit (archive/MVP_CLIENT_CHANNELS_AUDIT_2026-05-22.md) flagged 8+ MVP-blockers. The headline three have shipped:

  • #550 — outbound dispatch on Expert release for WhatsApp/Slack/Telegram → ✅ landed in PR #567 (ChannelDispatcherService).
  • #551 — plaintext integration_credentials → ✅ landed in PR #570 (decrypt on every read site).
  • #554 — Resend dispatch no-retry → ✅ closed completed.
  • #576 — auto-respond outbound dispatch (the agent_reply path mirror of #550) → ✅ landed in PR #621.

Remaining audit findings (hardcoded email subject, Expert workspace media rendering, channel badge gating, DB-backed DLQ) are tracked separately and are not launch-blocking under the revised launch channel scope below.


How this doc is organized

Each item carries an inline priority badge: 🔴 P0 (launch-blocking) · 🟡 P1 (ship before first client) · 🟢 P2 (post-launch month 1) · ⚪ P3 (later).

Items are grouped by domain, not by phase, so the question "what's launch criteria for billing?" (or channels, or onboarding) has one section to read. Each domain section starts with status + remaining P0 count so you can scan blockers at a glance.

Definition of launched lives at the bottom. Launch-day sequence + env-var audit also at the bottom.


Shipped since 2026-05-23 (doc refresh)

These landed on dev after the last full doc refresh and are reflected in the checkboxes below — listed here so readers don't have to diff git log against the doc date:

PRLandedWhat
#5892026-05-22#362 Phase B — per-Org sandboxed agent runtime: Railway provisioner with adapter pattern. Closes the last code-side P0 gap from the previous Implementation Status revision. ~25 follow-up fixes hardened idempotent teardown, Postgres advisory-lock serialization, agent-secret forwarding, and health-probe rotation. Currently in soak.
#6212026-05-23Closes #576 — auto-respond replies now hit ChannelDispatcherService so the agent_reply outbound path mirrors the Expert-release path (#550 / #567).
#6362026-05-24#631 — post-onboarding per-Org Specialist edit + rate-history audit table.
#6422026-05-24Closes #556 — Telegram outbound media attachments (image/audio/file).
#6342026-05-23Route refactor: /client/admin/specialists/*/client/specialists/*. Old paths redirect.
#6232026-05-23Client-side "Meet your Specialist" page wired to catalog profile.
#6242026-05-23AM "New Client" wizard P0 design cleanup.
#6042026-05-22AM "New Client" wizard: search Specialists by title.
#6052026-05-22AM "New Client" wizard: UI consistency polish.
#6002026-05-22Test-audit pass: fix 74 failing tests + add 33 hot-module unit tests.
#6472026-05-24Canonical issue-label taxonomy (52→30 labels) + CI enforcement.

Open P0 / mvp-blocker issues remaining (verified via GitHub API 2026-06-01): 0 with mvp-blocker. P0 punch-list under the locked channel scope is now: ECS migration (Infrastructure & Hosting, #701) · Lago billing (Billing & Revenue, #702 / #766 / #768) · AWS Secrets Manager (#830) · Kaito org provisioning (#1193) · Kaito KB seeding (#1195) · Specialist memory (#1197) · Pre-launch UAT (#1198) · Veena fast-path (#1208) · SLA breach alerting (#1209) · Official Slack App submission (#1159, promoted P1→P0 2026-06-01).


Launch-blocking channel scope (locked 2026-05-23)

Per product (E), MVP launches with these channels working inbound and outbound, text first:

ChannelTierWhy
Webchat (/client/chat, authenticated client portal + subdomain routing)P0Primary surface for client → Specialist + the only channel with Socket.io real-time.
WhatsApp (Twilio)P0Most-demanded customer channel.
Email (Cloudflare → Resend)P0Already fully working both ways; baseline.
Slack (events API)P0Required for paid clients. Outbound dispatch landed in #567.
TelegramP2Functional but not required to launch. Demoted from prior P1 framing.
TeamsP2Stub + credentials not configured (#122). Out of MVP scope.
WeChatP3XML parsing not implemented.

Media support (image/audio/file outbound + client composer) is P1, not launch-blocking. See #559.


Audit Summary

Code audit completed 2026-05-04 against the original P0 sections 1–8; refreshed 2026-05-10 (onboarding/email/WA), 2026-05-22 (PM audit findings filed as #550, #551, #554, #576), 2026-05-23 (audit blockers landed), 2026-05-25 (Kaito-lens scrub: strict P0/P1 definitions; sections 9–13 added; per-Org runtime ✅ landed via #696; full label-hygiene sweep — 95 open issues now satisfy exactly 1 priority + 1 type + 1 status), **2026-06-01 (Resend ✅ closed by E; identity-attribution #677 ✅ closed; billing rail switched Orb → Lago (self-hosted; Railway for dev, AWS for staging + prod — see #1287) per #702 / #766 / #768; priority shuffles: #1159 P1→P0, #1194 P0→P2, #1210 P0→P2).

StatusCount (of 65 P0 items)
✅ DONE52 (Onboarding + Core loop + Channels + Expert + Agent runtime ✅ + Resend ✅ + identity ✅)
⚠️ PARTIAL0
❌ TODO13 (Infrastructure & Hosting — ECS, plus Billing & Revenue — Lago)

Cross-channel ship-blockers from the PM audit (separate from the 46 above): all 4 mvp-blocker GitHub issues closed completed as of 2026-05-23 (#550, #551, #554, #576). Telegram demoted from P1 to P2 — no longer launch-blocking.

2026-05-08 delta: Pre-launch QA revealed AM setup UX blockers (PR #263); prod build broken since May 1 due to duplicate DTO field (PR #265, now fixed). DEMO_MODE=false set in Railway.

2026-05-09: Production API deploying successfully. Agent service running. Full QA re-run in progress.

2026-05-10: Onboarding rebuilt as 3-step SPA (PRs #267–271). Email channel hardened end-to-end (Cloudflare worker → API routing for @dev.h852.work and @h852.work, subdomain TO normalization, threading fallback for replies without In-Reply-To). WhatsApp channel: heldForReview respected (no auto-reply when expert review pending), media/attachments passed through, 4096-char reply truncation, boot guard on missing TWILIO_AUTH_TOKEN. Billing page now shows trial state instead of "Free plan". Deactivated-org guard added on email + WhatsApp inbound paths. R2 buckets confirmed across all envs. Railway dev stale-build issue fixed via environmentTriggersDeploy mutation.

Remaining ops items (not code):

(All previously listed ops items are resolved as of 2026-06-01. DKIM/SPF for h.work verified in Resend — see Channels → Email below.)

Resolved 2026-05-10:

  • Onboarding multi-page wizard with channel/teammates/billing steps → ✅ Rebuilt as 3-step SPA (Workspace Setup / Your h.work Team / Done) at single URL /onboarding/start?token=…. Email field disabled-input, inline [input].h852.work slug, flat sorted timezone list, AM email surfaced on team step. Continue → /client/chat (auto-authenticated). Idempotent re-visits supported (PRs #267–271).
  • validateInvitationToken 410s when org already active → ✅ Allows re-fetch when invitation accepted; org/specialist/slug/profile fetches succeed for active orgs.
  • Email worker stale subdomain routing → ✅ Cloudflare worker deployed: @dev.h852.work → dev API, @h852.work → staging API. Worker sends base64 RFC822; API parses with mailparser. Dev specialist emails fixed (atlas|nova|bob@h852.work).
  • Subdomain TO normalization → ✅ atlas@dev.h852.workatlas@h852.work for DB lookup; pre-normalization noisy check deferred until after mailparser extracts body.
  • Auto-reply from unverified subdomain → ✅ Auto-reply uses verified email domain, not dev.h852.work.
  • Welcome email referenced removed steps → ✅ "Invite your team" + "Set up billing" stripped from next-steps. Templates redesigned: no emojis, dark navy header, consistent CSS vars.
  • Threading fails without In-Reply-To → ✅ Fallback reuses most recent open conversation for sender+org pair.
  • WhatsApp auto-replies even when expert is reviewing draft → ✅ heldForReview respected on email + WA paths.
  • WhatsApp drops media attachments → ✅ Media passed through to sendMessage; media-only inbound gets [Media message] placeholder.
  • WA reply > 4096 chars rejected by Twilio → ✅ Truncated client-side before send.
  • Missing TWILIO_AUTH_TOKEN silently fails → ✅ Startup boot guard logs warning.
  • Billing page shows "Free plan" for trial orgs → ✅ Now shows "Trial — N days remaining" / "Trial expired".
  • Deactivated org email still creates conversations → ✅ Returns org_inactive + auto-reply "This workspace is no longer active". WhatsApp checks before creating any conversation.

Resolved since 2026-05-08:

  • DEMO_MODE=true in production → ✅ Set to false via Railway API 2026-05-08
  • Prod build broken (TS2300 duplicate avatarUrl) → ✅ Fixed PR #265, deployed 2026-05-08T12:40 UTC
  • CLOUDFLARE_EMAIL_WEBHOOK_SECRET not set → ✅ Worker deployed; HMAC verification active on inbound webhook.

Previously listed code blockers — all resolved in code:

  • WhatsApp /channels/whatsapp/status missing → ✅ Implemented with HMAC verify (channels.controller.ts:1217)
  • Email inbound no HMAC → ✅ Implemented via CLOUDFLARE_EMAIL_WEBHOOK_SECRET (channels.controller.ts:406)
  • Internal notes client-side only → ✅ DB-level filter at conversations.service.ts:398
  • Vercel hardcoded to staging API → ✅ vercel.json has no hardcoded URLs; uses NEXT_PUBLIC_API_URL env var
  • JWT_SECRET dev fallbacks → ✅ Boot guard in auth.module.ts; notifications also fixed

What We're Shipping

A named AI specialist (e.g. "Bob") sits inside a client's existing communication channels. Clients message Bob via WhatsApp, email, or the portal. Bob's AI agent drafts responses. Domain experts review, edit, and approve. Over time Bob gets smarter. Clients get work done without managing people.


Launch criteria by domain

Jump to: Onboarding · Core Conversation Loop · Channels · Expert Workflow · RAG / KB · Agent Runtime · Infrastructure & Hosting · Security & Secrets · Billing & Revenue


Onboarding

Status: ✅ Complete. AM → Client invite + onboarding wizard fully wired. Remaining P0: 0.

The Account Manager flow and client onboarding wizard are the entry point for every client. Nothing else works without these.

🔴 P0 — AM Setup

  • AM can create a new org — company info, slug, domains, invite email POST /orgs at organizations.controller.ts:181 (createAmOrg) accepts name/slug/industry/corporateDomains/adminEmail/timezone; UI at frontend/src/app/ops/clients/new/page.tsx. Slug + corporate-domain validation present.
  • AM can assign a specialist — at least one specialist available in pool that is not already fully assigned; "Available" badge is truthful POST /am/orgs/:orgId/specialist at organizations.controller.ts:361; UI at frontend/src/app/ops/clients/[id]/setup/step-2/page.tsx. Specialists returned with currentOrgAssignmentStatus ("available"/"assigned") so the badge reflects actual assignment state.
  • AM can send client invite — email delivered with working OTP link (Resend configured, DKIM passing) POST /am/orgs/:orgId/invite at organizations.controller.ts:266 calls EmailService.sendOnboardingInviteEmail (email.service.ts:101) via Resend; 32-byte token + bcrypt-hashed 6-digit OTP. DKIM domain verification is operational config (verify in Resend dashboard).
  • Invite token expiry works — expired tokens show clear error, not crash onboarding.service.ts:74 validateToken throws GoneException (HTTP 410) past 7d; confirmOtp:137 throws Unauthorized past 10min OTP TTL; accept-invite/page.tsx:75 catches 410 with friendly UI.

Verify: Create a brand-new org end-to-end in staging. AM → new org → assign specialist → send invite. Confirm email arrives.

🔴 P0 — Client Onboard

  • OTP email arrives and works — client clicks link, enters code, sets password accept-invite/page.tsx flows: validate token → enter 6-digit OTP → verifyInviteOtp issues JWT (lines 39–59); OTP delivered via email.service.ts:sendOtpEmail (line 443) using Resend.
  • Onboarding wizard completes — all 5 phases reachable without dead ends; no raw JSON errors shown frontend/src/app/onboarding/step/[step]/page.tsx routes steps 2–6 (slug, specialist, whitelist, invite teammates, complete); each backed by an endpoint in onboarding.controller.ts. Unknown-step fallback at lines 46–62 prevents dead ends.
  • [~] Slug is confirmed and live{slug}.h.work resolves correctly after phase 2.2 Backend slug-reservation.service.ts reserves transactionally and is immutable once set; CORS allows *.{HWORK_DOMAIN} (main.ts:40). However frontend/vercel.json has no rewrites/domains config and Vercel project must be configured to attach the wildcard — DNS + Vercel domain attach not visible in repo.
  • Client lands in portal — correct redirect to /client/chat after onboarding complete AuthContext.tsx:getRoleRedirect (lines 72–79) returns /client/chat for owner/admin/org_admin/customer; client/page.tsx redirects /portal/client/chat.
  • Welcome email delivered — Resend sends onboarding confirmation email.service.ts:sendWelcomeEmail (lines 245–355) called non-blocking from onboarding.service.ts:completeOnboarding; also sends sendAmCompletionNotification to assigned AM.

Verify: Full Phase 2 onboarding from invite link → wizard → portal landing. No broken steps.

🟡 P1 — AM polish

  • Specialist availability correctly reflects assignment status (no misleading "Available" badge)
  • AM can reassign specialist to a different org
  • Raw JSON errors replaced with human-readable messages throughout AM flows
  • Trial end date populated for all orgs (not just some)

🟡 P1 — Client polish

  • Client can invite teammates during or after onboarding
  • Client can see conversation volume chart (even if mocked)
  • "Leave workspace" self-service works (issue #148)
  • Accent color (--accent) resolves to Claret #7E1D1D on deployed instances (currently showing blue)

🟢 P2 — Onboarding extensions

  • Business context upload during onboarding (KB ingestion)

Core Conversation Loop

Status: ✅ Complete. Client → AI draft → (optional Expert review) → reply path is live end-to-end. Remaining P0: 0.

The core loop. If this doesn't work, nothing matters.

🔴 P0 — Send a Message and Get a Response

  • Portal chat sends messagesPOST /conversations/:id/messages returns 200 conversations.controller.ts:98–110 returns 201; JWT-guarded; throttled 100 req/min/org. Frontend calls sendMessage() in frontend/src/lib/api.ts:395.
  • AI agent responds — OpenRouter/OpenAI configured; agent generates a reply conversations.service.ts:470–499 calls agentClient.chat() over AGENT_SERVICE_URL; OPENAI_BASE_URL=https://openrouter.ai/api/v1 documented in .env.example:168. Fail-open stub returns flagForReview=true if agent unreachable.
  • Response appears in UI — polling or SSE delivers agent message to client without page refresh Socket.io agent_message_sent emitted in notifications.gateway.ts:172–187; consumed by useNotifications hook (line 128) with 8s polling fallback (line 30).
  • CORS fixed for Vercel preview deployments — PR #244: dynamic *-humanityprotocol.vercel.app URLs now allowed in non-production (was causing blanket 500s)
  • [~] Message attributed correctly — client sees "Bob" (specialist persona), not agent internals UI resolves specialist firstName from conversation.specialistId (PortalChat.tsx:527, 560–562) with fallback to conversation.agentName. Note: message.role enum is "user"|"agent"|"expert" — there's no specialistId denormalised on the message itself, so attribution depends on conversation-level lookup.
  • [~] AI vs expert label visible — client can see whether response was AI-generated or expert-reviewed Distinguished by message.role ("agent" vs "expert"); no explicit isAiGenerated / reviewedByExpert flag. riskLevel is stored on Message and confidence/flagForReview in metadata, but the client UI surfaces the role-based label only — verify it actually renders an "AI" vs "Expert" pill in PortalChat.
  • Prompt injection false-positives fixed — PR #244: Bob no longer rejects greetings like "Hi Bob" as prompt injection; specialist persona header injection also fixed

Verify: Log in as client, open new thread, send message, confirm reply arrives within 30s.

✅ Closed — Identity & attribution bugs (Kaito loop integrity)

Closed 2026-06-01#677 is closed. No outstanding launch-blocking identity/attribution bugs at this time. New regressions get filed under the standard bug flow.

  • #677 — Expert queue shows client as WhatsApp display name "E" instead of "Amy from Acme"; Specialist mis-attributed. Closed.

Channels

Status: ✅ Code complete on all P0 channels. Outbound media is P1, not launch-blocking. Remaining P0: 0 in code. Operational gates (Meta WABA limit increase, Slack official-app submission #1159) tracked under Infrastructure & Hosting / Slack respectively.

Channel scope locked 2026-05-23 (see table at top): Webchat + WhatsApp + Email + Slack are P0; Telegram is P2; Teams/WeChat are out of MVP scope.

🔴 P0 — WhatsApp End-to-End

  • Inbound webhook live+185****2888 routes to https://api-staging-bd3f.up.railway.app/channels/whatsapp/inbound channels.controller.ts:732 @Post("whatsapp/inbound") routes via routeTwilioWhatsApp(). Twilio number→Railway URL mapping is operational config (verify in Twilio console).
  • Sender whitelist enforced — whitelisted numbers get routed; unknown senders get auto-reply channels.controller.ts:796–811 queries WhatsAppSenderWhitelist; on miss, sends rate-limited auto-reply via Redis (24h gate, whatsapp.service.ts:300–308). Admin UI at whatsapp-org.controller.ts:26–60.
  • TWILIO_SUBACCOUNT_TOKEN set in Railway — outbound sends don't silently fail Confirmed set on Railway envs 2026-05-10. Boot guard added: API logs a startup warning if TWILIO_AUTH_TOKEN is missing so silent-fail mode is observable. Code reads it correctly at whatsapp.service.ts:265–273 for sends and channels.controller.ts:741 for HMAC verify.
  • Outbound reply delivered — agent response is sent back via WhatsApp to the sender whatsapp.service.ts:260–290 sendMessage() posts to Twilio with auth; channels.controller.ts:831–835 invokes after agent reply.
  • Status callback working — delivery receipts hitting /channels/whatsapp/status Handler implemented at channels.controller.ts:1217 with HMAC-SHA1 Twilio signature verification, structured logging, and audit trail.
  • Message deduplication working — duplicate Twilio webhooks don't create duplicate messages (Redis dedup key) channels.controller.ts:765–776 MessageSid dedup with 1h TTL via getRedisClient().

Verify: Send WhatsApp message from a whitelisted number to +185****2888. Confirm message appears in conversation. Confirm reply arrives on WhatsApp.

🔴 P0 — Email End-to-End

2026-06-05 architecture update (PR #1722, closes #1060): Gmail API polling + DWD-impersonated outbound is now the primary Specialist email path. The Cloudflare Worker + Resend outbound flow is retained as a fallback during a 2-week prod soak (decommission tracked in #1440 / #1441). Resend is otherwise reserved for invites/onboarding/transactional via email/email.service.ts.

  • Gmail API inbound polling (PRIMARY) — Gmail API polling via cron (*/30 * * * * * on Railway dev) reads each Specialist mailbox using ONE Google Workspace service account with domain-wide delegation. Shipped 2026-06-05 via #1722.
  • Gmail API outbound via DWD (PRIMARY) — Outbound replies sent from the Specialist's gsuite mailbox via DWD impersonation; no per-Specialist OAuth tokens. Specialist mailboxes provisioned automatically by organizations.service.ts enqueuing WORKSPACE_JOB_CREATE on Specialist↔Org assignment; processor calls Admin SDK users.insert. Shipped via #1722.
  • MX records → Googleh.work, h852.work, h853.work all MX-routed to Google Workspace; inbound delivery for the Gmail path is live.
  • 2-week prod soak in progress — Cloudflare Worker fallback retained until #1440 / #1441 close. Verify Gmail polling latency + RFC 2822 Message-ID / historyId observability follow-ups (#2064, #2065).
  • Cloudflare Email Routing (FALLBACK) — Cloudflare email worker deployed 2026-05-10 still routes @dev.h852.work → dev API, @h852.work → staging API. Worker sends base64 RFC822; API parses with mailparser at channels.controller.ts:303 @Post("email/inbound"). Subdomain TO normalization applied for DB lookup. Decommission after soak.
  • Inbound webhook authenticated (FALLBACK path) — HMAC verification via CLOUDFLARE_EMAIL_WEBHOOK_SECRET at channels.controller.ts:406–455 against HMAC-SHA256 of raw body.
  • Email whitelist enforced — only whitelisted corporate domains can open conversations resolveOrgFromEmail at channels.controller.ts:417–482 checks specialist (TO) first, then whitelist (FROM): exact email match, then domain match. Non-whitelist senders get auto-reply + drop.
  • Resend transactional outbound configuredRESEND_API_KEY in Railway; sending domain verified email.service.ts:29 reads RESEND_API_KEY with graceful no-op fallback. Resend is no longer the Specialist conversation channel — reserved for invites/onboarding/transactional email and auto-replies on the fallback path.
  • Per-OSA Gmail Send (ADR-0002 Phase 2) — superseded by DWD service-account model — Phase 2 originally shipped via #1095 (2026-05-29); inbound dual-read primary path (#1093); refresh token encryption (#1065 / #1079). Per-OSA OAuth tokens were replaced 2026-06-05 by the single Google Workspace service account + DWD model (#1722).
  • DKIM passing — outbound emails from @h.work don't land in spam Resolved 2026-06-01 by E. DKIM/SPF/return-path verified in Resend dashboard. See "Resend domain verification" closed-section below.
  • Threading works — reply-to-email stays in same conversation thread (In-Reply-To header) Inbound parses In-Reply-To at channels.controller.ts:329–344; routing maps it to externalThreadId (lines 603–610). Outbound preserves In-Reply-To + References (lines 697–700) and strips "Re:" from subject (line 621). Threading fallback (PR 2026-05-10): replies without In-Reply-To reuse the most recent open conversation for the sender+org pair.
  • heldForReview respected on auto-replies — when agent draft is held for expert review, no auto-reply is sent Verified on both email and WhatsApp paths 2026-05-10.
  • Deactivated-org guard — emails to a deactivated org return org_inactive and auto-reply "This workspace is no longer active" Email and WhatsApp both check Organization.status === 'deactivated' before creating any conversation.

Verify: Send email to bob@[org-slug].h.work. Confirm it creates a conversation. Reply from portal. Confirm reply arrives as email in correct thread.

✅ Closed — Resend / Email Sender Domain Verified

Closed 2026-06-01 — domain verification resolved by E. #146 closed. Retained for historical context; no remaining launch gates.

  • h.work verified in Resend (DKIM, SPF, return-path) — resolved 2026-06-01
  • onboarding@h.work is the From: header on invite + OTP + welcome emails (not hptestingsite.com or dev.h852.work)
  • DKIM passes on inbox-providers test (Gmail, Outlook, ProtonMail)

🟡 P1 — WhatsApp ops

  • Remaining numbers (+198****2905, +198****4124) registered and ONLINE after Meta WABA limit increase approved
  • Display name "Bob [h.work]" approved by Meta and visible to recipients
  • Pool routing works when sender messages different pool numbers across sessionsdropped 2026-05-25: pool model superseded by single-number-per-env routing in PR #213 (2026-05-03); item is obsolete under current architecture.

🟡 P1 — Other channels

  • Slack adapter unblocked — credentials configured for at least one test workspacepromoted to P0 (2026-06-01) via #1159 Official Slack App submission; tracked in this section's P0 column once submitted.
  • Channel health dashboard visible to superadmin

🟢 P2 — Channel extensions

  • Microsoft Teams adapter unblocked

Expert Workflow

Status: ✅ Code complete. Expert queue, draft edit/send/resolve, internal notes, and per-Org filtering all live. Remaining P0: 0.

🔴 P0 — Expert Workspace

  • Expert can log inexpert1@humanwork.dev (or real expert) reaches /workspace/queue AuthContext.tsx:74 maps expert/workspace/queue. Seed user david.kim@h852.work (Expert) — see api/src/database/seed.ts and api/src/auth/auth.service.ts:267 for the canonical demo-login mapping.
  • Queue shows inbound items — messages from clients appear with risk level and AI draft expert-queue.controller.ts:35–51 GET /expert-queue; UI at frontend/src/app/workspace/queue/page.tsx:122–148 renders risk + item.message?.confidence.
  • Expert can edit and send — edit AI draft, click send; message delivered to client POST /expert-queue/:id/respond (expert-queue.controller.ts:74–89); persists with role='expert' (expert-queue.service.ts:115–126). Edit mode in ExpertWorkspacePane.tsx:79–128.
  • Expert can write from scratch — clear draft, write manually, send Edit mode lets expert blank editText and submit a fresh response (ExpertWorkspacePane.tsx:116–128); fallback CTA "Compose a fresh response in the Client View" at line 146.
  • Expert can resolve ticket — ticket moves out of queue on resolve POST /expert-queue/:id/resolve (expert-queue.controller.ts:91–104); sets status='resolved' + resolvedAt/resolvedBy (expert-queue.service.ts:99–150).
  • Internal notes invisible to client — notes marked is_internal filtered at DB level conversations.service.ts:386–399 checks caller role; non-internal roles get andWhere('msg.is_internal = false') at query level, not just UI.
  • Ticket assigned correctly — conversation stays with the right specialist/expert after assignment expert-queue.service.ts:listPending (lines 50–87) joins org_experts.orgId so experts only see queues for orgs they're assigned to.

Verify: As expert, pick up a queued item, edit the draft, send, resolve. Confirm client sees the message.

🟡 P1 — Expert polish

  • Expert profile page exists — name, bio, skill tags
  • Queue auto-refresh works reliably (no stale items)
  • Resolved tab shows resolved items correctly
  • Analytics page loads without errors

🟢 P2 — Expert extensions

  • Push notifications for expert escalations (E-P3)
  • Native mobile app for experts

RAG / KB

Status: Haystack 2.x + Hayhooks is the canonical RAG runtime (ADR-024 Accepted 2026-06-02). Backend live in dev + staging + prod; one env-var hygiene item remains. Remaining P0: 1 (RAGFLOW_* env-var purge).

Added 2026-06-04 from the prior phase-first §14. ADR-024 was Accepted 2026-06-02; Haystack/Hayhooks is the canonical RAG runtime for h.work, superseding ADR-004 and ADR-009 (RAGflow). Implementation propagated across docs in PR #1455. Tracked under #1280 / haystack-deploy runbook.

🔴 P0 — Haystack canonical RAG

  • ADR-024 Accepted — Haystack 2.x + Hayhooks is the live RAG runtime (Railway for dev; AWS ECS Fargate for staging + prod via humanwork-rag Cloud Map service); all live docs reference Haystack, RAGflow callouts retained only as tombstones (see docs/api/reference.md:246-250).
  • KB search endpoint for Expert workspacePR #1526 (backend) + #1528 (UI panel).
  • Production Haystack/Hayhooks deployment — staging + prod run on AWS ECS Fargate as the humanwork-rag service (#1527 Terraform; #1541 rag-deploy.yml). Dev remains on Railway via rag/railway.toml.
  • RAGFLOW_* env vars purged from prod — only HAYSTACK_* / HAYHOOKS_* remain; legacy compatibility tombstones documented.

Agent Runtime

Status: ✅ Per-Org sandboxed agent runtime landed 2026-05-25 via #696 (RIG runtime hard-cut). 2026-06-03 update: Per-org Railway runtime kind (railway_per_org) retired via ADR-028 + #1584 / #1450. Supported runtime kinds are now shared_railway (default) and ecs_per_org (premium). ECS prod cutover tracked in #701. One nightly smoke-test soak still TBD but not launch-blocking. Remaining P0: 0.

Landed 2026-05-25 via PR #696 (RIG runtime hard-cut). #362 closed. Manifest-backed per-org runners (runner_manifests/runner_registrations), sandbox workers, no shared ambient runtime, no broad runner vendor secrets. 1:1 Org ↔ runtime supported; the Org×Specialist v2 path (#401) is structurally enabled via optional specialist_id on manifests/registrations/runs.

🔴 P0 — Per-Org Sandboxed Agent Runtime

  • 1:1 Org ↔ Agent runtime container — landed in #696
  • Per-Org OpenAI key / tool-permissions scope — landed in #696
  • Routing in ConversationsService.callAgent() picks the right runtime by org_id — landed in #696
  • Cross-org isolation enforcement (DB + runtime) — ADR-020 + Org×Specialist classification shipped 2026-05-29 via PR-A..E (#1148#1153); expert_agent_messages dropped (PR-E #1153); audit fields populated by every write site (#1185 / #1187 closed remaining gaps 2026-05-31).
  • Smoke: cross-org isolation test passes nightly (#237 — tracked under #947 isolation epic; code-side enforcement landed via #981 + #1024, nightly soak still TBD)

Infrastructure & Hosting

Status: ❌ Production cutover to ECS is the largest remaining P0 block. Railway is fine for dev; staging boot validated; prod bootstrap, secrets manager, webhook repointing all still TODO. Tracked in #701. Remaining P0: 7 (ECS provisioning + deploy + secrets + DB + webhook repointing × 3).

🔴 P0 — Production hosting baseline (Railway today)

  • Railway production env deployedapi-production-f95e.up.railway.app serving latest main branch railway.json configures DOCKERFILE builder + /health healthcheck. Confirmation that the URL serves latest main is operational, not visible in repo.
  • Vercel frontend deployed — env vars correct frontend/vercel.json has no hardcoded API URLs. api.ts:6 uses NEXT_PUBLIC_API_URL ?? 'http://localhost:3000'. Production URL set via Vercel dashboard env vars (see .env.production.example).
  • All required env vars set in production — no undefined configs causing silent failures (see env var audit below) .env.example is comprehensive but actual Railway production values are not visible in repo. Per the audit table at the bottom, DEMO_MODE is currently true and must be flipped; STRIPE_SECRET_KEY/SLACK_SIGNING_SECRET need verification.
  • Database migrations run — no pending migrations on production DB 38 migrations under api/migrations/; data-source.ts:67 sets synchronize: false and migrationsRun: false (explicit, manual). app.module.ts:133–135 keeps synchronize off in production. Operational confirmation needed that npm run typeorm:run was executed against prod DB.
  • Redis connected — session dedup, rate limiting, auto-reply gating all functional api/src/common/redis.util.ts:getRedisClient() builds an ioredis client from REDIS_URL; returns null when unset and callers degrade gracefully.
  • RLS enforced — no cross-org data leakage (verified by attempting cross-org API calls) OrgRlsInterceptor registered globally at app.module.ts:180 via APP_INTERCEPTOR; sets app.current_org_id per request. Note that DB-level RLS policies are still partial (per CLAUDE.md, full DB RLS tracked in issue #3); application-layer enforcement is in place.
  • DEMO_MODE=false in production — confirmed flipped in Railway 2026-05-08 Code is correctly gated: auth.controller.ts:84–93 blocks /auth/demo-login when NODE_ENV==='production' OR DEMO_MODE!=='true'.

🔴 P0 — Production Hosting on AWS ECS (cutover)

Added 2026-05-25 (Kaito-lens). Railway is fine for dev; staging + prod target AWS ECS Fargate. Terraform + GitHub Actions landed #1101; staging boot validated #1303; prod bootstrap #1529; CI promotion enforcement #1559; manual terraform-apply #1577; rag-deploy workflow #1541. Tracked in #701 (owner: Emanuele).

  • ECS infrastructure provisioned for h.work — same pattern Emanuele built for HP recruiter; spin-up in flight as of 2026-05-25 sync. Staging/prod cutover tracked under #1101 / #1303 / #1529.
  • API + Agent + Frontend deployed to ECS — not Railway — for the production environment Kaito will use.
  • Production secrets in AWS Secrets Manager — not plain Railway env vars (also see Security & Secrets).
  • Production database (RDS) migrated and reachable from ECSsynchronize: false, migrations run, backups configured.
  • Twilio webhook base URL repointed to the ECS production URL.
  • Cloudflare email worker repointed to ECS production.
  • Slack events URL repointed to ECS production.
  • Promotion flow CI-enforced (dev → staging → main) — landed via PR #1559 on 2026-06-03; the guard workflow is a required status check that rejects PRs to main not originating from staging (and to staging not from dev). See docs/promotion-flow.md. Mandatory once ECS prod is live so emergency hotfixes can't bypass review.
  • terraform-apply is manual-only (workflow_dispatch) — landed via PR #1577; no infra change applies on push. Required for the ECS cutover so a runaway PR can't reshape the prod cluster.
  • DEMO_MODE production guards + CI checkPR #1510 added a CI guard + docs that fail-closed prevent DEMO_MODE=true from leaking into a production deploy. Pairs with the env-var audit row below; supersedes the 2026-05-08 one-off Railway flip.

Verify: Send a WhatsApp from Kaito's number to the production WhatsApp number → message appears in Veena's queue on the production frontend → Veena replies → Kaito gets the reply, all on ECS, with no Railway URL in the chain.


Security & Secrets

Status: ⚠️ Two operational items remain (Twilio token rotation, per-env MASTER_ENCRYPTION_KEY check). HMAC verification fully wired on every P0 webhook. Remaining P0: 2 (Twilio rotate + MASTER_ENCRYPTION_KEY per-env).

🔴 P0 — Critical Security

  • Twilio main auth token rotated — old token (fc59a4...) invalidated; shared in plaintext in chat Token doesn't appear hardcoded anywhere in repo (grep fc59a4 returns only this file). Rotation is a Twilio console action — confirm out-of-band that the leaked token was invalidated and Railway has the new value.
  • [~] HMAC verification on all webhooks — WhatsApp, Slack, email all reject unsigned requests Slack: verifySlackSignature (channels.controller.ts:261–287) HMAC-SHA256 + timing-safe compare + 5min replay window. WhatsApp: Twilio validateRequest(). Telegram: HMAC-SHA256. Email inbound has none (see Channels → Email).
  • JWT secret is production-strength — boot guard in place auth.module.ts:15–28 throws at startup if NODE_ENV==='production' and JWT_SECRET is unset, shorter than 32 chars, or starts with dev-. notifications.module.ts and notifications.gateway.ts use no fallback (cfg.get('JWT_SECRET') directly).
  • MASTER_ENCRYPTION_KEY is unique per env — staging and production keys differ common/crypto.ts:14–23 validates 32-byte length but doesn't enforce per-env uniqueness. Verify out-of-band that staging and production Railway projects have distinct values; falls back to unencrypted mode silently if absent.
  • No seed credentials accessible in production — demo login endpoints return 403 in prod auth.controller.ts:84–93 double-gates demo-login on NODE_ENV and DEMO_MODE. Seed runner short-circuits in prod at seed.service.ts:70–71 (APP_ENV==='production' returns).
  • CORS locked downALLOWED_ORIGINS only includes production domains main.ts:42–56 enforces explicit origin allowlist + *.{HWORK_DOMAIN} regex; rejects unknown origins with explicit error. Demo Vercel origins only allowed when DEMO_MODE==='true'.

Billing & Revenue

Status: 🟡 Lago integration partially complete — secrets confirmed, LagoProvider active. Immediate gap: specialist_monthly plan bootstrap (PR #2933) plus Kaito sync. Tracked in #702 (owner: Alexander K). Remaining P0: 4.

Promoted to P0 on 2026-05-25 (Kaito-lens). 2026-05-29 rail switch: Orb → Lago (self-hosted, open-source — Railway for dev; AWS for staging + prod (AWS bring-up tracked in #1287))LagoProvider adapter on PR #1069; docs propagated in PR #1071. Stripe is the legacy direct path; post-cutover Stripe lives only inside Lago's PSP config. Kaito is a paying client — without Lago live we cannot invoice them, which means we cannot have a paying client, which means we are not launched. Tracked in #702 and #766 (owner: Alexander K). Supersedes #452.

🟡 P0 — Lago Billing Live

Status as of 2026-06-19 (Pablo investigation): LagoProvider is confirmed active on staging — ops/billing/customer returns real Lago UUIDs (created Jun 10). LAGO_API_URL + LAGO_API_KEY confirmed in AWS Secrets Manager for staging + prod. ECS task wired correctly. Immediate bootstrap gap: specialist_monthly plan was never created in Lago → all subscriptions have lagoSubscriptionId=null. Run lago:bootstrap (PR #2933), then sync Kaito. Mid-month proration ledger remains P2 for MVP because Kaito launches on a fixed monthly Specialist rate; promote #514 back to P0 if mid-cycle rate edits become launch scope.

  • LAGO_API_KEY (+ LAGO_API_URL for self-hosted) set in production ECS env (confirmed in AWS Secrets Manager, 2026-06-19)
  • LagoProvider active (real Lago customer UUIDs returned from staging /ops/billing/customer, not MockProvider)
  • specialist_monthly plan created in Lago and Kaito subscription synced — run lago:bootstrap via PR #2933 workflow for staging then prod, then trigger ops/billing/sync-customer/:orgId for Kaito
  • Lago-backed invoice surfaces in /client/settings/billing — Kaito's admin can see the current invoice
  • Legacy Stripe / Request Finance / plan-tier teardown (#768) — remove deprecated endpoints, DB columns, seed data, UI before cutover
  • Smoke: end-to-end invoice for one full billing cycle in staging Lago sandbox before pointing Kaito at prod

Verify: AM creates Kaito's org → Lago shows subscription created → invoice line items appear in Kaito's billing tab → first invoice cycle closes correctly in the Lago sandbox.

🟢 P2 — Billing extensions

  • Specialist monthly rate visible to client at onboarding Step 2 (PR #518 closes #498)
  • Lago invoicing pipeline live before first paying client (now tracked as P0 above via #702 / #766; supersedes #452 / #447; revenue structurally impossible until landed)
  • Mid-month proration ledger (#514)
  • Invoice history + download
  • Per-org cost tracking

Other P2 / P3 (post-launch)

Items that don't fit a single launch domain and aren't blocking the first paying client.

  • Client approval UI for high-stakes agentic actions (C-W8) — demoted to P2 (2026-05-23). For MVP we ask clients for approval manually via the existing conversation channel; the dedicated UI is post-launch.
  • SLA performance dashboard for clients
  • Outbound org webhooks

Env Var Audit (Production Must-Haves)

These must be set and correct in Railway production before launch:

VariableStatusNotes
DATABASE_URL✅ Set
JWT_SECRET✅ SetVerify it's not the staging value
MASTER_ENCRYPTION_KEY✅ SetMust differ from staging
OPENAI_API_KEY / OPENROUTER_API_KEY✅ SetVerify balance / rate limits
RESEND_API_KEY✅ SetVerify sending domain verified
REDIS_URL✅ Set
TWILIO_ACCOUNT_SID✅ Set
TWILIO_API_KEY + TWILIO_API_SECRET✅ SetPer-env key SK79b70e...
TWILIO_SUBACCOUNT_SID✅ SetACfc8480...
TWILIO_SUBACCOUNT_TOKEN✅ SetVerify not rotated
TWILIO_WEBHOOK_BASE_URL✅ Sethttps://api-production-f95e.up.railway.app
STRIPE_SECRET_KEY🗑️ LegacyDirect Stripe is deprecated; post-cutover Stripe lives only inside Lago's PSP config. Removal tracked in #768.
LAGO_API_KEY✅ Set (staging + prod)In AWS Secrets Manager for staging + prod (confirmed Emanuele, 2026-06-19). dev uses Railway Lago. Remaining: run lago:bootstrap on staging/prod + verify BillingModule off MockProvider (#702).
LAGO_API_URL✅ Set (staging + prod)In AWS Secrets Manager for staging + prod (confirmed Emanuele, 2026-06-19). dev uses Railway Lago endpoint.
SLACK_SIGNING_SECRET⚠️ CheckOptional outside prod (env fallback). In production each Slack-connected org provides its own signing_secret via integration_credentials per ADR-002 — env-var fallback gated to non-prod (#1219, 2026-06-01). BLK-004 unblocked.
DEMO_MODEfalseSet 2026-05-08; verified post-deploy
NODE_ENV✅ Should be production
ALLOWED_ORIGINS✅ SetVerify only prod domains
HWORK_DOMAIN✅ Seth852.work (staging) / h.work (prod)
CLOUDFLARE_EMAIL_WEBHOOK_SECRET✅ SetShared with Cloudflare email worker; HMAC verify active
R2_BUCKET_NAME✅ Sethwork-dev / hwork-staging / hwork (prod) — all confirmed exist 2026-05-10
R2_ACCOUNT_ID✅ SetConfirmed across dev/staging/prod Railway envs
R2_ACCESS_KEY_ID✅ SetConfirmed across dev/staging/prod Railway envs

Launch Day Sequence

  1. T-48h — Full staging smoke test: AM setup → client onboard → WhatsApp message → expert queue → resolve
  2. T-24h — Production env var audit; rotate any test secrets; confirm DEMO_MODE=false
  3. T-12h — Run DB migrations on production; verify no pending
  4. T-4h — Deploy main to Railway production + Vercel; smoke test production URLs
  5. T-1h — Send test WhatsApp to production number; confirm end-to-end
  6. T-0 — First real client invite sent by AM

Definition of Done

MVP is launched when:

  • An Account Manager can set up a real client org in production
  • That client receives an invite, onboards, and sends their first message via WhatsApp or portal
  • An expert sees it in their queue, reviews the AI draft, and sends a response
  • The client receives the response

Everything else is iteration.