From f9cca1af5c1376d780f9d36ce140b0417b56537c Mon Sep 17 00:00:00 2001 From: Cristobal Dotte Date: Sat, 21 Mar 2026 00:27:49 -0300 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20Phase=203=20=E2=80=94=20Emisso=20Fa?= =?UTF-8?q?ctory=20seed=20data=20+=20SDR/Marketing=20skills?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seed the Emisso Factory company with 3 agents (Engineer, SDR, Marketing), all using emisso_sandbox adapter pointing to emisso-hq. Includes project, workspace, goal, sample issues, and budget policies. Port the SDR skill from emisso-hq and add a new Marketing content skill. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/db/src/seed.ts | 174 +++++++++++++--- skills/emisso-marketing/SKILL.md | 144 +++++++++++++ skills/emisso-sdr/SKILL.md | 344 +++++++++++++++++++++++++++++++ 3 files changed, 628 insertions(+), 34 deletions(-) create mode 100644 skills/emisso-marketing/SKILL.md create mode 100644 skills/emisso-sdr/SKILL.md diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index ff6e4f63d32..840b80a99cf 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -1,99 +1,205 @@ 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..."); +console.log("Seeding Emisso Factory..."); +// ── Company ────────────────────────────────────────────────────────── const [company] = await db .insert(companies) .values({ - name: "Paperclip Demo Co", - description: "A demo autonomous company", + name: "Emisso Factory", + description: + "AI-native software factory — engineering, sales, and marketing agents", status: "active", - budgetMonthlyCents: 50000, + budgetMonthlyCents: 100_000, }) .returning(); -const [ceo] = await db +// ── Agents ─────────────────────────────────────────────────────────── +const [engineer] = await db .insert(agents) .values({ companyId: company!.id, - name: "CEO Agent", - role: "ceo", - title: "Chief Executive Officer", + name: "Engineer", + role: "engineer", + title: "Software Engineer", + icon: "code", status: "idle", - adapterType: "process", - adapterConfig: { command: "echo", args: ["hello from ceo"] }, - budgetMonthlyCents: 15000, + 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 [engineer] = await db +const [sdr] = await db .insert(agents) .values({ companyId: company!.id, - name: "Engineer Agent", - role: "engineer", - title: "Software Engineer", + name: "SDR", + role: "general", + title: "Sales Development Representative", + icon: "mail", status: "idle", - reportsTo: ceo!.id, - adapterType: "process", - adapterConfig: { command: "echo", args: ["hello from engineer"] }, - budgetMonthlyCents: 10000, + 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 db + .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 db .insert(goals) .values({ companyId: company!.id, - title: "Ship V1", - description: "Deliver first control plane release", + title: "Launch AI Software Factory", + description: + "Ship emisso-os as the control plane for autonomous agents", level: "company", status: "active", - ownerAgentId: ceo!.id, }) .returning(); +// ── Project + Workspace ────────────────────────────────────────────── const [project] = await db .insert(projects) .values({ companyId: company!.id, goalId: goal!.id, - name: "Control Plane MVP", - description: "Implement core board + agent loop", + name: "Emisso Platform", + description: "Main product development and go-to-market", status: "in_progress", - leadAgentId: ceo!.id, }) .returning(); +await db.insert(projectWorkspaces).values({ + companyId: company!.id, + projectId: project!.id, + name: "emisso-hq", + sourceType: "git_remote", + repoUrl: "https://github.com/emisso-ai/emisso-hq.git", + repoRef: "main", + isPrimary: true, +}); + +// ── Issues ─────────────────────────────────────────────────────────── 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", + 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, - createdByAgentId: ceo!.id, }, { companyId: company!.id, projectId: project!.id, goalId: goal!.id, - title: "Add budget auto-pause", - description: "Pause agent at hard budget ceiling", - status: "backlog", + 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", - createdByAgentId: ceo!.id, + assigneeAgentId: marketing!.id, + }, +]); + +// ── Budget Policies ────────────────────────────────────────────────── +await db.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"); +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/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 From 3c8cac1eb680ca6bad47618dcec7e817f64ad504 Mon Sep 17 00:00:00 2001 From: Cristobal Dotte Date: Sat, 21 Mar 2026 00:27:58 -0300 Subject: [PATCH 2/4] fix: address Phase 2 PR review comments - Remove dead process launch in emisso-sandbox execute (P1: spawned claude then immediately killed it, wasting sandbox compute) - Add missing "-" after --print in runner script (P1: stdin prompt was silently ignored without the positional arg) - Move emisso-specific fields out of shared CreateConfigValues into EmissoCreateConfigValues to avoid polluting upstream types - Fix MCP textarea stale state by always writing to mcpServersJson - Use higher-entropy session token (256-bit hex vs UUID) Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/adapter-utils/src/types.ts | 5 --- server/src/adapters/emisso-sandbox/execute.ts | 39 ++----------------- server/src/auth/supabase-bridge.ts | 2 +- .../adapters/emisso-sandbox/build-config.ts | 20 +++++++--- .../adapters/emisso-sandbox/config-fields.tsx | 15 +++---- 5 files changed, 24 insertions(+), 57 deletions(-) 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/server/src/adapters/emisso-sandbox/execute.ts b/server/src/adapters/emisso-sandbox/execute.ts index 871a50a5ae7..cfe594a3efa 100644 --- a/server/src/adapters/emisso-sandbox/execute.ts +++ b/server/src/adapters/emisso-sandbox/execute.ts @@ -271,41 +271,8 @@ export async function execute(ctx: AdapterExecutionContext): Promise s + b.toString(16).padStart(2, "0"), ""); const now = new Date(); const expiresAt = new Date(now.getTime() + SESSION_DURATION_MS); 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..34f2f85d7f8 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,13 +165,7 @@ export function EmissoSandboxConfigFields({ config.mcpServers ? JSON.stringify(config.mcpServers, null, 2) : "", )} onChange={(e) => { - 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); - } + mark("adapterConfig", "mcpServersJson", e.target.value); }} placeholder={'{\n "github": {\n "command": "mcp-server-github"\n }\n}'} /> From df3f35268dfadc4ab80b50fd6aeb988804db5f7e Mon Sep 17 00:00:00 2001 From: Cristobal Dotte Date: Sat, 21 Mar 2026 00:32:20 -0300 Subject: [PATCH 3/4] fix: address PR #1 edge case review comments - Log warning when instructionsFilePath not found (was silently ignored) - Clamp cloneDepth to [1, 10000] (depth 0 caused full clones) - Skip additionalRepos with empty repoUrl (was causing cryptic git errors) - Broaden timeout detection to catch SDK-specific errors (TimeoutError, SandboxTimeoutError, 408, 504) - Cap fullStdout accumulation at 5 MB to prevent unbounded memory growth - Upgrade Vercel auth check from warn to error (missing auth blocks execution) - Collect all tool_result blocks per user message, not just the first Co-Authored-By: Claude Opus 4.6 (1M context) --- server/src/adapters/emisso-sandbox/execute.ts | 18 +++++++++++++----- server/src/adapters/emisso-sandbox/test.ts | 8 ++++---- ui/src/adapters/emisso-sandbox/parse-stdout.ts | 6 ++++-- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/server/src/adapters/emisso-sandbox/execute.ts b/server/src/adapters/emisso-sandbox/execute.ts index cfe594a3efa..91e49067e7a 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 0) return toolResults; } if (event.type === "result") { From 5005bfe96163c68e746d5d02bb43c5db1807b229 Mon Sep 17 00:00:00 2001 From: Cristobal Dotte Date: Sat, 21 Mar 2026 00:44:25 -0300 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20address=20PR=20#2=20edge=20case=20re?= =?UTF-8?q?view=20=E2=80=94=20P1=20and=20P2=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1 fixes: - Add proc.on('error') and proc.stdin.on('error') handlers in runner script to prevent unhandled EPIPE crashes - Throw on SSH/invalid URLs in embedGitCredentials instead of silently returning the original URL without credentials P2 fixes: - Edit-mode MCP textarea now writes both mcpServersJson (display) and mcpServers (parsed object the server reads) - Seed is now idempotent (checks if company exists) and wrapped in a transaction to prevent partial state on failure - Parse-stdout result entry now extracts real token counts from modelUsage/usage instead of hardcoding 0 Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/db/src/seed.ts | 378 +++++++++--------- server/src/adapters/emisso-sandbox/execute.ts | 5 + server/src/adapters/emisso-sandbox/helpers.ts | 9 +- .../adapters/emisso-sandbox/config-fields.tsx | 8 + .../adapters/emisso-sandbox/parse-stdout.ts | 22 +- 5 files changed, 238 insertions(+), 184 deletions(-) diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index 840b80a99cf..22df07d007e 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -1,3 +1,4 @@ +import { eq } from "drizzle-orm"; import { createDb } from "./client.js"; import { companies, @@ -14,192 +15,209 @@ if (!url) throw new Error("DATABASE_URL is required"); const db = createDb(url); +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..."); -// ── Company ────────────────────────────────────────────────────────── -const [company] = await db - .insert(companies) - .values({ - name: "Emisso Factory", - description: - "AI-native software factory — engineering, sales, and marketing agents", - status: "active", - budgetMonthlyCents: 100_000, - }) - .returning(); - -// ── Agents ─────────────────────────────────────────────────────────── -const [engineer] = await db - .insert(agents) - .values({ +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, - 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, + projectId: project!.id, + 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, }, - runtimeConfig: { heartbeat: { enabled: false } }, - budgetMonthlyCents: 50_000, - }) - .returning(); - -const [sdr] = await db - .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.", + { + 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, }, - runtimeConfig: { heartbeat: { enabled: false } }, - budgetMonthlyCents: 25_000, - }) - .returning(); - -const [marketing] = await db - .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.", + { + 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, }, - runtimeConfig: { heartbeat: { enabled: false } }, - budgetMonthlyCents: 25_000, - }) - .returning(); - -// ── Goal ───────────────────────────────────────────────────────────── -const [goal] = await db - .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 db - .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 db.insert(projectWorkspaces).values({ - companyId: company!.id, - projectId: project!.id, - name: "emisso-hq", - sourceType: "git_remote", - repoUrl: "https://github.com/emisso-ai/emisso-hq.git", - repoRef: "main", - isPrimary: true, + ]); + + // ── 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}`); }); -// ── Issues ─────────────────────────────────────────────────────────── -await db.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 db.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 91e49067e7a..a77feebe97d 100644 --- a/server/src/adapters/emisso-sandbox/execute.ts +++ b/server/src/adapters/emisso-sandbox/execute.ts @@ -427,6 +427,11 @@ const proc = spawn("claude", args, { }, }); +proc.on("error", (err) => { + 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/ui/src/adapters/emisso-sandbox/config-fields.tsx b/ui/src/adapters/emisso-sandbox/config-fields.tsx index 34f2f85d7f8..381cfc295e3 100644 --- a/ui/src/adapters/emisso-sandbox/config-fields.tsx +++ b/ui/src/adapters/emisso-sandbox/config-fields.tsx @@ -165,7 +165,15 @@ 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 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 06575b2649e..ea09c4bec9d 100644 --- a/ui/src/adapters/emisso-sandbox/parse-stdout.ts +++ b/ui/src/adapters/emisso-sandbox/parse-stdout.ts @@ -61,13 +61,29 @@ export function parseEmissoSandboxStdoutLine(line: string, ts: string): Transcri } 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,