---
status: ACTIVE
---

# CEO Plan: notfair CMO — AI Marketing OS

Generated by /plan-ceo-review on 2026-05-17
Mode: SCOPE EXPANSION | Approach: B (CMO-first Marketing OS)
Repo: notfari-workspace/notfair-cmo (greenfield)

---

## Guiding tenet

**Don't rebuild the wheels.** Before any new abstraction, registry, broker, or service in this architecture, identify what OpenClaw, Vercel platform services, AI SDK, the AI Gateway, or existing skill ecosystems already provide. Default to: existing system as source of truth + thin attribution/glue layer + frontend view layer. The product is differentiated at the product surface (CMO UX, project memory, cross-channel signals, autonomy), not by reimplementing commodity infrastructure.

---

## PIVOT (Revision 5, 2026-05-17): open-source local CLI tool

**Major scope adjustment.** notfair CMO is now an **open-source local app** distributed as an npm package. User runs `npx notfair-cmo` in their terminal; it launches a local Next.js server on `localhost:3000` and opens their browser. Single-user, single-machine, no SaaS hosting, no auth, no multi-tenancy. **OpenClaw is assumed installed + running locally on the user's machine** (we verified its capabilities via live spike — see [[openclaw-capabilities]]).

This pivot eliminates significant infrastructure (multi-tenant auth, RLS, envelope encryption per tenant, hosted Postgres, Vercel deployment, AI Gateway tenancy tagging) AND leverages OpenClaw native capabilities we were about to rebuild (memory, cron, agent isolation, MCP tool exposure, WebSocket gateway). Net effect: V1 effort drops from ~3-4 weeks CC to ~1-2 weeks CC.

**Sections superseded by this pivot:**
- Original "Approach" → see "Approach: B-local" below
- Original "Architecture decisions (locked in eng-review)" → see "Architecture decisions (revised for local OSS)" below
- Original "Wheel-rebuilding audit" → see updated audit reflecting OpenClaw native primitives
- Original "Protocol definitions" → see simplified MCP-based versions
- Original "Code conventions" → simpler (no multi-tenant middleware)

