diff --git a/packages/adapter-utils/src/types.ts b/packages/adapter-utils/src/types.ts index 6653c365d09..b886f306f3d 100644 --- a/packages/adapter-utils/src/types.ts +++ b/packages/adapter-utils/src/types.ts @@ -349,9 +349,4 @@ export interface CreateConfigValues { maxTurns?: number; heartbeatEnabled: boolean; intervalSec: number; - // Emisso Sandbox fields - repoUrl?: string; - vcpus?: number; - timeoutSec?: number; - snapshotId?: string; } diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index ff6e4f63d32..22df07d007e 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -1,99 +1,223 @@ +import { eq } from "drizzle-orm"; import { createDb } from "./client.js"; -import { companies, agents, goals, projects, issues } from "./schema/index.js"; +import { + companies, + agents, + goals, + projects, + projectWorkspaces, + issues, + budgetPolicies, +} from "./schema/index.js"; const url = process.env.DATABASE_URL; if (!url) throw new Error("DATABASE_URL is required"); const db = createDb(url); -console.log("Seeding database..."); - -const [company] = await db - .insert(companies) - .values({ - name: "Paperclip Demo Co", - description: "A demo autonomous company", - status: "active", - budgetMonthlyCents: 50000, - }) - .returning(); - -const [ceo] = await db - .insert(agents) - .values({ - companyId: company!.id, - name: "CEO Agent", - role: "ceo", - title: "Chief Executive Officer", - status: "idle", - adapterType: "process", - adapterConfig: { command: "echo", args: ["hello from ceo"] }, - budgetMonthlyCents: 15000, - }) - .returning(); - -const [engineer] = await db - .insert(agents) - .values({ - companyId: company!.id, - name: "Engineer Agent", - role: "engineer", - title: "Software Engineer", - status: "idle", - reportsTo: ceo!.id, - adapterType: "process", - adapterConfig: { command: "echo", args: ["hello from engineer"] }, - budgetMonthlyCents: 10000, - }) - .returning(); - -const [goal] = await db - .insert(goals) - .values({ - companyId: company!.id, - title: "Ship V1", - description: "Deliver first control plane release", - level: "company", - status: "active", - ownerAgentId: ceo!.id, - }) - .returning(); - -const [project] = await db - .insert(projects) - .values({ - companyId: company!.id, - goalId: goal!.id, - name: "Control Plane MVP", - description: "Implement core board + agent loop", - status: "in_progress", - leadAgentId: ceo!.id, - }) - .returning(); - -await db.insert(issues).values([ - { - companyId: company!.id, - projectId: project!.id, - goalId: goal!.id, - title: "Implement atomic task checkout", - description: "Ensure in_progress claiming is conflict-safe", - status: "todo", - priority: "high", - assigneeAgentId: engineer!.id, - createdByAgentId: ceo!.id, - }, - { +const COMPANY_NAME = "Emisso Factory"; + +// Check if already seeded +const existing = await db + .select({ id: companies.id }) + .from(companies) + .where(eq(companies.name, COMPANY_NAME)) + .limit(1); + +if (existing.length > 0) { + console.log(`Seed skipped — "${COMPANY_NAME}" already exists (${existing[0]!.id})`); + process.exit(0); +} + +console.log("Seeding Emisso Factory..."); + +await db.transaction(async (tx) => { + // ── Company ────────────────────────────────────────────────────────── + const [company] = await tx + .insert(companies) + .values({ + name: COMPANY_NAME, + description: + "AI-native software factory — engineering, sales, and marketing agents", + status: "active", + budgetMonthlyCents: 100_000, + }) + .returning(); + + // ── Agents ─────────────────────────────────────────────────────────── + const [engineer] = await tx + .insert(agents) + .values({ + companyId: company!.id, + name: "Engineer", + role: "engineer", + title: "Software Engineer", + icon: "code", + status: "idle", + adapterType: "emisso_sandbox", + adapterConfig: { + model: "claude-sonnet-4-6", + vcpus: 2, + timeoutSec: 180, + maxTurns: 30, + repoUrl: "https://github.com/emisso-ai/emisso-hq.git", + cloneDepth: 1, + }, + runtimeConfig: { heartbeat: { enabled: false } }, + budgetMonthlyCents: 50_000, + }) + .returning(); + + const [sdr] = await tx + .insert(agents) + .values({ + companyId: company!.id, + name: "SDR", + role: "general", + title: "Sales Development Representative", + icon: "mail", + status: "idle", + adapterType: "emisso_sandbox", + adapterConfig: { + model: "claude-sonnet-4-6", + vcpus: 2, + timeoutSec: 120, + maxTurns: 20, + repoUrl: "https://github.com/emisso-ai/emisso-hq.git", + cloneDepth: 1, + promptTemplate: + "You are {{agent.name}}, an SDR agent for Emisso. You manage the folder-native CRM at sdr/. Read sdr/config/icp.yml and sdr/config/tone.md before any action. Follow the /sdr skill instructions. Continue your Paperclip work.", + }, + runtimeConfig: { heartbeat: { enabled: false } }, + budgetMonthlyCents: 25_000, + }) + .returning(); + + const [marketing] = await tx + .insert(agents) + .values({ + companyId: company!.id, + name: "Marketing", + role: "general", + title: "Marketing Specialist", + icon: "sparkles", + status: "idle", + adapterType: "emisso_sandbox", + adapterConfig: { + model: "claude-sonnet-4-6", + vcpus: 2, + timeoutSec: 120, + maxTurns: 20, + repoUrl: "https://github.com/emisso-ai/emisso-hq.git", + cloneDepth: 1, + promptTemplate: + "You are {{agent.name}}, a marketing agent for Emisso. You have access to the full product codebase and docs to create content, write blog posts, draft social media, and produce marketing materials. Continue your Paperclip work.", + }, + runtimeConfig: { heartbeat: { enabled: false } }, + budgetMonthlyCents: 25_000, + }) + .returning(); + + // ── Goal ───────────────────────────────────────────────────────────── + const [goal] = await tx + .insert(goals) + .values({ + companyId: company!.id, + title: "Launch AI Software Factory", + description: + "Ship emisso-os as the control plane for autonomous agents", + level: "company", + status: "active", + }) + .returning(); + + // ── Project + Workspace ────────────────────────────────────────────── + const [project] = await tx + .insert(projects) + .values({ + companyId: company!.id, + goalId: goal!.id, + name: "Emisso Platform", + description: "Main product development and go-to-market", + status: "in_progress", + }) + .returning(); + + await tx.insert(projectWorkspaces).values({ companyId: company!.id, projectId: project!.id, - goalId: goal!.id, - title: "Add budget auto-pause", - description: "Pause agent at hard budget ceiling", - status: "backlog", - priority: "medium", - createdByAgentId: ceo!.id, - }, -]); - -console.log("Seed complete"); + name: "emisso-hq", + sourceType: "git_remote", + repoUrl: "https://github.com/emisso-ai/emisso-hq.git", + repoRef: "main", + isPrimary: true, + }); + + // ── Issues ─────────────────────────────────────────────────────────── + await tx.insert(issues).values([ + { + companyId: company!.id, + projectId: project!.id, + goalId: goal!.id, + title: "Set up CI/CD pipeline for emisso-os", + description: + "Configure GitHub Actions for build, typecheck, lint, and test on every PR", + status: "todo", + priority: "high", + assigneeAgentId: engineer!.id, + }, + { + companyId: company!.id, + projectId: project!.id, + goalId: goal!.id, + title: "Run full SDR pipeline — prospect and draft outreach", + description: + "Execute a full /sdr pipeline run: scan leads, enrich due companies, draft outreach emails", + status: "todo", + priority: "medium", + assigneeAgentId: sdr!.id, + }, + { + companyId: company!.id, + projectId: project!.id, + goalId: goal!.id, + title: "Write launch blog post for emisso-os", + description: + "Draft a technical blog post announcing emisso-os as an open-source control plane for autonomous agents", + status: "todo", + priority: "medium", + assigneeAgentId: marketing!.id, + }, + ]); + + // ── Budget Policies ────────────────────────────────────────────────── + await tx.insert(budgetPolicies).values([ + { + companyId: company!.id, + scopeType: "company", + scopeId: company!.id, + metric: "billed_cents", + windowKind: "calendar_month_utc", + amount: 100_000, + warnPercent: 80, + hardStopEnabled: true, + }, + { + companyId: company!.id, + scopeType: "agent", + scopeId: engineer!.id, + metric: "billed_cents", + windowKind: "calendar_month_utc", + amount: 50_000, + warnPercent: 80, + hardStopEnabled: true, + }, + ]); + + console.log("Seed complete — Emisso Factory created"); + console.log(` Company: ${company!.id}`); + console.log(` Agents: Engineer (${engineer!.id}), SDR (${sdr!.id}), Marketing (${marketing!.id})`); + console.log(` Project: ${project!.id}`); +}); + process.exit(0); diff --git a/server/src/adapters/emisso-sandbox/execute.ts b/server/src/adapters/emisso-sandbox/execute.ts index 871a50a5ae7..a77feebe97d 100644 --- a/server/src/adapters/emisso-sandbox/execute.ts +++ b/server/src/adapters/emisso-sandbox/execute.ts @@ -91,7 +91,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise) : []; - const cloneDepth = asNumber(config.cloneDepth, 1); + const cloneDepth = clamp(asNumber(config.cloneDepth, 1), 1, 10000); const model = asString(config.model, DEFAULT_MODEL); const maxTurns = asNumber(config.maxTurns, DEFAULT_MAX_TURNS); const timeoutSec = clamp(asNumber(config.timeoutSec, DEFAULT_TIMEOUT_SEC), MIN_TIMEOUT_SEC, MAX_TIMEOUT_SEC); @@ -209,6 +209,10 @@ export async function execute(ctx: AdapterExecutionContext): Promise { + process.stderr.write("Failed to start claude: " + err.message + "\\n"); + process.exit(1); +}); +proc.stdin.on("error", () => {}); proc.stdin.write(prompt); proc.stdin.end(); proc.on("close", (code, signal) => process.exit(code ?? (signal ? 1 : 0))); diff --git a/server/src/adapters/emisso-sandbox/helpers.ts b/server/src/adapters/emisso-sandbox/helpers.ts index db33778fca7..e1fe9abe284 100644 --- a/server/src/adapters/emisso-sandbox/helpers.ts +++ b/server/src/adapters/emisso-sandbox/helpers.ts @@ -112,12 +112,19 @@ export function embedGitCredentials( url: string, auth: { username: string; token: string }, ): string { + if (url.startsWith("git@") || url.includes("ssh://")) { + throw new Error( + `SSH clone URLs are not supported for credential embedding. Use an HTTPS URL instead: ${url}`, + ); + } try { const parsed = new URL(url); parsed.username = encodeURIComponent(auth.username); parsed.password = encodeURIComponent(auth.token); return parsed.toString(); } catch { - return url; + throw new Error( + `Invalid clone URL — could not parse for credential embedding: ${url}`, + ); } } diff --git a/server/src/adapters/emisso-sandbox/test.ts b/server/src/adapters/emisso-sandbox/test.ts index 3095836a5eb..5447f74c548 100644 --- a/server/src/adapters/emisso-sandbox/test.ts +++ b/server/src/adapters/emisso-sandbox/test.ts @@ -59,14 +59,14 @@ export async function testEnvironment( } else if (!isNonEmpty(vercelToken) && !isNonEmpty(vercelTeamId)) { checks.push({ code: "sandbox_vercel_auth_missing", - level: "warn", - message: "Vercel authentication not configured. Sandbox creation may rely on OIDC (Vercel-hosted only).", - hint: "Set VERCEL_TOKEN and VERCEL_TEAM_ID for non-Vercel environments.", + level: "error", + message: "Vercel authentication not configured. Sandbox creation will fail.", + hint: "Set VERCEL_TOKEN and VERCEL_TEAM_ID in the environment or adapterConfig.", }); } else { checks.push({ code: "sandbox_vercel_auth_partial", - level: "warn", + level: "error", message: "Partial Vercel auth: both VERCEL_TOKEN and VERCEL_TEAM_ID are needed.", hint: "Set both values in the environment or adapterConfig.", }); diff --git a/server/src/auth/supabase-bridge.ts b/server/src/auth/supabase-bridge.ts index 26aaf3e0c29..8689724f5d6 100644 --- a/server/src/auth/supabase-bridge.ts +++ b/server/src/auth/supabase-bridge.ts @@ -134,7 +134,7 @@ export function supabaseBridgeRoute(db: Db): Router { // Create a Better Auth-compatible session directly in the DB const sessionId = crypto.randomUUID(); - const sessionToken = crypto.randomUUID(); + const sessionToken = crypto.getRandomValues(new Uint8Array(32)).reduce((s, b) => s + b.toString(16).padStart(2, "0"), ""); const now = new Date(); const expiresAt = new Date(now.getTime() + SESSION_DURATION_MS); diff --git a/skills/emisso-marketing/SKILL.md b/skills/emisso-marketing/SKILL.md new file mode 100644 index 00000000000..e58116b796f --- /dev/null +++ b/skills/emisso-marketing/SKILL.md @@ -0,0 +1,144 @@ +--- +name: marketing +description: Marketing agent — create content, blog posts, social media, and marketing materials for Emisso. Follows brand voice and positioning guidelines. +context: fork +allowed-tools: + - Read + - Write + - Edit + - Bash + - Glob + - Grep + - WebSearch + - WebFetch +--- + +# Marketing Agent Skill + +Create marketing content for Emisso — blog posts, social media, product announcements, and sales collateral. All content reflects Emisso's positioning as an AI-native software factory. + +## Arguments + +Parse `$ARGUMENTS` for the command: + +- `(no args)` — Review pending content tasks and suggest next actions +- `blog ` — Write a blog post on the given topic +- `social ` — Draft social media posts (Twitter/X + LinkedIn) +- `announce ` — Write a product announcement +- `case-study ` — Draft a case study from customer data +- `review` — Review and improve existing drafts + +## Brand Voice + +**Emisso** is an AI-native software factory. We don't just build tools — we deploy autonomous agents that ship real software, run sales pipelines, and produce marketing content. + +### Tone + +- **Direct and confident** — no hedging, no "we believe", no "we think" +- **Technical but accessible** — write for technical founders who value substance over hype +- **Concise** — every sentence earns its place. Cut filler ruthlessly +- **Show, don't tell** — concrete examples over abstract claims +- **Honest** — acknowledge limitations. Never overpromise + +### Positioning + +- **What we are:** AI-native software factory — autonomous agents that do real work +- **What we're not:** Another AI chatbot, copilot, or assistant +- **Key differentiator:** Agents operate in real environments (sandboxed VMs), write real code, manage real pipelines — not demos or toys +- **Control plane:** emisso-os is the open-source orchestration layer — hire agents, assign tasks, set budgets, approve work + +### Key Messages + +1. **"Software that builds itself"** — agents write code, run tests, deploy +2. **"Your AI team, managed like a real team"** — org charts, budgets, approvals +3. **"Open-source control plane"** — emisso-os is transparent and extensible +4. **"LatAm-first, global ambition"** — built in Chile, serving the world + +## Content Guidelines + +### Blog Posts + +Structure: +1. **Hook** (1-2 sentences) — Why should the reader care? Lead with the problem or insight +2. **Context** (1-2 paragraphs) — Set up the problem space briefly +3. **Core content** — The meat. Use headers, code blocks, diagrams where helpful +4. **Takeaway** — What should the reader do or think differently? + +Rules: +- 800-1500 words for standard posts, 1500-2500 for deep dives +- Include code examples when discussing technical topics +- Link to the emisso-os repo when relevant +- No marketing fluff — write like an engineer explaining to another engineer +- Save to `docs/blog/` as a Markdown file + +### Social Media + +**Twitter/X:** +- Max 280 characters per tweet +- Thread format for longer content (3-5 tweets) +- Lead with the insight, not the product +- Use code screenshots when showing technical features +- No hashtag spam — 0-2 relevant hashtags max + +**LinkedIn:** +- 150-300 words +- Professional but not corporate +- Tag relevant people/companies when appropriate +- Include a clear CTA (try emisso-os, read the blog post, etc.) + +### Product Announcements + +Structure: +1. **One-line summary** — What shipped and why it matters +2. **The problem** — What was painful before +3. **The solution** — What we built (with screenshots/demos) +4. **How to try it** — Clear next steps +5. **What's next** — Brief roadmap tease + +## Audience + +### Primary: Technical Founders & CTOs (LatAm) + +- Building SaaS products with small teams (5-20 engineers) +- Interested in AI automation but skeptical of hype +- Value open-source and transparency +- Bilingual (Spanish/English) — prefer Spanish for informal content, English for technical docs + +### Secondary: CFOs & Operations Leaders + +- Care about cost efficiency and measurable ROI +- Want to understand budgets and controls +- Less technical — need clear business value explanations + +## Language + +- **Spanish** for LatAm-targeted content (social media, outreach, regional blog posts) +- **English** for technical documentation, global blog posts, and the emisso-os project +- When writing in Spanish, use neutral Latin American Spanish (not Spain-specific) + +## Reference Material + +The repo contains extensive product knowledge: +- `docs/product/` — Product vision, agent architecture, context graph +- `docs/design/` — Design system, UI patterns, brand guidelines +- `docs/engineering/` — Technical architecture, API patterns, testing +- `app/` — The actual product codebase (Next.js application) + +Always read relevant docs before creating content to ensure accuracy. + +## Output Locations + +- Blog posts → `docs/blog/.md` +- Social media drafts → `docs/marketing/social/-.md` +- Announcements → `docs/marketing/announcements/.md` +- Case studies → `docs/marketing/case-studies/.md` + +## Rules + +- **Never publish automatically** — all content goes to draft files for human review +- **Read the docs first** — always check `docs/` for accurate product information before writing +- **No AI hype** — avoid words like "revolutionary", "game-changing", "cutting-edge" +- **Be specific** — use numbers, examples, and concrete details over vague claims +- **Respect the brand** — always use "Emisso" (capital E), "emisso-os" (lowercase) for the repo +- **Credit open source** — acknowledge Paperclip and other projects we build on +- **Log everything** — append content creation activities to daily logs diff --git a/skills/emisso-sdr/SKILL.md b/skills/emisso-sdr/SKILL.md new file mode 100644 index 00000000000..2388cd14458 --- /dev/null +++ b/skills/emisso-sdr/SKILL.md @@ -0,0 +1,344 @@ +--- +name: sdr +description: SDR agent — manage folder-native CRM pipeline, enrich companies, draft outreach emails, and track the sales pipeline. Use for prospecting, outreach, and pipeline management. +context: fork +allowed-tools: + - Read + - Write + - Edit + - Bash + - Glob + - Grep + - WebSearch + - WebFetch +--- + +# SDR Agent Skill + +Manage Emisso's sales pipeline through a folder-native CRM at `sdr/`. Find prospects, research companies, draft personalized emails, and track pipeline stages — all as YAML/Markdown files. + +## Arguments + +Parse `$ARGUMENTS` for the command: + +- `(no args)` — Full pipeline run: check all leads, execute due actions, update dashboard +- `prospect` — Find new companies matching ICP via web search +- `enrich ` — Deep-research a company, find contacts, find & verify emails +- `draft ` — Draft the next email in sequence for a company +- `move ` — Move company to a pipeline stage (e.g., `contacted`, `replied`) +- `add ` — Add a new lead to prospecting +- `dashboard` — Regenerate the pipeline summary +- `review` — Show all pending drafts for approval +- `status` — Quick pipeline stats + +## File Locations + +All data lives in `sdr/` at the repo root: + +``` +sdr/ +├── config/ +│ ├── icp.yml # ICP definition and scoring +│ ├── tone.md # Email voice guidelines +│ ├── sending.yml # Sending infrastructure config +│ └── sequences/ # Email sequence templates +│ ├── cold-intro.yml +│ └── warm-intro.yml +├── pipeline/ +│ ├── 01-prospecting/ # New leads +│ ├── 02-contacted/ # First email sent +│ ├── 03-replied/ # Got a response +│ ├── 04-meeting-booked/ # Meeting scheduled +│ ├── 05-qualified/ # Good fit confirmed +│ ├── 06-won/ # Closed +│ ├── 07-lost/ # Dead +│ └── _dashboard.md # Pipeline summary +├── contacts/ # Contact files +├── enrichment/ # Company research +├── drafts/ # Email drafts awaiting approval +└── logs/ # Daily activity logs +``` + +## Commands + +### `/sdr` (full pipeline run) + +This is the default when called from `/loop`. Execute in order: + +1. **Read config** — Load `sdr/config/icp.yml`, `sdr/config/tone.md`, `sdr/config/sending.yml` +2. **Scan pipeline** — Glob all `.yml` files across `sdr/pipeline/*/` and read each +3. **Identify due actions** — Find leads where `next_action_date <= today` +4. **Execute actions:** + - If `next_action: draft_step_N` → draft the next email, save to `sdr/drafts/` + - If `next_action: check_reply` → note it needs manual checking (Phase 1) + - If `next_action: follow_up` → draft follow-up email +5. **Update dashboard** — Regenerate `sdr/pipeline/_dashboard.md` +6. **Write log** — Append today's actions to `sdr/logs/YYYY-MM-DD.md` + +### `/sdr prospect` + +Find new companies that match the ICP: + +1. Read `sdr/config/icp.yml` for target criteria +2. Web search for companies matching signals: + - Search: `"[industry] startup [geography] Series A 2025 2026"` + - Search: `"[industry] company hiring engineers [geography]"` + - Search: `site:linkedin.com "[industry]" "[role]" "[geography]"` +3. For each candidate: + - Check if already in pipeline (search `sdr/pipeline/`) + - Score against ICP criteria + - If score >= threshold, create a prospect file +4. Save new prospects to `sdr/pipeline/01-prospecting/` +5. Report what was found + +### `/sdr enrich ` + +Deep-research a specific company, find decision-makers, and find & verify their emails. + +**Step 1 — Company Research:** + +1. Find the company file in `sdr/pipeline/` +2. Read the company's domain +3. Research using web search: + - Company website (about, team, careers pages) + - Recent news and press releases + - LinkedIn company page + - Tech stack / SaaS tools they use (job postings are great for this) + - Recent funding or growth events + - Estimate their SaaS spend (headcount × typical per-seat pricing) + +**Step 2 — Find Decision-Makers:** + +4. Search for people at the company matching target roles from `sdr/config/icp.yml`: + - Search: `site:linkedin.com "[company name]" (CFO OR COO OR CTO OR "gerente de finanzas" OR "gerente general")` + - Search: `"[company name]" "[role]" email OR contact` + - Check the company's team/about page +5. For each contact found, record: name, role, LinkedIn URL + +**Step 3 — Find & Verify Emails:** + +6. For each contact, run the email finder tool: + ```bash + node sdr/tools/find-email.mjs --first "Patrick" --last "Hardy" --domain "thewildfoods.com" --alt-domain "thewildbrands.com" + ``` + The tool automatically: + - Generates all common email patterns (first@, first.last@, flast@, etc.) + - Checks MX records to confirm the domain receives email + - Detects if the provider blocks SMTP verification (Google, Microsoft, etc.) + - For non-blocked providers: runs SMTP RCPT TO verification per candidate + - For blocked providers: returns top patterns as `pattern-match` + - Detects catch-all domains + - Returns JSON with confidence levels + + If the company has a parent/alternate domain (found during research), pass it as `--alt-domain`. + +7. Also web-search for their email directly as a cross-check: + - Search: `"[name]" "@[domain]" OR "email"` + - Search: `"[name]" "[company]" email OR contact` + - If found via web search, mark as `web-found` (higher confidence than pattern-match) + +8. Confidence levels: + - `verified` — SMTP confirmed the mailbox exists + - `web-found` — found via web search (LinkedIn, company page, public records) + - `pattern-match` — matches common pattern, MX valid, but couldn't SMTP verify + - `guess` — no MX records or domain doesn't receive email + +**Step 4 — Write Results:** + +11. Write enrichment to `sdr/enrichment/.md`: + +```markdown +# [Company Name] — Enrichment + +**Domain:** example.com +**Researched:** YYYY-MM-DD + +## Company Overview +[1-2 paragraphs: what they do, who they serve] + +## Team & Size +[Key people, team size, growth trajectory] + +## SaaS Spend Estimate +[Table: category, likely tool, estimated monthly cost] +[Total estimated SaaS spend] + +## Buying Signals +[Funding, hiring, growth indicators] + +## Personalization Hooks +[Specific things to reference in outreach — recent blog posts, product launches, hiring posts] + +## Contacts & Emails +| Name | Role | Email | Confidence | Source | +|------|------|-------|------------|--------| +| Patrick Hardy | COO | patrick@thewildfoods.com | verified | SMTP check | +| Rodrigo Paredes | Head of Digital Transformation | rparedes@thewildfoods.com | pattern-match | MX valid, common pattern | +``` + +12. Update the company's pipeline YAML with contacts and emails: + ```yaml + contacts: + - name: Patrick Hardy + role: COO + email: patrick@thewildfoods.com + email_confidence: verified + linkedin: https://linkedin.com/in/... + ``` + +13. Set `next_action: draft_step_1` if at least one email was found. + Set `next_action: find_email_manually` if no emails could be found (needs human help). + +### `/sdr draft ` + +Draft the next email in sequence: + +1. Find the company in `sdr/pipeline/` +2. Read its current `sequence` and `sequence_step` +3. Load the sequence template from `sdr/config/sequences/` +4. Load `sdr/config/tone.md` for voice guidelines +5. Load enrichment from `sdr/enrichment/` if available +6. Draft the email following: + - Sequence step guidelines + - Tone rules + - Personalization from enrichment data +7. Save draft to `sdr/drafts/--step-.md`: + +```markdown +# Draft: [Company Name] — Step [N] + +**To:** [contact email] +**Subject:** [subject line] +**Sequence:** [sequence name] +**Step:** [N] of [total] + +--- + +[email body] + +--- + +**Personalization used:** +- [list of specific details referenced] + +**To send:** Copy the email body above and send manually. Then run `/sdr move contacted` +``` + +### `/sdr move ` + +Move a company between pipeline stages: + +1. Find the company file in `sdr/pipeline/` +2. Move the file to the target stage directory: + - `prospecting` → `01-prospecting/` + - `contacted` → `02-contacted/` + - `replied` → `03-replied/` + - `meeting-booked` → `04-meeting-booked/` + - `qualified` → `05-qualified/` + - `won` → `06-won/` + - `lost` → `07-lost/` +3. Update the file's metadata: + - Set `last_action` to today + - Update `next_action` and `next_action_date` based on stage: + - `contacted` → `next_action: draft_step_2`, date: +3 days + - `replied` → `next_action: respond`, date: today (urgent) + - `meeting-booked` → `next_action: prepare_meeting`, date: meeting date - 1 day +4. Use `git mv` to preserve history + +### `/sdr add ` + +Add a new lead to the pipeline: + +1. Create `sdr/pipeline/01-prospecting/.yml`: + +```yaml +name: [Company Name] +domain: [domain] +industry: unknown +size: unknown +location: unknown +score: 0 +match_reasons: [] + +contacts: [] + +sequence: cold-intro +sequence_step: 0 +last_action: [today] +next_action: enrich +next_action_date: [today] + +notes: | + Added manually via /sdr add +``` + +2. Suggest running `/sdr enrich ` next + +### `/sdr dashboard` + +Regenerate `sdr/pipeline/_dashboard.md`: + +1. Glob all `.yml` files in each pipeline stage directory +2. Count leads per stage +3. List companies with their scores and next actions +4. Highlight urgent items (next_action_date <= today) +5. Write the updated dashboard + +### `/sdr review` + +Show all pending drafts: + +1. Glob `sdr/drafts/*.md` +2. Read each draft +3. Present them for review with send instructions + +### `/sdr status` + +Quick stats without full pipeline run: + +1. Count files in each pipeline stage +2. Count pending drafts +3. Show today's log if it exists + +## Company File Schema + +Every company in the pipeline uses this YAML structure: + +```yaml +name: string # Company name +domain: string # Company website domain +industry: string # Industry category +size: string # Employee count range (e.g., "50-200") +location: string # HQ location +score: number # ICP match score (0-100) +match_reasons: string[] # Why they match the ICP + +contacts: + - name: string + role: string + email: string + email_confidence: string # verified | pattern-match | web-found | guess + linkedin: string # LinkedIn profile URL + +sequence: string # Current sequence name (from config/sequences/) +sequence_step: number # Current step in sequence (0 = not started) +last_action: date # YYYY-MM-DD +next_action: string # What to do next +next_action_date: date # When to do it + +notes: | # Freeform notes, interaction history + Multi-line notes here. +``` + +## Rules + +- **Never send emails automatically in Phase 1** — always draft to `sdr/drafts/` +- **Always read `config/tone.md`** before writing any email +- **Always check enrichment** before drafting (run enrich first if missing) +- **Respect the "no"** — if a company is moved to `lost`, never re-add them +- **Log everything** — append to `sdr/logs/YYYY-MM-DD.md` after any action +- **Keep it short** — emails should be 4-6 sentences for first touch, shorter for follow-ups +- **One contact per company** — target the most senior relevant person +- **Use git mv** for moving files between pipeline stages (preserves history) +- **Score honestly** — don't inflate scores to fill the pipeline +- **Spanish for LatAm** — English for US/international prospects diff --git a/ui/src/adapters/emisso-sandbox/build-config.ts b/ui/src/adapters/emisso-sandbox/build-config.ts index d231dfeb0f9..d83b355cdf7 100644 --- a/ui/src/adapters/emisso-sandbox/build-config.ts +++ b/ui/src/adapters/emisso-sandbox/build-config.ts @@ -1,5 +1,12 @@ import type { CreateConfigValues } from "../types"; +export interface EmissoCreateConfigValues extends CreateConfigValues { + repoUrl?: string; + vcpus?: number; + timeoutSec?: number; + snapshotId?: string; +} + function parseJsonObject(text: string): Record | null { const trimmed = text.trim(); if (!trimmed) return null; @@ -13,14 +20,15 @@ function parseJsonObject(text: string): Record | null { } export function buildEmissoSandboxConfig(v: CreateConfigValues): Record { + const ev = v as EmissoCreateConfigValues; const ac: Record = {}; - if (v.model) ac.model = v.model; - if (v.repoUrl) ac.repoUrl = v.repoUrl; - if (v.vcpus) ac.vcpus = Number(v.vcpus); - if (v.timeoutSec) ac.timeoutSec = Number(v.timeoutSec); - if (v.maxTurns) ac.maxTurns = Number(v.maxTurns); - if (v.snapshotId) ac.snapshotId = v.snapshotId; + if (ev.model) ac.model = ev.model; + if (ev.repoUrl) ac.repoUrl = ev.repoUrl; + if (ev.vcpus) ac.vcpus = Number(ev.vcpus); + if (ev.timeoutSec) ac.timeoutSec = Number(ev.timeoutSec); + if (ev.maxTurns) ac.maxTurns = Number(ev.maxTurns); + if (ev.snapshotId) ac.snapshotId = ev.snapshotId; if (v.promptTemplate) ac.promptTemplate = v.promptTemplate; if (v.instructionsFilePath) ac.instructionsFilePath = v.instructionsFilePath; diff --git a/ui/src/adapters/emisso-sandbox/config-fields.tsx b/ui/src/adapters/emisso-sandbox/config-fields.tsx index 6494e2081b4..381cfc295e3 100644 --- a/ui/src/adapters/emisso-sandbox/config-fields.tsx +++ b/ui/src/adapters/emisso-sandbox/config-fields.tsx @@ -1,4 +1,5 @@ import type { AdapterConfigFieldsProps } from "../types"; +import type { EmissoCreateConfigValues } from "./build-config"; import { Field, DraftInput, @@ -14,13 +15,15 @@ const textareaClass = export function EmissoSandboxConfigFields({ isCreate, - values, - set, + values: rawValues, + set: rawSet, config, eff, mark, models, }: AdapterConfigFieldsProps) { + const values = rawValues as EmissoCreateConfigValues | null; + const set = rawSet as ((patch: Partial) => void) | null; return ( <> @@ -162,12 +165,14 @@ export function EmissoSandboxConfigFields({ config.mcpServers ? JSON.stringify(config.mcpServers, null, 2) : "", )} onChange={(e) => { + // Always store raw text for display + mark("adapterConfig", "mcpServersJson", e.target.value); + // When valid JSON, also update the actual mcpServers key the server reads try { const parsed = e.target.value.trim() ? JSON.parse(e.target.value) : undefined; mark("adapterConfig", "mcpServers", parsed); } catch { - // Keep the raw text until it's valid JSON - mark("adapterConfig", "mcpServersJson", e.target.value); + // Keep raw text until valid — server ignores mcpServersJson } }} placeholder={'{\n "github": {\n "command": "mcp-server-github"\n }\n}'} diff --git a/ui/src/adapters/emisso-sandbox/parse-stdout.ts b/ui/src/adapters/emisso-sandbox/parse-stdout.ts index 366444a58d3..ea09c4bec9d 100644 --- a/ui/src/adapters/emisso-sandbox/parse-stdout.ts +++ b/ui/src/adapters/emisso-sandbox/parse-stdout.ts @@ -35,6 +35,7 @@ export function parseEmissoSandboxStdoutLine(line: string, ts: string): Transcri } if (event.type === "user" && event.message?.content) { + const toolResults: TranscriptEntry[] = []; for (const block of event.message.content) { if (block.type === "tool_result") { const content = @@ -47,25 +48,42 @@ export function parseEmissoSandboxStdoutLine(line: string, ts: string): Transcri .join("\n") .substring(0, 500) : ""; - return [{ + toolResults.push({ kind: "tool_result", ts, toolUseId: block.tool_use_id ?? "", content, isError: block.is_error === true, - }]; + }); } } + if (toolResults.length > 0) return toolResults; } if (event.type === "result") { + // Extract token counts from modelUsage (aggregated across models) or top-level usage + let inputTokens = 0; + let outputTokens = 0; + let cachedTokens = 0; + if (event.modelUsage && typeof event.modelUsage === "object") { + for (const model of Object.values(event.modelUsage) as Record[]) { + inputTokens += model.inputTokens ?? 0; + outputTokens += model.outputTokens ?? 0; + cachedTokens += model.cacheReadInputTokens ?? 0; + } + } else if (event.usage) { + inputTokens = event.usage.input_tokens ?? 0; + outputTokens = event.usage.output_tokens ?? 0; + cachedTokens = event.usage.cache_read_input_tokens ?? 0; + } + return [{ kind: "result", ts, text: typeof event.result === "string" ? event.result : JSON.stringify(event), - inputTokens: 0, - outputTokens: 0, - cachedTokens: 0, + inputTokens, + outputTokens, + cachedTokens, costUsd: typeof event.total_cost_usd === "number" ? event.total_cost_usd : 0, subtype: "result", isError: false,