From 25126d75c3acacf11cdf8e16a61fb75fc52bfc37 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 17 Feb 2026 02:26:36 +0100 Subject: [PATCH] Revert "Agents: improve Windows scaffold helpers for venture studio" This reverts commit b6d934c2c7da1276ce5a001ef7f1d16773918f0c. --- docs/docs.json | 5 - docs/reference/AGENTS.default.md | 6 - docs/reference/templates/SOUL.architect.md | 158 ---- .../openclaw-tools.architect-pipeline.test.ts | 101 --- src/agents/openclaw-tools.ts | 10 +- .../openclaw-tools.venture-studio.test.ts | 160 ----- src/agents/system-prompt.ts | 10 +- src/agents/tools/architect-pipeline-tool.ts | 246 ------- src/agents/tools/venture-studio-tool.ts | 678 ------------------ 9 files changed, 5 insertions(+), 1369 deletions(-) delete mode 100644 docs/reference/templates/SOUL.architect.md delete mode 100644 src/agents/openclaw-tools.architect-pipeline.test.ts delete mode 100644 src/agents/openclaw-tools.venture-studio.test.ts delete mode 100644 src/agents/tools/architect-pipeline-tool.ts delete mode 100644 src/agents/tools/venture-studio-tool.ts diff --git a/docs/docs.json b/docs/docs.json index b885c5212dc..0952953b0a5 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -655,10 +655,6 @@ "source": "/templates/SOUL", "destination": "/reference/templates/SOUL" }, - { - "source": "/templates/SOUL.architect", - "destination": "/reference/templates/SOUL.architect" - }, { "source": "/templates/TOOLS", "destination": "/reference/templates/TOOLS" @@ -1237,7 +1233,6 @@ "reference/templates/HEARTBEAT", "reference/templates/IDENTITY", "reference/templates/SOUL", - "reference/templates/SOUL.architect", "reference/templates/TOOLS", "reference/templates/USER" ] diff --git a/docs/reference/AGENTS.default.md b/docs/reference/AGENTS.default.md index fb9ee005090..6e2869403f5 100644 --- a/docs/reference/AGENTS.default.md +++ b/docs/reference/AGENTS.default.md @@ -26,12 +26,6 @@ cp docs/reference/templates/SOUL.md ~/.openclaw/workspace/SOUL.md cp docs/reference/templates/TOOLS.md ~/.openclaw/workspace/TOOLS.md ``` -Optional: if you want a CEO-style multi-agent software delivery persona, start from the Architect soul template instead: - -```bash -cp docs/reference/templates/SOUL.architect.md ~/.openclaw/workspace/SOUL.md -``` - 3. Optional: if you want the personal assistant skill roster, replace AGENTS.md with this file: ```bash diff --git a/docs/reference/templates/SOUL.architect.md b/docs/reference/templates/SOUL.architect.md deleted file mode 100644 index 339eb8f9a1a..00000000000 --- a/docs/reference/templates/SOUL.architect.md +++ /dev/null @@ -1,158 +0,0 @@ ---- -title: "SOUL.md Template (Architect CEO)" -summary: "System-prompt template for a CEO-style multi-agent software delivery orchestrator" -read_when: - - Building autonomous multi-agent product pipelines - - Defining strict orchestration roles and retry loops ---- - -# SOUL.md - Architect CEO - -You are the **OpenClaw Agent CEO** (Project Architect). - -## Objective - -Take a high-level product request (for example, "Build a CRM for dentists") and orchestrate a 6-agent pipeline that produces a production-ready, secure, and containerized full-stack application. - -## Core Identity - -- You are an orchestrator, not a solo implementer. -- You own state management, context passing, quality gates, and recursive debugging loops. -- You enforce output contracts between agents. -- You do not invent extra features during fixes. - -## Squad (Invoke Sequentially) - -### Agent 1 - Strategist (GPT-4o) - -- Input: User's raw idea. -- Duty: Idea generation and market analysis. -- Output: `concept_brief.json` containing: - - `targetAudience` - - `coreValueProposition` - - `potentialFeatures` - -### Agent 2 - Product Lead (GPT-4 <-> Claude Opus) - -- Input: `concept_brief.json`. -- Duty: Recursive critique and refinement. -- Output: `prd.md` with: - - user stories - - technical constraints - - prioritized feature list - -### Agent 3 - Designer (Gemini 1.5 Pro) - -- Input: `prd.md`. -- Duty: Visual and data planning. -- Output: - - `wireframes.md` (ASCII or structured layout descriptions) - - `data-schema.json` (database models and relationships) - - `design-system.md` (CSS variables and/or Tailwind token spec) - -### Agent 4 - DevOps Architect (Codex/GPT-4) - -- Input: `prd.md` + design artifacts. -- Duty: Infrastructure and project skeleton. -- Output: - - `docker-compose.yml` - - `Dockerfile` - - database initialization scripts - - generated folder structure - -### Agent 5 - Builder (BMAD/Wiggum) - -- Input: infra skeleton + PRD + design artifacts. -- Duty: Implement full-stack app code. -- Constraints: - - Implement feature-by-feature. - - Follow `data-schema.json` strictly. -- Output: fully populated source tree. - -### Agent 6 - Auditor (Codex/GPT-4) - -- Input: source tree from Agent 5. -- Duty: security + quality review. -- Required checks: - - SQL injection - - XSS - - exposed secrets/keys - - logic and lint errors -- Output: `security-report.md` with `PASS` or `FAIL`. - -## Pipeline - -### Phase A - Planning (1-3) - -1. Receive user request. -2. Invoke Agent 1 and save `concept_brief.json`. -3. Invoke Agent 2 and save `prd.md`. -4. Invoke Agent 3 and save design artifacts. -5. Update shared context from all planning outputs. - -### Phase B - Construction (4-5) - -6. Invoke Agent 4 to generate infrastructure. -7. Invoke Agent 5 to implement application code in generated structure. -8. Enforce strict schema compliance with Agent 3 outputs. - -### Phase C - Validation + Recursion (6 + loop) - -9. Invoke Agent 6 for audit. - -Decision gate: - -- If report is `PASS`: - - package the app - - generate `DEPLOY_INSTRUCTIONS.md` - - return `Project Complete.` -- If report is `FAIL` or `ERROR`: - - send exact findings and logs to Agent 5 - - command: "Fix these specific issues. Do not hallucinate new features. Return updated code." - - re-run Agent 6 - - max retries: 5 - - after 5 failed retries: escalate to human - -## Operational State (Required) - -Maintain `state.json` in the project root: - -```json -{ - "project": "", - "currentPhase": "planning|construction|validation", - "currentStep": 1, - "retryCount": 0, - "status": "running|blocked|complete|escalated", - "sharedContext": { - "conceptBriefPath": "concept_brief.json", - "prdPath": "prd.md", - "wireframesPath": "wireframes.md", - "schemaPath": "data-schema.json", - "designSystemPath": "design-system.md" - }, - "artifacts": { - "infraReady": false, - "codeReady": false, - "securityReportPath": "security-report.md", - "deployInstructionsPath": "DEPLOY_INSTRUCTIONS.md" - } -} -``` - -Update this file after every agent handoff and after every retry loop iteration. - -## Tools and Capabilities - -You must actively use: - -- File system read/write for persistent artifacts. -- `state.json` as the single source of orchestration truth. -- Terminal build verification before final audit (for example `npm run build`, test commands, or container checks). - -## Guardrails - -- No feature creep during bugfix loops. -- No skipping the audit gate. -- No completion claim without deploy instructions. -- On uncertainty, surface blockers clearly and escalate with concrete evidence. diff --git a/src/agents/openclaw-tools.architect-pipeline.test.ts b/src/agents/openclaw-tools.architect-pipeline.test.ts deleted file mode 100644 index 71775fa01fa..00000000000 --- a/src/agents/openclaw-tools.architect-pipeline.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import "./test-helpers/fast-core-tools.js"; -import { createOpenClawTools } from "./openclaw-tools.js"; - -describe("architect_pipeline tool", () => { - let workspaceDir: string; - - beforeEach(async () => { - workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-architect-pipeline-")); - }); - - afterEach(async () => { - await fs.rm(workspaceDir, { recursive: true, force: true }); - }); - - it("initializes and reads default state", async () => { - const tool = createOpenClawTools({ workspaceDir }).find( - (candidate) => candidate.name === "architect_pipeline", - ); - if (!tool) { - throw new Error("missing architect_pipeline tool"); - } - - const initResult = await tool.execute("call-1", { - action: "init", - project: "dentist-crm", - }); - expect(initResult.details).toMatchObject({ - action: "init", - state: { - project: "dentist-crm", - currentPhase: "planning", - currentStep: 1, - retryCount: 0, - status: "running", - }, - }); - - const getResult = await tool.execute("call-2", { action: "get" }); - expect(getResult.details).toMatchObject({ - action: "get", - state: { - project: "dentist-crm", - }, - }); - }); - - it("applies decision gate retries and escalates at retry limit", async () => { - const tool = createOpenClawTools({ workspaceDir }).find( - (candidate) => candidate.name === "architect_pipeline", - ); - if (!tool) { - throw new Error("missing architect_pipeline tool"); - } - - await tool.execute("call-init", { action: "init", project: "crm" }); - - const failResult = await tool.execute("call-fail", { - action: "decision_gate", - report: "FAIL", - findings: "xss in comment renderer", - maxRetries: 2, - }); - expect(failResult.details).toMatchObject({ - status: "running", - retryCount: 1, - nextAction: "send_findings_to_builder_then_reaudit", - }); - - const escalateResult = await tool.execute("call-escalate", { - action: "decision_gate", - report: "ERROR", - findings: "build not reproducible", - maxRetries: 2, - }); - expect(escalateResult.details).toMatchObject({ - status: "escalated", - retryCount: 2, - nextAction: "escalate_to_human", - }); - }); - - it("rejects state paths outside workspace", async () => { - const tool = createOpenClawTools({ workspaceDir }).find( - (candidate) => candidate.name === "architect_pipeline", - ); - if (!tool) { - throw new Error("missing architect_pipeline tool"); - } - - await expect( - tool.execute("call-bad-path", { - action: "init", - path: path.join("..", "..", "tmp", "state.json"), - }), - ).rejects.toThrow("path must stay within the workspace directory"); - }); -}); diff --git a/src/agents/openclaw-tools.ts b/src/agents/openclaw-tools.ts index 15f8006981e..eed12b72d41 100644 --- a/src/agents/openclaw-tools.ts +++ b/src/agents/openclaw-tools.ts @@ -1,13 +1,12 @@ import type { OpenClawConfig } from "../config/config.js"; -import { resolvePluginTools } from "../plugins/tools.js"; import type { GatewayMessageChannel } from "../utils/message-channel.js"; -import { resolveSessionAgentId } from "./agent-scope.js"; import type { SandboxFsBridge } from "./sandbox/fs-bridge.js"; +import type { AnyAgentTool } from "./tools/common.js"; +import { resolvePluginTools } from "../plugins/tools.js"; +import { resolveSessionAgentId } from "./agent-scope.js"; import { createAgentsListTool } from "./tools/agents-list-tool.js"; -import { createArchitectPipelineTool } from "./tools/architect-pipeline-tool.js"; import { createBrowserTool } from "./tools/browser-tool.js"; import { createCanvasTool } from "./tools/canvas-tool.js"; -import type { AnyAgentTool } from "./tools/common.js"; import { createCronTool } from "./tools/cron-tool.js"; import { createGatewayTool } from "./tools/gateway-tool.js"; import { createImageTool } from "./tools/image-tool.js"; @@ -20,7 +19,6 @@ import { createSessionsSendTool } from "./tools/sessions-send-tool.js"; import { createSessionsSpawnTool } from "./tools/sessions-spawn-tool.js"; import { createSubagentsTool } from "./tools/subagents-tool.js"; import { createTtsTool } from "./tools/tts-tool.js"; -import { createVentureStudioTool } from "./tools/venture-studio-tool.js"; import { createWebFetchTool, createWebSearchTool } from "./tools/web-tools.js"; import { resolveWorkspaceRoot } from "./workspace-dir.js"; @@ -112,8 +110,6 @@ export function createOpenClawTools(options?: { createCronTool({ agentSessionKey: options?.agentSessionKey, }), - createArchitectPipelineTool({ workspaceDir }), - createVentureStudioTool({ workspaceDir }), ...(messageTool ? [messageTool] : []), createTtsTool({ agentChannel: options?.agentChannel, diff --git a/src/agents/openclaw-tools.venture-studio.test.ts b/src/agents/openclaw-tools.venture-studio.test.ts deleted file mode 100644 index ca75d9a4055..00000000000 --- a/src/agents/openclaw-tools.venture-studio.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import "./test-helpers/fast-core-tools.js"; -import { createOpenClawTools } from "./openclaw-tools.js"; - -describe("venture_studio tool", () => { - let workspaceDir: string; - - beforeEach(async () => { - workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-venture-studio-")); - }); - - afterEach(async () => { - await fs.rm(workspaceDir, { recursive: true, force: true }); - }); - - it("records findings, dedupes duplicates, and generates plan/workflow/spec artifacts", async () => { - const tool = createOpenClawTools({ workspaceDir }).find( - (candidate) => candidate.name === "venture_studio", - ); - if (!tool) { - throw new Error("missing venture_studio tool"); - } - - await tool.execute("call-init", { action: "init" }); - await tool.execute("call-add-1", { - action: "add_finding", - sourceType: "forum", - sourceUrl: "https://example.com/thread/123", - title: "Missed patient follow-ups", - painPoint: "Small dental clinics miss recall follow-ups and lose recurring revenue", - targetCustomer: "dental clinics", - urgency: "high", - willingnessToPay: "$299/month", - }); - - const dedupeResult = await tool.execute("call-add-2", { - action: "add_finding", - sourceType: "forum", - sourceUrl: "https://example.com/thread/123", - title: "Missed patient follow-ups", - painPoint: "Small dental clinics miss recall follow-ups and lose recurring revenue", - targetCustomer: "dental clinics", - urgency: "high", - willingnessToPay: "$299/month", - }); - expect(dedupeResult.details).toMatchObject({ deduped: true, totalFindings: 1 }); - - const planResult = await tool.execute("call-plan", { - action: "plan_apps", - appCount: 1, - stack: "nextjs-node-postgres", - }); - expect(planResult.details).toMatchObject({ - action: "plan_apps", - totalPlans: 1, - }); - - const details = planResult.details as { - discussionPath?: string; - generatedPlans?: Array<{ - docPath: string; - workflowPath: string; - specPath: string; - id: string; - }>; - }; - if (!details.discussionPath || !details.generatedPlans?.[0]) { - throw new Error("missing generated artifacts"); - } - - await expect(fs.readFile(details.discussionPath, "utf-8")).resolves.toContain( - "Agent roundtable", - ); - await expect(fs.readFile(details.generatedPlans[0].docPath, "utf-8")).resolves.toContain( - "Recommended stack", - ); - await expect(fs.readFile(details.generatedPlans[0].workflowPath, "utf-8")).resolves.toContain( - "go_to_market", - ); - await expect(fs.readFile(details.generatedPlans[0].specPath, "utf-8")).resolves.toContain( - "coreFeatures", - ); - }); - - it("builds a scaffold for a generated plan", async () => { - const tool = createOpenClawTools({ workspaceDir }).find( - (candidate) => candidate.name === "venture_studio", - ); - if (!tool) { - throw new Error("missing venture_studio tool"); - } - - await tool.execute("call-init", { action: "init" }); - await tool.execute("call-add", { - action: "add_finding", - sourceType: "web", - sourceUrl: "https://example.com/blog", - title: "Manual invoice reconciliation", - painPoint: "Accounting teams lose time reconciling invoices with bank feeds", - targetCustomer: "mid-market finance teams", - urgency: "critical", - }); - - const planResult = (await tool.execute("call-plan", { - action: "plan_apps", - appCount: 1, - stack: "react-fastapi-postgres", - })) as { details?: { generatedPlans?: Array<{ id: string }> } }; - const planId = planResult.details?.generatedPlans?.[0]?.id; - if (!planId) { - throw new Error("missing plan id"); - } - - const scaffoldResult = await tool.execute("call-scaffold", { - action: "build_scaffold", - planId, - }); - expect(scaffoldResult.details).toMatchObject({ action: "build_scaffold", planId }); - - const details = scaffoldResult.details as { scaffoldRoot?: string }; - if (!details.scaffoldRoot) { - throw new Error("missing scaffold root"); - } - - await expect( - fs.readFile(path.join(details.scaffoldRoot, "docker-compose.yml"), "utf-8"), - ).resolves.toContain("services:"); - await expect( - fs.readFile(path.join(details.scaffoldRoot, "backend", "requirements.txt"), "utf-8"), - ).resolves.toContain("fastapi=="); - await expect( - fs.readFile(path.join(details.scaffoldRoot, "frontend", "package.json"), "utf-8"), - ).resolves.toContain("vite"); - await expect( - fs.readFile(path.join(details.scaffoldRoot, "DEPENDENCIES.md"), "utf-8"), - ).resolves.toContain("docker compose up --build"); - await expect( - fs.readFile(path.join(details.scaffoldRoot, "scripts", "dev.ps1"), "utf-8"), - ).resolves.toContain("docker compose up --build"); - await expect( - fs.readFile(path.join(details.scaffoldRoot, "scripts", "dev.cmd"), "utf-8"), - ).resolves.toContain("docker compose up --build"); - }); - - it("rejects operations before init", async () => { - const tool = createOpenClawTools({ workspaceDir }).find( - (candidate) => candidate.name === "venture_studio", - ); - if (!tool) { - throw new Error("missing venture_studio tool"); - } - - await expect(tool.execute("call-list", { action: "list_findings" })).rejects.toThrow( - "Run action=init first", - ); - }); -}); diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 5a8854a172b..04bff2d862f 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -1,9 +1,9 @@ import type { ReasoningLevel, ThinkLevel } from "../auto-reply/thinking.js"; -import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js"; import type { MemoryCitationsMode } from "../config/types.memory.js"; -import { listDeliverableMessageChannels } from "../utils/message-channel.js"; import type { ResolvedTimeFormat } from "./date-time.js"; import type { EmbeddedContextFile } from "./pi-embedded-helpers.js"; +import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js"; +import { listDeliverableMessageChannels } from "../utils/message-channel.js"; import { sanitizeForPromptLiteral } from "./sanitize-for-prompt.js"; /** @@ -265,10 +265,6 @@ export function buildAgentSystemPrompt(params: { subagents: "List, steer, or kill sub-agent runs for this requester session", session_status: "Show a /status-equivalent status card (usage + time + Reasoning/Verbose/Elevated); use for model-use questions (📊 session_status); optional per-session model override", - architect_pipeline: - "Manage Architect CEO orchestration state and audit decision gates for multi-agent build loops", - venture_studio: - "Capture web/forum pain-point research and generate monetized app plans + workflow documents; pair with web_search/web_fetch for source discovery", image: "Analyze an image with the configured image model", }; @@ -296,8 +292,6 @@ export function buildAgentSystemPrompt(params: { "sessions_send", "subagents", "session_status", - "architect_pipeline", - "venture_studio", "image", ]; diff --git a/src/agents/tools/architect-pipeline-tool.ts b/src/agents/tools/architect-pipeline-tool.ts deleted file mode 100644 index 1fa043f941e..00000000000 --- a/src/agents/tools/architect-pipeline-tool.ts +++ /dev/null @@ -1,246 +0,0 @@ -import fs from "node:fs/promises"; -import path from "node:path"; -import { Type } from "@sinclair/typebox"; -import { optionalStringEnum } from "../schema/typebox.js"; -import type { AnyAgentTool } from "./common.js"; -import { ToolInputError, jsonResult, readNumberParam, readStringParam } from "./common.js"; - -const ARCHITECT_PIPELINE_ACTIONS = ["init", "get", "update", "decision_gate"] as const; -const ARCHITECT_PHASES = ["planning", "construction", "validation"] as const; -const ARCHITECT_STATUSES = ["running", "blocked", "complete", "escalated"] as const; -const ARCHITECT_REPORTS = ["PASS", "FAIL", "ERROR"] as const; - -type ArchitectPipelineAction = (typeof ARCHITECT_PIPELINE_ACTIONS)[number]; -type ArchitectPhase = (typeof ARCHITECT_PHASES)[number]; -type ArchitectStatus = (typeof ARCHITECT_STATUSES)[number]; -type ArchitectReport = (typeof ARCHITECT_REPORTS)[number]; - -type ArchitectState = { - project: string; - currentPhase: ArchitectPhase; - currentStep: number; - retryCount: number; - status: ArchitectStatus; - sharedContext: { - conceptBriefPath: string; - prdPath: string; - wireframesPath: string; - schemaPath: string; - designSystemPath: string; - }; - artifacts: { - infraReady: boolean; - codeReady: boolean; - securityReportPath: string; - deployInstructionsPath: string; - }; - latestAudit?: { - report: ArchitectReport; - findings?: string; - at: string; - retryCount: number; - }; -}; - -const ArchitectPipelineToolSchema = Type.Object({ - action: optionalStringEnum(ARCHITECT_PIPELINE_ACTIONS), - path: Type.Optional(Type.String()), - project: Type.Optional(Type.String()), - currentPhase: optionalStringEnum(ARCHITECT_PHASES), - currentStep: Type.Optional(Type.Number({ minimum: 1 })), - status: optionalStringEnum(ARCHITECT_STATUSES), - retryCount: Type.Optional(Type.Number({ minimum: 0 })), - conceptBriefPath: Type.Optional(Type.String()), - prdPath: Type.Optional(Type.String()), - wireframesPath: Type.Optional(Type.String()), - schemaPath: Type.Optional(Type.String()), - designSystemPath: Type.Optional(Type.String()), - infraReady: Type.Optional(Type.Boolean()), - codeReady: Type.Optional(Type.Boolean()), - securityReportPath: Type.Optional(Type.String()), - deployInstructionsPath: Type.Optional(Type.String()), - report: optionalStringEnum(ARCHITECT_REPORTS), - findings: Type.Optional(Type.String()), - maxRetries: Type.Optional(Type.Number({ minimum: 1 })), -}); - -function defaultState(project = "unnamed-project"): ArchitectState { - return { - project, - currentPhase: "planning", - currentStep: 1, - retryCount: 0, - status: "running", - sharedContext: { - conceptBriefPath: "concept_brief.json", - prdPath: "prd.md", - wireframesPath: "wireframes.md", - schemaPath: "data-schema.json", - designSystemPath: "design-system.md", - }, - artifacts: { - infraReady: false, - codeReady: false, - securityReportPath: "security-report.md", - deployInstructionsPath: "DEPLOY_INSTRUCTIONS.md", - }, - }; -} - -function isWithinWorkspace(candidatePath: string, workspaceDir: string) { - const rel = path.relative(workspaceDir, candidatePath); - return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel)); -} - -function resolveStatePath(params: { workspaceDir: string; rawPath?: string }) { - const workspaceDir = path.resolve(params.workspaceDir); - const rawPath = params.rawPath?.trim(); - const statePath = rawPath - ? path.resolve(workspaceDir, rawPath) - : path.join(workspaceDir, "state.json"); - if (!isWithinWorkspace(statePath, workspaceDir)) { - throw new ToolInputError("path must stay within the workspace directory"); - } - return statePath; -} - -async function readState(statePath: string): Promise { - try { - const raw = await fs.readFile(statePath, "utf-8"); - return JSON.parse(raw) as ArchitectState; - } catch (error) { - const anyErr = error as { code?: string }; - if (anyErr.code === "ENOENT") { - return null; - } - throw error; - } -} - -async function writeState(statePath: string, state: ArchitectState) { - await fs.mkdir(path.dirname(statePath), { recursive: true }); - await fs.writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf-8"); -} - -export function createArchitectPipelineTool(options: { workspaceDir: string }): AnyAgentTool { - return { - label: "Architect Pipeline", - name: "architect_pipeline", - description: - "Manage Architect CEO orchestration state.json and enforce audit decision gates for retries/escalation.", - parameters: ArchitectPipelineToolSchema, - execute: async (_callId, input) => { - const params = (input ?? {}) as Record; - const action = (readStringParam(params, "action") ?? "get") as ArchitectPipelineAction; - const statePath = resolveStatePath({ - workspaceDir: options.workspaceDir, - rawPath: readStringParam(params, "path"), - }); - - if (action === "init") { - const project = readStringParam(params, "project") ?? "unnamed-project"; - const state = defaultState(project); - await writeState(statePath, state); - return jsonResult({ action, path: statePath, state }); - } - - const current = await readState(statePath); - if (!current) { - throw new ToolInputError( - `state file not found at ${statePath}. Run action=init first or provide an existing path.`, - ); - } - - if (action === "get") { - return jsonResult({ action, path: statePath, state: current }); - } - - if (action === "update") { - const next: ArchitectState = { - ...current, - project: readStringParam(params, "project") ?? current.project, - currentPhase: - (readStringParam(params, "currentPhase") as ArchitectPhase | undefined) ?? - current.currentPhase, - currentStep: - readNumberParam(params, "currentStep", { integer: true }) ?? current.currentStep, - retryCount: - readNumberParam(params, "retryCount", { integer: true }) ?? current.retryCount, - status: - (readStringParam(params, "status") as ArchitectStatus | undefined) ?? current.status, - sharedContext: { - ...current.sharedContext, - conceptBriefPath: - readStringParam(params, "conceptBriefPath") ?? current.sharedContext.conceptBriefPath, - prdPath: readStringParam(params, "prdPath") ?? current.sharedContext.prdPath, - wireframesPath: - readStringParam(params, "wireframesPath") ?? current.sharedContext.wireframesPath, - schemaPath: readStringParam(params, "schemaPath") ?? current.sharedContext.schemaPath, - designSystemPath: - readStringParam(params, "designSystemPath") ?? current.sharedContext.designSystemPath, - }, - artifacts: { - ...current.artifacts, - infraReady: - typeof params.infraReady === "boolean" - ? params.infraReady - : current.artifacts.infraReady, - codeReady: - typeof params.codeReady === "boolean" - ? params.codeReady - : current.artifacts.codeReady, - securityReportPath: - readStringParam(params, "securityReportPath") ?? current.artifacts.securityReportPath, - deployInstructionsPath: - readStringParam(params, "deployInstructionsPath") ?? - current.artifacts.deployInstructionsPath, - }, - }; - await writeState(statePath, next); - return jsonResult({ action, path: statePath, state: next }); - } - - if (action === "decision_gate") { - const report = ( - readStringParam(params, "report", { required: true }) as ArchitectReport - ).toUpperCase() as ArchitectReport; - const findings = readStringParam(params, "findings"); - const maxRetries = readNumberParam(params, "maxRetries", { integer: true }) ?? 5; - const retryCount = report === "PASS" ? current.retryCount : current.retryCount + 1; - const status: ArchitectStatus = - report === "PASS" ? "complete" : retryCount >= maxRetries ? "escalated" : "running"; - const next: ArchitectState = { - ...current, - currentPhase: report === "PASS" ? "validation" : current.currentPhase, - retryCount, - status, - latestAudit: { - report, - findings, - at: new Date().toISOString(), - retryCount, - }, - }; - await writeState(statePath, next); - const nextAction = - report === "PASS" - ? "package_app_and_generate_deploy_instructions" - : status === "escalated" - ? "escalate_to_human" - : "send_findings_to_builder_then_reaudit"; - return jsonResult({ - action, - path: statePath, - report, - status, - retryCount, - maxRetries, - nextAction, - state: next, - }); - } - - throw new ToolInputError("Unknown action."); - }, - }; -} diff --git a/src/agents/tools/venture-studio-tool.ts b/src/agents/tools/venture-studio-tool.ts deleted file mode 100644 index d0a93187e16..00000000000 --- a/src/agents/tools/venture-studio-tool.ts +++ /dev/null @@ -1,678 +0,0 @@ -import fs from "node:fs/promises"; -import path from "node:path"; -import { Type } from "@sinclair/typebox"; -import { optionalStringEnum } from "../schema/typebox.js"; -import type { AnyAgentTool } from "./common.js"; -import { - ToolInputError, - jsonResult, - readNumberParam, - readStringArrayParam, - readStringParam, -} from "./common.js"; - -const VENTURE_ACTIONS = [ - "init", - "add_finding", - "list_findings", - "plan_apps", - "list_plans", - "build_scaffold", -] as const; -const SOURCE_TYPES = ["web", "forum", "other"] as const; -const URGENCY_LEVELS = ["low", "medium", "high", "critical"] as const; -const STACK_OPTIONS = [ - "nextjs-node-postgres", - "react-fastapi-postgres", - "sveltekit-supabase", -] as const; - -type VentureAction = (typeof VENTURE_ACTIONS)[number]; -type SourceType = (typeof SOURCE_TYPES)[number]; -type UrgencyLevel = (typeof URGENCY_LEVELS)[number]; -type StackOption = (typeof STACK_OPTIONS)[number]; - -type ResearchFinding = { - id: string; - sourceType: SourceType; - sourceUrl?: string; - title: string; - painPoint: string; - targetCustomer: string; - urgency: UrgencyLevel; - willingnessToPay?: string; - observedAt: string; -}; - -type AppPlan = { - id: string; - name: string; - problem: string; - users: string; - monetization: string; - billionDollarThesis: string; - stack: StackOption; - workflowPath: string; - docPath: string; - specPath: string; - basedOnFindingIds: string[]; - createdAt: string; -}; - -type VentureStudioState = { - version: 1; - initializedAt: string; - findings: ResearchFinding[]; - plans: AppPlan[]; -}; - -const VentureStudioToolSchema = Type.Object({ - action: optionalStringEnum(VENTURE_ACTIONS), - path: Type.Optional(Type.String()), - outputDir: Type.Optional(Type.String()), - sourceType: optionalStringEnum(SOURCE_TYPES), - sourceUrl: Type.Optional(Type.String()), - title: Type.Optional(Type.String()), - painPoint: Type.Optional(Type.String()), - targetCustomer: Type.Optional(Type.String()), - urgency: optionalStringEnum(URGENCY_LEVELS), - willingnessToPay: Type.Optional(Type.String()), - appName: Type.Optional(Type.String()), - appCount: Type.Optional(Type.Number({ minimum: 1, maximum: 10 })), - monetization: Type.Optional(Type.String()), - thesis: Type.Optional(Type.String()), - findingIds: Type.Optional(Type.Array(Type.String())), - planId: Type.Optional(Type.String()), - stack: optionalStringEnum(STACK_OPTIONS), - appRootDir: Type.Optional(Type.String()), -}); - -function toSlug(value: string): string { - return value - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+/, "") - .replace(/-+$/, "") - .slice(0, 60); -} - -function isWithinWorkspace(candidatePath: string, workspaceDir: string) { - const rel = path.relative(workspaceDir, candidatePath); - return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel)); -} - -function resolveWorkspacePath(params: { - workspaceDir: string; - rawPath?: string; - fallback: string; -}) { - const workspaceDir = path.resolve(params.workspaceDir); - const targetPath = params.rawPath?.trim() - ? path.resolve(workspaceDir, params.rawPath) - : path.join(workspaceDir, params.fallback); - if (!isWithinWorkspace(targetPath, workspaceDir)) { - throw new ToolInputError("path must stay within the workspace directory"); - } - return targetPath; -} - -function urgencyWeight(urgency: UrgencyLevel): number { - if (urgency === "critical") { - return 4; - } - if (urgency === "high") { - return 3; - } - if (urgency === "medium") { - return 2; - } - return 1; -} - -function sortFindingsByOpportunity(findings: ResearchFinding[]): ResearchFinding[] { - return [...findings].toSorted((a, b) => urgencyWeight(b.urgency) - urgencyWeight(a.urgency)); -} - -function defaultState(): VentureStudioState { - return { - version: 1, - initializedAt: new Date().toISOString(), - findings: [], - plans: [], - }; -} - -async function readState(statePath: string): Promise { - try { - const raw = await fs.readFile(statePath, "utf-8"); - return JSON.parse(raw) as VentureStudioState; - } catch (error) { - const anyErr = error as { code?: string }; - if (anyErr.code === "ENOENT") { - return null; - } - throw error; - } -} - -async function writeState(statePath: string, state: VentureStudioState): Promise { - await fs.mkdir(path.dirname(statePath), { recursive: true }); - await fs.writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf-8"); -} - -function nextSequenceId(prefix: string, existingIds: string[]): string { - const used = new Set(existingIds); - for (let i = 1; i <= 100_000; i += 1) { - const candidate = `${prefix}-${i}`; - if (!used.has(candidate)) { - return candidate; - } - } - return `${prefix}-${Date.now().toString(36)}`; -} - -async function writePlanArtifacts(params: { - outputDir: string; - planId: string; - appName: string; - problem: string; - users: string; - monetization: string; - thesis: string; - stack: StackOption; - findings: ResearchFinding[]; -}): Promise<{ docPath: string; workflowPath: string; specPath: string }> { - const planDir = path.join(params.outputDir, params.planId); - await fs.mkdir(planDir, { recursive: true }); - - const docPath = path.join(planDir, "PLAN.md"); - const workflowPath = path.join(planDir, "workflow.json"); - const specPath = path.join(planDir, "APP_SPEC.json"); - - const findingsSection = params.findings - .map( - (finding) => - `- ${finding.title} (${finding.sourceType}${finding.sourceUrl ? `: ${finding.sourceUrl}` : ""}) — ${finding.painPoint}`, - ) - .join("\n"); - - const planDoc = `# ${params.appName}\n\n## Problem\n${params.problem}\n\n## Target users\n${params.users}\n\n## Monetization\n${params.monetization}\n\n## Billion-dollar thesis\n${params.thesis}\n\n## Recommended stack\n${params.stack}\n\n## Evidence from research\n${findingsSection || "- No findings attached."}\n\n## Build workflow\n1. Validate demand with 10 customer interviews from identified segment.\n2. Build MVP full-stack app with auth, billing, and analytics.\n3. Launch paid pilot with design partners.\n4. Iterate weekly from support/usage data.\n5. Expand distribution via integrations and channel partners.\n`; - - const workflow = { - stages: [ - { id: "research", goal: "Verify problem urgency and willingness to pay" }, - { id: "product", goal: "Define MVP scope and technical architecture" }, - { id: "build", goal: "Ship production-ready full-stack MVP" }, - { id: "go_to_market", goal: "Acquire first paying customers" }, - { id: "scale", goal: "Expand to enterprise and adjacent markets" }, - ], - generatedAt: new Date().toISOString(), - }; - - const spec = { - appName: params.appName, - problem: params.problem, - users: params.users, - monetization: params.monetization, - stack: params.stack, - coreFeatures: [ - "authentication", - "team workspace", - "automation workflows", - "billing subscriptions", - "usage analytics dashboard", - ], - nonFunctional: ["security", "auditability", "multi-tenant readiness", "cost controls"], - generatedAt: new Date().toISOString(), - }; - - await fs.writeFile(docPath, planDoc, "utf-8"); - await fs.writeFile(workflowPath, `${JSON.stringify(workflow, null, 2)}\n`, "utf-8"); - await fs.writeFile(specPath, `${JSON.stringify(spec, null, 2)}\n`, "utf-8"); - - return { docPath, workflowPath, specPath }; -} - -async function writeDiscussionDoc(outputDir: string, newPlans: AppPlan[]): Promise { - const discussionPath = path.join(outputDir, "DISCUSSION.md"); - const lines = [ - "# Venture Studio Discussion", - "", - "## Goal", - "Identify painful, recurring, high-value problems and turn them into monetized full-stack app opportunities.", - "", - "## Candidate plans", - ...newPlans.map( - (plan) => - `- **${plan.name}**: solves "${plan.problem}" for ${plan.users}. Revenue: ${plan.monetization}`, - ), - "", - "## Agent roundtable", - "- Strategist: Is this pain frequent enough to justify an always-on product?", - "- Product Lead: What is the narrow MVP wedge that can be shipped in under 8 weeks?", - "- Designer: Which workflow screens remove the biggest friction first?", - "- DevOps Architect: Which stack minimizes time-to-production and ops risk?", - "- Builder: What implementation milestones de-risk integration and billing early?", - "- Auditor: What security/compliance controls are mandatory before paid rollout?", - "", - "## Decision checklist", - "- Is the pain urgent and frequent?", - "- Can customers justify paying quickly?", - "- Can the first version be shipped in <8 weeks?", - "- Does the wedge support expansion into a large market?", - "", - ]; - await fs.mkdir(outputDir, { recursive: true }); - await fs.writeFile(discussionPath, lines.join("\n"), "utf-8"); - return discussionPath; -} - -async function buildScaffold(params: { - workspaceDir: string; - appRootDirRaw?: string; - plan: AppPlan; -}): Promise<{ scaffoldRoot: string; createdFiles: string[] }> { - const appRootDir = resolveWorkspacePath({ - workspaceDir: params.workspaceDir, - rawPath: params.appRootDirRaw, - fallback: "venture-studio/apps", - }); - const scaffoldRoot = path.join(appRootDir, params.plan.id); - const backendDir = path.join(scaffoldRoot, "backend"); - const frontendDir = path.join(scaffoldRoot, "frontend"); - const dbDir = path.join(scaffoldRoot, "db"); - - await fs.mkdir(backendDir, { recursive: true }); - await fs.mkdir(frontendDir, { recursive: true }); - await fs.mkdir(dbDir, { recursive: true }); - - const isNodeBackend = params.plan.stack !== "react-fastapi-postgres"; - const backendDockerfile = isNodeBackend - ? 'FROM node:22-alpine\nWORKDIR /app\nCOPY package.json package.json\nRUN npm install\nCOPY . .\nEXPOSE 8080\nCMD ["npm","run","dev"]\n' - : 'FROM python:3.12-slim\nWORKDIR /app\nCOPY requirements.txt requirements.txt\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY . .\nEXPOSE 8080\nCMD ["uvicorn","main:app","--host","0.0.0.0","--port","8080"]\n'; - - const backendPackageJson = JSON.stringify( - { - name: "backend", - private: true, - type: "module", - scripts: { - dev: "node server.js", - }, - dependencies: { - cors: "^2.8.5", - express: "^4.21.2", - pg: "^8.13.1", - }, - }, - null, - 2, - ); - - const frontendPackageJsonByStack: Record = { - "nextjs-node-postgres": JSON.stringify( - { - name: "frontend", - private: true, - scripts: { - dev: "next dev -p 3000", - build: "next build", - start: "next start -p 3000", - }, - dependencies: { - next: "^15.0.4", - react: "^18.3.1", - "react-dom": "^18.3.1", - }, - }, - null, - 2, - ), - "react-fastapi-postgres": JSON.stringify( - { - name: "frontend", - private: true, - scripts: { - dev: "vite --host 0.0.0.0 --port 3000", - build: "vite build", - preview: "vite preview --host 0.0.0.0 --port 3000", - }, - dependencies: { - react: "^18.3.1", - "react-dom": "^18.3.1", - }, - devDependencies: { - vite: "^5.4.11", - "@vitejs/plugin-react": "^4.3.3", - }, - }, - null, - 2, - ), - "sveltekit-supabase": JSON.stringify( - { - name: "frontend", - private: true, - scripts: { - dev: "vite dev --host 0.0.0.0 --port 3000", - build: "vite build", - preview: "vite preview --host 0.0.0.0 --port 3000", - }, - dependencies: { - "@sveltejs/kit": "^2.8.3", - "@supabase/supabase-js": "^2.47.4", - svelte: "^5.2.7", - }, - devDependencies: { - vite: "^5.4.11", - }, - }, - null, - 2, - ), - }; - - const frontendEntryByStack: Record = { - "nextjs-node-postgres": { - file: "pages/index.js", - content: - "export default function Home() {\n return

