skills/review-demos/SKILL.md
Review InboxMate demos waiting for QA. Finds CRM opportunities at SCREENING with demoStatus=PENDING_REVIEW, opens each demo link, checks quality, and flags as OK_TO_SEND or NEEDS_FIX with a note explaining why. Optional parameter: track ('inbox' reviews only Demo-Postfach demos, 'chatbot' only chatbot demos).
npx skillsauth add psquared-development/psquared-skills review-demosInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
/review-demos [track] — optional. inbox → review only opportunities with demoType: INBOX; chatbot → only demoType: CHATBOT/null. Omitted → all pending, each with its matching checklist (2c).
Announce:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Demo Review Pipeline started. Checking environment... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Before doing anything, check if a .env file exists in the current working directory. Read it using the Read tool:
Do NOT use
source .env— values may contain semicolons that break shell parsing. Read the file directly and extract the values manually.
The .env file must contain all three of the following tokens:
PSQUARED_CRM_TOKEN — for querying and updating CRM opportunitiesNUXT_MCP_DEMO_TOKEN — for calling the InboxMate MCP API (auto-fixing widget styles)OPENBRAND_API_KEY — for extracting brand colors via the OpenBrand API (used in Step 2b2)If the .env file is missing or doesn't contain all three tokens, stop immediately and ask the user to provide them.
Once verified, announce:
Environment OK. Finding demos pending review...
Query CRM for opportunities at SCREENING stage with demoStatus = PENDING_REVIEW:
curl -s -X POST https://crm.psquared.dev/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PSQUARED_CRM_TOKEN" \
-d '{"query":"{ opportunities(filter: { stage: { eq: SCREENING }, demoStatus: { eq: PENDING_REVIEW } }, first: 50) { edges { node { id name stage demoStatus demoType outreachType noOutreach demoUrl { primaryLinkUrl } company { id name domainName { primaryLinkUrl } } } } } }"}'
Announce:
Found [N] demos pending review: 1. [Company Name] — [demoUrl] 2. [Company Name] — [demoUrl] ...
If none found, announce "No demos pending review" and stop.
For each opportunity, perform a quality check:
Use WebFetch to open the demo playground URL. The demo page is at demo.inboxmate.psquared.dev/?id=<demoId>.
Look at the page content for:
Use WebFetch on the company's domain (from company.domainName.primaryLinkUrl). Compare against the demo.
Call the OpenBrand API to extract the company's actual brand colors:
WebFetch: https://openbrand.sh/api/extract?url=https://[companyDomain]
From the response, find the primary color — look for the color tagged as "primary" in the colors array. Record this hex value as expectedPrimaryColor.
If OpenBrand fails or returns no colors, fall back to manually inspecting the company website HTML for dominant button/CTA colors.
Fetch the demo's stored data to check the countdown configuration:
WebFetch: https://app.psquared.dev/api/demo/[demoId]
Extract the demoId from the opportunity's demoUrl (the ?id= parameter or last path segment).
From the response, record:
offerText — the offer headline textofferExpiresAt — the countdown deadline (ISO date, or null if missing)agentId — needed for auto-fixesBranch on demoType first. demoType: INBOX opportunities are Demo-Postfach demos (seeded inbox, no agent/widget) — use the INBOX checklist below. CHATBOT or null (legacy) → use the chatbot checklist.
CHATBOT checklist — score each item as PASS or FAIL:
| Check | What to verify |
|-------|---------------|
| Company match | Demo mentions the correct company name |
| Language match | Demo language matches the company website language (DE/EN/both) |
| Greeting quality | Greeting is specific to the company, not generic ("Hi! How can I help?") |
| Quick questions | Questions are relevant to this company's products/services |
| Color match | Widget primary color matches OpenBrand expectedPrimaryColor. Compare hex values — minor shade differences (e.g. #1a365d vs #1e3a5f) are OK, but completely different hues are a FAIL. |
| Countdown set | offerExpiresAt is present AND is a future date (not null, not expired) — this is set by the campaign, not during review. The offerText should describe a time-limited offer — NOT "Kostenlose Erstberatung" or generic text. |
| Content accuracy | Any visible knowledge snippets reference real products/services from the website |
| No hallucinations | Demo doesn't mention products, pricing, or features not on the company website |
INBOX checklist (demoType: INBOX) — fetch the demo data from https://app.psquared.dev/api/demo/<demoId> (the inboxThreads array) in addition to the rendered page:
| Check | What to verify |
|-------|---------------|
| Company match | Page shows the correct company name + logo; mailbox label fits their domain |
| Language match | Threads, categories and drafts match the website language |
| Thread plausibility | Every seeded email is plausible for THIS business (industry-typical requests, regional names) |
| No hallucinations (CRITICAL) | AI drafts speak AS the prospect — they must NOT state prices, policies, opening hours or product names that are not on the company website. This is the top risk: check every draft against the site. |
| Draft quality | Drafts match the company's tone (Sie/du) and are sendable as-is by their staff |
| Action mix | ≥1 reply-with-draft, ≥1 archive (newsletter), ≥1 ticket/forward or urgent thread |
| Countdown set | Same rule as chatbot: future offerExpiresAt, time-limited offerText, no consultation language |
| CTA correct | Page shows "Inbox-Test starten — 14 Tage gratis" as primary CTA (renders automatically for type=inbox) |
INBOX auto-fixes go through the update_inbox_demo MCP tool (full inboxThreads replacement) — 2d (widget color) does not apply; 2e (countdown SQL) applies unchanged.
If the Color match check FAILED (widget color doesn't match OpenBrand primary color):
update_widget_style via the InboxMate MCP to fix the color:curl -s -X POST https://app.psquared.dev/api/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $NUXT_MCP_DEMO_TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"update_widget_style","arguments":{"agentId":"[agentId]","primaryColor":"[expectedPrimaryColor]"}}}'
curl -s -X POST https://app.psquared.dev/api/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $NUXT_MCP_DEMO_TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"publish_agent","arguments":{"agentId":"[agentId]"}}}'
Announce:
Auto-fixed: Updated [Company] widget color from [oldColor] to [expectedPrimaryColor]After auto-fixing, mark the Color match check as PASS (fixed).
If the Countdown set check FAILED (missing offerExpiresAt, expired date, or wrong offerText):
Important: Do NOT invent or default a deadline (e.g. "7 days from today"). Deadlines are set by campaigns — not during review. If no deadline is present in the CRM opportunity notes, flag the check as FAIL in the review note and set
NEEDS_FIXrather than guessing a date.
Only proceed with auto-fix if the CRM opportunity notes explicitly mention a specific deadline to use.
Determine the correct offer text:
Apply the fix — update the demo_pages table directly via Supabase SQL:
Use mcp__plugin_supabase_supabase__execute_sql with:
project_id: "fevtfywriufbqnvbgyrm"
query: UPDATE demo_pages SET offer_text = '[corrected offerText]', offer_expires_at = '[corrected ISO date]' WHERE id = '[demoId]'
Why raw SQL? The InboxMate MCP does not expose an
update_demo_pagetool for campaign-managed fields likeoffer_expires_at. Direct Supabase SQL is the only way to update these fields programmatically.
Announce:
Auto-fixed: Updated [Company] countdown — expires [date], text: "[offerText]"After auto-fixing, mark the Countdown set check as PASS (fixed).
OK_TO_SENDNEEDS_FIXOK_TO_SEND with note about improvements# Update demoStatus
curl -s -X POST https://crm.psquared.dev/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PSQUARED_CRM_TOKEN" \
-d '{"query":"mutation { updateOpportunity(id: \"[opportunityId]\", data: { demoStatus: OK_TO_SEND, demoReviewIssues: null }) { id } }"}'
Always clear campaignId when regressing to NEEDS_FIX. If the opportunity was already linked to a campaign, that campaign's send batch may have already gone out. Leaving the campaignId set creates an orphan: the demo later gets re-approved, but /plan-campaign ignores it (filter campaignId: { is: NULL }) and /setup-email-drafts never catches up. Clearing it releases the opp so the next /plan-campaign run picks it up cleanly.
curl -s -X POST https://crm.psquared.dev/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PSQUARED_CRM_TOKEN" \
-d '{"query":"mutation { updateOpportunity(id: \"[opportunityId]\", data: { demoStatus: NEEDS_FIX, demoReviewIssues: \"[Issue 1: description. Issue 2: description. Suggested fixes: ...]\", campaignId: null }) { id campaignId } }"}'
Announce:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Demo Review complete. ✅ OK to send: - [Company A] — [brief reason] - [Company B] — [brief reason] ❌ Needs fix: - [Company C] — [issue summary] Next step: Send approved demos to prospects ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
| Step | Field | Value | When |
|------|-------|-------|------|
| 3 (OK) | demoStatus | OK_TO_SEND | Demo passed QA |
| 3 (OK) | demoReviewIssues | null | Clear any previous issues |
| 3 (FIX) | demoStatus | NEEDS_FIX | Demo failed QA |
| 3 (FIX) | demoReviewIssues | "Issue 1: ... Issue 2: ..." | What's wrong and how to fix |
| 3 (FIX) | campaignId | null | Release campaign slot on status regression |
Reads: demoStatus (filter PENDING_REVIEW), demoUrl (demo page link), company domain
Does NOT touch: outreachSentAt, followupSentAt, agenthubAccountId, stage
Important: demoStatus is a GraphQL enum — use bare values (no quotes): demoStatus: OK_TO_SEND
tools
Set up a personalized InboxMate INBOX demo (Demo-Postfach) for a sales prospect: a public, read-only seeded inbox showing 5-7 pre-triaged emails in their industry's language, with categories, routing and ready AI drafts. Use for email-automation outreach (the €49-349 product), NOT for chatbot outreach. No agent is created.
development
Build InboxMate demos AND write personalised outreach drafts in a single pass per company — eliminating the double-research that happens when /inboxmate-batch-demo and /setup-email-drafts run separately. Use when kicking off a new campaign where the campaign already exists (plan via /plan-campaign first). For each target company, dispatches ONE subagent that researches the site, builds the demo, creates the CRM opportunity, and drafts the outreach email — reusing the same research across all three. After all subagents return, runs a single batch call to auto-generate follow-ups.
testing
Autonomous pilot for the InboxMate EMAIL outreach (Demo-Postfach/INBOX track). Assesses where the inbox pipeline stands (leads → demos → review → campaign → drafts) and executes the next sensible step end-to-end, always finishing with the inbox sanity check and a summary of what the user should do next (ideally: just schedule the mails). Runs in save mode by default: orchestration + all quality gates on the top model, data collection on haiku subagents, content generation on sonnet subagents (pass 'full' to disable). Use when asked to 'advance the email outreach', 'run the inbox pipeline', or 'what's next for the Demo-Postfach motion'.
tools
Generate a polished psquared client offer as a multi-page PDF (title, project description, screenshots, Angebot/pricing, AGB). Walks the user through gathering inputs (or accepts a JSON config), renders branded HTML templates with Playwright in two passes (title page edge-to-edge + body pages with margins and pagination), then merges with pdf-lib.