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
25 changes: 25 additions & 0 deletions .github/workflows/build-snapshot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,31 @@ jobs:
NODE_PATH=/home/node/.cheatcode/app-runtimes/next/node_modules node -e "require.resolve(\"next\");require.resolve(\"react\");require.resolve(\"react-dom\")"
NODE_PATH=/home/node/.cheatcode/app-runtimes/expo/node_modules node -e "require.resolve(\"expo\");require.resolve(\"react-native-web\");require.resolve(\"@expo/metro-runtime\")"

next_test_dir="$(mktemp -d)"
next_source="$(mktemp -d /workspace/.cheatcode-next-smoke.XXXXXX)"
next_mirror="$next_test_dir/mirror"
next_lock="$next_test_dir/project.lock"
cleanup_next_test() {
rm -rf "$next_test_dir" "$next_source"
}
trap cleanup_next_test EXIT
mkdir -p "$next_mirror"
cp -R /home/node/cheatcode-next-template/. "$next_source/"
/opt/cheatcode/project-source-sync.py preview-once \
"$next_source" "$next_mirror" "$next_lock"
ln -s /home/node/.cheatcode/app-runtimes/next/node_modules \
"$next_mirror/node_modules"
/opt/cheatcode/project-source-sync.py package-run \
"$next_source" "$next_mirror" "$next_lock" "$next_mirror" -- \
pnpm run build
test -L "$next_mirror/node_modules"
test "$(readlink -f "$next_mirror/node_modules")" = \
/home/node/.cheatcode/app-runtimes/next/node_modules
test -f "$next_mirror/.next/BUILD_ID"
echo "Next native-mirror production build smoke passed"
cleanup_next_test
trap - EXIT