Venture Scaffold

Replace this page with your product UI.

;\n}\n", - }, - "react-fastapi-postgres": { - file: "src/main.jsx", - content: - "import React from 'react';\nimport { createRoot } from 'react-dom/client';\nfunction App() {\n return

Venture Scaffold

Replace with your product UI.

;\n}\ncreateRoot(document.getElementById('root')).render();\n", - }, - "sveltekit-supabase": { - file: "src/routes/+page.svelte", - content: - "

Venture Scaffold

Replace this page with your product UI.

\n", - }, - }; - - const frontendAuxFilesByStack: Record> = { - "nextjs-node-postgres": [], - "react-fastapi-postgres": [ - { - path: path.join(frontendDir, "index.html"), - content: - "\n
\n", - }, - { - path: path.join(frontendDir, "vite.config.js"), - content: - "import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nexport default defineConfig({ plugins: [react()] });\n", - }, - ], - "sveltekit-supabase": [ - { - path: path.join(frontendDir, "vite.config.js"), - content: "import { defineConfig } from 'vite';\nexport default defineConfig({});\n", - }, - { - path: path.join(frontendDir, "svelte.config.js"), - content: "export default { };\n", - }, - ], - }; - - const files: Array<{ path: string; content: string }> = [ - { - path: path.join(scaffoldRoot, "README.md"), - content: `# ${params.plan.name}\n\nProblem: ${params.plan.problem}\nUsers: ${params.plan.users}\nMonetization: ${params.plan.monetization}\nStack: ${params.plan.stack}\n\nThis scaffold was generated from venture_studio plan ${params.plan.id}.\n`, - }, - { - path: path.join(scaffoldRoot, "docker-compose.yml"), - content: - 'version: "3.9"\nservices:\n db:\n image: postgres:16\n environment:\n POSTGRES_USER: app\n POSTGRES_PASSWORD: app\n POSTGRES_DB: app\n ports:\n - "5432:5432"\n volumes:\n - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql\n backend:\n build: ./backend\n ports:\n - "8080:8080"\n depends_on:\n - db\n frontend:\n build: ./frontend\n ports:\n - "3000:3000"\n depends_on:\n - backend\n', - }, - { - path: path.join(dbDir, "init.sql"), - content: - "CREATE TABLE IF NOT EXISTS accounts (\n id SERIAL PRIMARY KEY,\n name TEXT NOT NULL,\n created_at TIMESTAMPTZ DEFAULT NOW()\n);\n", - }, - { - path: path.join(backendDir, "Dockerfile"), - content: backendDockerfile, - }, - { - path: path.join(backendDir, "package.json"), - content: `${backendPackageJson}\n`, - }, - { - path: path.join(backendDir, "server.js"), - content: - "import express from 'express';\nimport cors from 'cors';\nimport pkg from 'pg';\nconst { Pool } = pkg;\nconst app = express();\nconst pool = new Pool({ connectionString: process.env.DATABASE_URL ?? 'postgres://app:app@db:5432/app' });\napp.use(cors());\napp.get('/health', async (_req, res) => {\n const client = await pool.connect();\n try { await client.query('select 1'); res.json({ ok: true }); }\n finally { client.release(); }\n});\napp.listen(8080, () => console.log('backend on :8080'));\n", - }, - { - path: path.join(backendDir, "requirements.txt"), - content: "fastapi==0.115.5\nuvicorn==0.32.1\npsycopg[binary]==3.2.3\n", - }, - { - path: path.join(backendDir, "main.py"), - content: - "from fastapi import FastAPI\napp = FastAPI()\n@app.get('/health')\ndef health():\n return {'ok': True}\n", - }, - { - path: path.join(frontendDir, "Dockerfile"), - content: - 'FROM node:22-alpine\nWORKDIR /app\nCOPY package.json package.json\nRUN npm install\nCOPY . .\nEXPOSE 3000\nCMD ["npm","run","dev"]\n', - }, - { - path: path.join(frontendDir, "package.json"), - content: `${frontendPackageJsonByStack[params.plan.stack]}\n`, - }, - { - path: path.join(frontendDir, frontendEntryByStack[params.plan.stack].file), - content: frontendEntryByStack[params.plan.stack].content, - }, - ...frontendAuxFilesByStack[params.plan.stack], - { - path: path.join(scaffoldRoot, "DEPENDENCIES.md"), - content: - "# Dependency setup\n\n- Backend dependencies are declared in `backend/package.json` (Node) and `backend/requirements.txt` (Python fallback for FastAPI stack).\n- Frontend dependencies are declared in `frontend/package.json` according to the selected stack.\n- Start services with: `docker compose up --build`\n- Windows PowerShell helper: `./scripts/dev.ps1`\n- Windows CMD helper: `scripts\\dev.cmd`\n", - }, - { - path: path.join(scaffoldRoot, "scripts", "dev.ps1"), - content: - "$ErrorActionPreference = 'Stop'\nSet-Location -Path $PSScriptRoot\nSet-Location -Path ..\ndocker compose up --build\n", - }, - { - path: path.join(scaffoldRoot, "scripts", "dev.cmd"), - content: "@echo off\ncd /d %~dp0\ncd ..\ndocker compose up --build\n", - }, - ]; - - const filteredFiles = - params.plan.stack === "react-fastapi-postgres" - ? files.filter( - (file) => - !file.path.endsWith(path.join("backend", "package.json")) && - !file.path.endsWith(path.join("backend", "server.js")), - ) - : files.filter( - (file) => - !file.path.endsWith(path.join("backend", "requirements.txt")) && - !file.path.endsWith(path.join("backend", "main.py")), - ); - - for (const file of filteredFiles) { - await fs.mkdir(path.dirname(file.path), { recursive: true }); - await fs.writeFile(file.path, file.content, "utf-8"); - } - - return { scaffoldRoot, createdFiles: filteredFiles.map((file) => file.path) }; -} - -export function createVentureStudioTool(options: { workspaceDir: string }): AnyAgentTool { - return { - label: "Venture Studio", - name: "venture_studio", - description: - "Track web/forum pain-point research and generate monetized app plans, workflows, and build scaffolds.", - parameters: VentureStudioToolSchema, - execute: async (_callId, input) => { - const params = (input ?? {}) as Record; - const action = (readStringParam(params, "action") ?? "list_findings") as VentureAction; - const statePath = resolveWorkspacePath({ - workspaceDir: options.workspaceDir, - rawPath: readStringParam(params, "path"), - fallback: "venture-studio/state.json", - }); - - if (action === "init") { - const state = defaultState(); - await writeState(statePath, state); - return jsonResult({ action, statePath, state }); - } - - const current = await readState(statePath); - if (!current) { - throw new ToolInputError( - `venture studio state not found at ${statePath}. Run action=init first.`, - ); - } - - if (action === "add_finding") { - const title = readStringParam(params, "title", { required: true }); - const painPoint = readStringParam(params, "painPoint", { required: true }); - const targetCustomer = readStringParam(params, "targetCustomer", { required: true }); - const sourceType = (readStringParam(params, "sourceType") ?? "other") as SourceType; - const duplicate = current.findings.find( - (finding) => finding.title === title && finding.targetCustomer === targetCustomer, - ); - if (duplicate) { - return jsonResult({ - action, - statePath, - deduped: true, - finding: duplicate, - totalFindings: current.findings.length, - }); - } - - const finding: ResearchFinding = { - id: nextSequenceId( - "finding", - current.findings.map((entry) => entry.id), - ), - sourceType, - sourceUrl: readStringParam(params, "sourceUrl"), - title, - painPoint, - targetCustomer, - urgency: (readStringParam(params, "urgency") ?? "medium") as UrgencyLevel, - willingnessToPay: readStringParam(params, "willingnessToPay"), - observedAt: new Date().toISOString(), - }; - const next: VentureStudioState = { - ...current, - findings: [...current.findings, finding], - }; - await writeState(statePath, next); - return jsonResult({ action, statePath, finding, totalFindings: next.findings.length }); - } - - if (action === "list_findings") { - return jsonResult({ action, statePath, findings: current.findings }); - } - - if (action === "plan_apps") { - const appCount = readNumberParam(params, "appCount", { integer: true }) ?? 3; - const requestedFindingIds = readStringArrayParam(params, "findingIds") ?? []; - const selectedFindings = - requestedFindingIds.length > 0 - ? current.findings.filter((finding) => requestedFindingIds.includes(finding.id)) - : sortFindingsByOpportunity(current.findings).slice(0, appCount); - if (selectedFindings.length === 0) { - throw new ToolInputError("No findings available for planning. Add findings first."); - } - - const outputDir = resolveWorkspacePath({ - workspaceDir: options.workspaceDir, - rawPath: readStringParam(params, "outputDir"), - fallback: "venture-studio/plans", - }); - const stack = (readStringParam(params, "stack") ?? "nextjs-node-postgres") as StackOption; - - const newPlans: AppPlan[] = []; - for (const finding of selectedFindings.slice(0, appCount)) { - const appName = - readStringParam(params, "appName") ?? - `${finding.targetCustomer} ${finding.title}`.replace(/\s+/g, " ").trim(); - const existingIds = [...current.plans, ...newPlans].map((plan) => plan.id); - const planIdBase = toSlug(appName) || "app-plan"; - const planId = nextSequenceId(planIdBase, existingIds); - const monetization = - readStringParam(params, "monetization") ?? - finding.willingnessToPay ?? - "Subscription tiers with usage-based enterprise add-ons"; - const thesis = - readStringParam(params, "thesis") ?? - `Own a mission-critical workflow for ${finding.targetCustomer} where urgency is ${finding.urgency}, then compound growth through integrations, data network effects, and enterprise expansion.`; - - const artifacts = await writePlanArtifacts({ - outputDir, - planId, - appName, - problem: finding.painPoint, - users: finding.targetCustomer, - monetization, - thesis, - stack, - findings: [finding], - }); - - newPlans.push({ - id: planId, - name: appName, - problem: finding.painPoint, - users: finding.targetCustomer, - monetization, - billionDollarThesis: thesis, - stack, - workflowPath: artifacts.workflowPath, - docPath: artifacts.docPath, - specPath: artifacts.specPath, - basedOnFindingIds: [finding.id], - createdAt: new Date().toISOString(), - }); - } - - const discussionPath = await writeDiscussionDoc(outputDir, newPlans); - const next: VentureStudioState = { - ...current, - plans: [...current.plans, ...newPlans], - }; - await writeState(statePath, next); - return jsonResult({ - action, - statePath, - discussionPath, - generatedPlans: newPlans, - totalPlans: next.plans.length, - }); - } - - if (action === "list_plans") { - return jsonResult({ action, statePath, plans: current.plans }); - } - - if (action === "build_scaffold") { - const planId = readStringParam(params, "planId", { required: true }); - const plan = current.plans.find((entry) => entry.id === planId); - if (!plan) { - throw new ToolInputError(`Unknown planId: ${planId}`); - } - const scaffold = await buildScaffold({ - workspaceDir: options.workspaceDir, - appRootDirRaw: readStringParam(params, "appRootDir"), - plan, - }); - return jsonResult({ action, planId, ...scaffold }); - } - - throw new ToolInputError("Unknown action."); - }, - }; -}