Revert "Agents: improve Windows scaffold helpers for venture studio"

This reverts commit b6d934c2c7.
This commit is contained in:
Peter Steinberger
2026-02-17 02:26:36 +01:00
parent 37064e5cc6
commit 25126d75c3
9 changed files with 5 additions and 1369 deletions

View File

@@ -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"
]

View File

@@ -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

View File

@@ -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": "<name>",
"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.

View File

@@ -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");
});
});

View File

@@ -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,

View File

@@ -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",
);
});
});

View File

@@ -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",
];

View File

@@ -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<ArchitectState | null> {
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<string, unknown>;
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.");
},
};
}

View File

@@ -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<VentureStudioState | null> {
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<void> {
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<string> {
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<StackOption, string> = {
"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<StackOption, { file: string; content: string }> = {
"nextjs-node-postgres": {
file: "pages/index.js",
content:
"export default function Home() {\n return <main><h1>Venture Scaffold</h1><p>Replace this page with your product UI.</p></main>;\n}\n",
},
"react-fastapi-postgres": {
file: "src/main.jsx",
content:
"import React from 'react';\nimport { createRoot } from 'react-dom/client';\nfunction App() {\n return <main><h1>Venture Scaffold</h1><p>Replace with your product UI.</p></main>;\n}\ncreateRoot(document.getElementById('root')).render(<App />);\n",
},
"sveltekit-supabase": {
file: "src/routes/+page.svelte",
content:
"<main><h1>Venture Scaffold</h1><p>Replace this page with your product UI.</p></main>\n",
},
};
const frontendAuxFilesByStack: Record<StackOption, Array<{ path: string; content: string }>> = {
"nextjs-node-postgres": [],
"react-fastapi-postgres": [
{
path: path.join(frontendDir, "index.html"),
content:
"<!doctype html>\n<html><body><div id='root'></div><script type='module' src='/src/main.jsx'></script></body></html>\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<string, unknown>;
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.");
},
};
}