expo_test_dir="$(mktemp -d)"
expo_source="$(mktemp -d /workspace/.cheatcode-expo-smoke.XXXXXX)"
expo_mirror="$expo_test_dir/mirror"
Expand Down
9 changes: 6 additions & 3 deletions apps/agent-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,12 @@ dependency tree, and build cache are disposable;
wake and restart reconstruct them from the durable project without changing the Files surface.
Persisted pnpm-backed preview commands restore a missing sandbox-local dependency tree before the
server starts. Exact scaffold manifests link the disposable mirror to the matching immutable runtime
dependency tree. That shared runtime is image-owned and read-only. Read-only pnpm validation and
script commands disable pnpm's pre-run dependency verification and keep the link intact, while the
package boundary detaches it before a dependency mutation. Dependency mutations advance a
dependency tree. The immutable scaffold also owns the TypeScript configuration; package resolution
uses the linked `node_modules` tree rather than runtime-specific compiler path aliases, so editor,
typecheck, and production-build resolution share one contract. That shared runtime is image-owned
and read-only. Read-only pnpm validation and script commands disable pnpm's pre-run dependency
verification and keep the link intact, while the package boundary detaches it before a dependency
mutation. Dependency mutations advance a
generation under the same package lock; the native preview supervisor observes that generation and
restarts only the app child after the transaction releases the lock, preserving its process session,
source synchronizer, and signed launch environment. Metro and other long-lived resolvers therefore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@ import {
} from "@cheatcode/agent-core/tools/code";
import { APIError, type createLogger } from "@cheatcode/observability";
import type { CodeRuntimeContext } from "@cheatcode/sandbox-contracts";
import {
appBuilderGlobalStylesSource,
appBuilderLayoutSource,
appBuilderPageSource,
appBuilderTypeScriptConfigSource,
} from "./app-builder-template";
import { appBuilderPageSource } from "./app-builder-template";
import { metroForwardedHostFixScript } from "./expo-metro-forwarded-host";
import {
EXPO_RUNTIME_BIN,
Expand All @@ -26,44 +21,20 @@ type AgentRunLogger = ReturnType<typeof createLogger>;

interface AppBuilderSeedInput {
messageText: string;
workspaceSlug: string;
}

export function writeAppBuilderFiles(
input: AppBuilderSeedInput,
sandbox: ProjectSandboxStub,
dir: string,
): Promise<void> {
return Promise.all([
executeWriteFile(
{
path: `${dir}/src/app/layout.tsx`,
content: appBuilderLayoutSource(),
},
{ sandbox },
),
executeWriteFile(
{
path: `${dir}/src/app/globals.css`,
content: appBuilderGlobalStylesSource(),
},
{ sandbox },
),
executeWriteFile(
{
path: `${dir}/src/app/page.tsx`,
content: appBuilderPageSource(input.messageText),
},
{ sandbox },
),
executeWriteFile(
{
path: `${dir}/tsconfig.json`,
content: appBuilderTypeScriptConfigSource(input.workspaceSlug),
},
{ sandbox },
),
]).then(() => undefined);
return executeWriteFile(
{
path: `${dir}/src/app/page.tsx`,
content: appBuilderPageSource(input.messageText),
},
{ sandbox },
).then(() => undefined);
}

export async function scaffoldExpoApp(
Expand Down
79 changes: 0 additions & 79 deletions apps/agent-worker/src/durable-objects/app-builder-template.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,3 @@
export function appBuilderLayoutSource(): string {
return `import type { ReactNode } from "react";
import "./globals.css";

export const metadata = {
title: "Cheatcode Preview",
description: "Generated by Cheatcode",
};

export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
`;
}

export function appBuilderGlobalStylesSource(): string {
return `@import "tailwindcss";

:root {
color-scheme: dark;
}

html,
body {
min-height: 100%;
}

body {
margin: 0;
background: #0b0b0b;
font-family: ui-monospace, "SFMono-Regular", "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", monospace;
}
`;
}

export function appBuilderPageSource(messageText: string): string {
return `const cards = [
["Gateway", "Clerk auth and rate limits route the request."],
Expand Down Expand Up @@ -76,46 +37,6 @@ export default function Home() {
`;
}

export function appBuilderTypeScriptConfigSource(workspaceSlug: string): string {
const localModules = `/home/node/.cheatcode/projects/${workspaceSlug}/source/node_modules`;
const runtimeModules = "/home/node/.cheatcode/app-runtimes/next/node_modules";
return `${JSON.stringify(
{
compilerOptions: {
allowJs: true,
esModuleInterop: true,
incremental: true,
isolatedModules: true,
jsx: "react-jsx",
lib: ["dom", "dom.iterable", "esnext"],
module: "esnext",
moduleResolution: "bundler",
noEmit: true,
paths: {
"@/*": ["./src/*"],
"*": [`${localModules}/*`, `${runtimeModules}/*`],
},
plugins: [{ name: "next" }],
resolveJsonModule: true,
skipLibCheck: true,
strict: true,
target: "ES2017",
},
exclude: ["node_modules"],
include: [
"next-env.d.ts",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts",
"**/*.ts",
"**/*.tsx",
],
},
null,
2,
)}\n`;
}

function escapeForTsxText(value: string): string {
return value.replace(/[<>{}]/g, "");
}
5 changes: 5 additions & 0 deletions infra/containers/sandbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ generated images: only manifests and the starter route cross the persistent obje
boundary. Its dependency tree remains the exact reviewed runtime installed on native
sandbox disk. These locks prevent a snapshot rebuild from resolving a different dependency
tree while the application source stays unchanged.
The Next scaffold's production build explicitly uses webpack because its immutable dependency tree
lives outside the project mirror; Next's Turbopack build rejects that deliberate cross-root symlink.
Snapshot smoke runs the checked-in build script through the same native-mirror package boundary and
requires a real production `BUILD_ID`, so preview and verification cannot silently use incompatible
bundlers.

The root-owned `/opt/cheatcode/project-source-sync.py` helper is the single runtime
boundary between persistent project source and the native-disk project mirror. It is
Expand Down
2 changes: 1 addition & 1 deletion infra/containers/sandbox/app-templates/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"scripts": {
"dev": "next dev",
"build": "next build",
"build": "next build --webpack",
"start": "next start",
"lint": "biome check",
"format": "biome format --write"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};

export default config;
17 changes: 17 additions & 0 deletions infra/containers/sandbox/app-templates/next/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@import "tailwindcss";

:root {
color-scheme: dark;
}

html,
body {
min-height: 100%;
}

body {
margin: 0;
background: #0b0b0b;
font-family: ui-monospace, "SFMono-Regular", "Menlo", "Monaco", "Consolas",
"Liberation Mono", "Courier New", monospace;
}
16 changes: 16 additions & 0 deletions infra/containers/sandbox/app-templates/next/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Metadata } from "next";
import type { ReactNode } from "react";
import "./globals.css";

export const metadata: Metadata = {
title: "Cheatcode Preview",
description: "Generated by Cheatcode",
};

export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
32 changes: 32 additions & 0 deletions infra/containers/sandbox/app-templates/next/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const cards = [
["Gateway", "Clerk auth and rate limits route the request."],
["Agent session", "Durable Object stores resumable stream parts."],
["Sandbox", "Daytona serves this live preview."],
];

export default function Home() {
return (
<main className="min-h-screen bg-[#0b0b0b] px-6 py-16 text-zinc-100">
<section className="mx-auto flex max-w-5xl flex-col gap-10">
<div>
<p className="font-mono text-xs uppercase tracking-[0.3em] text-orange-400">
Cheatcode Preview
</p>
<h1 className="mt-5 max-w-3xl font-mono text-4xl tracking-tight md:text-6xl">
Sandbox preview is live.
</h1>
</div>
<div className="grid gap-4 md:grid-cols-3">
{cards.map(([title, body]) => (
<article className="border border-zinc-800 bg-zinc-950 p-5" key={title}>
<h2 className="font-mono text-sm uppercase tracking-[0.22em] text-zinc-200">
{title}
</h2>
<p className="mt-4 text-sm leading-6 text-zinc-500">{body}</p>
</article>
))}
</div>
</section>
</main>
);
}
30 changes: 30 additions & 0 deletions infra/containers/sandbox/app-templates/next/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"compilerOptions": {
"allowJs": true,
"esModuleInterop": true,
"incremental": true,
"isolatedModules": true,
"jsx": "react-jsx",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"moduleResolution": "bundler",
"noEmit": true,
"paths": {
"@/*": ["./src/*"]
},
"plugins": [{ "name": "next" }],
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ES2017"
},
"exclude": ["node_modules"],
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts",
"**/*.ts",
"**/*.tsx"
]
}
13 changes: 11 additions & 2 deletions knip.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
// `cloudflare:workers` and `cloudflare:workflows` are platform modules, not
// installable package dependencies. Knip cannot infer either edge.
"ignoreDependencies": ["@cheatcode/tsconfig", "cloudflare"],
// Copied into the generated Next scaffold by the sandbox Dockerfile.
"ignore": ["infra/containers/sandbox/app-templates/next/next.config.ts"],
"includeEntryExports": true,
"workspaces": {
".": {
Expand Down Expand Up @@ -51,6 +49,17 @@
"react-native-worklets"
]
},
"infra/containers/sandbox/app-templates/next": {
// Next loads config and App Router modules by filesystem convention after the
// Docker image copies this complete scaffold into a user workspace.
"entry": ["src/app/*.tsx"],
"includeEntryExports": false,
// Next CLI is invoked by package scripts rather than imported by source.
"ignoreBinaries": ["next"],
// Next's client runtime and generated route types consume these without a
// repository-owned import edge.
"ignoreDependencies": ["@types/react-dom", "react-dom"]
},
"apps/agent-worker": {
// Wrangler reaches the platform module through its `alias` deployment
// configuration, which is intentionally outside TypeScript's import graph.
Expand Down
6 changes: 6 additions & 0 deletions packages/agent-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ inference and returns page-bound element refs. Execution accepts only a single-u
latest state tree plus a bounded method/value, resolves its server-held XPath, and atomically binds
the returned post-action tree as the next actionable state. A click or fill therefore cannot
invoke a hidden model decision, expose a selector, reuse a stale ref, or cross the active origin.
Every first-party browser tool advertises its strict JSON schema to providers that support strict
tool calling, while the same Zod contract remains the provider-independent runtime boundary.
Action methods use explicit no-value, value, and drag shapes, so a model cannot omit a required ref
or value and still produce an executable call. A tool-validation or driver failure is verification
failure, not evidence that generated application state is broken; app-builder agents preserve the
framework event model and correct the browser call instead of injecting page scripts.
The managed
preview tool owns Computer-visible dev servers, remaps a requested port to the
project’s allocated port when necessary, injects the supported framework binding when the model
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core/src/mastra/system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ Build the Expo Router screens for a polished, native-feeling app: real screens,
// keeps WEB_MODULE's "start the dev server yourself" guidance; this note only applies here.
const APP_BUILDER_PREVIEW_NOTE = `## Your preview is already running — do not start your own

