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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
720 changes: 106 additions & 614 deletions AGENTS.md

Large diffs are not rendered by default.

21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# vibe-code-demo
# vibe-coding-app

vibe-code-demo turns a plain-English app idea into a live, verified
vibe-coding-app turns a plain-English app idea into a live, verified
application on Render. You type "Build a website for a local barbershop", and a
few minutes later you have a public URL. When the idea needs saved data, you
also get an API and a Postgres database, all wired together and smoke-tested.
Expand All @@ -9,7 +9,7 @@ It is a reference app: real, running TypeScript that shows how Render
Workflows, Sandboxes, Blueprints, Postgres, and the read-only Render MCP server
fit together in an agent product.

[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/render-examples/vibe-code-demo)
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/render-examples/vibe-coding-app)

Before you deploy, prepare:

Expand All @@ -21,8 +21,8 @@ Before you deploy, prepare:
- Anthropic and Render API keys, plus the Render workspace ID; and
- a lowercase UI username and a UI password of at least 16 characters.

After the factory deploys, complete the
[one-time apps Blueprint step](docs/0_QUICKSTART.md#connect-the-apps-blueprint).
After the factory deploys, run `npm run setup:apps-repo` and complete the
[one-time apps Blueprint step](docs/0_QUICKSTART.md#3-connect-the-apps-blueprint).

## Why it matters

Expand Down Expand Up @@ -80,8 +80,8 @@ product: see [Current limitations](docs/0_QUICKSTART.md#current-limitations).
its own managed database.
- **Blueprints** are the only way the factory creates infrastructure: the
workflow commits a `render.yaml`, and Render's Blueprint sync deploys it.
- **Render MCP** gives two agents read-only views of the workspace, enforced
by an allowlist in code.
- **Render MCP** gives the architect a read-only view of the workspace,
enforced by an allowlist in code.

## Prerequisites

Expand All @@ -99,11 +99,12 @@ product: see [Current limitations](docs/0_QUICKSTART.md#current-limitations).
## Quick start

The [Quickstart](docs/0_QUICKSTART.md) is the canonical setup guide. It covers
installation, local processes, the first app, the one-time apps Blueprint,
installation, local processes, the one-time apps Blueprint, the first app,
deployment, diagnostics, operations, and cleanup.

Local development still creates real Render Sandboxes and deploys real,
billable services.
Local development still creates real Render Sandboxes and deploys real
services. Generated static sites are free; a generated API and database use
paid plans and bill until you delete the app.

## Deploy the factory to Render

Expand Down
51 changes: 5 additions & 46 deletions app/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@
import { type TaskContext, task } from "@renderinc/sdk/workflows";
import { factoryConfig } from "../factory.config.js";
import { type Agent, md, runClaude, zodToJsonSchema } from "./claude.js";
import {
buildOutputSchema,
deployDiagnosisSchema,
deployPlanSchema,
} from "./contracts.js";
import { buildOutputSchema, deployPlanSchema } from "./contracts.js";
import { RENDER_READ_ONLY_TOOLS } from "./policy.js";
import { connectSandbox } from "./sandbox.js";
import { sandboxTools } from "./tools.js";
Expand Down Expand Up @@ -138,7 +134,8 @@ export const builder: Agent = {
.git directory in the app directory; copy a file instead of linking
to it.
- When you receive build output or Render deploy logs describing a
failure, fix exactly what the output names and nothing else.
failure, find the first real error, fix exactly what it names, and
nothing else. Deploy logs show each secret as [REDACTED].

When the app has a database:

Expand Down Expand Up @@ -197,42 +194,6 @@ export const builder: Agent = {
`,
};

export const deployManager: Agent = {
id: "deploy-manager",
model: "medium",
effort: "low",
renderTools: RENDER_READ_ONLY_TOOLS,
maxTurns: 20,
timeoutSeconds: 15 * 60,
plan: "flex",
prompt: md`
You are the deploy manager. You diagnose Render deploys that failed.

Your instructions give each service that failed: its name and ID, the
ID and the status of the deploy that failed, and the last lines of the
logs of that deploy. They can include the build, the pre-deploy
command, and the new instance. Each secret in them is [REDACTED]. Your
Render tools cannot read logs. Use them only to read the settings of a
service and the details of a deploy.

For each service that failed, find the error in its logs. Then tell the
builder what to fix. Be specific: quote the exact error from the logs.
If the logs do not show the cause, say so.

Respond ONLY with JSON, with one entry for each service that failed:
{
"failures": [
{
"serviceName": "the service that failed",
"status": "build_failed",
"diagnosis": "exact error and what to fix",
"logs": "relevant log excerpt"
}
]
}
`,
};

/* ── Registration ─────────────────────────────────────────────────────── */

export interface AgentTaskInput {
Expand All @@ -249,7 +210,6 @@ export interface AgentTaskInput {
const OUTPUT_SCHEMAS: Record<string, Record<string, unknown>> = {
architect: zodToJsonSchema(deployPlanSchema),
builder: zodToJsonSchema(buildOutputSchema),
"deploy-manager": zodToJsonSchema(deployDiagnosisSchema),
};

/**
Expand Down Expand Up @@ -295,13 +255,12 @@ export function agentTask(agent: Agent) {
}),
);

// The output that the SDK checked against the schema of the agent, or
// the text of an agent that has no schema.
// The output the SDK checked against the agent's schema, or the text
// of an agent that has none.
return run.structuredOutput ?? run.result;
},
);
}

export const architectTask = agentTask(architect);
export const buildTask = agentTask(builder);
export const deployManagerTask = agentTask(deployManager);
4 changes: 2 additions & 2 deletions app/blueprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ function slugify(name: string): string {
*/
export function appBlueprint(spec: AppSpec): string {
return [
`# ${spec.user}/${spec.appName} — generated by the Vibe Code factory.`,
`# ${spec.user}/${spec.appName} — generated by vibe-coding-app.`,
`# Prompt: ${oneLine(spec.prompt)}`,
"",
"projects:",
Expand All @@ -168,7 +168,7 @@ export function rootBlueprint(specs: readonly AppSpec[]): string {
);

const header = [
"# Generated by the Vibe Code factory. Do not edit by hand.",
"# Generated by vibe-coding-app. Do not edit by hand.",
"#",
"# One Blueprint holds every app the factory has built, each in its own",
"# project. A run appends its app here, and Render's Blueprint sync",
Expand Down
3 changes: 2 additions & 1 deletion app/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export async function buildAndVerify(opts: {
sandbox: Sandbox;
appDir: string;
user: string;
appName: string;
plan: DeployPlan;
prompt: string;
runId: string;
Expand All @@ -51,7 +52,7 @@ export async function buildAndVerify(opts: {
const { failures } = await opts.tasks.run(verifyAppTask, {
sandboxId: opts.sandbox.id,
user: opts.user,
appName: opts.plan.appName,
appName: opts.appName,
manifest: buildOutput.manifest,
databaseUrl: opts.databaseUrl,
});
Expand Down
2 changes: 1 addition & 1 deletion app/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export function md(
* Call an agent that must return structured output, and check the output
* with its Zod schema. The SDK checks it against the JSON Schema of the
* agent, but a JSON Schema cannot hold each Zod rule, such as a refinement.
* So a failure goes back to the agent one time, with the Zod error, and a
* So a failure goes back to the agent once, with the Zod error, and a
* second failure stops the stage.
*/
export async function agentJson<T>(
Expand Down
2 changes: 1 addition & 1 deletion app/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function apiKey(): string {
export function uiCredentials(): { username: string; password: string } {
const username = requireEnv("UI_USERNAME");
const password = requireEnv("UI_PASSWORD");
// The gateway gives it to every run of the UI as the namespace `user`.
// The gateway uses it as the `user` namespace for every UI run.
if (!slug.safeParse(username).success) {
throw new Error("UI_USERNAME must be a lowercase slug");
}
Expand Down
55 changes: 18 additions & 37 deletions app/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ export const deleteAppInputSchema = z.object({

export type DeleteAppInput = z.infer<typeof deleteAppInputSchema>;

/** The input of the step of a delete that waits for the syncs of the apps Blueprint. */
/** Input to the delete step that waits for apps Blueprint syncs to finish. */
export const waitForSyncsInputSchema = deleteAppInputSchema.extend({
/**
* When the delete pushed the commit that took the app out of the
* When this delete pushed the commit that removed the app from the
* Blueprint, or null if an earlier attempt pushed it.
*/
pushedAt: z.number().int().nonnegative().nullable(),
Expand Down Expand Up @@ -74,9 +74,9 @@ export const deployPlanSchema = z.object({
.min(1)
.max(4),
/**
* The subjects that the workflow finds photographs of, one each. Capped
* low on purpose — each subject is a Commons round trip, and a landing
* page needs a few good photographs, not a gallery.
* Subjects the workflow finds one photograph for each. Capped low on
* purpose: each subject is a Commons round trip, and a landing page needs
* a few good photographs, not a gallery.
*/
assetQueries: z.array(z.string().min(3).max(120)).max(4).optional().default([]),
brief: z.object({
Expand Down Expand Up @@ -199,25 +199,6 @@ export const buildOutputSchema = z.object({

export type BuildOutput = z.infer<typeof buildOutputSchema>;

/* ── Deploy manager ───────────────────────────────────────────────────── */

export const deployFailureSchema = z.object({
serviceName: z.string().min(1).max(120),
status: z.string().min(1).max(60),
diagnosis: z.string().min(1).max(4000),
logs: z.string().max(8000).optional(),
});

/**
* The workflow runs the deploy manager only when Render reported a failed
* deploy, so the diagnosis cannot declare the services healthy.
*/
export const deployDiagnosisSchema = z.object({
failures: z.array(deployFailureSchema).max(10),
});

export type DeployDiagnosis = z.infer<typeof deployDiagnosisSchema>;

/* ── The stored app spec ──────────────────────────────────────────────── */

/**
Expand All @@ -235,18 +216,18 @@ export const appSpecSchema = z.object({
resourcePrefix: slug,
manifest: manifestSchema,
/**
* Set when a delete of the app starts. The root Blueprint leaves the app
* out, and this file stays until the app's Render resources are gone,
* because a retry of the delete reads it to find them.
* Set when the app's delete starts. The root Blueprint leaves the app out,
* and this file stays until the app's Render resources are gone, because a
* retried delete reads it to find them.
*/
deletedAt: z.string().min(1).optional(),
});

export type AppSpec = z.infer<typeof appSpecSchema>;

/**
* The input of the step of a delete that deletes the app's Render resources:
* only the fields of the spec that name them.
* Input to the delete step that removes the app's Render resources: only the
* spec fields that name them.
*/
export const deleteResourcesInputSchema = appSpecSchema.pick({
user: true,
Expand All @@ -259,9 +240,9 @@ export type DeleteResourcesInput = z.infer<typeof deleteResourcesInputSchema>;
/* ── Verify and publish ───────────────────────────────────────────────── */

/**
* The input of verify-app. The sandbox holds the builder's files, and
* workflow code gives its id, never a model. The app directory comes from
* user and appName, so the input names no path.
* Input to verify-app. The sandbox holds the builder's files, and workflow
* code supplies its ID, never a model. The app directory comes from user and
* appName, so the input names no path.
*/
export const verifyAppInputSchema = z.object({
sandboxId: z.string().min(1),
Expand All @@ -275,11 +256,11 @@ export const verifyAppInputSchema = z.object({
export type VerifyAppInput = z.infer<typeof verifyAppInputSchema>;

/**
* The input of publish-app. `sandboxId` is the sandbox of the build, which
* publish-app reads the files of the app from. It pushes from a sandbox of
* its own. The spec gives factory.json and both Blueprints. The repository
* and the GitHub token come from the configuration of the workflow, never
* from the input: the Render Dashboard shows the input of each task run.
* Input to publish-app. `sandboxId` is the build sandbox that publish-app
* reads the app's files from; it pushes from its own sandbox. The spec yields
* factory.json and both Blueprints. The repository and GitHub token come from
* the workflow's environment, never from the input, because the Render
* Dashboard shows every task run's input.
*/
export const publishAppInputSchema = z.object({
sandboxId: z.string().min(1),
Expand Down
38 changes: 17 additions & 21 deletions app/delete.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* The four steps of delete-app. removeApp() in app/workflow.ts runs them in
* order, and AGENTS.md tells why the order is as it is.
* order and explains why the order matters.
*/
import { type TaskContext, task } from "@renderinc/sdk/workflows";
import {
Expand All @@ -25,20 +25,16 @@ import { type Sandbox, shellEscape } from "./sandbox.js";
import { setDeleteProgress } from "./store.js";
import { deleteAppResources, waitForBlueprintSyncs } from "./teardown.js";

/**
* A step of a delete makes one push, waits for the syncs of the Blueprint, or
* makes the deletes on Render.
*/
/** Each delete step makes one push, waits for Blueprint syncs, or deletes resources. */
export const DELETE_STEP_TIMEOUT_SECONDS = 10 * 60;
/** How long an unfinished sync of the apps Blueprint gets before a delete. */
/** How long an unfinished apps Blueprint sync may run before the delete gives up. */
const SYNC_TIMEOUT_MS = 6 * 60 * 1000;

/**
* The options of each step. Render does not retry a step: a failed step
* fails the delete, and the next attempt starts again at the first step. A
* retry of the first step after its push finds nothing to commit, so the wait
* would not wait for the push event. A retry of the wait would give a sync
* more time than SYNC_TIMEOUT_MS.
* Render does not retry a step: a failed step fails the delete, and the next
* attempt starts again at the first step. A retried first step would find
* nothing to commit after its push, so the wait would not know when the push
* happened. A retried wait would give a sync more than SYNC_TIMEOUT_MS.
*/
const DELETE_STEP = {
plan: "flex",
Expand All @@ -47,7 +43,7 @@ const DELETE_STEP = {
};

interface RemovedFromBlueprint {
/** As in factory.json. It is in the name of each Render resource of the app. */
/** From factory.json. Every Render resource name of the app starts with it. */
resourcePrefix: string;
/** The commit that took the app out, or null if an earlier attempt pushed it. */
commit: string | null;
Expand All @@ -57,9 +53,9 @@ interface RemovedFromBlueprint {

/**
* Write deletedAt into the app's factory.json, and push a root Blueprint that
* leaves the app out. The commit changes only these two files: a commit that
* removed the source of a service would start a build of it, and that build
* would fail before the service is deleted.
* leaves the app out. The commit changes only these two files: removing a
* service's source would start a build of it, and that build would fail
* before the service is deleted.
*
* Returns null when the repository has no spec for the app. Only the spec
* names the resources, so then no step deletes anything on Render.
Expand Down Expand Up @@ -118,9 +114,9 @@ export const removeFromBlueprintTask = task(
);

/**
* Wait until no sync of the apps Blueprint waits or runs. When no Blueprint
* watches the apps repository, no sync can bring a resource back, and there
* is nothing to wait for.
* Wait until no apps Blueprint sync is pending or running. When no Blueprint
* watches the apps repository, no sync can bring a resource back, so there is
* nothing to wait for.
*/
export const waitForSyncsTask = task(
{ name: "wait-for-blueprint-syncs", ...DELETE_STEP },
Expand Down Expand Up @@ -202,9 +198,9 @@ export const removeFilesTask = task(
);

/**
* The spec of one app in the clone, or null when the repository has no spec
* for it. The names of the resources to delete come from this file, so a
* file that names a different app stops the delete.
* One app's spec in the clone, or null when the repository has none. The
* resource names to delete come from this file, so a file that names a
* different app stops the delete.
*/
async function readSpec(
sandbox: Sandbox,
Expand Down
Loading
Loading