Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 44 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,12 @@ L1 Conductor query() — 1 SDK call, returns plan
│ │ ├─ sales-mgr
│ │ ├─ cs-mgr
│ │ ├─ revops-mgr
│ │ └─ insight-mgr
│ │ ├─ insight-mgr
│ │ └─ content-mgr — defined but NOT yet exported from managers/index.ts
│
▼
L2 Specialist queries (separate query() calls, dispatched by L0)
│ 13 personas × Sonnet 4.6 mostly, Haiku for tagger
│ 13 shipping personas + 6 content personas (half-wired) × Sonnet 4.6 mostly, Haiku for tagger
│ each with scoped Composio MCP via allowedTools
│
▼
Expand All @@ -68,16 +69,23 @@ L3 Composio MCP HTTP server — actions executed via Composio

---

## Personas (exactly 13 — DO NOT add Health Monitor; it was dropped per audit)
## Personas (5 departments — DO NOT add Health Monitor; it was dropped per audit)

| Department | Specialists |
|---|---|
| Sales | researcher, qualifier, strategist, writer, scheduler, brief-writer |
| CS | activation |
| RevOps | crm-logger, pipeline-reporter, slack-digest |
| Insight | feedback-tagger, theme-synthesizer, linear-filer |
| Content (blogs pivot) | linkedin-researcher, x-researcher, reddit-researcher, synthesizer, blog-writer, blog-designer |

Plus Conductor (L1) and 4 Department Heads (L2) which exist as `AgentDefinition` objects nested inside the Conductor's `query()` call.
Plus Conductor (L1) and 5 Department Heads (L2) which exist as `AgentDefinition` objects nested inside the Conductor's `query()` call.

> **Content department is half-wired — treat as not-yet-runnable in real mode.**
> - The 6 content `PersonaId`s and `Department = "content"` exist in `lib/shared/types.ts:38-45` and `lib/personas/registry.ts:233-280` (with permissive `z.unknown` passthrough output schemas).
> - `contentManager` is defined in `lib/orchestrator/managers/content.ts` but is **not exported** from `lib/orchestrator/managers/index.ts` — `managerAgents` and `MANAGER_AGENT_NAMES` still ship just the original 4.
> - The on-disk prompts are `content-researcher.md`, `content-writer.md`, `content-designer.md`, which **do not match** the registry's `PersonaId`s (`linkedin-researcher`, `x-researcher`, `reddit-researcher`, `synthesizer`, `blog-writer`, `blog-designer`). The content manager's prompt also only references the 3-name set. Loading any content persona via `systemPromptPath` will 404 until the names are reconciled.
> - Mock mode and the DAG renderer reference these — keep them callable from there. Don't wire content into the live Conductor without resolving the name mismatch first.

---

Expand Down Expand Up @@ -174,25 +182,32 @@ If a parallel session needs a change to any of these, raise it with the human co

```
lib/orchestrator/conductor.ts
lib/orchestrator/managers/{index,sales,cs,revops,insight}.ts
lib/orchestrator/managers/{index,sales,cs,revops,insight,content}.ts
lib/orchestrator/title.ts ← run-title generator (LLM, used by recent-runs UI)
lib/orchestrator/context-synth.ts ← one-shot LLM synth of CompanyContext from voice + leads
lib/dispatch/execute.ts ← deterministic post-approval Composio dispatcher (no LLM)
lib/dispatch/providers.ts ← artifactType × toolkit → Composio action map
lib/state/workflows.ts
lib/state/approvals.ts
lib/state/work-context.ts ← WorkContext snapshot threaded into Conductor prompt
lib/state/company-context.ts ← singleton CompanyContext row per founder (load/save)
lib/state/gtm-metrics.ts ← live counts for GtmObjective targets (read-time, no snapshot)
app/api/runs/route.ts
app/api/runs/list/route.ts ← lists recent runs for the dashboard drawer
app/api/approvals/[id]/route.ts
app/api/approvals/bulk/route.ts ← bulk-resolve endpoint (one click approves all)
app/api/context/route.ts ← GET/PUT singleton CompanyContext
app/api/context/refresh/route.ts ← POST → returns a *proposed* CompanyContext (does not save)
```