This project is scaffolded at the workspace root and its dev server + live preview are ALREADY running and managed for you before your turn begins (for a mobile app that's Metro serving the app on web plus the Expo Go QR). Do NOT initialize, scaffold, or create another app or nested project. Do NOT start, restart, or reconfigure the server yourself — no code_start_dev_server, \`expo start\`, \`npm run dev\`/\`web\`, or \`npx expo …\`: a second server fights the managed one for the project's port and breaks the preview. Use pnpm, never npm/npx, only when dependency changes are necessary. Inspect and edit the existing root files; the preview hot-reloads on save. Verify by opening the running app in the sandbox's headed Chromium at its INTERNAL localhost address; it's shown to the user automatically in the Computer/App panel — never paste the preview URL. Metro may briefly show an empty document while rebuilding the first web bundle after edits: wait for page content once and reload at most once before treating it as a defect. Take one screenshot with the exact visual acceptance criterion and use its returned PASS/FAIL assessment; never judge screenshot byte size. Exercise one representative interaction by calling browser_observe once, choosing one exact hyphenated element ref from its accessibility tree, and calling browser_act with that ref plus the required method/value. Use browser_act's actionable post-action tree as the result check; do not observe or extract again. If the request explicitly requires another interaction, chain it from a fresh ref in the returned tree; observe again only after navigation, an external page change, or when that tree lacks the required element. Never invent a ref or selector, write a separate Playwright/Python test, or install another browser. If a check fails, fix the concrete defect and repeat only that changed check once. Once the requested content renders, that interaction passes, and no blocking browser error remains, finish.`;
This project is scaffolded at the workspace root and its dev server + live preview are ALREADY running and managed for you before your turn begins (for a mobile app that's Metro serving the app on web plus the Expo Go QR). Do NOT initialize, scaffold, or create another app or nested project. Do NOT start, restart, or reconfigure the server yourself — no code_start_dev_server, \`expo start\`, \`npm run dev\`/\`web\`, or \`npx expo …\`: a second server fights the managed one for the project's port and breaks the preview. Use pnpm, never npm/npx, only when dependency changes are necessary. Inspect and edit the existing root files; the preview hot-reloads on save. Verify by opening the running app in the sandbox's headed Chromium at its INTERNAL localhost address; it's shown to the user automatically in the Computer/App panel — never paste the preview URL. Metro may briefly show an empty document while rebuilding the first web bundle after edits: wait for page content once and reload at most once before treating it as a defect. Take one screenshot with the exact visual acceptance criterion and use its returned PASS/FAIL assessment; never judge screenshot byte size. Exercise one representative interaction by calling browser_observe once, choosing one exact hyphenated element ref from its accessibility tree, and calling browser_act with that ref plus the required method/value. Use browser_act's actionable post-action tree as the result check; do not observe or extract again. If the request explicitly requires another interaction, chain it from a fresh ref in the returned tree; observe again only after navigation, an external page change, or when that tree lacks the required element. Never invent a ref or selector, write a separate Playwright/Python test, or install another browser. A browser-tool validation or driver error is a verification failure, not evidence of an app defect: correct the tool call once and preserve the framework's state and event model. Never replace React or React Native behavior with injected page scripts or manual DOM listeners to work around browser automation. If the rendered page or build output identifies a concrete code defect, fix that defect and repeat only the changed check once. Once the requested content renders, that interaction passes, and no blocking browser error remains, finish.`;

const DOCS_MODULE = `## Building documents & slides

Expand Down
Loading