Live at hubv2.apphappenventures.com — NestJS API + Next.js 15 on ECS Fargate (us-east-2). The unified human-in-loop queue shipped: reviewers approve, edit and send AI drafts from one screen, reading a single DynamoDB table (hv-hub-v2-development-tasks). Generation, review and send have all cut over from the VPS/n8n onto AWS.
The tasks-v2 review screen over DynamoDB, source-agnostic: sales (Gabrielle) + Robin in one surface, with the 6 reviewer actions and account-card grouping. Google OAuth gated to @happenventures.com.
Sales generation moved off the VPS → /generate → SQS → task_consumer Lambda → DynamoDB. Approve & Send is live; the message-dispatcher Lambda delivers each draft via the right channel adapter (Gmail / Slack / WhatsApp).
GOV-01 ALLOW / DEFER / BLOCK / DROP gate runs in the consumer with a per-contact touch log + frequency caps. Live in dev, fail-closed.
A legacy n8n “AWS Send Worker (Maestro) — DRY-RUN” is still active, polling the same approved queue as the live AWS send path. The AWS message-dispatcher sends for real (SEND_DRY_RUN=false) and, as last verified 2026-08-07, the Governor sendDevelopmentMode=false — so email reaches the real client today (setting sendDevelopmentMode=true would redirect every email to internal test inboxes instead; the older maestro_sender Lambda is inert: dry-run + EventBridge DISABLED). The live symptom is the n8n worker marking the shared task status=failed; but because email is already live, the deeper hazard is a double-send to the real client if the n8n worker also delivers — retire it.
Generation cut over fully: DynamoDB shows 100% of recent drafts from the AWS consumer (maestro-consumer), zero from the VPS, and no live cron, Lambda or code path still hits 187.77.29.73. The box has since been retired (#152) — it is no longer a single point of failure.
The Governor governance docs declared four pieces “Phase 2 pending” that are in fact built and deployed (held tasks, resurfacer, send/approve hooks, live reads). A reader trusting only the docs underestimates the real state.
Prefix /api. Domains: queue (incl. the source-agnostic QueueActionsService), governor, ai-tasks, hubspot, tickets, meetings.
App Router, Tailwind v4, TanStack Query, Zustand. The tasks-v2 review screen is the queue UI.
claude-sonnet-4-6, SSE streaming for chat. The Maestro + Hermes agents reason with Bedrock Claude.
hv-hub-v2-development-tasks — one table for TASK / GOVDECISION / TOUCH / TOUCHACCT / CONFIG / DEAL items. Robin + sales tasks both live here (source distinguishes the bot). HubSpot / Airtable still read for meetings & deal context.
Generation: /generate → SQS → task_consumer Lambda → DynamoDB. Send: API → SQS outbound-messages → message-dispatcher Lambda → per-channel adapter (Gmail / Slack / WhatsApp). Legacy n8n send worker being retired; the VPS is already retired.
ALB + ECR + Terragrunt CD. Cluster hv-hub-v2-development, services hv-hub-v2-{api,web}. CI on GitHub Actions / AWS CodeBuild runners.
All source-agnostic in QueueActionsService — one action surface, source distinguishes the bot. The primary button is renamed per task type (Approve & Send / Apply / Approve category).
| Action | What it does | Result |
|---|---|---|
| Approve & Send | Two-click confirm (#222). Fires the task’s callback_url / drops it into the send path; writes before/after to Audit_Log. | “Sent ✓” · auto-advance |
| Edit | Inline edit-manual → Save & Approve. The original draft is preserved; edited_content is the learning signal. Edit-with-AI is wired: POST /queue/:id/revise returns a Bedrock rewrite as a preview, saved via Approve revised. | edited & approved |
| Reject | Optional reason. Status → Rejected. | “Rejected ✓” |
| Snooze | Sets snooze_until (days, pinned to America/New_York); the scheduler resurfaces it. | “Snoozed ✓” |
| Comment | Appends to the thread (comments_thread); no status change. On the DRV/CMP (Robin) flows a Slack notify is enqueued to the dispatcher; on the sales surface it is state-only. | “Done ✓” |
| Escalate | Optional reason (DEV-471) + target. Most types reassign to a teammate (records the original assignee, requires escalateTo); driver_exception / cmp_complaint fire an urgent Slack #amazon alert via the dispatcher. | “Done ✓” |
Deliberate one-at-a-time review — no bulk actions or multi-select by design. The modal header shows “Task N of M” with back/next arrows; after a decision the UI flashes a confirmation and auto-advances to the next task. Comment, Escalate and Reject sit under the ··· overflow. Drafts below the confidence threshold (Governor confidenceThreshold, default 60, authoritative for all playbooks — the per-playbook SSOT threshold is deprecated for gating; the old spec cites 70) route to a separate needs_review manual-triage lane. “Call in Aircall” opens the contact for a click-to-call without resolving the task.
1. n8n “Auto Tasks” crons → POST /generate → SQS → task_consumer Lambda.
2. Consumer calls the Governor /check — only ALLOW writes a draft; DEFER writes a held task; BLOCK/DROP are suppressed.
3. Hub GET /api/queue?assigned_to=me reads DynamoDB; tasks-v2 shows pending + held, grouped by account.
4. Reviewer acts via QueueActionsService → Audit_Log + /record touch + send path.
1. Approved sales drafts drain via GET /api/queue/approved (DynamoDB resolved).
2. message-dispatcher Lambda → channel adapter (Gmail DWD send-as for email), with per-rep daily cap + 14-day recipient cooldown.
3. Today the dispatcher runs SEND_DRY_RUN=false and the Governor sendDevelopmentMode=false (as last verified 2026-08-07) — so email reaches the real client (flip sendDevelopmentMode=true to redirect to the Governor’s developmentTestEmails inboxes); the legacy n8n send worker is still active (dual-worker risk).
4. Cutover: retire the n8n worker so the message-dispatcher is the sole sender — email is already live to real clients (the VPS has already been retired, #152).
NestJS module apps/api/src/governor/. POST /governor/check runs the spec rules in order: hard-suppress → BLOCK; one governing family (WIN > POST-JOB > REVIVE > SC > GROW); frequency caps; per-account cap. Only ALLOW writes a draft.
POST /governor/record appends confirmed touches. Default caps 1/14d & 2/30d per contact, 1/week per account. Email and Aircall calls share one budget. Keyed by the consumer’s exact dedup id (c_<sha1>).
DEFER writes a real status='held' task (defer_until, held_reason) — deliberately not named deferred (a send-retry state). A resurfacer Lambda re-checks held touches when defer_until ≤ now (EventBridge rule disabled until cutover).
/{slug}/admin/governor edits caps, windows, tiers, precedence, cooldowns and the playbook→family map (RBAC governor:read / governor:update), stored as a CONFIG#governor#default item. Default fail-mode is fail-closed (#297).
| Question | Resolution | State |
|---|---|---|
| Where does the queue live? | One DynamoDB single-table; sales + Robin unified, source distinguishes the bot. | settled |
| “Approved → Send”: n8n or AWS? | Migrated to the AWS Unified Sender (message-dispatcher Lambda over outbound-messages); the interim maestro_sender poll Lambda is inert and the legacy n8n worker is being retired. | in cutover |
| Reads: Airtable or AWS? | Read & act on DynamoDB (#194 / #221). HubSpot & Airtable read only for live signals. | settled |
| Send caps / cooldown? | Per-rep daily cap + 14-day recipient cooldown in the send path; Governor adds per-contact / per-account caps. | settled |
| Contact frequency control? | The Contact Governor (GOV-01) gate — central ALLOW / DEFER / BLOCK / DROP. | live |
| Edit-with-AI rewrite? | Wired: the Bedrock rewrite is the preview endpoint POST /queue/:id/revise; the reviewer saves it via Approve revised. | settled |
| New task types (action / bookkeeping) + Aircall + new row fields? | Scoped in the Tasks Screen spec; consumer doesn’t emit the new fields yet. | scoped |
| Retire the VPS + remove SSOT Enabled (BO-25)? | Kill-switch is Status-only; consumer decoupled from Enabled. The VPS has been retired (#152); dropping the now-unused SSOT Enabled field is the only cleanup left. | partial |
POST /queue/:id/revise returns an in-place Bedrock rewrite (preview) with the same guardrails; the reviewer saves it via Approve revised. UI panel + endpoint both ship.
“Apply change” on approve (e.g. HubSpot deal-stage move) — needs a new write-back callback executor reusing the robin callback path.
“Approve category” for bank/credit-card transactions, likely fed by Frank / FIN flows.
hubspot_deal_id, contact_phone, based_on, risk_flags + bookkeeping fields — the consumer doesn’t write these yet.
Click-to-call exists; a logged call must hit the Governor touch budget (channel='call').
Retire the legacy n8n send worker so the message-dispatcher is the sole sender — email is already live to real clients (SEND_DRY_RUN=false, Governor sendDevelopmentMode=false; the VPS has already been retired, #152).
The old Airtable-polling senders are off. The AWS path is the message-dispatcher Lambda (Unified Sender over outbound-messages) — but a legacy n8n worker still polls the old approved queue.
| Worker | Where | Active | Source of truth | State |
|---|---|---|---|---|
| message-dispatcher | AWS Lambda | on | outbound-messages SQS (enqueued on approve) | Routes by channel; runs SEND_DRY_RUN=false and, as last verified 2026-08-07, the Governor sendDevelopmentMode=false — email reaches the real client (setting sendDevelopmentMode=true redirects every email to the developmentTestEmails inboxes). Per-channel send flags in the Governor config gate each channel (Slack on, WhatsApp off). |
| AWS Send Worker (Maestro) — DRY-RUN | legacy n8n | on | same approved queue | Polls the same rows (in dry-run) — today it marks tasks failed; now that email is live to clients it must be retired to avoid a double-send |
The cutover (ADR 0001, Path B) kept the n8n “Auto Tasks” crons as the schedule but re-pointed them off the VPS to an AWS API. The generation engine (Maestro) is a Lambda consumer behind SQS — drafts land in DynamoDB, gated by the Governor.
The n8n “Auto Tasks” crons fire on schedule and POST {playbook_id, view_id, mode, limit} to the /generate API — no longer to 187.77.29.73:8788.
task-producer enqueues to the ingest SQS queue. A DLQ + CloudWatch alarms (#246) cover failures; SNS relays to Slack #hv-alerts.
Pulls config from the SSOT, calls the Governor /check, drafts with Bedrock, dedups per contact, and writes pending_review / needs_review / held to DynamoDB.
The generation engine is the task_consumer Lambda (Maestro pipeline), decoupled from the Hub API by SQS — it can fail, retry and back-pressure without touching the API that reviewers depend on. The kill-switch is Status-only (per-playbook, read from the SSOT), so a single playbook can be stopped without redeploying. The Hub API owns the read/act surface (/queue + the Governor); the Lambda owns generation.
The unified queue, the 6 reviewer actions, Approve & Send and the Contact Governor are live in dev. To finish: retire the legacy n8n send worker so the message-dispatcher is the sole sender (email is already live to real clients), wire the Edit-with-AI rewrite endpoint, and add the new task types (action / bookkeeping) + Aircall touch-budget fields from the Tasks Screen spec. The VPS has already been retired (#152). The as-built Governor docs also lag the deployed code — reconcile them.