**Session 2 owns (worktree `feat/personas`):**

```
lib/personas/registry.ts
lib/personas/runtime.ts
lib/personas/prompts/*.md ← 13 stubs; non-tech teammate writes content
lib/personas/prompts/*.md ← persona system prompts; non-tech teammate writes content
(note: 13 shipping personas + 3 content stubs whose names
don't yet match the registry's content PersonaIds)
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
Expand All @@ -217,8 +232,12 @@ app/api/composio/callback/route.ts
app/api/connections/start/route.ts ← mints Composio Connect Link OAuth URL
app/api/stream/mock-emit/ ← dev-only SSE injection endpoint
app/api/mock/runs/ ← dev-only mock recent-runs feed (NEXT_PUBLIC_USE_MOCKS=1)
app/api/mock/context/route.ts ← dev-only mock CompanyContext for NEXT_PUBLIC_USE_MOCKS=1
lib/ui/components/*.tsx ← custom components (NOT components/ui/)
lib/ui/components/company-context-card.tsx ← dashboard card showing CompanyContext + GTM metrics
lib/ui/components/edit-company-context-dialog.tsx ← edit/refresh dialog (pre-fills from synth)
lib/ui/hooks/*.ts
lib/ui/hooks/use-company-context.ts ← client hook; routes to /api/mock/context under NEXT_PUBLIC_USE_MOCKS
lib/ui/persona-meta.ts ← display labels, icons, status colors for personas/nodes
lib/realtime/bus.ts
lib/realtime/events.ts
Expand Down Expand Up @@ -280,6 +299,22 @@ Always swap mocks for real imports just before merging your branch to `main`.
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.
20. **CompanyContext synth proposes, never persists.** `lib/orchestrator/context-synth.ts` reads voice samples + recent qualified leads and returns a *proposed* `CompanyContext`. The dashboard's edit dialog opens pre-populated with that proposal — the founder confirms before `saveCompanyContext` writes. `POST /api/context/refresh` returns the proposal; only `PUT /api/context` writes. Don't add a code path that auto-saves the synth output.
21. **GTM objective counts are read-time, not snapshotted.** `lib/state/gtm-metrics.ts` exhaustively maps each `GtmMetric` to a Drizzle counter against existing tables (booked meetings, qualified leads, sent drafts). The `Record<GtmMetric, …>` shape forces the typechecker to flag missing counters when a new metric is added. There is no metrics history table — adding a metric without a clean DB source breaks this contract.

---

## Company context (dashboard surface)

A singleton `CompanyContext` row per `userId` stores the founder-confirmed snapshot of who the agents understand the company to be: `companyOverview`, `keyFacts`, `icps[]` (priority + industry + size + seniority), and `gtmObjectives[]` (a `GtmMetric` + target + optional `since` date). Schema in `lib/shared/types.ts:422-455`; persistence in `lib/state/company-context.ts`; migration `drizzle/migrations/0003_company_context.sql`.

Lifecycle:
- **Read:** `GET /api/context` → `loadCompanyContext(userId)`. Joined with live counts from `lib/state/gtm-metrics.ts` for the dashboard card.
- **Write:** `PUT /api/context` → `saveCompanyContext(input)`. Founder-driven only.
- **Synth:** `POST /api/context/refresh` → `synthCompanyContext()` → returns a proposed `CompanyContext`. Does NOT persist; the edit dialog opens pre-populated and the founder confirms.
- **Mock:** `app/api/mock/context/route.ts` serves a fixture under `NEXT_PUBLIC_USE_MOCKS=1`; `useCompanyContext` routes there client-side.

Threading into orchestration: managers don't currently read `CompanyContext` from the DAG planner — `WorkContext` (leads + trial-signals snapshot) is what the Conductor sees. Treat `CompanyContext` as a dashboard-facing surface for now.

---

Expand All @@ -292,7 +327,9 @@ Always swap mocks for real imports just before merging your branch to `main`.
| Claude Agent SDK over LangGraph/CrewAI | Native sub-agents, native MCP, fewer deps |
| Composio MCP HTTP, not provider package | Documented integration path, no extra package |
| Drop Health Monitor persona (13 not 14) | Overlapped with Activation, was P1+ anyway |
| Keep all 4 Department Heads | Visual depth in DAG sells the org-chart pitch |
| Keep all 4 original Department Heads | Visual depth in DAG sells the org-chart pitch |
| Add Content department (5th) for blogs pivot | Topic-shaped pipeline (researcher → writer → designer); ignores leads/trial-signals; post-approval Linear ticket is automatic |
| `CompanyContext` synth proposes, never auto-saves | Founder is the source of truth for company self-description; LLM seeds, founder confirms |
| Static voice training, not cross-run learning | Hackathon scope; cross-run = P2 |
| Prompted JSON over `structured_output` API | Simpler, works today, retry on parse fail |
| Public repo from day 1 | Enables agentic install demo |
Expand Down
21 changes: 21 additions & 0 deletions app/api/context/refresh/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { NextResponse } from "next/server";
import { synthesizeCompanyContext } from "@/lib/orchestrator/context-synth";
import { env } from "@/lib/shared/env";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

export async function POST() {
try {
const proposed = await synthesizeCompanyContext(env().GMAESTRO_USER_ID);
return NextResponse.json({
proposed: { ...proposed, updatedAt: proposed.updatedAt.toISOString() },
});
} catch (err) {
console.error("[api/context/refresh] synth failed:", err);
return NextResponse.json(
{ error: "AI synthesis failed", message: (err as Error).message },
{ status: 500 },
);
}
}
57 changes: 57 additions & 0 deletions app/api/context/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { NextResponse } from "next/server";
import { env } from "@/lib/shared/env";
import { CompanyContextInputSchema } from "@/lib/shared/schemas";
import type { CompanyContext, GtmMetric } from "@/lib/shared/types";
import {
loadCompanyContext,
saveCompanyContext,
} from "@/lib/state/company-context";
import { countAllMetrics } from "@/lib/state/gtm-metrics";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

interface ContextResponse {
context: (Omit<CompanyContext, "updatedAt"> & { updatedAt: string }) | null;
liveCounts: Record<GtmMetric, number>;
}

function payload(context: CompanyContext | null): ContextResponse {
const liveCounts = countAllMetrics(
context?.gtmObjectives.map((o) => ({ metric: o.metric, since: o.since })) ??
[],
);
return {
context: context
? { ...context, updatedAt: context.updatedAt.toISOString() }
: null,
liveCounts,
};
}

export function GET() {
return NextResponse.json(payload(loadCompanyContext(env().GMAESTRO_USER_ID)));
}

export async function PUT(request: Request) {
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}

const parsed = CompanyContextInputSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: "Invalid request", issues: parsed.error.issues },
{ status: 400 },
);
}

const saved = saveCompanyContext({
...parsed.data,
userId: env().GMAESTRO_USER_ID,
});
return NextResponse.json(payload(saved));
}
23 changes: 23 additions & 0 deletions app/api/mock/context/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* GET /api/mock/context — fixture for `NEXT_PUBLIC_USE_MOCKS=1` demos.
*
* Mirrors the live route's response shape. Source of truth is
* `makeMockCompanyContext()` in `lib/shared/mocks.ts`.
*/

import { NextResponse } from "next/server";
import {
makeMockCompanyContext,
makeMockGtmLiveCounts,
} from "@/lib/shared/mocks";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

export function GET() {
const context = makeMockCompanyContext();
return NextResponse.json({
context: { ...context, updatedAt: context.updatedAt.toISOString() },
liveCounts: makeMockGtmLiveCounts(),
});
}
8 changes: 8 additions & 0 deletions drizzle/migrations/0003_company_context.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE `company_context` (
`user_id` text PRIMARY KEY NOT NULL,
`company_overview` text NOT NULL,
`key_facts` text NOT NULL,
`icps` text NOT NULL,
`gtm_objectives` text NOT NULL,
`updated_at` integer NOT NULL
);
Loading
Loading