**Sections that stay valid:**
- Vision (the "hire a CMO" framing still works for OSS, just with developer-marketer hybrid audience)
- Design specifications (UI doesn't change with runtime; sidebar, screens, states all stay)
- Test strategy structure (concrete tests change but discipline stays)

---

## Vision

### Product positioning
**"The AI marketing team you hire on day one."** Not "manage 6 agents" — *hire a CMO that runs your marketing function.* The user sets direction, the CMO orchestrates execution, specialist agents do channel work, results flow back. Specialists are an implementation detail; the CMO is the product.

### 10x check
The user sets direction at 9am, reviews outcomes at 5pm. Overnight, agents ran experiments, killed losers, escalated winners, drafted tomorrow's plan. The user wakes up to a 3-paragraph briefing with one decision to make today. By day 90, project memory has compounded so the CMO's recommendations are noticeably sharper than day 1's. By day 180, agencies run 10-brand portfolios on this and switching is painful because nothing else has the per-brand memory.

### Platonic ideal — solo founder onboarding
1. Paste site URL (20 seconds)
2. CMO scrapes site → derives brand voice fingerprint + ICP hypothesis (2 minutes)
3. CMO proposes 30-day experiment program across 2 channels (1 minute review)
4. Founder approves budget + caps
5. Day 7: first weekly review with what shipped and what's working
6. Day 30: noticeably sharper recommendations from accumulated project memory

---

## Approach: B-local — CMO-first Marketing OS (open-source local)

Substrate:
- **Distribution:** npm package; user runs `npx notfair-cmo` → spawns local Next.js server + opens browser
- **Frontend:** Next.js App Router (standalone build, runs locally — no Vercel)
- **Local DB:** SQLite (better-sqlite3) at `~/.notfair-cmo/db.sqlite` for our own state (tasks, approvals, cost events, oauth tokens, project metadata)
- **AI orchestration:** AI SDK v6 with user's own LLM API key (OpenAI/Anthropic/Google — they supply); no AI Gateway tenancy needed
- **Agent runtime:** **OpenClaw** (assumed installed + running on user's machine). We invoke via subprocess (`openclaw ... --json`) and connect to its WebSocket Gateway on loopback.
- **Agent memory:** **OpenClaw memory subsystem** (built-in: index, search, REM reflection, promotion to MEMORY.md). We don't run pgvector.
- **Agent cron:** **OpenClaw cron** (built-in: `openclaw cron add/list/edit/disable/runs`). We don't run our own scheduler.
- **Tool bridge:** Our Next.js exposes an MCP server with tools (`approve_action`, `record_cost`, `log_action`, `query_project_state`, etc.). Registered with OpenClaw via `openclaw mcp set`. OpenClaw agents call our tools via MCP.
- **Skills:** preloaded per agent via OpenClaw's existing skill/MCP system. Reuse `notfair-googleads` MCP (user already has it configured). Add SEO MCP for V1.
- **No auth:** loopback-only Next.js server, single user, single machine. Gateway runs in `--auth none` mode bound to loopback.
- **OAuth (Google Ads, GSC):** `localhost:<port>/oauth/callback` redirect; tokens stored encrypted in SQLite using OS keychain (keytar) for the master key. No tenancy — just one set per user per channel.

We own:
- Local Next.js UI (sidebar shell, CMO chat, approval inbox, cron tab, cost meter, project home, onboarding)
- MCP server exposing tools to OpenClaw agents
- Subprocess wrapper around `openclaw` CLI for agent/cron/memory management
- Project concept (group OpenClaw agents + crons under a project namespace via name prefix)
- OAuth flows for Google Ads + GSC
- Local SQLite schema (tasks, approvals, cost events, oauth tokens, projects)
- npm package + CLI launcher

We do NOT own:
- Agent reasoning logic (OpenClaw)
- Agent memory (OpenClaw memory)
- Cron scheduler (OpenClaw cron)
- Individual channel skills (reuse `notfair-googleads`; create curated SEO MCP)
- LLM cost (user's own API key + their provider's billing)
- Hosting (it's local)

---

## Scope Decisions

| # | Proposal | Decision | Reasoning |
|---|----------|----------|-----------|
| D1 | Project memory + brand voice fingerprint | **V1 (via OpenClaw memory)** | Use OpenClaw's built-in memory subsystem (per-agent + REM promotion). Brand voice fingerprint is a memory entry tagged for cross-agent retrieval. We don't run pgvector. |
| D2 | CMO autonomous within user-set guardrails | **V1** | The "review outcomes at 5pm" promise requires this. Guardrails enforced by our MCP-side `approve_action` tool — autonomous below threshold, else routes to approval inbox. |
| D3 | Cross-agent signal sharing | **V1 (via OpenClaw memory + MCP)** | Agents write signals as memory entries with a `signal:` tag; CMO + specialists query via `memory search` MCP tool. No custom pub/sub. |
| D4 | Agency multi-brand mode as first-class | **DROPPED entirely** | Single-user local app. Users who want multi-brand just create multiple projects in their local install. If agency demand emerges post-OSS-launch, consider a hosted SaaS edition as a separate product. |

**V1 effort revised post-pivot:** ~1-2 weeks of CC-time. No OAuth verification calendar wait — `localhost` callback is exempt from Google's app verification for non-production-restricted scopes.

---

## Accepted scope (V1) — post-pivot

### CLI + launcher
1. **`notfair-cmo` CLI** — npm package; `npx notfair-cmo` or `pnpm dlx notfair-cmo` starts local Next.js server, opens browser at `http://localhost:<port>`. Subcommands: `start` (default), `stop`, `doctor` (checks OpenClaw is running), `version`.
2. **Local data dir** at `~/.notfair-cmo/` (SQLite + logs + cached scrape data). Override via `--data-dir`.
3. **Doctor command** — verifies OpenClaw is installed + running (`openclaw health`), LLM API key set, port available; surfaces clear fix-instructions for any failure.

### Core product (UI)
4. **Project management** — create/archive/switch projects; project = a name + slug + the OpenClaw agents/crons whose names start with that slug
5. **Agent setup — 2 V1 agent templates** (Google Ads, SEO). Creating a project provisions OpenClaw agents per template via `openclaw agents add <project-slug>-<template>`. Each gets preloaded skills via MCP (existing `notfair-googleads` for Google Ads; new curated SEO MCP for SEO).
6. **CMO agent** per project — `openclaw agents add <project-slug>-cmo` with system prompt + access to our tool MCP. Orchestrates specialists.
7. **CMO chat UI** — AI SDK `useChat` connecting to our Next.js route that bridges to OpenClaw via `openclaw agent --json` per turn (or WebSocket Gateway if streaming required).
8. **Task board** — UI view of tasks stored in our SQLite. Tasks are user-facing units the CMO creates via the `assign_task` MCP tool. State lifecycle in protocol section.

### Memory + intelligence (D1, D3 — via OpenClaw)
9. **Project memory via OpenClaw memory** — each agent has its own memory file (`agent.md` per OpenClaw convention). For project-shared memory (brand voice, ICP, competitor intel), all project agents share a common workspace dir so the memory pool is shared. CMO can `memory search` across project memory before any decision.
10. **Brand voice fingerprint** — derived during onboarding from URL scrape + uploaded samples, written as a structured memory entry tagged `voice:`. Every agent's system prompt pulls this via memory search.
11. **Cross-agent signals via tagged memory + MCP** — agents emit observations as memory entries tagged `signal:<type>` with payload + suggested_actions. The CMO and specialists query `memory search "signal:" --agent <project-slug>-*` to discover unconsumed signals. No custom pub/sub.

### Autonomy + safety (D2)
12. **Guardrail config** — stored in SQLite + surfaced in Settings UI. Same threshold schema as before.
13. **Autonomy enforcement via the `approve_action` MCP tool** — every potentially-spending agent action MUST call `approve_action(action_summary, cost_estimate, type)` first. Our tool checks against the guardrail config; either returns `{approved: true}` (within threshold) or stores in approvals queue + returns `{approved: false, approval_id}`. Agent retries after user approves.
14. **Approval inbox UI** — reads approvals queue from SQLite, one-click approve/edit/reject, fires a webhook (loopback) back to a waiting agent to resume.
15. **"Why?" inline in chat** — every agent action message has expandable reasoning fetched from `agent_actions` log table.

### Always-on ops
16. **Cost meter** — sums `cost_events` table; LLM costs reported by agents via `record_cost` MCP tool after each LLM call (using AI SDK usage data they receive). Ad spend reported by Google Ads MCP after every spend change. Always-visible in sidebar.
17. **OAuth flows for Google Ads + GSC** — localhost redirect `http://localhost:<port>/oauth/<provider>/callback`. Tokens encrypted with OS keychain master key (via `keytar`), stored in SQLite `oauth_tokens` table.
18. **Cron tab** — wraps `openclaw cron list --json`; groups by project (parsing project slug prefix from cron name) and by `--agent` (already a column in OpenClaw output). Create/pause/resume/delete forward to OpenClaw via CLI subprocess.
19. **Daily briefing** — in-app card on Project Home; generated nightly via an OpenClaw cron that calls our `generate_briefing` MCP tool, result stored in SQLite. No email/external delivery in V1.

### Distribution
20. **GitHub repo** — public, MIT license (recommended; user can choose), README with quickstart, ARCHITECTURE.md, CONTRIBUTING.md
21. **npm publish workflow** — GitHub Actions: on tag, build standalone Next.js + bundle + publish to npm registry
22. **Telemetry: opt-in only** — no phone-home by default; opt-in posts anonymous usage events to a (TBD) endpoint. OSS users will be hostile to default telemetry.

## Deferred to v1.1+

- **More agent templates** — GEO, Cold Email, Reddit, X (gated until their channel skills/MCPs are curated)
- **Channel delivery via OpenClaw native (Telegram, Slack, iMessage)** — OpenClaw already supports it; we don't surface it in V1 UI but advanced users can wire it manually
- **Standalone binary distribution** (Tauri/pkg) for users without Node installed
- **Docker distribution** for users who want isolation
- **Settings export/import** — for backup/migration between machines
- **Multi-machine sync** — Tailscale-based or via OpenClaw's existing remote-gateway feature
- **Hosted SaaS edition** — if agency demand emerges, separate product with auth + multi-tenancy + managed Postgres
- **Competitive observation loop, pause-and-handoff to human, automatic post-mortems** — all post-V1

---

## Deferred to backlog (v1.1+)

- **Competitive observation loop** — weekly competitor scan, response suggestions
- **Pause-and-handoff to human freelancer** — Loom-style brief export
- **Automatic post-mortems** when campaigns underperform (stretch; project memory makes this easy to add later)
- **Cross-project intelligence** — learnings from one brand inform another (carefully — competitive overlap risk for agencies)
- **Slack integration** — daily briefing in Slack channel
- **Mobile companion app**

---

## Out of scope (explicitly)

- **Hosted SaaS** — local-only V1; if hosted demand emerges, separate product
- **Authentication** — local loopback, single user
- **Multi-tenancy** — no organizations, no roles, no RLS
- **Custom agent runtime** — OpenClaw owns
- **Custom memory layer** — OpenClaw memory owns
- **Custom cron scheduler** — OpenClaw cron owns
- **Custom pub/sub for signals** — OpenClaw memory (tagged entries) owns
- **Channel skills from scratch** — reuse existing MCPs (`notfair-googleads` already exists; curate SEO MCP)
- **Marketing site / brand identity** — OSS project; README + GitHub repo is the marketing site V1
- **Closed-source distribution** — open source by design
- **White-label / reseller** — OSS, users fork if they want

## Design specifications (V1)

### Design direction

**Locked: shadcn/ui defaults.** Zinc neutral palette, Inter font, default radii. No custom brand in V1. Defer brand work to post-GA via /design-consultation. AI slop avoidance even with defaults: no purple gradients, no icon-in-colored-circle decoration, no generic hero copy ("Welcome to notfair CMO!"), no centered-everything layouts, no decorative blobs.

### App shell

**Locked: Pattern A — persistent left sidebar + main area.**

```
┌──────────────────────────────────────────────────────────────────────┐
│  SIDEBAR (256px, collapsible to 56px <1024px)   │  MAIN              │
│  ┌──────────────────────────────────┐            │                    │
│  │ [Logo] Project switcher ▾         │            │  Page content      │
│  ├──────────────────────────────────┤            │                    │
│  │  🏠  Home (project dashboard)    │            │                    │
│  │  💬  Chat with CMO                │            │                    │
│  │  ✅  Approvals  ⓐ                │ (badge on  │                    │
│  │  📋  Tasks                        │  unread)   │                    │
│  │  ⏱   Crons                        │            │                    │
│  │  🔌  Connections                  │            │                    │
│  │  ⚙   Settings                     │            │                    │
│  ├──────────────────────────────────┤            │                    │
│  │ COST METER (always visible)       │            │                    │
│  │ ▓▓▓▓▓░░░░ $124 / $300 today      │            │                    │
│  ├──────────────────────────────────┤            │                    │
│  │ [User avatar] Account ▾          │            │                    │
│  └──────────────────────────────────┘            │                    │
└──────────────────────────────────────────────────────────────────────┘
```

Use shadcn `<Sidebar>` (already designed). Sidebar collapses to 56px icon rail below 1024px. Mobile (<768px) hides sidebar behind a hamburger; sheet/drawer overlay.

**Why not chat-first:** considered (Pattern B), rejected. Sidebar pattern is more legible to agency operators managing many things; chat-first risks burying tasks/approvals/crons behind a drawer where users miss them.

### Hard-cap state (app-wide)

When daily spend cap hits: red banner across top of every screen, dismissible only after raising cap or waiting until rollover. Pauses ALL autonomous agent actions. Approval inbox still works for manual override. Cost meter turns red. Surfaces in CMO chat as the next agent turn.

### Screen 1: Project Home (default landing after project select)

**Information architecture:**
```
┌─ TOP STRIP ─────────────────────────────────────────────────┐
│ Project name • period selector (7d / 30d / 90d)             │
├─ PRIMARY: 3 KPI cards ──────────────────────────────────────┤
│ [Active campaigns]  [Spend MTD]  [Conversions MTD]          │
├─ SECONDARY: Two columns ────────────────────────────────────┤
│ Recent CMO recommendations  │  Pending approvals (top 3)   │
│ (last 5 items)              │  with [Review] link          │
├─ TERTIARY: Agent status grid ───────────────────────────────┤
│ [Google Ads: 🟢 active, 4 crons]  [SEO: 🟢 active, 2 crons]│
│ [GEO: 🔒 beta]  [Cold Email: 🔒 beta]  ...                 │
└─────────────────────────────────────────────────────────────┘
```

Hierarchy reasoning: a marketer landing here wants to know "is it working" first (KPIs), "what needs me" second (recommendations + approvals), "what's running" third (agent grid).

**Interaction states:**
| State | What user sees |
|---|---|
| Loading | shadcn `<Skeleton>` rectangles for each card; no spinners |
| Empty (new project) | KPI cards show "—"; CMO recommendations area shows full-width CTA "Start onboarding to see your first plan" |
| Error | Toast (shadcn `<Toaster>`) + per-card retry; KPIs that failed show "—" with retry icon |
| Success | Live numbers, recommendations clickable into chat with that thread, approvals clickable into inbox |
| Partial | Mix — some KPIs may load before others; render as they arrive, no blocking |

**Responsive:** KPI cards stack 1-column <768px. Recommendations/approvals stack <1024px.
**A11y:** `<main>` landmark, skip-to-content link, KPI cards announce value + label, focus order: project switcher → period selector → KPI 1-3 → recommendations → approvals → agent grid.

### Screen 2: CMO Chat

**Information architecture:**
```
┌─ CONTEXT STRIP (sticky top) ────────────────────────────────┐
│ Project X • SEO + Google Ads active • 3 tasks running       │
├─ MESSAGE THREAD (scrollable) ──┬─ CONTEXT DRAWER (toggle)──┤
│                                 │                            │
│   USER: Run a 30-day            │  ACTIVE TASKS              │
│         experiment to triple    │  • Bid optimization        │
│         our trial signups       │  • SEO audit               │
│                                 │                            │
│   CMO:  [streaming response]    │  RECENT SIGNALS            │
│         📋 Created task "Audit  │  • 12 users on Reddit      │
│         current CPA"             │    complaining about       │
│         📋 Created task "SEO    │    competitor pricing      │
│         opportunity scan"        │                            │
│         [Want me to proceed?]   │  MEMORY INJECTED           │
│                                 │  • ICP: B2B sales teams    │
│   [Approve all] [Review each]   │  • Voice: direct, no buzz  │
│                                 │                            │
├─ COMPOSER ─────────────────────┴───────────────────────────┤
│ [Message CMO...]                                  [Send →]  │
└─────────────────────────────────────────────────────────────┘
```

Use AI SDK `useChat` + chat-sdk reference components. Tool calls render inline as collapsible badges with status (running/done/failed). Context drawer is collapsed by default, toggle in top-right.

**Interaction states:**
| State | What user sees |
|---|---|
| Loading (first load) | Skeleton message; thread fetches in background |
| Empty (new project chat) | CMO greets first: "I've looked at your site. Here's what I recommend for your first 30 days. Want me to walk through it?" |
| Error (LLM failure mid-stream) | Inline error bubble with retry; partial message preserved |
| Streaming | Token-by-token render; tool calls appear as inline badges (yellow=running, green=done, red=failed) |
| Partial (tool call mid-flight) | Tool call badge spinning; message continues streaming around it |

**Responsive:** Context drawer hidden by default <1024px; toggle to reveal as sheet overlay. Composer stays sticky.
**A11y:** Live region (`aria-live="polite"`) for streamed messages, tool call status announcements, keyboard send (Enter), shift-Enter for newline.

### Screen 3: Onboarding Magic Moment

**Information architecture:**
```
┌─────────────────────────────────────────────────────────────┐
│                                                              │
│                    "Let's set up your CMO."                  │
│                                                              │
│            [https://yourcompany.com           ] [Go →]      │
│                                                              │
│      Or use one of our example sites: Acme, Plaid, Linear   │
│                                                              │
├─ PROGRESS (after URL submit) ───────────────────────────────┤
│  ● Scraping your site...                       (5s)         │
│  ○ Deriving brand voice fingerprint            (10s)        │
│  ○ Drafting ICP hypothesis                     (15s)        │
│  ○ Building your 30-day plan                   (20s)        │
│                                                              │
│  [Each step expands inline as it completes, showing preview]│
│                                                              │
├─ FINAL: 30-day plan preview ────────────────────────────────┤
│  Week 1: Google Ads bid optimization on 12 keywords         │
│  Week 1: SEO audit + 3 page content proposals                │
│  Week 2: ...                                                 │
│                                                              │
│  [Approve & launch]  [Edit plan]  [Save & decide later]     │
└─────────────────────────────────────────────────────────────┘
```

Single column, max-width 720px, centered. Use shadcn `<Card>` for the progress steps. Live updates via SSE.

**Interaction states:**
| State | What user sees |
|---|---|
| Loading (initial) | Just the URL input; placeholder examples |
| Empty | Default state — input + example chips |
| Error (URL unreachable) | Inline error under input: "Couldn't reach that URL. Try https:// prefix or a different page." |
| Per-step running | Spinner + step name + elapsed time |
| Per-step error | Red icon + retry button + "Use placeholder" option (don't block whole flow) |
| Success (all steps done) | 30-day plan preview expanded; CTAs prominent |
| Partial (skipped step) | Step shows "Used placeholder for [thing]; CMO will refine over time" |

**Responsive:** Works at 375px; max-width breaks intelligently. Plan preview cards stack 1-col on mobile.
**A11y:** Progress announced as each step changes state; focus moves to "Approve & launch" when plan ready; error states announced.

### Screen 4: Approval Inbox

**Information architecture:**
```
┌─ TOP STRIP ─────────────────────────────────────────────────┐
│ Approvals (12)  [All ▾] [Spend] [Content] [Settings]        │
├─ CARDS (stacked) ───────────────────────────────────────────┤
│ ┌──────────────────────────────────────────────────────┐   │
│ │ [Google Ads]  Increase bid for "marketing automation"│   │
│ │ Current: $4.20 → Proposed: $5.80 (+38%)              │   │
│ │ Why ▾   Cost: ~$45/day  Expected: 3-5 more conv/day  │   │
│ │                                                       │   │
│ │ [Approve]  [Edit]  [Reject]  Created 5m ago by CMO   │   │
│ └──────────────────────────────────────────────────────┘   │
│ ┌──────────────────────────────────────────────────────┐   │
│ │ [Cold Email]  Send sequence to 47 new leads           │   │
│ │ ...                                                    │   │
│ └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
```

Use shadcn `<Card>` per item, `<Tabs>` for filter. "Why ▾" expands inline to show CMO's reasoning chain.

**Interaction states:**
| State | What user sees |
|---|---|
| Loading | Skeleton cards |
| **Empty (the most important state)** | Large, calm centered text: "All caught up. CMO will surface decisions here." Subtle subtext: "Items above your thresholds appear here for approval. Adjust thresholds in Settings." No CTAs — empty is the *goal*. |
| Error (action failed) | Per-card error banner + retry; card stays in inbox |
| Success (approved) | Card slides out with green toast: "Approved — running now" |
| Partial (some cards still loading details) | Card shows summary, expandable details show spinner |

**Responsive:** Cards stack natively. Buttons stay 44px+ tappable. Edit opens shadcn `<Dialog>` modal.
**A11y:** Each card is a `<region aria-label="...">`, buttons keyboard-accessible, edit dialog traps focus, escape closes.

### Screen 5: Cron Tab

**Information architecture:**
```
┌─ TOP STRIP ─────────────────────────────────────────────────┐
│ Scheduled work • [Project ▾] [Agent ▾]  [+ New cron]        │
├─ GROUPED LIST ──────────────────────────────────────────────┤
│ ▾ Google Ads agent (4 active)                                │
│   ┌────────────────────────────────────────────────────┐   │
│   │ Name         Schedule     Last     Next    Status  │   │
│   ├────────────────────────────────────────────────────┤   │
│   │ Daily bid opt  09:14 daily  9h ago  15h    🟢 ok   │   │
│   │ Hourly metrics  hourly      45m ago  15m    🟢 ok   │   │
│   │ Negative kw...  Mon 06:00   2d ago   5d     ⚠ warn │   │
│   │ Budget pacing   :30 hourly  20m ago  10m    🟢 ok   │   │
│   └────────────────────────────────────────────────────┘   │
│ ▾ SEO agent (2 active)                                       │
│   ┌────────────────────────────────────────────────────┐   │
│   │ Weekly rank check  Mon 06:14  3d ago  4d   🟢 ok   │   │
│   │ Daily content idea  09:23 daily  9h    15h  🟢 ok  │   │
│   └────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
```

Each row click expands to show last 7 runs + pause/resume/delete actions. Schedule shown in human-readable form, not raw cron syntax (use cronstrue). Times in user's local timezone.

**Interaction states:**
| State | What user sees |
|---|---|
| Loading | Skeleton table rows |
| Empty per agent | "No scheduled work for [agent]. Ask CMO to schedule something." |
| Empty whole tab | "No crons yet. CMO and specialists schedule work here when they need recurring jobs." |
| Error per row | Row shows ⚠ icon + tooltip; click for detail + reschedule action |
| Status: missed run | Row shows ⚠ + "Missed last run, see history"; click expands history |
| Success | Live status updates from OpenClaw via SSE |
| Hard halt (project archived mid-flight) | All rows show "halting..." then disappear; toast confirms |

**Responsive:** Collapse table to stacked card list <768px; each cron becomes a card with project+agent labels inline.
**A11y:** Table is a proper `<table>` with `<th scope="col">` headers; sortable; keyboard nav between rows; expandable rows announce state.

### Cross-cutting: Cost meter (sidebar persistent component)

**States:**
| State | Visual |
|---|---|
| Under 50% budget | Zinc bar, white text, no flag |
| 50-75% | Same, slight emphasis on number |
| 75-90% | Amber bar |
| 90-100% | Red bar + "Approaching cap" tooltip on hover |
| At/over cap | Solid red + "HALTED" label + clickable → opens cap settings |

Click anywhere on the meter → opens detailed cost panel: breakdown by source (LLM by agent, ad spend by channel), historical 7-day chart, edit cap.

### Cross-cutting: Activity log

Every autonomous action by every agent gets an entry in a per-project `agent_actions` log. Accessible from settings or via "Why?" command in chat. Each entry: timestamp, agent, action, cost, outcome, reasoning summary, [undo if applicable]. Builds the trust foundation that the autonomy promise depends on.

### User journey emotional arc

| Step | User does | User feels | Plan supports it via |
|---|---|---|---|
| 1 | Lands on signup | Curious, skeptical | Clean, no marketing fluff; "Sign in with Google" prominent |
| 2 | Creates first project | Hopeful | Onboarding magic moment loads fast, paste-URL is one field |
| 3 | Watches CMO build first plan | "Is this actually smart?" | Live step progress, real previews per step (not just spinners) |
| 4 | Approves first plan | "I'm committing money to this" | Plan shows projected cost + cap visible + "you can pause anytime" |
| 5 | Returns next day | "Did anything happen?" | Project home shows yesterday's results, CMO chat top-of-mind |
| 6 | Sees first approval | "Wait, what is it doing?" | Approval inbox card has "Why?" prominently; reasoning is concrete |
| 7 | Sees first results | Relief, validation | KPI card shows delta vs last period; "your CMO did this" attribution |
| 8 | Day 30 | Sticky | Project memory shows compounding learning; switching costs feel real |

The most emotionally-charged moments are (3) and (6) — the trust-building moments. Both have explicit UX investment: live previews during build, concrete reasoning per approval.

### Responsive breakpoints

- `<640px` (mobile): single column, sidebar as bottom sheet via hamburger, tables → cards, max 1 KPI per row
- `640-1023px` (tablet): sidebar collapses to icon rail, KPIs 2-up, tables OK
- `≥1024px` (desktop): full sidebar, KPIs 3-up, chat context drawer visible

### Accessibility baseline

- All interactive elements ≥44px touch target
- Color contrast ≥4.5:1 for body text (shadcn defaults pass)
- Keyboard navigable: all screens fully usable without mouse
- Focus visible: shadcn `focus-visible` rings respected
- Live regions: chat messages, approval status changes, cost meter state changes
- Skip-to-content link in every layout
- ARIA landmarks: `<header>`, `<nav>`, `<main>`, `<aside>` for sidebar context drawer
- Forms: labels visible, never placeholder-as-label
- Hard-cap banner: `role="alert"` so screen readers announce immediately

## Design decisions deferred (intentionally)

- **Brand identity** — typography, color palette, illustration style, motion language → /design-consultation post-GA
- **Marketing site / landing page** — V1 ships product-first; marketing site is separate work
- **Specialist agent chat detail design** — inherits CMO chat layout; revisit per-agent unique tools
- **Settings page detail** — inherits standard shadcn form patterns; revisit when autonomy config gets more dimensions
- **Connection manager detail** — inherits OAuth-button + connected-account-card pattern; revisit per channel

## Wheel-rebuilding audit (post-pivot, post-OpenClaw-spike)

Per the guiding tenet, every custom thing must be justified. Updated after live OpenClaw capability spike:

| Component | Verdict | Reasoning |
|---|---|---|
| Agent runtime | ✅ NOT REBUILDING | `openclaw agents add` per project + template |
| Agent memory | ✅ NOT REBUILDING (was building!) | `openclaw memory` (search, REM, promote, index) — full subsystem, fully replaces our planned pgvector |
| Cron scheduler | ✅ NOT REBUILDING | `openclaw cron` (add/list/edit/runs/show/status) — has agent attribution + name + description metadata |
| Signal pub/sub | ✅ NOT REBUILDING (was building!) | Tagged memory entries (`signal:opportunity`, `signal:risk`) discoverable via `memory search` — eliminates need for Postgres LISTEN/NOTIFY |
| Auth | ✅ NOT REBUILDING (was building!) | Local loopback, no auth needed; OpenClaw gateway runs `--auth none --bind loopback` |
| Multi-tenant DB isolation | ✅ NOT REBUILDING (was building!) | Single user, single SQLite file, no RLS, no envelope encryption per tenant |
| Webhook signing/reconciliation | ✅ NOT REBUILDING (was building!) | Loopback only, trusted; agent state is OpenClaw's source of truth, we read on demand |
| Chat UI | ✅ NOT REBUILDING | AI SDK `useChat` + chat-sdk patterns |
| Channel skills | ✅ NOT REBUILDING | Existing `notfair-googleads` MCP + new curated SEO MCP |
| Hosting/deploy infra | ✅ NOT REBUILDING (was building!) | Local Next.js standalone build, no Vercel |
| AI Gateway tenancy | ✅ NOT REBUILDING (was building!) | User's own LLM API key directly to provider; AI SDK reports usage |
| **Local SQLite for our state** | ✅ JUSTIFIED CUSTOM | Tasks/approvals/cost events/oauth tokens/project metadata are notfair-CMO-specific; OpenClaw doesn't model them |
| **MCP server exposing our tools** | ✅ JUSTIFIED CUSTOM | `approve_action`, `record_cost`, `log_action`, `query_project_state` etc. are notfair-CMO-specific; how OpenClaw agents reach our state |
| **OAuth flows + token storage** | ✅ JUSTIFIED CUSTOM | Localhost callback flow + OS-keychain-encrypted SQLite storage. OpenClaw doesn't manage external API OAuth tokens for our purpose |
| **Project concept (name-prefix grouping)** | ✅ JUSTIFIED CUSTOM | OpenClaw is flat-namespaced; project = our UX concept layered on top via consistent agent/cron name prefixes |
| **CLI launcher (`npx notfair-cmo`)** | ✅ JUSTIFIED CUSTOM | Distribution; no off-the-shelf option fits |

---

## Protocol definitions (post-pivot — MCP-based, OpenClaw-native)

### Our MCP server: tools exposed to OpenClaw agents

Our Next.js runs an MCP server (stdio when the agent invokes via OpenClaw runtime, or HTTP-over-loopback for the gateway-mode setup). Registered with OpenClaw via `openclaw mcp set notfair-cmo '{ ... }'`. Every project's CMO + specialist agents are configured to load this MCP.

Tools:
- `assign_task(project_slug, specialist_agent_id, brief, success_criteria, deadline_iso)` → `{task_id}`. Creates row in our SQLite `tasks` table. Returns task_id. Specialist picks up the brief by querying task status at next turn or via cron trigger.
- `query_task_status(task_id)` → `{status, latest_result, agent_id}`. Reads our SQLite.
- `update_task_status(task_id, status, result?, error?)` → `{ok: true}`. Specialist agents call when finishing.
- `cancel_task(task_id, reason)` → `{cancelled: bool}`.
- `approve_action(action_summary, cost_estimate_usd, type, agent_id)` → `{approved: true}` if within guardrails, else `{approved: false, approval_id}` and the agent waits/exits and retries later when approval comes in.
- `record_cost(amount_usd, source, project_slug, agent_id, ref?)` → `{ok: true}`. Source = `llm|google_ads|gsc|...`. Used by cost meter.
- `log_action(agent_id, action_type, summary, reasoning, payload?)` → `{ok: true}`. Append-only audit log; powers "Why?" in chat.
- `query_project_state(project_slug)` → `{guardrails, recent_signals, active_tasks, cost_today}`. Cheap aggregate query agents call at turn start.
- `emit_signal(project_slug, source_agent_id, signal_type, summary, evidence, confidence)` → writes structured memory entry tagged `signal:<type>` via `openclaw memory` write (or shell out). Specialists discover via memory search at next turn.
- `get_oauth_credentials(provider, project_slug)` → decrypts + returns the access token + handles refresh-if-near-expiry. Agents never see refresh tokens directly.

### CMO ↔ specialist orchestration (no custom protocol; uses MCP)

CMO is an OpenClaw agent. When user chats:
1. Our Next.js receives message, calls `openclaw agent --agent <project-slug>-cmo --message "..." --json` (or via WebSocket Gateway for streaming).
2. CMO agent runs its turn, can call our MCP tools (`assign_task`, `approve_action`, etc.) and OpenClaw's native tools (`memory search`).
3. When CMO calls `assign_task(specialist, brief)`, our tool creates SQLite row, returns task_id, AND triggers the specialist agent via either:
   - `openclaw agent --agent <project-slug>-<specialist> --message "TASK <task_id>: <brief>"` (synchronous, if user is waiting)
   - `openclaw cron add ... --at +1m` (deferred, if user can wait)
4. Specialist agent reads brief from its message, optionally fetches latest task state via `query_task_status`, executes, calls `update_task_status` when done.
5. CMO sees result on next user turn via `query_task_status` (we inject "tasks updated since last turn: ..." into context).

No webhooks needed for V1: loopback subprocess calls are synchronous; the UI updates via polling our SQLite or via SSE that reads our `tasks` table on change.

### Signal sharing: tagged memory entries

When specialist agents observe something useful, they call `emit_signal(...)`. Our tool writes a structured memory entry via OpenClaw memory:
```
agent: <project-slug>-<specialist>
tags: signal, signal:<type>, project:<project-slug>
body: |
  summary: <one-liner>
  evidence: <json>
  suggested_actions: [{target_agent: seo, brief: "..."}]
  confidence: 0.8
  ttl_until: 2026-05-24T00:00:00Z
```

Other agents in the project discover via `openclaw memory search "signal:" --agent <project-slug>-*` at task start or as a "morning brief" routine for the CMO.

### Autonomy / approval threshold model (unchanged from pre-pivot, simpler enforcement)

Same threshold schema. Enforcement: every spending agent action MUST first call `approve_action(...)`. Our tool checks SQLite `guardrails` table; if within thresholds → returns `{approved: true}` immediately. If above → writes to `approvals` queue, returns `{approved: false, approval_id}`, agent exits gracefully. When user approves in UI, we fire a webhook (loopback) to OpenClaw gateway: `openclaw agent --agent <project>-<specialist> --message "RESUME approval_id=<id>"`. Agent re-runs the action.

```yaml
# Stored as JSON in SQLite guardrails table
autonomy:
  max_daily_spend_usd: 200
  max_concurrent_experiments: 3
  require_approval_above:
    spend_per_action_usd: 50
    new_channel_first_action: true
    content_publishing: true
    bid_changes_percent: 25
    audience_change: true
  fully_autonomous:
    keyword_pause: true
    budget_reallocation: true
    ab_test_winner_selection: true
    negative_keyword_add: true
```

### Cron management (OpenClaw native + project-prefix naming)

Tenet: OpenClaw cron is the source of truth. Our value-add is project attribution + UI grouping.

**Naming convention** (simplified — no account level since single user):
```
<project-slug> / <agent-slug> / <cron-slug>

Example: acme-q4 / google-ads / daily-bid-opt
```

Constraints:
- Lowercase, alphanumeric + hyphens; spaces and `/` allowed as visual separators
- Slugs immutable once created (decoupled from project display name)
- Total length: OpenClaw shows full name in cron list — keep readable, no hash truncation needed at this length

**Operations (wrappers over `openclaw cron` CLI):**
- Create: `openclaw cron add --name "<proj> / <agent> / <name>" --description "..." --agent <project-slug>-<agent-slug> --cron "..." --message "RUN <task_id>: <brief>" --json`
- List: `openclaw cron list --json`, filter client-side by name prefix
- Halt: `openclaw cron disable <id>`
- Delete: `openclaw cron rm <id>`
- Run history: `openclaw cron runs <id> --json`
- Update: `openclaw cron edit <id> --cron "..." --message "..."`

**Frontend cron tab:**
- Calls `openclaw cron list --json` via our backend
- Groups by project (parsed from name prefix); within project, groups by `--agent` column (which OpenClaw already provides)
- Per-row actions forward to the CLI commands above
- Per-row click: shows last 7 runs from `openclaw cron runs <id>`

**Autonomy + crons:** cron creation goes through `approve_action` if cron count > cap or if cron is content-publishing.

### Task lifecycle (unchanged, stored in our SQLite)

States: `proposed` → `approved` → `running` → (`succeeded` | `failed` | `cancelled`)

Tasks live in our SQLite `tasks` table. OpenClaw agents reference them by task_id in messages and in cron `--message` payloads. State transitions via the MCP tools.

---

## Architecture decisions (revised for local OSS, post-OpenClaw-spike)

1. **CMO loop: OpenClaw agent per project.** `openclaw agents add <project-slug>-cmo` with our MCP server attached. CMO calls our tools via MCP (zero HTTP roundtrips — stdio). Specialist agents same pattern: `<project-slug>-google-ads`, `<project-slug>-seo`.
2. **No auth.** Local loopback only. OpenClaw gateway started with `--auth none --bind loopback`. Our Next.js binds to 127.0.0.1 only. No Clerk. Settings page is single-user; no roles, no permissions.
3. **Storage: SQLite via better-sqlite3.** Single file at `~/.notfair-cmo/db.sqlite`. Tables: `projects`, `tasks`, `approvals`, `cost_events`, `oauth_tokens`, `guardrails`, `agent_actions`. No pgvector — memory is OpenClaw's job.
4. **Long-running work: OpenClaw cron + cursor in our SQLite.** Cold email sequences = a cron that runs the agent every N minutes; agent reads `cursor` from our `sequence_runs` table, processes next chunk, updates cursor, exits. Same pattern as before but now OpenClaw is running, not Vercel — no 300s timeout pressure (OpenClaw can run longer turns), but pattern is unchanged for resilience.
5. **Inter-process integration: subprocess + MCP.** Three integration surfaces with OpenClaw:
   - **CLI subprocess** for management ops (`agents add`, `cron add/list/edit/rm`, `memory search/index`): spawn `openclaw <cmd> --json`, parse stdout. Wrapped in `apps/web/lib/openclaw/cli.ts`.
   - **WebSocket Gateway** for agent invocation with streaming (`openclaw gateway` running; we connect via `ws://localhost:<port>`). Wrapped in `apps/web/lib/openclaw/gateway.ts`.
   - **MCP server** (we run) exposing our tools to OpenClaw agents. Registered once at install via `openclaw mcp set notfair-cmo '{...}'`. Implementation in `apps/web/lib/mcp-server/`.
6. **OAuth: localhost callback + OS-keychain master key.** Localhost redirect (Google Ads + GSC accept this for desktop apps). Master key generated on first launch + stored in OS keychain via `keytar`. OAuth tokens AES-256-GCM encrypted with this key, stored in SQLite. Refresh handled by a single function called before any agent action; no cron needed.
7. **Real-time UI: SSE from our Next.js to browser, gateway WebSocket from Next.js to OpenClaw.** Chat uses AI SDK streaming through our Next.js (which proxies to gateway WS). Task board / approvals / cron tab updates via SSE that listens to a local pub/sub (Node `EventEmitter` is sufficient — single-process).
8. **Distribution: npm package + standalone Next.js build.** `notfair-cmo` package exports a CLI bin. CLI uses `next start` against a pre-built `.next/standalone` directory. Targets Node 20+. No native deps beyond `better-sqlite3` and `keytar` (both have prebuilds for major platforms).
9. **No monorepo.** Single-package npm project. TypeScript types live in `src/types/` and are imported via relative paths. (Monorepo was needed when CMO ran in OpenClaw with separate tool definitions; now everything is in our Next.js codebase or in OpenClaw config.)
10. **Project = name-prefix grouping.** Project slug is the prefix on OpenClaw agent names and cron names. Project metadata (display name, slug, created_at, archived_at) lives in our SQLite. Archive = mark archived + disable all matching crons + optionally remove agents.

## Code conventions (adopt from line 1)

1. **`withProjectScope(handler)` middleware** at `src/server/with-project-scope.ts`. Wraps every API route that takes a project param. Validates project exists + not archived, attaches project metadata, returns standard error envelope on failure. NO auth check (no auth).
2. **`defineMcpTool({name, schema, handler})` factory** at `src/server/mcp-server/define-tool.ts`. Single source of truth for our MCP tools: Zod schema validation, structured logging, audit log entry to `agent_actions`, standard error envelope. Every tool registered through it.
3. **Single slugification utility** at `src/lib/slug.ts`. `slugify(input, maxLen)` → kebab-case, ASCII, length-capped, validated against reserved words. Used by project/agent/cron name.
4. **Standard error envelope** in `src/types/errors.ts`: `{ok: false, error_code, message, retryable, user_message}`. All MCP tools return this shape on error so agents have predictable handling.
5. **`openclaw()` wrapper** at `src/server/openclaw/cli.ts`. Single function: `openclaw(args: string[]) → Promise<JsonValue>`. Spawns subprocess with `--json` flag, parses stdout, throws structured error on stderr. Every interaction with OpenClaw CLI goes through this.
6. **`OpenClawGateway` class** at `src/server/openclaw/gateway.ts`. Singleton; lazy-connects to `ws://localhost:<port>`; reconnects on drop. Streaming chat goes through here.
7. **ASCII state diagram inline in `tasks` schema file.** 5-state lifecycle (proposed → approved → running → succeeded/failed/cancelled) deserves a diagram in code comments next to the SQLite schema.
8. **Single-file SQLite migrations** in `src/server/db/migrations/`. Sequential numbered (`001_init.sql`, `002_add_oauth.sql`). Auto-applied on startup; no separate migration command. No down-migrations (forward-only).

## Test strategy (V1 ship gate)

### Coverage diagram

```
CODE PATHS                                          USER FLOWS / E2E
[+] CMO orchestration loop                          [+] Onboarding (URL → first plan)
  ├── tool: assign_task                               ├── [→E2E] paste URL → scrape → voice →
  │     [→EVAL] right specialist + brief shape        │           ICP → 30-day plan rendered
  │     [GAP]   invalid agent_type rejection          └── [→E2E] approve plan → first task runs
  │     [GAP]   deadline-in-past rejection          [+] Project isolation (security-critical)
  ├── tool: query_task_status                         ├── [→E2E] User A cannot read User B's
  │     [GAP]   200 OK valid task                     │           project data via any endpoint
  │     [GAP]   404 cross-project                     └── [→E2E] User A cannot list/halt
  ├── tool: schedule_recurring_work                   │           User B's crons
  │     [GAP]   cap enforcement + approval inbox    [+] OAuth flows
  │     [GAP]   content-publishing always needs apr   ├── [→E2E] Google Ads connect → token
  ├── tool: query_signals + query_memory              │           stored encrypted → agent uses
  │     [GAP]   project-scoped, no cross-project      │           token → token refresh
  │     [GAP]   ttl + consumed_by filtering           └── [→E2E] GSC connect (same)
  └── [→EVAL] CMO weekly briefing quality           [+] Cron lifecycle
                                                      ├── [→E2E] create → fires → archive
[+] Signal bus                                        │           project → cron halted
  ├── [GAP] publish + LISTEN dispatch                 └── [→E2E] orphan detection
  ├── [GAP] ttl expiry filters from query           [+] Autonomy guardrails
  └── [GAP] confidence scoring honored                ├── [→E2E] daily spend cap actually halts
                                                      └── [→E2E] action above threshold → approval
[+] OAuth vault (security-critical)                                inbox, not executed
  ├── [GAP] encrypt at rest, decrypt for use        [+] Cost meter
  ├── [GAP] per-project key derivation                └── [→E2E] spend visible + hard cap halts
  ├── [GAP] refresh before expiry
  └── [GAP] cross-project key isolation             EVALS (critical, V1 gate)
                                                      ├── [→EVAL] CMO orchestration: 20 scenarios;
[+] Approval inbox + autonomy thresholds              │            gate on right specialist + brief
  ├── [GAP] every require_approval_above rule         ├── [→EVAL] Brand voice fingerprint: derive
  │         catches the action                       │            then generate; human review
  └── [GAP] approved item executes; rejected halts                matches source voice
                                                      └── [→EVAL] CMO weekly briefing quality

V1 ship gate: all E2E + EVAL paths green
```

### Eval harness (light, V1)

- `packages/evals` workspace with 20 curated scenarios at V1
- Scenario shape: project context + user message → assert right specialist + brief shape + no obvious errors
- Runs in CI on every PR touching CMO prompts or orchestration logic
- Gates CI on regression vs baseline (no merge if scenarios that passed yesterday fail today)
- Uses AI SDK + AI Gateway; budget ~$0.50 per full eval run
- Expand to 100+ scenarios + LLM-as-judge quality scoring post-GA

### Project-isolation harness

- Test helper `withTwoProjects(testFn)` spins up Project A (user A) + Project B (user B)
- Every API route test must include cross-project access test
- Static lint rule: any route handler test file without a `cross-project` test fails CI

### OAuth E2E uses real provider sandboxes

- Google Ads sandbox + GSC sandbox accounts maintained for V1
- Don't mock the OAuth dance — Google's verification cycle means we validate the real flow early
- E2E runs nightly + on PR touching OAuth code

## Performance notes (V1)

1. **pgvector: HNSW index, single index with `project_id` filter column.** Better recall than IVFFlat at low volumes; memory tradeoff fine at V1. Re-evaluate at 100k+ projects.
2. **AI Gateway caching aggressive on deterministic prompts.** Brand voice fingerprint, ICP analysis, post-mortems have stable inputs → 7-day cache TTL. Saves real money + speeds onboarding.
3. **Cron jitter at creation time.** Add random 0-59 min offset per cron to spread "9am daily" across the hour. One-line fix at create; saves an incident later.
4. **CMO chat SLOs:** first-token <2s, full-turn <10s for ≤5 tool calls. Measure post-spike. If OpenClaw → Next.js roundtrip >300ms, add `batch_tool_calls([...])` so CMO issues parallel queries in one roundtrip.

## Failure modes (V1, post-pivot)

| Codepath | Failure | Test | Handling | User sees |
|---|---|---|---|---|
| CLI launch | OpenClaw not installed/running | doctor cmd | `notfair-cmo doctor` runs at startup; surfaces clear install instructions if missing | Terminal output: "OpenClaw not running. Start it with `openclaw gateway` then retry." |
| CLI launch | Port already in use | Unit | Try N+1 up to 5 times; surface chosen port | "notfair-cmo running on http://localhost:3247 (3000 was busy)" |
| CMO turn | OpenClaw subprocess fails | Integration | Catch, retry once with backoff; surface error to UI | "Couldn't reach agent. Try `openclaw doctor` in another terminal." |
| MCP tool call | Tool throws unexpectedly | Unit | `defineMcpTool` wraps in try/catch, returns standard error envelope | Agent sees structured error, retries or surfaces to user |
| `approve_action` | Race: two agents check cap simultaneously | Unit | SQLite transaction with `BEGIN IMMEDIATE` around check+record | Second action sees updated state, halts correctly |
| Cron tick | OpenClaw cron fires while our app not running | Manual + integration | OpenClaw runs autonomously; cron triggers agent which calls our MCP. If our MCP server unreachable, agent fails gracefully and OpenClaw records as failed run. Next user open shows failed cron. | Cron tab shows ⚠ on failed runs; click for retry |
| OAuth refresh | Refresh token revoked | Unit + manual | Mark credential as `needs_reauth`; agent calls to `get_oauth_credentials` return error envelope; UI banner | "Reconnect your Google Ads account" banner in Connections page |
| Cost meter | Cost event missed (agent crashed before reporting) | Manual | Underreport possible but bounded; doctor cmd cross-checks against OpenClaw logs | Slight cost lag possible; doctor surfaces drift |
| SQLite | DB file corrupted | Manual | better-sqlite3 detects on open; backup auto-restored from `db.sqlite.bak` if recent | "Restored from automatic backup (data from <time>)" |
| Project archive | In-flight cron mid-tick | Integration | Archive halts crons via `openclaw cron disable` loop; in-flight ticks complete; new ticks blocked | Archive progress: "halting 12 crons..." then done |
| OS keychain | Locked / unavailable | Unit | Surface error at startup; offer fallback (password prompt for master key) | "Unlock your keychain to use stored OAuth credentials" |

**Critical gaps:** None at V1 plan stage. Failure modes mostly simpler than pre-pivot since loopback + single-process eliminates classes of distributed-system failures.

## Implementation lanes (parallel-friendly)

Plan implementation in 3 lanes, all parallel:

| Lane | Workstream | Files | Depends on |
|---|---|---|---|
| **A (parallel)** | CLI + UI shell: package.json + bin/cli.ts + sidebar shell + project CRUD + SQLite schema | `package.json`, `bin/`, `src/app/(shell)/`, `src/server/db/` | — |
| **B (parallel)** | OpenClaw integration: CLI subprocess wrapper + gateway WS client + MCP server scaffold | `src/server/openclaw/`, `src/server/mcp-server/` | — |
| **C (parallel)** | CMO chat UI + AI SDK integration + first agent template (Google Ads via existing MCP) | `src/app/api/chat/`, `src/components/chat/`, `src/server/agent-templates/` | — |

After all 3 land: lane D integrates them — onboarding flow + approval inbox UI + cron tab UI + cost meter + OAuth flow for Google Ads + SEO MCP curation.

No conflict flags — different file trees per lane.

## Still deferred (handle in implementation or post-V1)

- **Concrete SQLite schema DDL** — draft during lane A; refine as features land. Migrations are forward-only single SQL files.
- **Telemetry endpoint + opt-in UX** — design + ship in V1.1 once we have an OSS user base to measure
- **Standalone binary distribution** (Tauri/pkg) — V1 ships npm only; revisit if user feedback says install friction is real
- **Multi-machine sync of projects** — use Tailscale + OpenClaw's remote-gateway mode; post-V1
- **SEO MCP curation** — V1 needs a curated SEO MCP. Either build inline during lane C/D, or vendor from an existing OSS source. Decide during scaffolding.

---

## OpenClaw capabilities spike — COMPLETED 2026-05-17

Live spike against the running OpenClaw v2026.5.7 install on user's machine. All results passed; full findings in [[openclaw-capabilities]] memory file. Summary:

| Capability | Status | Notes |
|---|---|---|
| Isolated agents per workspace | ✅ | `openclaw agents add <name> --workspace <dir> --model <id>` — existing install has agents `main` + `supa` |
| Cron with metadata + run history | ✅ | `openclaw cron add` accepts `--name`, `--description`, `--agent`, `--cron`/`--every`, `--message`, `--session-key`, `--tools`, `--tz`. List shows ID/Name/Schedule/Next/Last/Status/Target/Delivery/Agent ID/Model. `cron runs <id>` shows history. |
| Memory subsystem | ✅ | `openclaw memory search/index/promote/REM`. Per-agent by default. Has REM (reflective episodic memory) with deep promotion to MEMORY.md. |
| MCP server registration | ✅ | `openclaw mcp set/list/show`. Existing install already has `notfair-googleads` MCP registered. |
| WebSocket Gateway | ✅ | `openclaw gateway` configurable auth (none/token/password/trusted-proxy), bind (loopback/lan/tailnet). |
| Tool-calling via MCP | ✅ | Agents call MCP-registered tools natively. |
| Long-running work | ✅ via cron chunks | Crons with `--every` reawaken agents; combined with our SQLite cursor pattern. |
| Channels (Telegram/Slack/iMessage/etc) | ✅ (bonus) | Existing install has iMessage + Slack + Telegram configured. Crons can deliver to channels via `--announce`. Out of V1 scope but available for power users. |

**No hard blockers found.** Spike validated the architecture. No further validation needed before scaffolding.

---

## Success criteria (V1 ship readiness)

- `npx notfair-cmo` works on fresh macOS + Linux + WSL machine where OpenClaw is installed
- Solo founder can complete onboarding (URL → first plan) in <5 minutes
- CMO can launch and monitor a Google Ads campaign within guardrails (using user's connected Google Ads account)
- CMO can launch SEO work (audit, content recommendations) and surface signals consumed by Google Ads agent on next turn
- CMO + specialist agents can schedule recurring work via `openclaw cron`; cron tab shows them grouped by project + agent
- Cost meter always visible; hard cap actually halts spending
- Approval inbox catches all `require_approval_above` events; no false negatives in test
- Archive a project → all of its OpenClaw crons disabled, agents optionally removed
- GitHub repo: README + ARCHITECTURE + CONTRIBUTING + LICENSE
- npm package published; install instructions verified

**Stretch (validate but don't gate):** project memory measurably improves agent output between week 1 and week 4 (requires eval harness + 4 weeks wall-clock; design harness now)

---

## Risk register (post-pivot)

| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|-----------|
| Users don't have OpenClaw installed | High | High | `doctor` command surfaces install instructions; README leads with OpenClaw prereq; consider bundling OpenClaw install script as part of `notfair-cmo install` |
| OpenClaw version drift breaks our subprocess wrapper | Med | High | Test against multiple OpenClaw versions in CI; record min-supported version; fail fast with clear error |
| Google Ads OAuth complexity (localhost callback edge cases) | Med | Med | Use Google's standard desktop-app OAuth flow; document well; provide troubleshooting in doctor |
| User runs out of disk for SQLite/memory growth | Low | Med | Default cap on memory entries per agent; quarterly cleanup cron; doctor shows db size |
| CMO autonomy generates user fear ("it spent my money!") | High | High | Hard caps + always-visible cost meter + approval inbox for big actions + activity log + dry-run mode |
| LLM costs surprise user | Med | Med | Cost meter visible; daily cap with hard halt; AI SDK reports per-call usage |
| MCP tools become a security hole (e.g., shell-injection via cron payload) | Med | High | Strict Zod validation on every MCP tool input; no shell-out from MCP tool bodies; security audit before V1 |
| OSS user base hostile (license complaints, telemetry pushback) | Med | Low | MIT/Apache license, telemetry opt-in only, README addresses both upfront |
| `keytar` not available on user's OS | Low | High | Fallback to encrypted file with prompt-based password on every launch; doctor surfaces fallback mode |
| Single-process EventEmitter pub/sub doesn't scale | Low | Low | Single user, single project active at a time — won't hit limits in V1 |

---

## Recommended next steps (post-pivot)

1. **Scaffold the project** — `pnpm create next-app notfair-cmo`, set up directory structure (`src/server/`, `src/app/`, `src/types/`, `bin/`), install deps (`ai`, `@ai-sdk/openai`, `@ai-sdk/anthropic`, `better-sqlite3`, `keytar`, `zod`, `commander`, shadcn-cli), set up `package.json` with `bin` entry.
2. **Doctor command** — `bin/cli.ts doctor` that verifies OpenClaw is installed + running + accessible. Build first; surfaces what's missing for users.
3. **OpenClaw CLI wrapper** — `src/server/openclaw/cli.ts` with `openclaw(args)` function. Cover the operations we need (agents add/list, cron add/list/disable, memory search, mcp set).
4. **SQLite schema + migrations** — `src/server/db/migrations/001_init.sql` with `projects`, `tasks`, `approvals`, `cost_events`, `oauth_tokens`, `guardrails`, `agent_actions`, `sequence_runs`.
5. **MCP server skeleton** — `src/server/mcp-server/` with `defineMcpTool` factory + stub implementations of the 9 tools.
6. **Sidebar shell + project CRUD UI** — Lane A.
7. **CMO chat UI + first agent template** — Lane C (after lanes A + B partially landed).
8. **Onboarding magic moment + approval inbox + cron tab + cost meter** — Lane D.
9. **README + ARCHITECTURE + CONTRIBUTING + LICENSE** — before any GitHub push.
10. **npm publish workflow + initial release as v0.1.0**.

---

## Spec-review log

- Initial plan written 2026-05-17
- Independent adversarial review: 6/10, surfaced scope/protocol/feasibility gaps
- Revision 1: cut V1 from 6 channels to 2, deferred D4 agency mode to v1.1, added protocol definitions (CMO↔specialist tool-calling, signal envelope schema, autonomy threshold model, task lifecycle), restructured OpenClaw spike with fallback plan, marked memory-improvement criterion as stretch
- Revision 2: added "don't rebuild the wheels" as guiding tenet, added cron management as V1 item (OpenClaw-backed via naming convention `notfair-<account-slug>-<project-slug>-<agent-slug>-<cron-slug>` with immutable slugs decoupled from display names), added wheel-rebuilding audit table, extended CMO orchestration tools with `schedule_recurring_work` + `query_scheduled_work`, extended OpenClaw spike with cron capability checks
- Revision 3 (eng-review): locked 8 architecture decisions (CMO in OpenClaw, Clerk, cron-driven chunks for long-running work, HMAC webhooks + idempotency + reconciliation, AES-256-GCM OAuth vault, AI SDK streaming + SSE, pnpm monorepo + `@notfair/types`), added 5 code conventions, added test strategy (coverage diagram + light eval harness + project-isolation harness + real OAuth sandboxes), added performance notes, added failure modes table, added worktree parallelization lanes
- Revision 4 (design-review): locked shadcn defaults + no brand in V1, locked Pattern A app shell (sidebar + main), added complete design specs for 5 screens (Project Home, CMO Chat, Onboarding, Approval Inbox, Cron Tab) + cost meter + activity log, defined interaction states per screen, defined responsive breakpoints, defined a11y baseline, defined user journey emotional arc
- **Revision 5 (PIVOT, 2026-05-17): open-source local CLI tool.** Major scope adjustment after user pivot. Eliminated: auth (Clerk), multi-tenancy, hosted Postgres + pgvector, Vercel deployment, AI Gateway tenancy, webhook signing infrastructure, per-tenant envelope encryption, D4 agency mode entirely, OAuth verification calendar wait. Leveraged after live OpenClaw spike: OpenClaw memory (replaces pgvector), OpenClaw cron (already had it but now no naming convention needed beyond project prefix — OpenClaw tracks agent + name + description natively), OpenClaw agents (workspace per agent), MCP server pattern (how we expose tools to OpenClaw agents). New scope: npm CLI launcher, local SQLite, OS-keychain master key for OAuth, doctor command, GitHub OSS repo. V1 effort revised: ~1-2 weeks CC. All architecture, code conventions, protocol definitions, failure modes, risk register, recommended next steps revised. Design specs unchanged (UI doesn't depend on runtime).
- **Revision 6 (2026-05-17): defer MCP-from-agents to v1.1.** User decision: skip wiring the MCP server into OpenClaw agents for V1. The 12 MCP tools remain built and registered (the server runs) but agents do not call them mid-turn. V1 becomes an "agent runner + chat portal" rather than the orchestrated-CMO product. Drops from critical path: CMO orchestration loop verification, `get_oauth_credentials` runtime path, `emit_signal` → OpenClaw memory integration, `approve_action`-driven autonomy enforcement, MCP server auto-registration. Re-promoted to V1: backend-driven cron creation UI (user clicks "Schedule recurring work" on an agent → our API calls `openclaw cron add` directly), manual approval queue (placeholder for v1.1), zero-cost meter default (real cost tracking comes with MCP). Re-framed V1 promise: "talk to an OpenClaw agent scoped to a marketing project, schedule recurring work, manage cron jobs cleanly." Full CMO orchestration promise moves to v1.1.
