Brand Guide MCP

G&M internal product — a Slack-native brand guide assistant that lets client teams query their brand guide conversationally (colors, typography, voice, logos, messaging) and generate on-brand copy, without leaving Slack.

California Forever is the first tenant. Architecture is multi-tenant from day one so onboarding a second client is a config change, not a redeploy.

Owners

Product Reference

Status Phase 0 live end-to-end in G&M Slack + per-user agent memory shipped (2026-04-25). CF install remains.
First tenant California Forever
Repo https://github.com/ericdowns/brand-guide-mcp (private)
Local Path /Users/edowns/Projects/brand-guide-mcp
Production URL https://app.heybrandbot.com (apex heybrandbot.com 308s here; reserved for future marketing site)
Vercel Project ID prj_qAixR7ZedkXbAICAzuyqOFx438bM (team: team_gvCJfcW5DZLXrevbHJdCrmir)
Stack Next.js 16 · React 19 · TypeScript · Tailwind v4 · Drizzle ORM on Neon Postgres · @slack/web-api · @anthropic-ai/sdk
LLM runtime Anthropic Messages API direct (claude-sonnet-4-6) with MCP server attached + per-user memory tool. Per-tenant cost attribution via tenant_id on every usage_events row in our DB.
Slack app Single shared G&M-branded app (@brandbot) — multi-tenant via OAuth install per workspace
Location Contents
../brand-guide-research/ Original PRD and deep research defining the brand guide product overall
../california-forever-brand-guide/ First client brand guide — source of truth for content, california-forever tenant of the MCP
../grainandmortar-brand-guide/ G&M's own brand guide — grain-and-mortar tenant of the MCP. Scaffolded from CF on 2026-04-26 to dogfood the platform.

Docs

File Description
README.md This file — project index and status
PRD.md Product requirements doc — architecture, scope, phasing
PRODUCT-EXTENSIONS.md Product line riding on the platform — Brand Check (live), Design Check (in flight), Strategy Bot (discovery), generative tools (deferred)
FIGMA-INTEGRATION-RESEARCH.md Research on integrating brand-guide MCP with Figma. Three paths (dual-MCP-in-Claude-Code now, native Figma plugin Q3, Figma Make later). Source URLs + designer workflow examples + public-proof gap analysis.
FIGMA-PATH-A-POC-PRD.md PRD for Path A proof-of-concept. Four scripted prompts, acceptance criteria, phased execution plan (pre-flight → Eric solo → artifacts → developer test → Path B decision). Stretch / unconfirmed.

Project-scoped skills

Skills that live in the brand-guide-mcp repo (.claude/skills/) and ship with the codebase:

Skill Trigger Purpose
stickiness-audit /stickiness-audit [feature] Audit any Brand Bot feature for "what makes the user come back after the first useful interaction." Walks the user journey, identifies drop-off + dead-end moments, ranks next-turn moves by effort/impact. Drove the Design Check report redesign shipped 2026-05-05.

Integrations

Service Status Details
Harvest Not billable Internal product work
Todoist Active Brand Bot 🎨6gWH8jhcQFC7Qhv7
Slack Active #brandbot (G&M workspace) — internal channel for the brand-guide-mcp + brand-guide site work
Vercel Active prj_qAixR7ZedkXbAICAzuyqOFx438bM (team team_gvCJfcW5DZLXrevbHJdCrmir) → https://app.heybrandbot.com
Cloudflare Active Zone heybrandbot.com (id 5c794dfe3541b28faeb6517ccfb32a40, account 5078281af676ac2003a394b4ac758262). Subdomain-per-tenant: app (platform), cf / gm / guardify (tenant content). See ~/Projects/brand-guide-mcp/docs/HOSTNAMES.md.

Project Status

2026-05-06 (morning) — per-key install snippets + Figma integration research/PRD/cards

Continuation after the very-early-morning IA + portal chrome run. Two shippable PRs landed on main, one new product surface (/portal/[tenantId]/integrations), and a substantial planning chunk (research doc + PRD + 7 Todoist cards) for the next stretch initiative (Figma + brand-guide-mcp Path A POC). All work direct on main.

Per-tenant per-key install snippets (PR 49e9f0b). New /portal/[tenantId]/integrations page — lists each active API key grouped by surface, renders Claude Code / Claude Desktop / Cursor / Codex install snippets with copy buttons; server-only surfaces (chat, design_check, direct) render a "no client install needed" guidance row instead. Backed by new lib/install-snippets.ts helper keyed by ExternalSurface. New docs/MCP-INSTALL.md is the canonical reference. Centralized ALL_SURFACE_LABELS in lib/api-keys.ts (the portal home was duplicating this inline and missing labels for chat and design_check). ROADMAP entry deferred Smithery/DXT/OAuth as Track B.

Guardify claude_code key issued. New surface=claude_code API key for guardify tenant (...488b), saved to 1Password Claude Bot vault as "Brand Guide MCP — Guardify Claude Code key". Verified end-to-end: tools/list returns 11 tools, get_brand_foundation writes a usage_events row with surface=claude_code + matching apiKeyId. Replaces what would otherwise have been ambiguous direct traffic for designer-driven Claude Code use.

Figma integration combo docs (PR c956c86). Path A from the research below — designers register Figma's Dev Mode MCP and our brand-guide MCP in the same Claude Code config; Claude orchestrates both. New "Pairing with Figma" section in MCP-INSTALL.md with combined ~/.claude/mcp.json snippet + 4 example prompts mapped to underlying tool calls. Callout under the Claude Code / Claude Skill snippet groups on the portal integrations page linking to Figma's Dev Mode MCP setup.

Figma research doc + Path A POC PRD authored. Two new project-notes docs: - FIGMA-INTEGRATION-RESEARCH.md — landscape (Figma Dev Mode MCP + third-party catalog + design token tools), three-path integration matrix (dual-MCP-in-Claude-Code now, native Figma plugin Q3, Figma Make later), § 4 public-proof gap analysis (the canvas-write half is proven via azukiazusa.dev but the brand-guide-MCP combo isn't in any public tutorial we can find — we'd be paving the path). Sources block has 25+ URLs. - FIGMA-PATH-A-POC-PRD.md — full PRD with the four scripted prompts in detail (apply primary color, headline in voice, frame audit, 6-slide deck cover set), 7-row acceptance table, 5-phase execution plan (Phase 0 pre-flight → Phase 1 Eric solo → Phase 2 artifacts → Phase 3 dev test → Phase 4 Path B decision), risks, decisions recorded for all 5 § 9 open questions.

Cards filed (Brand Bot 🎨 → Ready, 7 total): - 🧪 Parent — Path A POC (stretch / unconfirmed) — 6gXR44mX3wMG34Wf (repurposed from yesterday's stub, full PRD now in description) - Phase 0 — Pre-flight content + file + Figma install audit — 6gXRFhjRV2xx2QF7 - Phase 1 — Eric runs four prompts solo + captures wordings — 6gXRFpCJG2Fvfjgf - Phase 2 — Loom + walkthrough + companion docs — 6gXRFrHcxxCQWx67 - Phase 3a — G&M dev runs the test pass first — 6gXRFwQfJqx8Cg8f - Phase 3b — Nicholas validates the polished version — 6gXRR64qvPvmMjFf - Phase 4 — Path B build/skip decision — 6gXRG2JQVpq3Vmgf - 🚧 (orphan) Audit Guardify brand-guide content depth — 6gXRG5QpRJRM3G8f

Plus the earlier 🧪 portal integrations test card from yesterday's stub work — 6gXQmxHRC3Fcrw47 — which is the smaller test of just the portal install flow (separate from Path A's broader Figma demo).

§ 9 PRD decisions locked in. Test file: fresh-built (not a real Guardify file) so we don't depend on Nicholas. Dev tester: G&M dev first, then Nicholas. Walkthrough doc: repo docs/. Loom: G&M workspace. Content gap behavior: pause and fill before Phase 2 if voice/messaging/typography are TODO-placeholder.

State at parking time: - brand-guide-mcpmain at c956c86, in sync with origin, no outstanding branches, no open PRs, latest Vercel deploy Ready. - 7 new Todoist cards in Brand Bot 🎨 → Ready, all linked to the new PRD and research doc. - Two new project-notes docs (FIGMA-INTEGRATION-RESEARCH.md, FIGMA-PATH-A-POC-PRD.md) committed via README index, ready for next session pickup.

Open follow-ups (carried forward): - Phase 0 of the Path A POC is the next concrete action — when Eric has 1h, run the Guardify content depth audit and create the fresh Figma test file. - Slack admin-UI field swap from prior session still queued (6gXHC96wHRqqQQMf). - Chrome MCP regression from prior session still flagged for diagnosis on next session start.

Continuation of the "late night" run after Eric finished the rebrand + allowlist + dark-mode pass. Picked up from a clean main and shipped four more PRs on top of his work, then closed out the IA implementation chain.

