From a797e283b2dcb9d01fd7ebfcc600cea23a5f5603 Mon Sep 17 00:00:00 2001 From: Sunny Wu Date: Sat, 9 May 2026 21:50:51 -0400 Subject: [PATCH 1/2] docs(claude): oauth+ollama env, persona e2e harness, Pattern B rule - Add CLAUDE_CODE_OAUTH_TOKEN and OLLAMA_API_KEY rows to env table; the one-of-three footnote makes the fallback chain (api key | oauth | ollama | auto-mocks) explicit so future sessions stop second-guessing which is required. - Document the persona e2e harness (scripts/_test-personas.ts via app/api/test-persona/route.ts) under verification methods, since typecheck alone doesn't catch persona regressions. - New rule #19 codifying Pattern B: pre-fetch external data deterministically in TS, feed personas as text. Downstream personas must NOT get MCP tool access for reasoning. Updates rule #6 to mark Reddit/Jira as roadmap (no auth config registered) so they don't get reached for prematurely. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8e19667..0160f11 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -120,15 +120,19 @@ GMAESTRO_MODEL_HAIKU= | Variable | Default | Purpose | |---|---|---| -| `ANTHROPIC_API_KEY` | required | Anthropic auth (auto-set from `OLLAMA_API_KEY` in ollama mode) | +| `ANTHROPIC_API_KEY` | required* | Anthropic auth (auto-set from `OLLAMA_API_KEY` in ollama mode) | +| `CLAUDE_CODE_OAUTH_TOKEN` | optional | Long-lived `sk-ant-oat01-…` token from `claude setup-token`; routes Anthropic calls through a Claude Pro/Max subscription instead of API billing. SDK picks it up via the bundled `claude-code` binary. Use *or* `ANTHROPIC_API_KEY`. | | `COMPOSIO_API_KEY` | required | Falls back to `~/.composio/anonymous_user_data.json` if unset | | `GMAESTRO_LLM_PROVIDER` | `anthropic` | Switch to `ollama` for Ollama Cloud | +| `OLLAMA_API_KEY` | required when provider=ollama | Auto-mirrored into Anthropic SDK auth vars | | `GMAESTRO_TIER` | `auto` | Force `tier1` (sequential) or `tier2plus` (concurrency=10) | | `GMAESTRO_USER_ID` | `default` | Composio sessions + DB foreign keys | | `GMAESTRO_BASE_URL` | `http://localhost:3000` | Composio OAuth callback base | | `NEXT_PUBLIC_USE_MOCKS` | unset | Mock all Session 1 API calls client-side | | `COMPOSIO_MCP_CONFIG_ID` | unset | Skip MCP config lazy-create; use this existing config ID | +\* One of `ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`, or `OLLAMA_API_KEY` must be set. If none are present, persona calls auto-fall back to mocks (`GMAESTRO_MOCK_PERSONAS=1` behavior). + ### Anthropic tier requirement Tier 2+ ($40 cumulative spend) for parallel fanout. Tier 1 = 50 RPM = bottleneck. CLI `doctor` warns and falls back to sequential dispatch (concurrency=1) if Tier 1. Override with `GMAESTRO_TIER=tier1`. @@ -190,6 +194,7 @@ lib/personas/registry.ts lib/personas/runtime.ts lib/personas/prompts/*.md ← 13 stubs; non-tech teammate writes content lib/personas/researcher/fetch.ts ← Pattern B: deterministic TS fetches → pure-LLM synthesizer +app/api/test-persona/route.ts ← dev-only HTTP entry point used by the persona e2e harness lib/tools/composio.ts lib/tools/connect.ts lib/tools/connections.ts ← stateless Composio connection-status reader (cached 30s) @@ -261,7 +266,7 @@ Always swap mocks for real imports just before merging your branch to `main`. 3. **15-second SSE heartbeat** (`: heartbeat\n\n`) to prevent EventSource browser timeout. 4. **`globalThis.__gmaestroEventBus`** singleton pattern — Next.js bundles API routes and pages separately; module-level singletons duplicate. Same applies to `__gmaestroDb` and `__gmaestroComposio`. 5. **Composio MCP wiring:** one shared MCP config (`"gmaestro-default-v2"`) is lazy-created via `composio.mcp.create(name, { toolkits, allowedTools, manuallyManageConnections: true })`, then `composio.mcp.generate(userId, configId)` mints a per-user instance URL. Drop the result into `mcpServers: { composio: { type: "http", url: instance.url, headers: {} } }`. Override the lazy-create flow by setting `COMPOSIO_MCP_CONFIG_ID` in env. Per-persona scoping via `allowedTools: ["mcp__composio__GMAIL_DRAFT", ...]` on each SDK `query()` call. -6. **Connect Link API:** use `composio.connectedAccounts.link(userId, authConfigId, { callbackUrl })`, NOT `initiate()` (deprecated for new orgs as of 2026-05-08). For `authConfigId`, import `getAuthConfigId(toolkit)` from `@/lib/shared/auth-configs` — Foundation pre-created auth configs for all 10 Tier-S toolkits + Discord/Intercom/Calendly via the agent-native Composio signup. Apollo, Loom, and Twitter are out of scope for the demo (need BYO OAuth). +6. **Connect Link API:** use `composio.connectedAccounts.link(userId, authConfigId, { callbackUrl })`, NOT `initiate()` (deprecated for new orgs as of 2026-05-08). For `authConfigId`, import `getAuthConfigId(toolkit)` from `@/lib/shared/auth-configs` — Foundation pre-created auth configs for all 10 Tier-S toolkits + Discord/Intercom/Calendly via the agent-native Composio signup. Apollo, Reddit, Jira, Loom, and Twitter (X) are surfaced in the connections picker as "Popular" but their persona-level wiring is roadmap (BYO OAuth needed); don't reference them from any persona's `allowedTools` until an auth config is registered. 7. **LinkedIn is READ-ONLY.** Researcher persona only: `LINKEDIN_SEARCH_PERSON`, `LINKEDIN_GET_PROFILE`, `LINKEDIN_GET_COMPANY`. All outbound = Gmail. 8. **Writer NEVER sends.** Writer drafts (`GMAIL_DRAFT`); only the Approval Gate flips drafts to sent. 9. **Conductor and Manager output is prompted JSON + Zod validation.** Schema is `WorkflowDAGSchema` in `lib/shared/schemas.ts`. One retry on parse failure. @@ -274,6 +279,7 @@ Always swap mocks for real imports just before merging your branch to `main`. 16. **Batch partial-failure threshold:** ≥80% coverage → keep valid items, skip-cascade missing ids. <80% → re-chunk into groups of 10 and retry once. Researcher's `maxConcurrency` is capped at 5 to match LinkedIn's token bucket (1 req/sec). 17. **WorkContext threading:** `loadWorkContext()` snapshots leads + trial-signals from the local DB and formats a `summary` string injected into the Conductor prompt. Managers reason about item counts from this snapshot; Specialists receive denormalized `item: { fields }` splatted into each materialized task input at dispatch time. 18. **`POST /api/runs` is fire-and-forget.** Returns `{ workflowRunId }` with HTTP 202 immediately; the workflow runs detached. Always attach `.catch(markRunFailed)` to the detached promise — uncaught rejection kills the dev server. +19. **Pattern B: personas reason in pure LLM over local data.** The Researcher's deterministic Composio fetch → pure-LLM synth (`lib/personas/researcher/fetch.ts`) is the model: pre-fetch any external data deterministically in TypeScript, then feed it to the persona as text in the prompt. Do NOT give downstream personas (Qualifier, Strategist, Writer) MCP tool access for reasoning — they reason over the lead's denormalized fields and prior personas' outputs. Composio is an automation handoff fired by the post-approval dispatcher, not a tool the LLM picks mid-thought. --- @@ -308,7 +314,9 @@ pnpm typecheck # tsc --noEmit pnpm db:studio # browse SQLite via Drizzle Studio ``` -There is no test suite. `pnpm typecheck` + `pnpm build` are the verification methods. CI (`.github/workflows/typecheck.yml`) runs `pnpm typecheck` on every PR and push to `main` — Node 22, pnpm 11, frozen lockfile. A red typecheck blocks merge. +There is no traditional unit-test suite. Verification is `pnpm typecheck` + `pnpm build` + the persona e2e harness. CI (`.github/workflows/typecheck.yml`) runs `pnpm typecheck` only on every PR and push to `main` — Node 22, pnpm 11, frozen lockfile. A red typecheck blocks merge. + +**Persona e2e harness:** in one shell run `pnpm dev`; in another run `pnpm tsx scripts/_test-personas.ts`. The harness reads lead/trial fixtures from the local DB and `POST`s to `app/api/test-persona/route.ts` once per persona, feeding synthetic upstream `previousOutputs` where needed. Why HTTP and not direct import: `lib/personas/runtime.ts` is `import "server-only"`, which refuses to load under `tsx`. Each persona reports pass/fail with a 1-line preview; non-zero exit if any persona failed. The route is dev-only and is not registered with the production build — do not reference it from app code. For UI changes, run `pnpm dev` and exercise the path in a browser before marking the task done — typecheck does not catch render bugs, and the dashboard's SSE/approval flows have several states (idle, running, awaiting approval) that only fail at runtime. From 75e73fbf4f1c44f0754d1c59a233a1c597555885 Mon Sep 17 00:00:00 2001 From: Sunny Wu Date: Sat, 9 May 2026 21:54:19 -0400 Subject: [PATCH 2/2] feat(connections): swap PostHog for HubSpot, add Apollo/Jira/Reddit to Popular HubSpot has a real auth config (Tier-S) so promoting it to Popular is free. Apollo/Jira/Reddit join PostHog's slot as "Setup required" cards to surface common founder integrations earlier in the picker; their persona wiring is still roadmap. Type POPULAR_TOOLKITS as `keyof typeof TOOLKIT_META` so any future typo or drift between the popular list and the toolkit registry fails typecheck instead of silently rendering a broken card. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/ui/components/connection-meta.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/ui/components/connection-meta.ts b/lib/ui/components/connection-meta.ts index 6dcfd91..2fa4db4 100644 --- a/lib/ui/components/connection-meta.ts +++ b/lib/ui/components/connection-meta.ts @@ -56,17 +56,20 @@ export const CATEGORY_ORDER: ToolkitCategory[] = [ // Pinned section at top of Connections page; order is the suggested setup sequence. export const POPULAR_CATEGORY_ID = "popular"; -export const POPULAR_TOOLKITS: readonly string[] = [ +export const POPULAR_TOOLKITS = [ "GMAIL", "GOOGLECALENDAR", "GOOGLESHEETS", "SLACK", - "POSTHOG", + "HUBSPOT", "LINKEDIN", + "APOLLO", "NOTION", "LINEAR", + "JIRA", "TWITTER", -]; + "REDDIT", +] as const satisfies readonly (keyof typeof TOOLKIT_META)[]; export const TOOLKIT_LOGO_URL: Record = { // Google — must use gstatic, not S2 favicon