Customer portal top-bar + sign-out (PR #70). New app/portal/[tenantId]/layout.tsx — tenant-scoped chrome that renders a top bar with the signed-in identity (email for magic_link, userName / Slack id for slack_oidc) and a Sign out button. New /api/tenant-auth/sign-out POST route clears the session cookie (matching the same config as the slack + email callbacks) and 303s back to /portal/[tenantId]/sign-in?reason=signed_out with a friendly "you've been signed out" banner. Sign-in renders without the top bar (no session = nothing to identify). Per-page auth checks stay where they are — the layout only decides whether to render the chrome. The card's footer ask was already covered by the global SiteFooter in app/layout.tsx, so the work scoped down to dropping three duplicate inline footers from portal/page.tsx, sign-in/page.tsx, and design-check/page.tsx. The PR was rebased onto Eric's 82c4bf5 "dark layout" commit and slimmed — the redundant bg-[#0a0a0a] wrapper sat in the new outer app/portal/layout.tsx Eric had pushed, so the nested layout dropped to a fragment.

/explorer/reference rename in Guardify bot (guardify-brand-bot PR #31). Phase 2 implementation of the IA + linking card. Per the platform IA-AND-LINKING.md decision: the Tier 1 structured surface on a tenant bot site is Brand Reference, not Explorer. Reserves the word "brand guide" exclusively for Tier 2. - 10 routes moved (app/explorer/*app/reference/*) - ExplorerLayoutReferenceLayout component + import sweep - User-facing copy swept: "Brand Explorer" → "Brand Reference", "brand guide" → "brand reference" everywhere it pointed at the Tier 1 surface - 308 redirects /explorer + /explorer/:path*/reference/* in next.config.ts so external links / Slack pastes / search index keep working - Smoke-tested live: /reference 200, /explorer/colors 308 → /reference/colors

Tier 0 outbound link (guardify-brand-guide PR #3). Per the same IA doc — Tier 0 keeps its dev-portal framing but should signpost non-dev visitors to the actual Brand Bot. Small "Looking for the Brand Bot? → guardify.heybrandbot.com" line below the existing dev paragraph. Closes the IA card's Phase 2 chain (CF / G&M retrofit deferred until those tenants get bot sites).

FEATURE-CATALOG (PR #71). New operational rollup at docs/FEATURE-CATALOG.md — 6 columns (Feature | Type | Status | Surfaces | Docs | Notes), grouped Surfaces (23 user-facing, e.g. /brand slash command, App Home, Web Chat, Customer Portal, every /admin/* tab) and Capabilities (18 platform-internal, e.g. per-tenant API keys, resolveBearer auth funnel, hard-cap auto-pause, end-user identity primitive). Live + in-flight only — deferred items stay in ROADMAP.md. Cross-linked to HOW-IT-WORKS, ARCHITECTURE, COMMANDS, USAGE-TRACKING-SPEC, PRODUCT-EXTENSIONS. New scripts/gen-feature-catalog-html.sh mirrors gen-ia-html.sh for the styled HTML output. Maintenance trigger documented inline at the top: ad hoc when Eric notices drift, light-touch by design. Indexed in docs/README.md and from HOW-IT-WORKS.md's "what to read next" section.

Stickiness skill surfaced in indexes (commit ccb4364). The repo ships a project-scoped /stickiness-audit skill at .claude/skills/stickiness-audit/SKILL.md but nothing in the doc indexes pointed at it. Added a "Project-scoped skills" section to both docs/README.md and this project-notes README so future sessions discover it.

Cards completed (Brand Bot 🎨): - ✨ Customer portal — logged-in identity, sign-out, fixed footer (#70) — 6gXHjRWw75m4PPcf - 📋 Documentation hub / feature catalog parent + 5 subtasks (#71) — 6gXGP7XgqJRGfjg7 - 🚧 IA + linking structure across bot site + brand-guide site — 6gXG58fCXmcWc8r7 (Phase 2 across guardify-brand-bot#31 and guardify-brand-guide#3) - 🎯 Design Check Stickiness parent — 6gXGV4ch7q8vQWm7 (housekeeping; subtasks already shipped earlier in the day)

Board housekeeping. Six cards were stale-In-Progress at session start (A1, A2, A4, A5, P1, magic-link-timestamp) — work had shipped via prior PRs but the move-to-Complete step was missed. All moved to Complete to make the board reflect reality.

Chrome MCP regression (worth flagging). chrome-devtools MCP returned No page selected on every call this session, even after a Chrome restart via /chrome-mcp-dashboard. Chrome itself was up on port 9233 and the dashboard reported mcpReady: true, but the MCP server in this Claude session was stuck. The Slack-app-config-on-api.slack.com swap that needs Chrome to drive was queued as Todoist subtask 6gXHC96wHRqqQQMf (under custom-domain parent 6gXHC8pjrxCcMcP7) rather than being completed. Fix is likely a Claude Code session restart — surfaced for next session. (Note: Eric also did the live Slack OAuth redirect URI updates manually as part of the earlier "late night" run, so that part is no longer pending.)

Open follow-ups (carried forward): - Slack admin-UI field swap on api.slack.com — 5 fields to point at app.heybrandbot.com (Slash Command, Events, Interactivity URLs — OAuth redirect URIs already handled). Manual UI swap (~2 min) or future-session Chrome MCP revival. - A5 enforcement smoke + Slack regression smoke (carried over from prior session). - CF / G&M IA retrofit — deferred until those tenants get bot sites. - P2 freemium gating — unblocked since A4 + magic_links + per-tenant allowlist all live.

State at parking time: - brand-guide-mcpmain at 1662b01 (FEATURE-CATALOG merge), in sync with origin, all session feature branches deleted. - guardify-brand-botmain at 386975d (rename merge), in sync, branch deleted. - guardify-brand-guidemain at b8be906 (Tier 0 link merge), in sync, branch deleted, stray Icon\r removed. - All three Vercel deploys Ready.

Closed out the email-auth path end-to-end and rebranded the public surface to "Hey Brand Bot." Three big landings, all live on app.heybrandbot.com.

Landing-page rebrand (commit 6ad0407). Dropped the "Slack-native brand guide assistant" framing on the home page. H1 is now "Hey Brand Bot," with the full ecosystem listed: Slack, web chat, Claude Code & Codex via MCP, customer portal. Internal "Brand Guide MCP" name preserved everywhere it matters (repo, admin, docs).

Cloudflare Email Routing on heybrandbot.com. Three forwards live: hello@, support@, catch-all → eric@grainandmortar.com. Token scope expanded to include Email Routing Rules + Addresses (Read + Edit on both). Caveat: listing endpoints occasionally 403 (CF quirk on Email Routing perms), but writes work and dashboard works, so not blocking. Saved as a known "if you need to enumerate from CLI later, reverify token cache" note.

SendGrid Domain Authentication for heybrandbot.com. DKIM s1/s2 + mail-from em.heybrandbot.com validated, SPF updated to include sendgrid.net, DMARC p=none at _dmarc.heybrandbot.com reporting to eric@grainandmortar.com. Scoped SendGrid API key (mail.send only) saved to 1Password as "SendGrid API Key (brand-guide-mcp)." Wired into Vercel: SENDGRID_API_KEY + SENDGRID_FROM_EMAIL=noreply@heybrandbot.com across prod/preview/dev. Live test fired and landed clean: "Sign in to Grain & Mortar" from noreply@heybrandbot.com.

Slack OAuth redirect URIs (commit chain through 82c4bf5). Added https://app.heybrandbot.com/api/tenant-auth/slack/callback and https://app.heybrandbot.com/api/slack/install to the live Slack app config. Old brand-guide-mcp.vercel.app URIs left in place during the transition. Eric hit the "redirect_uri did not match" error mid-test, which is what surfaced the gap.

Customer portal dark mode (commit 82c4bf5). New app/portal/layout.tsx paints the portal bg-[#0a0a0a] so existing dark cards (sign-in, design-check, post-signin home) stop floating on white. Sign-in card rebuilt: Slack mark on the OAuth button, magic-link email form below an "or" divider, sage #7d8b67 eyebrow that gestures at the G&M brand, tighter copy. Triggered by Eric's "the background's supposed to be dark, but it's not. It's really poor design" critique.

Per-tenant allowlist (commit a8b041a). Replaced PR #67's open "anyone can request a magic link" model with a per-tenant allowlist. Decision driven by Eric: "ask for the client's email to go on file, and only if their email matched, a magic link came to them." New tenant_allowed_emails table in Neon (exact-email + @domain.com wildcard matching, idempotent inserts via lib/auth/allowlist.ts). Portal sign-in form has live green/red validation as the user types (debounced 400ms hitting /api/tenant-auth/email/check, per-IP rate-limited at 60 req/min so it can't be scraped at speed). Submit button gated until authorized. Red state shows a mailto:support@heybrandbot.com help link for users who forgot which email they signed up with. Start endpoint returns explicit 403 + friendly message on deny (Eric's call: "the allowlist isn't sensitive intel anyway"). New /admin/allowed-emails page (G&M-side admin only for v1) with single-add, bulk paste, notes, remove. Seeded G&M with eric@grainandmortar.com + @grainandmortar.com wildcard. Tenant-side self-serve UI deferred. PR #69 followed up with a per-second timestamp on email subjects so Gmail stops threading successive sign-in requests (small fix from a parallel agent during a deploy wait, merged squash).

Admin password. Saved to 1Password as "brand-guide-mcp ADMIN_PASSWORD" (Claude Bot vault). Username field is ignored by basic auth — any value works.

Verification. All four allowlist smoke tests pass on prod: eric@grainandmortar.com authorized, random@example.com denied, anyone@grainandmortar.com authorized via wildcard, deny path returns 403 with the support contact line.

Outstanding for next session: - Open-loop magic-link mode is still anonymous on every tenant in tenants.json. The first surface flip from anonymousmagic_link is the trigger that makes the allowlist actually do user-facing work (P2 freemium gating). - vercel inspect on the latest deploy shows Email Routing API listing endpoints still 403'ing despite Read perms saved on the token. Functional state fine, dashboard works, can revisit if a CLI workflow needs it. - DMARC reports start landing in eric@grainandmortar.com over the next 24-72 hours. After 1-2 weeks of clean reports, ratchet _dmarc.heybrandbot.com from p=none to p=quarantine. - Tenant-side self-serve UI for managing allowlist (deferred — Eric said manual G&M-side add at signup is fine for v1). - "Forgot which email I signed up with" → currently a mailto:support@. Could be a proper inbound-email handler later if volume justifies.

2026-05-05 (overnight) — Platform moved to app.heybrandbot.com + apex 308 + HOSTNAMES decision log

Capped the heybrandbot.com rollout from the prior session by moving the platform itself off brand-guide-mcp.vercel.app onto app.heybrandbot.com. Decision filed: apex heybrandbot.com is reserved for a future marketing site (also Vercel) and 308-redirects to app.* until that ships. Picked app.* over apex for the platform because (1) cookie scope on app.* is bounded vs an apex cookie that can bleed to tenant subdomains, (2) apex stays free for marketing without forcing a future platform migration, (3) SaaS convention.

DNS (Cloudflare zone 5c794dfe3541b28faeb6517ccfb32a40): - Added A heybrandbot.com → 76.76.21.21 (Vercel apex) - Added CNAME app → cname.vercel-dns.com - Existing cf / gm / guardify CNAMEs unchanged

Vercel (brand-guide-mcp project prj_qAixR7ZedkXbAICAzuyqOFx438bM): - Attached both app.heybrandbot.com and heybrandbot.com - Configured project-level 308 redirects via Vercel REST API (/v10/projects/.../domains/<host> PATCH with redirect + redirectStatusCode): - apex heybrandbot.comapp.heybrandbot.com - legacy brand-guide-mcp.vercel.appapp.heybrandbot.com - 308s preserve method + path, so /admin, /portal/<id>, /api/... all forward correctly

Code + env (commit 6cf27ff): - MCP_BASE_URL production env var swapped to https://app.heybrandbot.com - Every process.env.MCP_BASE_URL ?? "https://..." fallback in lib/agent.ts, lib/chat-agent.ts, lib/slack/client.ts, lib/usage/alerts.ts, lib/security/alerts.ts, app/api/slack/{commands,events}/route.ts updated - slack-app-manifest.json 5 URLs swapped (slash command, OAuth redirects ×2, events, interactivity) - README, CLAUDE.md, docs/SLACK-APP-SETUP.md, docs/SLACK-UX.md, docs/ADMIN-ACCESS.md, scripts/* swept for old URL refs - Vercel build deployed Ready (brand-guide-nyzd4tjam)

Decision log shipped: docs/HOSTNAMES.md — the URL/apex source of truth Eric asked for. Covers: apex 308 reasoning, why app.* over apex, full Cloudflare zone metadata + DNS records table, helper command examples for adding new tenant subdomains, the project-level Vercel API call to set the apex 308, and the surfaces that must stay in sync (Slack manifest, MCP_BASE_URL, code defaults, docs, memory). Indexed in docs/README.md.

Memory updated: - MEMORY.md index entry for heybrandbot.com rewritten to reflect apex 308 → app - project_heybrandbot_custom_domain.md — added app.heybrandbot.com row to the live-subdomains table, removed the "decision still open" caveat - feedback_full_urls.md and brand_bot_product_model.md — old URL refs updated

Verified end-to-end via curl: - https://app.heybrandbot.com → 200 - /admin → 401 (basic-auth gate) - /api/mcp/california-forever POST without bearer → 401 {"error":"Unauthorized"} - https://heybrandbot.com → 308 with location: https://app.heybrandbot.com/ - https://heybrandbot.com/admin → 308 with location: https://app.heybrandbot.com/admin - https://brand-guide-mcp.vercel.app → 308 → app.*

Still outstanding (queued in Todoist): - Live Slack app config swap at api.slack.com — 5 fields (slash command request URL, OAuth redirects ×2, events URL, interactivity URL) all need to point at app.heybrandbot.com. Subtask 6gXHC96wHRqqQQMf under custom-domain parent 6gXHC8pjrxCcMcP7 on Brand Bot 🎨 holds the full brief. Tried to drive via Chrome MCP this session — chrome-devtools MCP returned No page selected on every call despite Chrome debug being up on port 9233. Manual UI swap (~2 min) or future-session MCP revival.

State at parking time: - brand-guide-mcpmain at 6ad0407 (Eric's follow-on landing rebrand commit on top of 6cf27ff), in sync with origin, no local branches outstanding, clean tree. - Cloudflare zone, Vercel domains, prod env all converged on app.heybrandbot.com. - Slack still POSTs to the old URL (308 cascade) — works for now because path is preserved, but will break if Vercel ever returns the redirect with a method-changing status. Don't leave the Slack admin swap parked indefinitely.

2026-05-05 (late evening) — Auth/middleware silo planning + 4 PRs shipped + Neon migration

Picked up from the prior park entry's open thread (PR #63 needed rebase, A4/A5 didn't exist yet). Drove a planning pass on two parallel silos — Auth & Middleware and Pricing & Packaging — then ran the queue through to merge.

Silo plan filed. /Users/edowns/.claude/plans/go-ahead-and-immutable-conway.md — single doc covering both silos, with the current auth surface inventory (the copy-pasted resolveAuth pattern across 6 routes), the architectural target for each silo, and proposed Todoist cards in priority order. Decisions captured: hybrid GUI hosting (unified portal default, per-tenant Vercel apps as upsell), magic-link as the default end-user auth method (tenant-configurable per surface), tiered features (free / starter / pro / enterprise) with a freemium chat hook, refactor + Next 16 proxy.ts extension as the first middleware card.

14 Todoist cards filed in Brand Bot 🎨. 7 under auth-middleware label (A1–A7), 7 under pricing-packaging (P1–P7), all tagged with the 5-section task brief format and back-references to the plan file.

4 PRs merged in priority order: - #66 — CLAUDE.md layout refresh — pure docs. Brings the project-layout block in sync with shipped surfaces (design-check-share, brand-check-share, lib/auth/). - #63 — A2 proxy edge (request-id, Origin allowlist, CORS preflight) — rebased onto current main (trivial conflict in lib/tenants.ts — both branches added an optional Tenant field). Stamps x-request-id (UUIDv4, reuses inbound or mints) on every tenanted-API request, adds optional tenants.json[].allowedOrigins (empty/missing = unrestricted, status quo), CORS preflight on the matched paths. Reads tenants.json directly to keep drizzle/neon out of the middleware bundle. - #68 — A5 per-tenant auth config + per-surface enforcement — rebased post-A2, same trivial Tenant-type conflict. New auth?: TenantAuthConfig block with defaultMode: 'anonymous' | 'magic_link' | 'siws' + optional perSurface overrides. New getTenantAuthMode(tenant, surface) resolver. New enforceAuthForSurface option on resolveBearer — if mode is magic_link AND auth.endUserId is null → 401. Slack (MCP_SHARED_SECRET) always bypasses. 5 routes opt in (agent/chat, design-check + voice-rewrite + 2 share routes/design_check); MCP route intentionally skips. Every tenant ships defaultMode: 'anonymous' so behavior is unchanged today. - #67 — A4 magic-link email sign-in — new magic_links table (id, tenantId, email, tokenHash unique, expiresAt, consumedAt, reqIp); lib/auth/email-magic.ts (issue/consume helpers); lib/auth/email-transport.ts (auto-detects SENDGRID_API_KEY + SENDGRID_FROM_EMAIL, falls back to console.log); /api/tenant-auth/email/start POST + /callback GET; extended PortalSession to support both slack_oidc and magic_link auth methods sharing the same cookie/signing key. Brand-guide-mcp is the first G&M project to wire SendGrid programmatically — the pattern in email-transport.ts is intended as the model for future apps.

Neon migration appliednpx drizzle-kit push --verbose added the tenant_subscription.tier column (DEFAULT 'starter' NOT NULL) AND created the magic_links table with all indexes. Both additive, no data loss. This was the unblocker — without it, /admin would 500 because listTenantSubscriptions() selects tier.

Smoke verified end-to-end: - All 9 /admin/* tabs load (200) — Tier column rendered, 12 tier dropdown options across 3 tenants, zero error markers. - A2: x-request-id echoed on responses, CORS preflight returns 204 with Access-Control-Allow-* + Vary: Origin. - A4: console transport logs the link in Vercel runtime; first click → 307 with Set-Cookie: brand_guide_portal_session carrying authMethod: magic_link, email: ... + redirect to /portal/grain-and-mortar; replay → 307 to sign-in?reason=invalid_or_expired (single-use enforced). - Custom domain guardify.heybrandbot.com regression-clean.

Off-plan recovery (worth flagging): Earlier in the session I committed A3 directly onto local main instead of a feature branch. Cherry-picked to a branch for a clean PR flow before realizing the commit had already been pushed to origin/main as 90b3173 (Eric had pushed it himself). The duplicate branch was deleted. Behavior on the bot's local environment isn't deterministic — multiple commits ended up authored by Eric on what should have been Claude branches. Working hypothesis: a hook or background agent is auto-staging+committing+pushing during long sessions. Not investigated this session.

Vercel daily-deploy ceiling. PR #64 (P1 standalone branch) failed to deploy with Resource is limited - try again in 24 hours, code: api-deployments-free-per-day. Closed as superseded — P1 had already squash-merged via #65. Plan upgraded mid-session per the prior entry's note; today's 4 merges all deployed cleanly.

Cards completed (Brand Bot 🎨): - ✨ A1 → already complete from prior session - ✨ A2 — task/a2-middleware rebased + merged (#63) — 6gXGv6pwV5m2PFQ7 - ✨ A3 → already complete from prior session - ✨ A4 — magic-link sign-in (#67) — 6gXGv6qW8XRXM8X7 - ✨ A5 — per-tenant auth config (#68) — 6gXGvQQ8FpwRM4vf - ✨ P1 → already complete (squash-merged via #65)

State at parking time: - brand-guide-mcpmain at the post-A4-merge tip, in sync with origin, no local branches outstanding (all session feature branches deleted on merge). Stray 5 0-byte file untracked (predates this session). - Neon — schema in sync with lib/db/schema.ts. tier column populated on existing tenant_subscription rows via DEFAULT. - Vercel — production Ready.

Open follow-ups (deferred, not started): - SendGrid wiring (config task, not a card) — drop SENDGRID_API_KEY + SENDGRID_FROM_EMAIL into Vercel env to flip A4 from console transport to real send. ~3 minutes if 1Password has the key. - A5 enforcement smoke (low-risk) — temporarily flip Guardify chat to magic_link mode, verify 401 without x-end-user-id, revert. Proves the rail before anyone leans on it. - Slack regression smoke — /brand command, @brandbot mention, message shortcut. Should be a no-op given the refactor was preserve-semantic, but not yet eyeballed.

Next on the plan: P2 — Freemium "Free Chat" surface gating. Now unblocked since A4 + magic_links table are live. Touches lib/chat-agent.ts (omit memory tool on free tier) and the chat UI (free-tier banner + upgrade CTA → /portal/[tenantId]/upgrade from P3). Then P3, then A6 (CNAME hybrid hosting — multi-week, wants a focused session), then the rest of the order from the plan file.

2026-05-05 (evening) — Brand Check share + resolveBearer auth funnel + cost cleanup + end-user-id primitive

Continuation of the same day. Big themes: ship the Brand Check share feature end-to-end (parallel to the design-check share that already existed), consolidate the per-route auth code into a single helper, strip every client-facing per-image cost reference, and lay the rail for end-user identity (P2 free-tier surface gating + P5 MAU metering build on top).

Brand Check shareable result links + dynamic OG (the headline ship). New brandCheckShares table + /api/brand-check-share/[tenantId] POST/GET + daily purge cron on brand-guide-mcp. Guardify portal got the share button under the result, a public read-only page at /brand-check/r/[uuid], and a dynamic next/og image (1200×630) that renders the verdict in the Slack unfurl card — "Brand Check Passed · 5 of 5 rules · Guardify" in Baby Blue (pass) or coral (findings) on the Guardify gradient. Privacy posture deliberately diverges from design-check share: brand-check renders inline highlights over the user's submitted copy, so the share blob persists { text, result } (not just result). UI disclaimer reflects this. Two satori gotchas caught during chrome-devtools verification: (1) Next 16 dynamic route params are Promise-typed even in opengraph-image.tsx — must await; (2) satori chokes on bare text nodes in flex containers — wrap every text in its own display: flex div. Live on https://guardify-brand-bot.vercel.app/brand-check. Parent + 11 subtasks moved to Complete on Brand Bot 🎨 (6gXGp7xqGX2Rc6M7).

Auth funnel consolidation. New lib/auth/resolve.ts — single resolveBearer() helper consolidating bearer parsing + MCP_SHARED_SECRET match + per-tenant API key lookup + tenant existence + subscription gate + per-key rate limit. Refactored every external API route (/api/mcp, /api/agent, /api/design-check, /api/voice-rewrite, /api/design-check-share, /api/brand-check-share) to use it. Net ~420 fewer lines across six routes; new helper ~150 lines. Same 401/402/404/429 semantics — pure refactor, no behavior change. PR #62.

Client-facing cost references stripped. Per Eric: per-image cost has nothing to do with the value of the service we're providing. Removed the run-cost token from the Guardify design-check report footer, the "typically cost $0.01 to $0.03 per image" intro clause on Guardify, and the ≈ $0.014 per image references in the brand-guide-mcp customer portal (home, design-check page, upload form intro, result footer). Kept (deliberate, flagged for separate decision): the cumulative usage / cap dollar display on portal home (fmtUsd formatter, e.g. "<$0.01 / $20.00") — that's plan-billing transparency, not per-action pricing. PRs guardify-brand-bot#25 + brand-guide-mcp#65.

End-user identity primitive (90b3173, direct to main). New lib/auth/end-user.ts with signEndUserId / verifyEndUserId (HMAC-SHA256 over SESSION_SECRET, format <id>.<base64url-hmac>, constant-time comparison). resolveBearer reads the x-end-user-id header and exposes auth.endUserId (string | null). Decoupled from bearer auth — invalid signature returns null, doesn't 401. P2 free-tier surface gating + P5 MAU metering build on this rail. DB persistence (usage_events.triggered_by / check_events.triggered_by) deferred to P5 where the MAU query needs it.

Home-page line-icons (Lucide). Added scan-eye / spell-check / book-open / messages-square icons above the four surface cards on Guardify's homepage, gray-on-rest → Baby Blue on hover. Stroke-width 1.5 + currentColor inheritance honors Guardify's icon spec. Documented in brand-guide-mcp docs/ROADMAP.md under "Tenant bot portals as a render of the brand guide" — the strategic frame Eric named: bot portal quality scales linearly with brand-guide depth, UI fallbacks ARE the audit instrument, the bot is its own sales artifact. Memory note saved.

New Todoist cards filed (Ready): - ✨ Auto-downsize oversized design-check uploads (with approval) — 6gXH26FQQm73MMg7 - ✨ Retrofit dynamic OG to design-check share public page — 6gXGvFJr6j2q9PV7

Cards completed: - 🧹 Strip all client-facing cost-per-image references — 6gXH2GjfqfCVJqXf - ✨ Brand Check shareable result links (sign-off) parent + 11 subtasks — 6gXGp7xqGX2Rc6M7

Vercel daily-deploy limit hit. Guardify and brand-guide-mcp auto-deploys for the cost-cleanup PRs (#25 and #65) didn't fire — hit the 100/day Hobby-plan ceiling. Project upgraded to Pro mid-session; manual vercel --prod retried both repos and confirmed Ready. Worth noting: gh pr merge --squash doesn't always cause Vercel to rebuild on busy days; check vercel ls for the timestamp of the latest deploy vs the merge timestamp before declaring something live.

Memory notes added this session: none new — strategic principle "Tenant bot portal is a render of the brand guide" was captured earlier today and applied to this session's icon work.

2026-05-05 (afternoon) — Insights MVP shipped + chat-surface design + AI-tell removal pass

Long product-execution session. Big themes: ship the entire Chat / Q&A surface end-to-end (backend + frontend + key + deploy), QA it, design-pass it, and lay the foundation for a brand-guide intelligence layer that turns the bot from a Q&A tool into an analytics product the owner can't get elsewhere.

Insights MVP (the strategic move). New chat_insights table + capture layer + /admin/insights page. One row per chat turn, tagged with derived topic category (free, from MCP tool calls — get_colors invocation = "colors" topic) and a hedge boolean (regex over response text for "doesn't specify" patterns the system prompt instructs the bot to use when the brand guide is silent). No raw question or response text stored — privacy promise on the chat surface stays clean. Per-tenant admin view shows top topics with hedge-count overlay + 30-day daily volume sparkline. Foundation for phases 4-7 (tenant-facing portal view, weekly email digest to brand owners, LLM-generated "suggested brand-guide updates" from clustered hedged questions). The reasoning + market-validation write-up captured in PRODUCT-EXTENSIONS under "Brand Insights." Live at https://brand-guide-mcp.vercel.app/admin/insights. Eric's framing locked: this is what makes the bot sticky — the brand owner gets a research report on what their team needs every week, not just a chat history.

Chat surface design pass against Guardify brand. Started with /critique on a real screenshot, ran a /design-pass against the live brand guide. Two passes shipped: (1) full screen redesign — shrunk the hero, moved Send inline with textarea, "New chat" to a discreet header link, added remark-gfm tables with on-brand styling, made hex swatches context-aware (interactive in tables/lists, plain in narrative paragraphs), bubble corners 8px → 4px to match Guardify's button rule, removed the decorative blue accent bar that read as generic-AI-chatbot, swapped the pulse-dot loader for italic "Looking that up." text. (2) System prompt nudge: prefer tables for list-shaped data, no redundant prose-after-table.

End-to-end Chat surface shipped earlier in session. /api/agent/[tenantId] on brand-guide-mcp + lib/chat-agent.ts (surface-agnostic agent loop, separate from Slack agent). New chat ExternalSurface in lib/api-keys.ts. Companion frontend on guardify-brand-bot: /api/agent-proxy route + app/chat/chat-interface.tsx with ReactMarkdown + suggestion chips. Per-tenant chat API key issued autonomously (admin script + 1Password Claude Bot vault + Vercel env + redeploy). Live at https://guardify-brand-bot.vercel.app/chat.

QA + ops housekeeping wave. Re-enabled basic auth on /admin/roadmap (PR #47, closes the TEMP from 2026-05-02). Fixed the Icon? / icon? gitignore glob bug across grainandmortar-brand-guide + guardify-brand-guide that was silently shadowing legitimate icons/ directories. Shipped admin hard-cap editor (/admin/cap-editor.tsx + /api/admin/tenants/[id]/monthly-cap) so admins no longer need to touch Neon. Authored partial DESIGN-CHECK-SPIKE-FINDINGS.md (architecture + cost shape sections complete; calibration sections await Nicholas's asset bundle). Numbers brand-check rule no longer false-positives on hex/RGB/CMYK/Pantone/ΔE measurements (PR #41). PDF source archived to ~/Projects/guardify-brand-guide/source/. Five-fix design pass shipped on the chat bubble specifically (PR #5 on guardify-brand-bot) per /design-pass output.

QA log infrastructure — new docs/qa/chat-surface/ directory with fixture JSON + append-only run files. Pre/post-tuning runs from today captured verbatim with side-by-side comparison. README documents the "tests-surface-feature-opportunities" pattern with the swatch feature filed as the canonical example.

Memory question, answered. Eric asked whether chat needs short-term memory across sessions. Answer: little to none for this surface. The brand guide IS the memory, the bot just queries fresh each time. Per-conversation (within tab) is enough; cross-session would be solving a problem nobody has on a public anonymous brand-Q&A surface. Documented at length in the conversation transcript.

Todoist housekeeping — moved 4 mislabeled Guardify Ready cards (calibration work) to Blocked with blocked-needs-credential since they all gate on Nicholas's asset bundle. Closed 7+ shipped cards. Filed: interactive swatches feature (Brand Bot 🎨 → completed same session).

Memory notes added this session: - feedback_api_key_issuance.md — full pattern for autonomous key issuance (Eric explicitly delegated this; don't bounce back as at-computer step). - feedback_full_urls.md — always give full URLs, never bare paths.

Repos + branches state at parking time: - brand-guide-mcpmain clean, all session PRs merged (#41, #42, #43, #44, #45, #46, #47, #48, #49, #50). Vercel auto-deploy current. - guardify-brand-botmain clean, all session PRs merged (#1, #2, #3, #4, #5, #6). Vercel auto-deploy current. - guardify-brand-guidemain clean. PR #1 (PDF archive) + #2 (gitignore fix) merged. - grainandmortar-brand-guidemain clean. PR #8 (gitignore fix) merged. - california-forever-brand-guide — untouched this session.

Pickup for next session: - Asset thread still blocked on Nicholas's reply (sent 2026-05-04). When his Figma file or asset bundle lands, drop assets into ~/Projects/guardify-brand-guide/public/brand/*, push, verify each via /api/brand/*. - Insights — let real traffic accumulate (1-2 weeks) before building phase 4 (tenant-facing portal view). The MVP report needs enough rows to look meaningful. - Slack smoke-test of per-tenant bot identity is still in Brand Bot 🎨 Ready (needs Eric in his real workspace). - Strategy Bot + Design Check Phase 1 rollout discovery cards still need Eric's strategic input.

2026-05-05 — Engagement-model framing locked: Figma-first ingestion, Brand Bot as extension of brand engagement

Pivot in product framing during the Guardify asset-collection thread. Two related decisions:

1. Brand Bot is an extension of a bigger client engagement, not standalone. The default G&M play: we build the client's brand AND run their Brand Bot. Brand Bot is the always-on tail of the brand engagement, not a separate product line we sell standalone. Sales messaging should reflect that.

2. Figma is the canonical source-of-truth format for ingestion. When we built the brand, we already have the Figma file as a byproduct. When we didn't (Guardify case: less.is authored), we ask the client designer to share their Figma. Either way, "share your brand Figma" becomes the standard first onboarding step. brand-guide-mcp already has the mcp__claude_ai_Figma__* toolset wired up (get_design_context, get_screenshot, get_metadata, get_variable_defs) so logos / icons / hex tiles / color swatches / type specimens / components all pull programmatically.

Guardify is the canonical "external designer" tenant example. The current asset-collection email to Nicholas Petersen / less.is leads with the Figma ask, falls back to the explicit file list. Memory captured at ~/.claude/projects/-Users-edowns-Projects-brand-guide-mcp/memory/brand_bot_engagement_model.md.

What it doesn't replace (still separate from the Figma file regardless of who built the brand): - Photography library (or curated stock list with asset IDs) - Acquired-product marks if they live outside the main brand Figma (CAC Manager, NCAtrak) - Pantone print references (if not embedded as Figma swatches)

Implication for tenants we DIDN'T build for: "Other people's brand MCPs" is a real lane and we're happy to do it. But onboarding has more friction (extra discovery, more back-and-forth) and pricing should reflect that vs the G&M-built-brand path.

Asset audit for Guardify filed as Todoist sub-task 6gXCWwfxGcj7jp6f under 6gX55HvgWX7vrJC7 ("Source Guardify brand assets"). Full checklist of what's missing in the guardify-brand-guide repo's public/brand/* (basically everything; only Next.js scaffold favicons exist). Email draft to Nicholas at r274605847179742599 in Gmail.

TODO when we pick this back up: - Reflect the Figma-first onboarding in PRD.md and PRODUCT-EXTENSIONS.md (or wherever onboarding flows are documented) - Consider building a pull-from-figma tool that takes a Figma file URL + tenant id and seeds the content/*.json from variables + components automatically - File a card for the live-linked Figma source-of-truth feature (this is a Brand Bot product extension, not just an internal shortcut)

2026-05-04 — QA Framework P1 shipped (admin + portal dashboards, runner, schema)

What landed (PR #33, branch feat/qa-framework): - New qa_runs + qa_results tables in Neon (drizzle-kit push applied) - lib/qa/{manifest,compare,runner,queries}.ts — pure comparator + in-process engine driver + DB queries - CLI runner: npx tsx scripts/qa/run-suite.ts [--tenant=…] - Manual trigger route: POST /api/admin/qa/run - /admin/qa — all-time consistency headline, per-tenant rollup, recent runs, Run Now buttons, run-detail drill-down at /admin/qa/runs/[id] - /portal/[tenantId]/qa — client-facing headline %, recent regressions, methodology block, empty state - "QA" tab added to /admin nav - New /api/qa-fixtures endpoint on grainandmortar-brand-guide (commit 95e2a60) serving 3 bootstrap brand_check fixtures

Architecture notes: - Fixtures live tenant-side (designer of brand also authors off-brand variants — "credibility story") at content/qa-fixtures.json + public/qa-fixtures/* in each brand-guide repo - Runner drives engines in-process to skip HTTP roundtrip; tags Anthropic spend with surface="qa" so /admin/usage can break QA cost out from real client traffic - Comparator (lib/qa/compare.ts) is pure: pass/fail derived from band match + score range + must-flag substring requirements declared per fixture

Verified end-to-end: - 3/3 G&M fixtures passed, $0 cost (text-only), 1.1s - chrome-devtools at 1280px: dashboard + run-detail render clean, no console errors - Portal page redirects to sign-in when unauthed - POST /api/admin/qa/run creates runIds + DB rows - Vercel preview deploy Ready (commit feat/qa-framework)

Decisions locked this session: - Fixture corpus lives tenant-side, not centralized - Both admin + portal views shipped in P1 (per Eric's call — different from initial recommendation to defer portal to P3) - New tenant-agnostic Todoist project for tracking: Brand Bot QA Framework 🎨 (6gX5gwcjHXv8mWX4) with 9 cards covering P2-P5 + tech-debt items. Separate from per-tenant integration kanbans.

What's queued (Todoist): - P2 Admin dashboard polish — sparkline, per-dimension precision/recall, regression detector - P3 Nightly cron + drift tracking - P4 Real fixtures: G&M (Eric authors), Guardify (gated on Nicholas's bundle, blocked-needs-credential), CF - P5 Public /methodology marketing page (deferred — needs ≥30 fixtures for stat power) - Tech-debt: add "qa" to /admin/usage SURFACE_LABELS map; switch /api/admin/qa/run to fire-and-forget when design_check fixtures land; doc sweep (CLAUDE.md / ROADMAP / PRD)

Plan file: ~/.claude/plans/twinkly-honking-tower.md

Related project hub updates: none in ~/.claude/project-notes/guardify-brand-bot/ yet — the Guardify-side fixture work is captured in the Brand Bot QA Framework Todoist project, but a status pointer in the Guardify hub would close the loop next session.

2026-05-01 — Design Check Phase 0 spike landed end-to-end (engine + API + portal UI)

The whole Phase 0 spike from docs/ROADMAP.md → built, verified, and shipped in one long session. As of 540b376, Design Check exists as: (1) a function call (lib/design-check/engine.checkDesign), (2) a JSON API endpoint at /api/design-check/[tenantId] with bearer auth + 5MB cap + subscription gate, and (3) an OIDC-gated tenant-portal page at /portal/[tenantId]/design-check with drag-drop upload + rendered result. Eight commits, each behind explicit per-action push approval.

Engine pieces (all live + verified): - lib/design-check/colors.ts (e0bbcb1) — Sharp dominant-color extraction + hand-rolled CIE76 Delta E in LAB space, bucket-clustered at 32 steps/channel. Score 1-5 from pixel-share-weighted avg ΔE. 8 synthetic test cases pass (solid on-brand → 5, off-brand → 1, mixed → 2-3) plus real-PNG sanity check (grain-and-mortar.png → 5, 91% near-black + 4.7% cream correctly identified). Sharp added to serverExternalPackages alongside resvg-js. - lib/design-check/ocr.ts (279be5f) — Anthropic Sonnet vision OCR through recordedMessagesCreate with callType: design_check_ocr, surface: design_check. Sharp normalises input to 1024px JPEG q90 before base64. 3 cases pass: single-line tagline recovered exactly, multi-line in reading order, brand-icon B monogram → confidence=low. Avg cost $0.003/image. - lib/design-check/vision.ts (0e7c4ee) — Sonnet 5-dim brand-fit scoring (color_palette_fit, typography_fit, tone_in_imagery, messaging_alignment, overall_brand_feel). buildBrandContext() compresses raw brand-guide JSON into <1k token system-prompt summary. Strict JSON mode with regex-extracted parse + clampScore guards. 2 synthetic cases pass: on-brand G&M ad → 5/5 across all 5 dimensions, off-brand ad → 1/1 across all 5 dimensions (model named all 5 banned terms by name, called the layout "used-car sale, not a trusted studio"). Avg cost $0.01/image. - lib/design-check/engine.ts (726eb94) — composes the three sub-engines via Promise.allSettled for partial-failure tolerance, routes OCR'd text back through the existing runBrandCheck rule engine (key reuse — same naming/contractions/numbers/jargon checks that power the Slack message shortcut, applied unchanged to image copy). Returns one DesignCheckResult with overall score (avg of color score + vision overall), per-cluster color breakdown, OCR text + confidence, brand-check rule rows, vision dimensions, cost summary, warnings. 3 end-to-end cases pass. - app/api/design-check/[tenantId]/route.ts (73da647) — POST multipart entry. Bearer auth via per-tenant design_check-scoped API key OR MCP_SHARED_SECRET, subscription gate matches /api/mcp (401/402/404/413/400/500 status codes uniform across both endpoints). design_check added to EXTERNAL_SURFACES + SURFACE_LABELS so /admin/api-keys dropdown auto-flows. Build clean, route in build output.

G&M brand JSON shape fix (prereq, 9dcdd67): Existing brand-check engine was silently passing on G&M content because voice.json.writingRules was bare strings (engine expects {name, rule} objects) and messaging.json.namingConventions lacked the example + context fields the checkNaming function reads. Reshaped both files. voice-and-tone/page.tsx updated to render the new object shape (now shows rule name in bold + body, strict improvement). All 4 brand-check rules now fire on G&M copy.

Bug fix during engine.ts verification (09463c4): Caught a real false positive — runBrandCheck was flagging grainandmortar.com as a banned name because \bGrainAndMortar\b regex case-insensitively matched the lowercase URL form. Removed GrainAndMortar from the banned-variants list (URL-safe lowercase concatenation is legitimate; only Grain and Mortar and Grain&Mortar stay banned). Inline context note added so future content edits don't re-introduce it.

MCP tool wrapper deferred (cleared from board, not built): Decision logged on the spike subtask — registering check_design as an MCP tool was scoped as Optional Phase 0. Skipped because image-as-base64 in MCP tool args is a bad pattern (huge token bloat, no streaming, no chunking). If clients want Design Check from Claude Code or Codex, the right path is hitting the multipart /api/design-check/[tenantId] route directly.

Portal preview UI (scope addition, 5b57e57540b376): Eric was on his phone and asked to see the experience. The spike's stated scope was engine + JSON API only (no UI), so option 1 was build a tiny tenant-portal page even though it bent the spike scope. Built at /portal/[tenantId]/design-check: server-rendered shell with Slack OIDC + tenant-match gate, server action runDesignCheck that wraps checkDesign(), client form with drag-drop + result view (overall score badge, color clusters with hex swatches + ΔE, 5-dim vision scoring with per-dimension reasoning notes, brand-check rule rows with issue details, OCR text, cost footer). Portal index page gains a "Tools" section with a Design Check link card. Tagged surface=design_check_demo on usage events so admin views can separate preview-page traffic from real API traffic. Briefly lived at /design-check-demo/[tenantId] (no auth, tunnel-only) for a Sites Dashboard tunnel preview at https://brand-guide-mcp.myflywheelsites.com/...; promoted to /portal/... after Eric chose option A. Tunnel + dev server now stopped, tunnel flag disabled — production OIDC handles all access.

Sites Dashboard + tunnel infrastructure changes (local-only, not in any repo): - Added brand-guide-mcp entry to ~/.claude/skills/sites-dashboard/sites.json with tools.tunnel: true initially, flipped to false after the spike-UI move - next.config.ts gained top-level allowedDevOrigins: ["*.myflywheelsites.com"] (Next 16 moved this out of experimental:) to keep the tunnel from breaking HMR

Authoring + meta: - PRODUCT-EXTENSIONS.md written (67e9853 cross-link from CLAUDE.md). Sibling doc to PRD.md — describes the product line that rides on the platform (Brand Check live, Design Check / Brand Guardrails in flight, Strategy Bot in discovery, Voice Writer + Compliance Check deferred, plus Future possibilities). Surface-agnostic engine pattern is the default mental model. README Docs table cross-links it. - Memory entry brand_bot_product_model.md (both copies — Desktop session + Projects session) updated to drop the "in flight" caveat and point at the new doc with a 2026-05-01 timestamp.

Cost discipline: Per-image run of the full pipeline (color local-compute + OCR + vision) averages $0.014 — about 3.5x headroom under the $0.05/image flag in the spike card. Slack-tenant recordedMessagesCreate choke point handles cost attribution; calls land on usage_events tagged callType=design_check_ocr or callType=design_check_vision with surface=design_check (API path) or surface=design_check_demo (portal preview).

Verification deliverables for the at-computer wrap-up: - scripts/benchmark-design-check.ts — reads PNGs from scripts/design-check-fixtures/, posts each to the live API, prints per-fixture detail + roll-up table, writes timestamped JSON results blob. Configurable via DESIGN_CHECK_API_KEY / _BASE_URL / _TENANT / _FIXTURES env. Fixtures dir ships with a README explaining the calibration set to aim for (3-5 spanning clearly on-brand, borderline, mixed, deliberately off-brand). - scripts/verify-design-check-{colors,ocr,vision,engine}.ts — repo-permanent sanity scripts for each sub-engine + the compose. Useful for regression testing if the brand-guide JSON changes or model behavior shifts.

Outstanding (explicit Eric-only at-computer work — last subtask of the spike): 1. Sign in once at https://brand-guide-mcp.vercel.app/portal/grain-and-mortar/design-check (Slack OIDC, ~10 sec) 2. Drop 3-5 real G&M ads into the portal upload OR into scripts/design-check-fixtures/ for the benchmark loop 3. Eyeball each result — does the score match brand-director judgment? 4. Author ~/.claude/project-notes/brand-guide-mcp/DESIGN-CHECK-SPIKE-FINDINGS.md with accuracy notes, cost shape, what worked / what missed, recommendations for Phase 1

After findings authoring, Phase 0 is officially closed and Phase 1 decisions can be made: real Slack image-message shortcut for Design Check, pricing model (per-surface upcharge vs bundle), widen to CF tenant, keep the portal preview UI or redesign for production.

End-of-session state: main branch clean, all 8 commits pushed (9dcdd67 on grainandmortar-brand-guide; 67e9853 / e0bbcb1 / 279be5f / 0e7c4ee / 726eb94 / 73da647 / 75393b9 / 5b57e57 / 540b376 on brand-guide-mcp). Vercel auto-deploys in flight from 540b376. Tunnel stopped, dev server stopped, tunnel flag disabled. Brand Bot Todoist board: 7/8 spike subtasks Complete, 1/8 In Progress (the calibration + findings work above). Two earlier-session parent tasks also Complete (the doc-author task and the audit subtask).


2026-05-01 (discovery): Brand Guardrails framing locked, two new feature requests captured

Discovery sprint earlier in the day, before the spike build. Two-client inbound prompted scoping work that grounded the surface model and named the new product line.


Three deferred ROADMAP items shipped. CF onboarding readiness keeps stacking up.

Docs sync (commit a478987): CLAUDE.md, docs/ARCHITECTURE.md, docs/COMMANDS.md drifted hard — CLAUDE.md still claimed Managed Agents was stubbed. Refreshed: stack reflects Messages API direct + memory tool, project layout grew from ~10 to ~40 paths, MCP endpoint contract documents the two-bearer split (MCP_SHARED_SECRET vs per-tenant API keys + 402 gate), Phase Scope marks 0 / 0.5 / most of 1 SHIPPED. ARCHITECTURE sequence diagram now shows kill-switch + memory + MCP tool calls.

Privacy Option B (PR #25, merged + verified live): New 🔒 Interaction logging is *on/off* for this workspace. Privacy details → context block on /brand help and App Home. Reads through getEffectiveLogInteractions() so /admin toggle propagates without redeploy. Toggled G&M ON → footer read "on", toggled OFF → footer read "off". Live, no redeploy. Removed from ROADMAP (1df8a34).

Per-tenant bot identity / Phase 0.5d (PR #27, merged + scope granted): chat:write.customize added to bot scopes via OAuth & Permissions UI (simpler than full manifest paste). Reinstalled to G&M workspace via OAuth consent flow — bot token did NOT rotate (suffix …AkA7mHp unchanged), so no Vercel env update needed. New resolveBotIdentity(tenant) helper in lib/slack/client.ts returns { username, icon_url } keyed off tenant.id + MCP_BASE_URL. Spread into all four user-facing chat.postMessage sites (DM + mention, success + error). Ops alert DMs in lib/usage/alerts.ts deliberately keep default identity (platform signal, not tenant impersonation). public/brand-icons/grain-and-mortar.png shipped as a copy of brandbot.png so the per-tenant convention works without relying on Slack's 404 fallback.

Message shortcut re-registered (closed #28): check_brand shortcut restored at api.slack.com/.../interactive-messages. Shortcut Name capped at 24 chars (the original "Check against brand guide" was 25, shortened to "Check brand guide"). No code change — handler at app/api/slack/interactivity/route.ts:136 was already wired. Removed from ROADMAP, flipped messageShortcuts: true on both tenants in tenants.json (cosmetic — flag isn't enforced as a gate today). Commit 6d105d5.

Real-Slack smoke tests deferred to card #29 — needs Eric in his actual workspace running through @mention / DM / /brand help / shortcut tests.

Status: Phase 0 + 0.5 fully shipped. CF go-live blockers from ROADMAP mostly cleared; remaining is per-tenant bot identity reinstall on CF's workspace whenever they onboard. Next session: real-Slack verification (#29), then PRD propagation if PRD is signed off.


2026-04-27 — SaaS productization track kicked off, ecosystem docs cross-pollinated

Card #3 shipped — per-tenant subscription kill switch (PR #14, awaiting review): - New tenant_subscription table (subscription_status text default 'active'). - 402 gate in /api/mcp/[tenantId] for non-Slack surfaces when status ≠ active. Slack path (MCP_SHARED_SECRET) bypasses — paying customers get cut off, in-house bot keeps working. - 3-state admin toggle on /admin (Active / Paused / Canceled) + POST endpoint at /api/admin/tenants/[id]/subscription-status. - Foundation for card #4 (auto-pause at credit threshold) which extends this table with billing fields. - End-to-end gate verified four ways: Slack bypass, paused→402, active→200, bad payload→400.

PRD refresh — MCP-as-a-service framing locked in: - Old PRD was dated 2026-04-21 and still referenced abandoned Anthropic Managed Agents architecture. Rewrote PRD.md to reflect current state: the MCP itself is the product; surfaces (Slack, Claude Code skill, Codex, planned Claude Design / Figma-style design-tool skill, direct MCP) are togglable access points sold per tenant. - New Section 4.5 Surfaces table replaces old Lite/Plus/Pro tiers. New Section 14.6 Commercial Model captures the working pricing — $100/mo retainer + $20 token credit included, auto-pause at threshold, surface bundling-vs-à-la-carte flagged open. - Phase 0 + 0.5 marked SHIPPED. Phase 1 in progress (#3 done, #4 next, #11 blocked on portal auth decision).

Ecosystem cross-pollination (audit + fix across all four projects): - Found Projects/california-forever-brand-guide/README.md was still the create-next-app boilerplate — replaced with proper README naming the dual role (rendered site + JSON content API) and cross-linking the platform repo (cf-brand-guide PR #1, awaiting review). - brand-guide-mcp/README.md Tenants table now links each tenant's brand-guide repo (was URLs only); added new "Brand-guide ecosystem" section. Same Related Projects table added to repo CLAUDE.md (PR #15, awaiting review). - brand-guide-research/brand-guide-prd.md got a "historical document" banner pointing at the current PRD. brand-guide-research/README.md reframed as "the seed; product evolved into MCP platform." Both tenant project-notes READMEs cross-link the research origin. - Stale-PR cleanup: card #10 was labeled in-progress with no PR (carryover from prior session) — reset to ready. Cards #3 and #4 had no work-lane labels — #3 → ready (then claimed for this session), #4 → blocked on #3.

Card #10 also shipped — real-time activity strip on /admin/usage (PR #16, merged): - New "Activity" section above per-tenant summary: trailing 5-min big-number + trailing 60-min monochromatic sparkline. - New getRecentActivity() query, sparse minute-buckets densified into a fixed 60-slot grid. - Auto-refresh via tiny client component (auto-refresh.tsx) calling location.reload() every 15s — switched away from <meta http-equiv="refresh"> because React 19 + Next 16 hoists <meta> to <head>, shifting the body's first child between server and client and tripping a hydration mismatch.

End-of-session — all PRs merged + production work shipped: - brand-guide-mcp PR #14 — subscription kill switch (Fixes #3) ✓ merged - brand-guide-mcp PR #15 — ecosystem cross-links ✓ merged - brand-guide-mcp PR #16 — real-time activity strip (Fixes #10) ✓ merged - brand-guide-mcp PR #17 — auto-pause at credit threshold (Fixes #4) ✓ merged - brand-guide-mcp PR #19 — memory onboarding + tighter saves + diagnostic logs (Fixes #18) ✓ merged - brand-guide-mcp PR #20 — per-tool breakdown on /admin/usage (Fixes #9) ✓ merged - brand-guide-mcp PR #21 — customer portal at /portal/[tenantId] (Fixes #11) ✓ merged - brand-guide-mcp PR #22 — OIDC state_mismatch fix attempt #1 (NextRequest.cookies — insufficient) ✓ merged - brand-guide-mcp PR #23 — explicit decodeURIComponent on the state cookie (the real fix) ✓ merged - cf-brand-guide PR #1 — boilerplate README replacement ✓ merged

Production deployment work (manual, post-merge): - SESSION_SECRET (32-byte hex) generated and set on Vercel production / preview / development via vercel env add. Required by the new portal JWT signer. - Slack app manifest updated via Chrome MCP at app.slack.com/app-settings/T029TMHTR/A0AUJHASEA0/app-manifest: added users:read bot scope (needed for the is_admin check) AND added https://brand-guide-mcp.vercel.app/api/tenant-auth/slack/callback to redirect URLs (needed by the OIDC flow). Both saved cleanly — Event Subscriptions stayed Verified, no silent-disable banner. Repo slack-app-manifest.json synced to match. - Brand Guide app reinstalled in G&M workspace via OAuth Allow flow. Bot token did NOT rotate (suffix …AkA7mHp unchanged), so no Vercel TENANT_GRAIN_AND_MORTAR_BOT_TOKEN update was needed. - End-to-end OIDC sign-in verified in production: Eric Downs signs in via Slack, lands on /portal/grain-and-mortar with a real session showing ACTIVE / $0.00 / $20.00 / Slack 8/30d / get_messaging 2/30d.

1Password service-account write-token discovery (resolves the 2026-04-23 "read-only" gotcha): The CLI-active service account is ClawBot Reader v2 (Integration ID SOLSAEWC7VDAXKWYXKJ5YANTCA, read-only by design — has visibility on Claude Bot and Licenses). For writes, there's a separate OpenClaw Bot Write service account with read+write on Claude Bot. Its auth token lives at Claude Bot → "OpenClaw Bot — 1Password Service Account (write)" → credential (read it with op item get ... --field credential --reveal). Sub-shell with OP_SERVICE_ACCOUNT_TOKEN="$WRITE_TOKEN" for write operations on Claude Bot. The G&M vault holding canonical client production credentials is NOT in the OpenClaw Bot Write scope — for items there, ask Eric to write directly.

SESSION_SECRET (production) is saved at Claude Bot → "Brand Guide MCP — SESSION_SECRET (production)" → credential. The canonical "Brand Guide MCP — Production" item in the G&M vault still holds everything else (Anthropic key, MCP shared secret, Slack secrets, tenant tokens, ADMIN_PASSWORD, CRON_SECRET).

Pre-existing hydration error in DailySpendChart (from card #6 work) emits a console error from float-precision rect coordinates (y={71.40411099691676} etc.). Out of scope for cards #3 / #10 — left as a small follow-up. My ActivityStrip rounds coordinates to integers and drops SVG <title> tooltips so it hydrates cleanly.

Pending: PRD review — PRD.md was rewritten with MCP-as-a-service framing; once approved, the repo README + ROADMAP get a propagation PR.

Memory entry saved: feedback_ask_user_question.md — always route questions through AskUserQuestion, never inline.

2026-04-26 — Two real tenants: G&M brand guide stood up, dogfood swap complete

Big session, mostly product surface work + the long-overdue dogfood completion:

Per-tenant API keys + surface tagging shipped (PR #1, merged): - New api_keys table (per-tenant, per-surface, SHA-256-hashed, revocable) - MCP route accepts both MCP_SHARED_SECRET (Slack internal path → surface='slack') and per-tenant API keys (pk_live_<tenant>_<surface>_<32hex> → surface from key row) - usage_events.surface column tags every call so we can break down by Slack vs Claude Skill vs Claude Code etc. - /admin/api-keys page with create-with-once-revealed plaintext + revoke - Rationale: foundation for selling MCP access outside the in-house Slack bot. Slack stays internal, paying customers get scoped keys.

Shared MCP infrastructure fix (huge): - Discovered G&M's Claude Code account had been silently loading ZERO MCP servers for weeks because ~/.claude.json (legacy location with all 17 MCPs) wasn't being read — Claude was reading ~/.claude/.claude.json (active CLAUDE_CONFIG_DIR) which had no mcpServers block. - Built ~/.claude-shared/ as canonical source of truth: mcps.json + sync.mjs injector + README documenting the rule. Both claude and claude-royal zsh wrappers now run sync before launch. - Added op run --env-file=~/.env.op -- to the wrappers so any MCP using ${VAR} placeholders (like the new gm-brand-guide MCP) resolves secrets from 1Password at process start. Plaintext never on disk.

G&M as second real tenant — dogfood completion: - Issued first per-tenant API key for Grain & Mortar (pk_live_grain_and_mortar_claude_skill_*…77dd), saved to 1Password G&M vault as a field on "Brand Guide MCP — Production". - Created ~/.claude/skills/gm-brand-guide/SKILL.md so any G&M Claude Code session knows when to call the brand-guide tools. - Stood up grainandmortar-brand-guide — G&M's own brand guide site, scaffolded from CF, populated with real content (colors, typography, foundation, voice, messaging, applications) from the gm-v8 WordPress theme tokens and llms.txt. - Swapped tenants.json (commit fdebbe3) so the grain-and-mortar tenant now points at the new G&M brand guide instead of CF's URL. The "dogfood off CF" hack from 2026-04-21 is over — both tenants now serve their own real content. - Initial scope error: I asked Eric "API-only or sanitize the marketing pages" and recommended API-only; he correctly pushed back that he expected a visible brand-guide site like CF's. Reversed: restored the rendered app/(guide)/* pages, rewrote them for G&M's content schema, themed in G&M tokens.

End state: - 2 fully-functional brand-guide tenants (CF + G&M) each serving via their own deployed Next.js site - The gm-brand-guide Claude skill now returns real G&M content end-to-end through the MCP - Per-tenant API key system in place, ready to sell access to a third party when the time comes - Card #5 closed (G&M brand-guide stand-up). Cards #3 and #4 (pause flag + usage cap) still backlogged per Eric's call to defer billing until later.

2026-04-25 — Per-user agent memory shipped end-to-end

Wired Anthropic's Memory tool (memory_20250818) into the agent so brandbot can remember per-user notes across threads — role, format preferences, recurring projects. Built, tested, deployed in one session.

Architecture: - New agent_memory Postgres table (drizzle migration 0003_agent_memory, applied to Neon). PK is (tenant_id, slack_user_id, path). Memory is scoped per-user-per-tenant — what one teammate's chats produce is invisible to others, and not shared across workspaces. - lib/agent/memory-store.ts — full Postgres-backed implementation of the SDK's MemoryToolHandlers interface (view, create, str_replace, insert, delete, rename). Mirrors BetaLocalFilesystemMemoryTool from the SDK behaviorally but persists to DB. Directory listings are virtualized: a "directory" exists iff at least one file path begins with that prefix. Path traversal guard rejects anything not starting with /memories. - lib/agent.ts refactored from a single Messages API call into a tool-loop. Beta header context-management-2025-06-27 enabled. The MCP toolset continues to be handled server-side by Anthropic; client-side memory tool_use blocks are intercepted, run against the store, and the tool_result is fed back. Hard cap at 6 round-trips per askAgent call. - System prompt teaches the agent how to use memory: view /memories at the start of each new conversation, save things like role/preferences/projects, don't store brand guide content there, curate (don't accumulate cruft). - Each loop iteration goes through recordedMessagesCreate, so each round-trip is a separate usage_events row — diagnostic-friendly and matches existing usage cap behavior.

Admin tooling: - New /admin/memory page added to the dashboard (Tenants · Usage · Interactions · Memory tabs in app/admin/layout.tsx). Index lists per-user summaries (file count, total bytes, last updated). Drill-in shows each file's path, mtime, and full content with a per-file delete button + a "Delete all N files for this user" button. - lib/agent/memory-queries.ts — read/delete helpers for the admin path. Uses coalesce(sum(octet_length(...)), 0) for byte counts to handle empty rows. - app/api/admin/memory/route.ts — DELETE endpoint accepting { tenantId, slackUserId, path? }. Single-file or wipe-all depending on whether path is supplied.

Retention: - New daily cron at /api/cron/purge-memory, scheduled in vercel.json for 04:30 UTC (offset from the 04:00 interactions purge). 365-day inactivity TTL — long because the model is expected to actively curate; short windows would defeat the purpose. Bearer-auth gated on CRON_SECRET like the existing purge.

Privacy disclosure (v3): - /privacy bumped to effective 2026-04-25. New "Personal memory" section disclosing: scoped per individual Slack user, not shared across teammates or workspaces; intended for short notes about how the user works with the bot (not personal data, credentials, or message history); 365-day inactivity purge; user can ask the bot to forget specific things; never used for training, never sold; admin can wipe on request. - "Your choices" section extended to mention memory deletion alongside log deletion.

Testing: - scripts/smoke-memory-store.ts — exercises every command + edge cases (create-duplicate-throws, path traversal rejection, file-then-directory rename, recursive directory delete) against the real Neon DB. All 16 cases pass. - Caught and fixed one bug during the smoke test: directory rename was using a sql\...substring(...)`template where Postgres couldn't infer the int-parameter type, throwing on every dir rename. Refactored to per-row UPDATEs (we expect ≤ a handful of files per directory anyway). - Browser-verified empty + populated states of/admin/memory` plus the DELETE API end-to-end against the local dev server before push.

Drizzle journal backfill: Found Neon's drizzle.__drizzle_migrations table was empty even though prior migrations were applied (probably via push originally). Backfilled all 4 entries (0000–0003) with their content hashes so future drizzle-kit migrate calls behave correctly.

.env.local got ADMIN_PASSWORD so future local /admin/* rendering works (proxy.ts requires it; was missing locally, present on Vercel).

Commit: bac85bb — 15 files changed, 1981 insertions / 48 deletions. Auto-deployed to https://brand-guide-mcp.vercel.app. Verified live: /admin/memory returns 200 with auth, /api/cron/purge-memory returns 401 without bearer (auth gate firing), /privacy shows v3.

Design call locked in via AskUserQuestion at the start of the session: - Scope: per-(tenant, slack_user). Personal scratchpad model. Workspace-wide knowledge belongs in the brand guide content, not here. - Tool variant: Anthropic's official memory_20250818 (not a custom MCP tool). Lets Claude reuse its native file-tree mental model for memory.

What this unlocks: brandbot can now build up a persistent picture of each teammate over time — what team they're on, how they like answers, what projects they keep asking about. The thread-history-as-memory pattern still handles in-thread context; this is the layer above that.

Watch list: First few real interactions in G&M and CF Slack will tell us whether the model actually views /memories at the start of fresh conversations like the system prompt instructs, and whether what it saves is signal vs. noise. Check /admin/memory in a few days. If it accumulates junk, tighten the prompt.

2026-04-23 — Slack app visual identity + marketplace listing polished

Picked up with Eric asking what's outstanding on brandbot and deciding to tackle the "generic Slack icon" problem. Design-first per docs/SLACK-UX.md.

Icons shipped (both in public/brand-icons/, committed 1afe7cc): - brandbot.png — 1024×1024 PNG, near-black #0F0F14 bg + cream "B" monogram. Universal face for every workspace. - california-forever.png — 1024×1024, CF Yellow #FFBC21 bg + CF Navy #112D40 "CF" monogram. Tenant-specific face for @mention replies once chat:write.customize is wired (Phase 0.5d).

Proposed 3×3 grid of options (near-black / warm orange gradient / navy gradient × A/B/C), Eric picked near-black brandbot + yellow CF. Generated via inline Python/PIL (same rendering logic as ~/.claude-royal/skills/folder-icons/tools/make_letter_icon.py, just output PNG instead of .icns iconset).

Slack app updated via Chrome MCP: - Uploaded brandbot.png to Basic Information → App icon. Upload saves immediately (independent of Save Changes button). - Background color #5B4EDB#0F0F14 via App Manifest route (discovered the Basic Information "Save Changes" button validates ALL marketplace fields once Public Distribution is on — so landing page / privacy / support / etc. must ALL be filled or save fails. Editing the manifest JSON bypasses that validation entirely.) - Filled marketplace fields to make the listing real: - Landing page: https://brand-guide-mcp.vercel.app - Privacy URL: https://brand-guide-mcp.vercel.app/privacy - Support URL: https://brand-guide-mcp.vercel.app/support - Support email (public): web@grainandmortar.com - Supported language: English (U.S.) - Pricing: Free - Contact (private to Slack): Eric Downs / web@grainandmortar.com / 402-880-5637 - Install mode: Install from landing page

Marketing-site additions (committed ca8e8b6): - app/privacy/page.tsx — real privacy policy (what we collect, how we use it, third-party disclosure for Anthropic). Contact: hey@grainandmortar.com. - app/support/page.tsx — common issues + contact. Contact: hey@grainandmortar.com.

Universal Slack-dev skill created: ~/.claude-royal/skills/slack-app-dev/SKILL.md. Covers: - Manifest-first app creation (skip the feature-by-feature UI flow) - OAuth scopes cheat sheet - Signature verification (Next.js req.text() FIRST, then verify, then parse) - 3-second ack rule + after() pattern - Events API subscriptions + url_verification handshake - Slash commands + response_url + trigger_id - Interactivity (button clicks, modal submits, message actions, global shortcuts) - App Home - Distribution modes + marketplace validation gating (+ the manifest-bypass trick) - App identity layers (app-level icon, bg color, bot display name, per-message chat:write.customize overrides) - Per-tenant identity pattern (public PNGs hosted in repo + chat.postMessage overrides; only works on chat.postMessage, NOT on slash-command HTTP responses) - Chrome MCP automation patterns for api.slack.com (manifest edits via CodeMirror cm.setValue(), 1Password-overlay Escape workaround, URL map for every admin page) - Testing patterns (signed webhook simulation via openssl, user-token preview DM) - Rate limits + tier table - Common gotchas

Complements /slack-formatting (which covers Block Kit message rendering). The two are explicitly linked — "this skill covers the APP itself, not what it says."

Non-obvious learnings saved to the new skill so they persist: 1. Manifest Save Changes bypasses marketplace form validation — critical for distributed apps. 2. 1Password browser extension injects an autocomplete overlay on credential-shaped fields; Escape after fill before next click. 3. App icon upload saves immediately on upload — decoupled from the form's Save Changes button. 4. Slack's admin UI splits between api.slack.com/apps/<id>/... (most pages) and app.slack.com/app-settings/<team>/<app>/... (manifest, distribution, collaborators). Must navigate to the right host.

Commits: - 1afe7cc — Brand icons (brandbot + CF) - ca8e8b6 — Privacy + Support pages

Parked state: - Slack app visual identity fully shipped; @brandbot now shows near-black squircle instead of default placeholder everywhere. - CF per-tenant icon is in public/ ready to wire up once chat:write.customize scope + reinstall is done (Phase 0.5d). - /slack-app-dev skill is globally callable from any project.

Remaining to-do: CF install for Phase 0 completion (P1), message-shortcut discoverability pivot (P2). Agent wiring (P3) is DONE — the "parked until new Anthropic org" language from the 2026-04-22 notes is stale. lib/agent.ts calls the Messages API directly with MCP tools attached; @mentions and DMs respond live in G&M as of tonight's test. The earlier "Managed Agents" architecture was never adopted; anthropicAgentId field removed from the tenant schema tonight.

2026-04-23 (night) — Post-incident: silent event-subscription outage

After the icon/marketplace polish session above, Eric tried @brandbot in his DM and got no response. Debugging turned up a full event-delivery outage that I caused with today's manifest save.

Root cause: Today's manifest save to update background_color raced with an in-flight Vercel deploy (privacy/support pages commit ca8e8b6 was mid-build when I pushed the manifest via CM setValue + Save Changes). Slack fired its URL verification challenge for /api/slack/events; the endpoint returned non-200 because the deploy wasn't ready. Slack silently disabled event subscriptions server-side. UI kept showing "Enabled: On." The only visible signal was a buried alert banner on the Event Subscriptions page: "Oh no! We couldn't save the event subscriptions for this app."

Symptoms: - @brandbot in DM, App Home Messages tab, and @-mentions in channels — all silent. No reply, no error. - Slash commands /brand colors etc. continued working (different transport). - usage_events table showed zero agent_response rows for G&M after the incident window; only testing rows from 2026-04-22 night were present. - auth.test succeeded — bot token was valid. - All env vars correct, tenants.json correct (mention: true).

Fix: 1. On Event Subscriptions page → clicked Change on Request URL → re-entered the same URL → "Verified ✓" banner appeared. Save button remained disabled (same URL = no diff), but the verification action itself restored Slack's internal event-delivery state. 2. Confirmed by having Eric re-@-mention brandbot → response arrived as expected.

Collateral finding: The check_brand message shortcut was missing from the live Slack app (not in Interactivity & Shortcuts page, not in the manifest JSON either). The repo manifest still had it in features.shortcuts. When I tried to push it back via the manifest editor, Slack rejected the shortcuts schema with a line-29 validation error. Appears Slack migrated message-shortcut storage out of the manifest into Interactivity & Shortcuts admin UI (separate form). Handler code + rule engine are still intact in the codebase; the shortcut can be re-registered via admin UI when we pick up the discoverability pivot (parked P2).

Persisted the lesson: - scripts/check-usage.mjs — one-shot query for recent usage_events per tenant. Lets any future session diagnose "did the agent even get called?" in one command. - /slack-app-dev skill updated with a dedicated "The silent manifest-save/deploy race" section: failure pattern, UI symptoms, the fix procedure, and the prevention (wait for deploy Ready before saving manifest; curl-check the events URL as a liveness test). - slack-app-manifest.json in repo now synced to match live Slack state (bg color, pkce_enabled, is_mcp_enabled, shortcuts removed).

Process learning (for the project notes): When making any UI config changes that touch Slack's Event Subscriptions indirectly (manifest saves do this, even for display-only fields), ALWAYS verify event delivery still works afterward. The "is it working" test is: have someone @-mention the bot and watch for a reply (or check usage_events in DB for a new row). Don't assume UI green-checks = actual working state.

Commits: 2f2d78a (manifest sync + diagnostic script).

2026-04-23 (late night) — Interaction logging, admin gate, Blackout redesign, privacy v2

Long session after the events-subscription fix. All shipped + live on Vercel.

Interaction logging end-to-end (commits 29a8402, b051439): - New interactions + tenant_overrides tables (drizzle migration 0002_safe_legion, applied to prod via scripts/apply-migration.mjs). - lib/interactions/log.ts helper wired into respondToMention, respondToDM, and the shortcut check_brand paths. Opt-in per tenant via features.logInteractions + DB override. 30-day retention via daily Vercel Cron (/api/cron/purge-interactions, CRON_SECRET bearer). - New /admin/interactions page: filter by tenant/surface/days, expandable rows showing full user message + bot response. Consolidated admin shell at app/admin/layout.tsx with Tenants · Usage · Interactions tabs. - G&M has logInteractions: true as dogfood; CF is off by default. - scripts/read-interactions.mjs — terminal debugging tool (--tenant, --surface, --days, --limit). Future Claude sessions can use this to read the last N exchanges when tuning responses. - Verified end-to-end: Eric @mentioned brandbot → row appeared in /admin/interactions with his question and the bot's palette reply.

Admin dashboard gated (commit 29a8402): - Previously /admin was wide open on the public internet. Now behind basic auth via proxy.ts (Next 16 renamed from middleware). - Credentials saved to docs/ADMIN-ACCESS.md (private repo, explicitly committed with password + CRON_SECRET per Eric's ask). Couldn't write to 1Password — service-account session is read-only on the visible vaults.

Blackout dark redesign (commit b051439): - Applied the /blackout skill across the entire admin. #0d0d0d page, #141414 cards, #262626 inner borders, pure monochrome palette. Grayscale stacked bars on the usage chart replace the earlier hsl(hue) rainbow. Feature pills in white/[0.04] replace the comma-joined feature text.

Docs drift cleanup (commit 73e1b1d): - Removed anthropicAgentId field from the Tenant type + tenants.json + admin UI + docs/SLACK-APP-SETUP.md. Was a vestigial Managed Agents relic; misled the admin into showing "not provisioned" on working agents. - docs/SLACK-APP-SETUP.md Step 7 rewritten to describe actual agent wiring (Messages API + MCP tools via lib/agent.ts). - New docs/ROADMAP.md captures deferred items (privacy Option B, Phase 0.5d per-tenant identity, message shortcut re-registration, OAuth install completion). Cross-linked from docs/README.md.

Privacy policy v2 (commit 73e1b1d): - Option A (MVP) per Eric's call. Rewrite of /privacy names three sub-processors (Anthropic, Vercel, Neon), describes access control for interaction logs, lists retention mechanism, adds "Your choices" + "Changes to this policy" sections, adds effective date. - Option B (in-Slack transparency line in /brand help) queued in docs/ROADMAP.md with the trigger "before CF goes live." - Scope decision locked: workspace-wide automatic logging when the toggle is on, not per-user opt-in. Simpler + matches today's behavior.

Brandbot icon transparent-corner fix (commit 0e9b489): - Eric noticed a light halo behind the avatar in some Slack surfaces. Root cause: the icon was a rounded-squircle PNG with transparent corners; Slack's container clip doesn't align exactly with the squircle, so Slack's background bleeds through. Fix: re-rendered both brandbot.png and california-forever.png as solid-color squares (no internal rounding — let Slack's clip do all corner-rounding). Re-uploaded brandbot.png via Chrome MCP.

Environment: - New Vercel env vars: ADMIN_PASSWORD, CRON_SECRET. - Dashboard live at https://brand-guide-mcp.vercel.app/admin (basic auth). - Creds in docs/ADMIN-ACCESS.md.

Next session pickup: finish Phase 0 for CF — install the Slack app in CF's workspace, capture their team ID + bot token, set Vercel env vars, redeploy. See docs/SLACK-APP-SETUP.md Steps 6–7.

Commits tonight: 29a8402 · b051439 · 2fb4c5a · 73e1b1d · 0e9b489.

2026-04-22 (morning) — Message shortcut live; discoverability pivot pending

Built and shipped the "Check with brand guide" message shortcut end-to-end. Tightly scoped per MESSAGE-SHORTCUT-DESIGN-PROPOSAL.md.

What shipped: - Rule engine: lib/brand-check/rules.ts + engine.ts. Pure functions, content-driven (no CF hardcodes), skipped silently when source content is missing. - Four deterministic rules, all sourced from the tenant's brand guide content: - Naming — banned aliases from messaging.namingConventions entries with usage containing "never" - Contractions — non-contracted forms when voice.writingRules says "use them" - Numbers — standalone digits 1-9 when voice.writingRules requires spelling them out - Jargon — 3-5 word fingerprints extracted from voice.examples[].bad, fuzzy-matched - Modal via views.open — quoted message + per-rule ✓/✗ rows + summary line + Voice & Tone footer link - /api/slack/interactivity now handles message_action payloads with callback_id check_brand - Slack manifest + Interactivity & Shortcuts config updated via chrome-devtools automation - App reinstalled to G&M workspace to propagate the shortcut (token did NOT rotate — confirmed same bot token post-reinstall) - scripts/test-brand-check.ts — exercises the rule engine against crafted test messages for local iteration without Slack

Commit: 36b2529.

Open question — PARKED: Slack nests app message shortcuts under the "Connect to apps" submenu in the More Actions menu. Eric flagged this as discoverability-hostile for clients. Needs decision before we can ship to CF:

Option Summary
/brand check + modal New slash subcommand. /brand check opens a modal with a paste-text area. Discoverable via /brand help. Keep message shortcut too.
Home tab "Check a draft" button Add a primary button section to the App Home. Click opens the same modal. Premium surface anchor. Keep message shortcut.
Both Two entry points, one shared modal. Max discoverability.
Do nothing new Document the shortcut path in /brand help so users can find it. Accept the buried-menu limitation.

Next session pickup — pick one of the four options above and implement. Will need: - Shared modal view builder (paste-in textarea + submit button) — reuses formatBrandCheckModal() for the result view - View submission handler in /api/slack/interactivity (parses the input from the modal's state, runs the engine, responds with response_action: "update" showing the result view OR opens a new modal) - If /brand check: new switch case in app/api/slack/commands/route.ts that calls views.open with the paste modal via trigger_id from the slash command payload - If Home button: add an actions block with an action_id button to formatHome(); handler routes on action_id in the interactivity endpoint

Parked state: - Working tree clean, all commits pushed. - .env.production still on local disk (has live secrets — clean up before session ends). - The current message shortcut IS live and working in G&M Slack. Eric tested the modal output — works, looks good. Only the discoverability of the trigger is the open issue.

2026-04-22 — Phase A + B + App Home tab shipped; full response coverage live

Heavy build day. Shipped the remaining pieces of Phase 0 Slack UX coverage plus Phase 1's persistent-surface centerpiece.

Phase A (complete — per PHASE-A-REDESIGN-PROPOSAL.md): - Redesigned /brand logo, /brand search, /brand help to match the shared design language. - Added three new subcommands: /brand applications, /brand icons, /brand photography. - All nine content categories now exposed + one search surface + help. - Shared footer now deep-links to each category's section page on the brand guide site ("Logo — Brand Guide →") instead of the root.

Phase B (complete): - /api/logo-png/[tenantId]/[variant].png — server-side SVG→PNG rasterizer via @resvg/resvg-js. Fetches tenant SVGs, rasterizes at requested width (default 600, max 2000), composites against optional hex background, returns with 1d browser / 7d CDN caching. - /brand logo now shows inline PNG previews for each wordmark variant (cream on navy bg, navy on beige bg), auto-adapting to the variant's intended background. - @resvg/resvg-js declared as serverExternalPackages in next.config.ts — Turbopack can't bundle the native .node binding.

App Home tab (complete — per APP-HOME-DESIGN-PROPOSAL.md): - Persistent branded surface — renders on every app_home_opened event for any user who opens the bot's Home tab. - Layout: logo hero → header → Tagline → Mission → 4-color palette preview → 2 rows of URL buttons (one per category + "Open full brand guide →") → footer. - Per-category URL buttons link directly to that section on the brand guide site. - /api/slack/interactivity stub route added — no action buttons yet, but the endpoint's existence suppresses Slack's "not configured to handle interactive responses" warning on the Home surface. - Slack manifest updated (via chrome-devtools automation) to enable app_home_opened event, home_tab_enabled, and interactivity URL.

Infrastructure/polish: - MCP_BASE_URL env var added to Vercel prod — needed by the Home tab + logo rasterizer since image URLs must be absolute for Slack to fetch. - docs/SLACK-UX.md extended with: emoji anchor table for all 11 surfaces, paired-vs-standalone Do/Don't pattern, spec-string colon-split helper, section-aware footer pattern, App Home publish flow, rasterizer architecture. - scripts/preview-home.ts — publish Home directly to Eric's user via user token for visual review without deploying. - Design process + validation toolchain fully established and documented.

Commits: 75c5d33 (Phase A impl) · 79e7d17 (Phase A docs) · 3906922 (section footer) · fd9a034 (Phase B rasterizer) · 60c7f42 (Home tab).

Eric confirmed all surfaces visually, including post-iteration fixes (Home color rendering, interactivity warning removal).

2026-04-21 (night) — Design-first redesign shipped; living Slack UX doc established

Pivoted from reactive formatting fixes to a full design pass. Applied /critique + /arrange + /typeset skills to each of the four /brand responses, wrote SLACK-UX-DESIGN-PROPOSAL.md, got approval, then implemented.

Design language (shared across all 5 responses): - Top header with emoji prefix per category (🎨🔤🗣📣🧭) — visual signature - Subsection header blocks (no emoji) — real size hierarchy above body text - Dividers between subsections only — rhythm via tight-group + generous-gap - rich_text_preformatted for copy-hero content (tagline, pitch, boilerplate, spec strings) — native copy button - rich_text_list with rtBold labels for labeled lists - context block for captions, metadata, and muted ❌ examples in voice - Inline-code pills (rtCode) for personality traits - Shared context footer on every response linking to the full brand guide

Voice hero move: ✅ examples render full-weight bold, ❌ examples render as muted context blocks — the bad example visually recedes, mirroring the brand principle.

Foundation signature: Personality traits render as inline-code pills — each trait a monospace chip with native copy button, together reading like a controlled tag cloud.

Infrastructure now in place: - docs/SLACK-UX.md — living design reference with mandatory design process - CLAUDE.md at theme root now surfaces the Slack-UX-is-product-surface principle so any future session starts with the right framing - scripts/preview-formatters.ts, scripts/test-slack-blocks.ts, scripts/simulate-slash.sh, scripts/post-redesign-previews.ts — full local validation toolchain for Block Kit payloads without needing to deploy - Memory entries saved: premium-bar is a product principle, design-first feedback rule, Slack testing reference

Eric confirmed the redesign looks "much better" after previewing all 5 payloads in his self-DM.

2026-04-21 (late) — First rich_text pass shipped but cramped; pivoting to design-first redesign

Pushed commits a3ec7e2 + 6dbade8 upgrading /brand messaging, typography, voice, foundation to rich_text blocks with native copy buttons, inline-code chips, and bulleted lists. Deploy live on Vercel.

Caught in review: the first pass was a reactive "fix the formatter" exercise, not a designed product surface. Every response runs into the next with no visual hierarchy between sections. Eric flagged that this misses the PRD's "premium brand agent, not chat log" principle — that the rendered Slack output quality is a first-class product requirement, not cosmetic polish.

Validation infrastructure shipped during debugging: - scripts/preview-formatters.ts — dump block JSON for any formatter against real CF content - scripts/test-slack-blocks.ts — post rendered blocks via chat.postMessage to a channel/DM to validate without deploying - scripts/simulate-slash.sh — signed slash-command POST to production endpoint

Validated during debug: block payloads are STRUCTURALLY valid (chat.postMessage returns ok: true for all four). The rendering issue is purely visual hierarchy + spacing, not block-schema errors. Server response confirmed HTTP 200 in ~1.3s for typography subcommand.

Uncommitted WIP in ~/Projects/brand-guide-mcp/lib/slack/format.ts: partial reactive changes (added dividers, started swapping bold labels to header blocks for messaging + typography). Not committed — these represent the abandoned reactive approach, not the design-first direction. Either roll back or let the design pass overwrite.

2026-04-21 — Slack app live in G&M, rich UX shipped, premium-agent framing added

Picked up from the Vercel deploy (see prior entry) and went from "infrastructure reachable" to "working end-to-end in G&M Slack with a polished response surface."

Slack app: - Created the shared Brand Guide app (bot display name brandbot, App ID A0AUJHASEA0) in G&M's Slack workspace via the manifest paste flow (Chrome MCP clicking through api.slack.com/apps) - Captured SLACK_SIGNING_SECRET, SLACK_CLIENT_ID, SLACK_CLIENT_SECRET and wrote them into Vercel env vars across production/preview/development - Verified the Events API URL on Slack's side after the redeploy picked up the signing secret - Activated Public Distribution so the app is installable in any workspace (CF next) - Installed in G&M workspace (workspace T029TMHTR) and captured the bot token - Added grain-and-mortar tenant to tenants.json (pointed at CF's brand guide URL for dogfooding since G&M has no guide of its own yet) - Set TENANT_GRAIN_AND_MORTAR_WORKSPACE_ID + TENANT_GRAIN_AND_MORTAR_BOT_TOKEN env vars on Vercel

End-to-end verification against production: - Simulated signed Slack webhooks hitting https://brand-guide-mcp.vercel.app/api/slack/commands for every subcommand (colors, typography, logos, voice, messaging, foundation, search, help) + unknown-subcommand fallback + bad-signature case (401) — all pass - Eric confirmed /brand colors works in G&M Slack with the new rich rendering

Rich slash-command UX (Phase 0.5a → 0.5b): - 0.5a: Refactored lib/slack/format.ts to return Block Kit payloads instead of plain mrkdwn strings. Colors went to attachment-with-color-stripe rendering; messaging got native copy buttons via code fences; typography got 2-column fields layout. Fixed a pre-existing bug where typography.usage (an object) rendered as [object Object]. - 0.5b: Researched Slack formatting deeply and discovered the rich_text.color element — a native inline hex-color swatch. Switched /brand colors to rich_text with bulleted lists per palette: each row now shows a real colored dot + bold name + inline-code hex + usage. No legacy attachments, no image hacks, any plan, any workspace.

Tooling & docs: - Created ~/.claude/skills/slack-formatting/SKILL.md — full Block Kit reference covering decision tree, every block type, rich_text deep dive, mrkdwn quirks, visual treatments (color swatches, copy buttons, tables, images), bot identity layers, App Home patterns, scope cheat sheet, gotchas. Auto-discoverable via the skills system for any future Slack UX work across clients. - Updated PRD with the "premium brand agent, not chat log" design principle in Product Vision. Added Phase 0.5a/b/c/d breakdown (0.5c = logo PNG previews, 0.5d = per-tenant bot identity via chat:write.customize). Added new Section 14.5 on Bot Identity & Personality (app-level, per-message, per-workspace, content-voice layers). - Saved every credential to 1Password (G&M vault, item: Brand Guide MCP — Production): MCP_SHARED_SECRET, SLACK_SIGNING_SECRET, SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_APP_ID, TENANT_GRAIN_AND_MORTAR_WORKSPACE_ID, TENANT_GRAIN_AND_MORTAR_BOT_TOKEN.

Git state at park: both repos clean, main branches pushed. Last commits: - ericdowns/brand-guide-mcp: d9aea7b (rich_text color swatches) - ericdowns/cf-brand-guide: e7609fd (structured content + /api/brand)

2026-04-21 — Deployed to Vercel, end-to-end MCP verified

Deployed brand-guide-mcp to Vercel. Live at https://brand-guide-mcp.vercel.app.

End-to-end smoke tests all pass in production: - GET / → 200 (marketing landing renders) - GET /admin → 200 (tenant list shows California Forever) - POST /api/mcp/california-forever without Authorization → 401 (auth gate works) - POST /api/mcp/california-forever with Bearer <secret> + tools/list → returns 10 tools - POST /api/mcp/california-forever with tools/call get_colors → fetches CF's brand data via https://california-forever-brand-guide.vercel.app/api/brand/colors and returns palette JSON

The full MCP pipeline (MCP server on Vercel → HTTP fetch to CF brand guide → structured JSON response) is validated end-to-end. Adding a second tenant is now a tenants.json entry + Vercel env vars — no infra work.

Remaining Phase 0 work (needs user/admin access): - Register shared G&M Slack app at api.slack.com/apps → capture SLACK_SIGNING_SECRET, SLACK_CLIENT_ID, SLACK_CLIENT_SECRET → store in Vercel env vars - Install Slack app in G&M's own Slack workspace (dogfood tenant), then in CF's workspace via Anders or Jan - Set per-tenant env vars (TENANT_CALIFORNIA_FOREVER_WORKSPACE_ID, TENANT_CALIFORNIA_FOREVER_BOT_TOKEN) - Provision CF's Managed Agent via Anthropic API → write anthropicAgentId to tenants.json - Wire real Managed Agent invocation inside lib/agent.ts (currently throws a stub error)

2026-04-21 — Phase 0 scaffold committed

Built the foundational skeleton of the multi-tenant MCP app. Work landed in two repos:

ericdowns/cf-brand-guide (California Forever, first tenant): - Refactored brand-foundation, voice, messaging, photography, applications content from React pages into structured JSON (content/*.json) - Added /api/brand index + /api/brand/[category] endpoints exposing all 9 categories with CORS + 60s SWR caching - Deployed to production — live at https://california-forever-brand-guide.vercel.app/api/brand/* - Commit e7609fd

ericdowns/brand-guide-mcp (new, private): - Next.js 16 + TypeScript + Tailwind v4 scaffold (matches CF stack) - tenants.json + lib/tenants.ts — tenant registry + env-var secrets scheme - lib/brand-guide.ts — HTTP client for fetching client brand guide data - lib/slack/* — HMAC signature verification, WebClient factory, mrkdwn formatters per category - lib/mcp/tools.ts — 10 read-only tools (9 category lookups + search) - lib/agent.ts — Managed Agent wrapper (stubbed until CF's agent is provisioned) - app/api/slack/commands/brand slash command dispatcher - app/api/slack/eventsapp_mention handler with next/after async reply - app/api/slack/install — OAuth callback stub (Phase 1 will complete it) - app/api/mcp/[tenantId] — JSON-RPC 2.0 MCP endpoint, bearer-auth gated - app/admin — minimal tenant list dashboard - Build passes, 19-file initial scaffold commit, pushed to github.com/ericdowns/brand-guide-mcp

2026-04-21 — PRD drafted

Worked with Eric on initial requirements. Locked major architecture decisions:

Phase 0 MVP scope (California Forever first pass) intentionally excludes generative tools — ships structured content refactor, read-only MCP, and Slack bot (slash + @mention). Generative features (voice writer, compliance check, asset delivery) slot into Phase 1.

Pricing model deferred until after CF launch — CF treated as paid beta inside the existing retainer.

Next Session Pickup

Compact-proof status as of 2026-05-01. Replaces the 2026-04-27 priorities — Phase 1 SaaS productization items (cards #3, #4, #9, #10, #11) all shipped, Phase 0.5d (per-tenant identity), Privacy Option B, and message shortcut all shipped. Brand Bot Todoist board now manages active work (Brand Bot 🎨 project, ID 6gWH8jhcQFC7Qhv7).

Awaiting Eric (do nothing until resolved)

  1. Design Check Phase 0 spike — final calibration step. Last subtask of the spike (Brand Bot Todoist 6gWMvCCPwCxr6qFf). At-computer work: issue a design_check-scoped API key via /admin/api-keys (the new dropdown option), drop 3-5 real G&M ads into either the portal upload at /portal/grain-and-mortar/design-check OR scripts/design-check-fixtures/ for the benchmark script, eyeball results vs brand-director judgment, author ~/.claude/project-notes/brand-guide-mcp/DESIGN-CHECK-SPIKE-FINDINGS.md. After findings authoring, Phase 0 is officially closed.
  2. Real-Slack smoke tests (6gWGgVPXp2g5xr67). Carryover from 2026-04-27 — verify @brandbot per-tenant identity + message shortcut + memory tool in the actual G&M Slack workspace. Test list is in the card body. Only Eric can do it (needs to be in his real Slack).
  3. PRD reviewPRD.md in this folder. Sign-off unblocks the repo README + ROADMAP propagation PR (MCP-as-a-service framing + Surfaces table + Commercial Model into the GitHub-visible docs).
  4. Discovery cards with Client: TBD placeholders: - Strategy Bot discovery (6gWJ2wjmG9FgPQRf) — needs client name + their strategy guide before scoping moves forward - Design Check discovery (6gWJ32QRVqjRh727) — needs client name; competitive scan running on Desktop is partial work that can advance independently

Ready to pick up (Phase 1 decisions, after Design Check findings authored)

Decision Notes
Real Slack surface for Design Check Right-click on an image message → "Check brand guide" runs Design Check. Mirrors the existing text-only check_brand shortcut. Decide whether to ship now (closes the loop on the Slack surface) or defer until at least one client asks for it.
Surface bundling vs à la carte design_check is the third external surface (after claude_code/claude_skill/codex/direct). Pricing model still open — $X bundle for all surfaces vs per-surface upcharge. Drives admin / portal billing UI shape.
Widen Design Check to California Forever tenant Today the brand-context summary works only because G&M's brand JSON was reshaped to the schema runBrandCheck expects. CF's brand JSON probably has the original CF schema (cleaner — engine was designed against it). Verify by running the benchmark against a CF asset to confirm + document any schema drift.
Keep the portal preview UI or redesign for production The /portal/[tenantId]/design-check page was a quick spike-scope addition (scope-violation, built so Eric could see something on his phone). Decide whether it stays as the canonical surface or gets a proper Phase 1 design pass per docs/SLACK-UX.md-style discipline.

Recently shipped (last session, 2026-05-01)

Not yet a card (consider creating one)

Operational reminders

Key Files & Locations

What Where
Codebase ~/Projects/brand-guide-mcp (GitHub: ericdowns/brand-guide-mcp, private)
Production URL https://app.heybrandbot.com
Vercel project ID prj_qAixR7ZedkXbAICAzuyqOFx438bM (team team_gvCJfcW5DZLXrevbHJdCrmir)
Slack app admin https://api.slack.com/apps/A0AUJHASEA0
PRD PRD.md in this folder
Slack formatting skill ~/.claude/skills/slack-formatting/SKILL.md
Credentials vault item 1Password G&M vault → "Brand Guide MCP — Production"
First client brand guide ~/Projects/california-forever-brand-guide (GitHub: ericdowns/cf-brand-guide). Production: https://california-forever-brand-guide.vercel.app. API: /api/brand/*

Product PRD in PRD.md.