Skip to content

feat(runtime): connect Studio to a real runtime and generate real React from approved plans - #102

Merged
7shep merged 33 commits into
mainfrom
agent/runtime-docs
Aug 3, 2026
Merged

feat(runtime): connect Studio to a real runtime and generate real React from approved plans#102
7shep merged 33 commits into
mainfrom
agent/runtime-docs

Conversation

@7shep

@7shep 7shep commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Takes the local runtime from "Studio talks to a fixture" to "a prompt produces a real, buildable site you can open in Preview."

What was broken

DeterministicReactProvider returned the same eleven fixture files regardless of the design plan, so a prompt for a membership site produced a mechanical-keyboard sample site. The review gate correctly rejected it, the build was quarantined, and Preview had nothing to serve. The live provider seam existed in provider-config.ts but nothing filled it.

The live generation provider

Generation now spawns an already-authenticated agent CLI — claude or codex — as a subprocess. No API key, no per-token billing, no SDK dependency: it runs on the operator's existing subscription. The deterministic provider stays the default, so pnpm dev still needs no credentials and every existing test runs with no network and no CLI.

Activate with UNIVERSAL_GENERATION_PROVIDER=claude-code (or codex).

Three decisions worth calling out:

The self-check runs the real gate, not a copy of it. The provider calls reviewGeneratedImplementation directly rather than reimplementing its predicates. An independent copy would drift, and the checks that actually reject generated work are the ARCH_* findings from the TypeScript compiler analysis, which a regex reimplementation would miss entirely. This turns a thirty-second install-and-build round trip into a millisecond check, and feeds its exact messages into the repair prompt.

At most two CLI invocations, ever. An unbounded repair loop against a metered subscription is the expensive failure mode, so the invocation ceiling is asserted in tests. Both passes share one ten-minute budget.

A failing repair returns its flawed output. The review gate stays the authority on correctness; a provider that suppressed bad output would only hide the diagnostics the operator needs.

Supporting changes

  • ProviderError lets a provider name its failure. Without it every throw collapsed to internal, leaving authentication, rate-limit, timeout, and unavailable unreachable anywhere in the codebase and every failure marked retryable.
  • runSupervisedCommand gains stdin piping (a design plan far exceeds Windows' 32767-character command line) and a configurable output cap (the 64 KB default would truncate a project payload into malformed JSON).
  • cli.ts probes the selected CLI at startup, so a missing tool fails immediately with a message naming it instead of ten minutes into a generation.

Also in this branch

  • Windows MAX_PATH fix — prompt-derived project ids produced a 225-character revision directory; the generated project's pnpm tree added ~101 more, pushing past 260 and failing the build with an ESM resolver error that named vite rather than the path. Segments are now bounded to 40 characters with a digest suffix. retention.ts shares the same helper, since it recomputes these paths and would otherwise silently skip every revision as unsafe-path.
  • The art director bridge — Studio reaches design-mcp over stdio, with the child spawned by path to avoid closing a workspace dependency cycle, stderr inherited rather than piped into an undrained buffer, and the HTTP server closed before the bridge on shutdown.
  • Studio bootstrap fixes — the single-use token is redeemed exactly once, the memo clears on rejection so a retry can succeed, and swallowed initialization errors are logged before falling back to fixture mode.

Verification

pnpm -w test 23/23 packages green, pnpm -w build clean.

Verified end to end against both real CLIs. Each passed the deterministic review gate on the first pass with no repair:

provider time files
claude-code 429s 18
codex 126s 11

Reproduce with pnpm --filter @universal/local-runtime smoke:live [codex]. It is opt-in and consumes real subscription usage, so it is not part of pnpm test.

Known limitation

There is no progress signal during generation. runSupervisedCommand buffers the child's output rather than streaming it, so Studio shows generating for several minutes with nothing behind it. Claude Code supports --output-format stream-json --include-partial-messages, so a follow-up could stream progress into the existing /api/v1/events.

🤖 Generated with Claude Code

7shep and others added 30 commits July 29, 2026 12:17
Adds the implementation status summary covering completed phases 1-3 and
the remaining phase 4 release-hardening milestones.

Co-Authored-By: Claude <noreply@anthropic.com>
Removes the frontend/ ignore entry so the directory can be tracked. The
directory itself remains untracked until its contents are added
deliberately.

Co-Authored-By: Claude <noreply@anthropic.com>
`pageMap` was declared `z.unknown()` in start_art_direction,
submit_discovery_answers, and revise_creative_brief. Zod compiles that to an
empty JSON Schema, and MCP hosts serialize untyped parameters as strings, so
the value reached the engine as a string and `validatePageMap` rejected it on
its `isRecord` check every time.

The error surfaced as "Page map must declare single-page or multi-page kind",
pointing at the page map contents rather than at the wire format, which reads
as a malformed payload when the object never arrived as an object at all.

Because `topicResolved` requires a valid `session.pageMap` for the page-map
topic and has no fallback path, this made the topic unresolvable and blocked
the Phase 2 workflow past discovery for every MCP host.

Declaring the object shape keeps the payload structured over the wire. The
design engine still owns full validation. `interpretations`, `answers`, and
`decisions` were unaffected because `z.array()` emits a real type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Local scratch directory for MCP workflow testing. Holds session state,
per-step tool responses, authored source, and driver scripts that should
not enter the repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mirrors PR #100 and PR #101 onto this branch for local testing:

- Studio, Preview, and demo-site now bind server.host to 127.0.0.1
  instead of defaulting to IPv6-loopback-only, matching what the
  trusted local runtime's origin allowlist already expects.
- apps/studio/public/dev-runtime.local.js and
  apps/preview/public/dev-runtime.local.js let window.__UNIVERSAL_RUNTIME__
  be set before app code runs, so a real local runtime can be connected
  for manual testing instead of always falling back to the fixture client.

Co-Authored-By: Claude <noreply@anthropic.com>
# Conflicts:
#	.gitignore
#	ROADMAP.md
#	apps/preview/vite.config.ts
#	apps/studio/vite.config.ts
#	docs/RUNTIME_CONTRIBUTOR_WORKFLOW.md
frontend/vite.config.ts had no server config, defaulting to port 5173
(colliding with Studio) and IPv6-loopback-only binding. Since a
browser's localhost resolution tries IPv6 first, frontend silently won
that race over Studio's IPv4 binding, serving the marketing site at
localhost:5173 instead of Studio with no error from either side.

Mirrors PR #100's follow-up commit: explicit 127.0.0.1:5176 with
strictPort, and updates SETUP.md's port table and override examples.

Co-Authored-By: Claude <noreply@anthropic.com>
Follow-up to 5d1ca55 -- the doc edit wasn't staged in time for that commit.

Co-Authored-By: Claude <noreply@anthropic.com>
Records the design for wiring ArtDirectorBridge into the local runtime so
Studio can produce a real Design Plan v2 and generate an actual project.

The bridge is fully implemented and tested but has never been constructed
outside tests, so "Generate React site" fails on the missing enginePlan.
A second defect compounds it: Studio probes the bridge before bootstrap,
so the probe 401s and Studio latches to the fixture client permanently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Overwrites the first" read as a limit on prompts or projects. Neither is
restricted: prompting is unlimited, and projects are unaffected once a plan
is captured in enginePlan. The only lost case is an unfinished wizard, which
Studio cannot currently reach with its single-project state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six tasks: probe for the design-mcp entry, spawn a real stdio session,
construct the bridge in the runtime CLI, memoize Studio's single-use
bootstrap, probe the bridge only after a session exists, and verify the
whole path manually.

Task 6 is manual by design. Every existing bridge test passes against a
FakeSession, which is why a bridge that was never constructed in production
went unnoticed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The main-ordering test never imported main.tsx, so it asserted the contract
by hand and would pass even if the fix were reverted. Task 5 now extracts
initializeStudio() into its own module and tests that.

Task 1's probe test also returned early instead of skipping, which reads as
a pass when nothing was asserted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
local-runtime declared @7shep/universal-mcp as a workspace dependency while
design-mcp devDepends on @universal/local-runtime. Turbo builds its task graph
from dependencies and devDependencies alike, so the pair closed a cycle and
`turbo run build|typecheck|test` all aborted at graph construction.

design-mcp's edge is load-bearing and correctly placed: runtime-build-mcp.ts
imports RuntimeService/RuntimeFailure from production source, and bundle.mjs
derives esbuild `external` from `dependencies` only, so promoting it out of
devDependencies would break the published bundle. So local-runtime drops its
manifest edge instead and resolves design-mcp's entry by relative path.

resolveArtDirectorEntry now separates the two failures the old single existsSync
conflated: a missing package directory means the monorepo layout moved and is
logged loudly, because a silent undefined would start the CLI without an art
director and skip the integration tests; a missing dist/ is just an unbuilt
package and stays quiet.

The build ordering returns as a task-level edge in turbo.json, which does not
reintroduce a manifest cycle, so local-runtime's integration tests still get
design-mcp's dist/.

Also corrects the isTransportFailure comment, which claimed a contract keyed on
INVALID_SESSION/ILLEGAL_TRANSITION/IDEMPOTENCY. No design-mcp code contains
"IDEMPOTENCY"; the bridge regex matches only 2 of the 8 real codes and the rest,
including REQUEST_ID_CONFLICT, are retried as transport failures. The regex
itself is tracked as separate follow-up.

Finally, Studio now warns when the bridge probe returns false. hostBridgeAvailable
swallows every error and returns false, so the headline failure mode previously
produced no devtools output at all before latching to the fixture client.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ed on Windows

Project and revision ids derived from a user prompt are long and appear in both
the project and revision directory names. The revision directory then carries a
deep pnpm tree (node_modules/.pnpm/<pkg>@<version>_<peers>/node_modules/<pkg>/...
adds 200+ characters), pushing resolution past Windows MAX_PATH. The generated
project's production build failed with ERR_PACKAGE_IMPORT_NOT_DEFINED naming
vite's "#module-sync-enabled", which pointed at the package rather than the path.

Verified: same sources and lockfile, installed offline, build fails at the
306-character path and succeeds at a short one. For the observed ids the full
resolution path drops from 326 to 237 characters.

Segments are truncated with a digest of the full id appended, so the mapping
stays deterministic and collision-free. retention.ts recomputes revision paths
and compares them to the stored workspacePath, so it now shares the same helper;
diverging would have made retention silently skip every revision as unsafe-path.

Ids already under the bound are unchanged, so existing revisions stay reachable.
Also logs a warning when a revision path is long enough to be at risk, since the
build error names a package rather than the path that actually broke.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The deterministic provider returns eleven fixture files and never reads the
plan's routes, so a membership-site prompt produces a keyboard sample site and
the implementation review correctly rejects it. The LiveProviderFactory seam
exists but cli.ts passes no factory.

Design: one streamed schema-constrained call generates the whole project, then
a self-check runs the review's six predicates and, on a gap, makes exactly one
repair call. The provider never claims success the review would refuse.

The provider lives in its own package because design-mcp bundles local-runtime
into its published artifact; a re-export from local-runtime's index.ts would
inline the entire Anthropic SDK into an npm package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7shep and others added 3 commits August 2, 2026 16:29
The table listed a `provider-error` fallback that does not exist. The union is
authentication | rate-limit | timeout | cancelled | malformed-output |
unavailable | internal, so 5xx maps to `unavailable` and everything else to
`internal`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Revision 1 assumed a metered UNIVERSAL_PROVIDER_API_KEY, which contradicts a
hard project constraint: generation must run on an existing subscription and
must not add per-token billing.

Both agent CLIs are installed and authenticated locally (Claude Code 2.1.220,
codex-cli 0.146.0), and both support schema-constrained headless runs. The
provider now spawns one of them behind a small adapter seam, reusing the
subprocess pattern already proven by the art director bridge.

Dropping the SDK also drops revision 1's package-boundary problem, so the
modules live in local-runtime beside the other subprocess code.

Corrects two claims revision 1 got wrong: provider-config.ts does need to
change (its live path requires an API key), and there is no generic
provider-error failure code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…iption CLI

DeterministicReactProvider returned eleven fixture files regardless of the plan,
so every generation produced a mechanical-keyboard sample site that the review
gate correctly rejected. The live seam in provider-config.ts had no factory.

This fills it by spawning an already-authenticated agent CLI -- claude or codex --
as a subprocess. Generation runs on the operator's existing subscription: no API
key, no per-token billing, no SDK dependency. The deterministic provider stays the
default, so `pnpm dev` still needs no credentials.

The self-check calls reviewGeneratedImplementation directly rather than
reimplementing its predicates. An independent copy would drift, and the checks
that actually reject generated work are the ARCH_* findings from the TypeScript
compiler analysis, which a regex reimplementation would miss entirely. Running the
real gate inside the provider turns a thirty-second install-and-build round trip
into a millisecond check, and feeds its exact messages into one bounded repair.

At most two CLI invocations, ever: an unbounded repair loop against a metered
subscription is the expensive failure mode. Both passes share one ten-minute
budget. A repair that still fails returns its flawed output rather than
suppressing it -- the review gate stays the authority on correctness.

Supporting changes:
- ProviderError lets a provider name its failure. Without it every throw collapsed
  to `internal`, leaving authentication, rate-limit, timeout and unavailable
  unreachable anywhere in the codebase, and every failure marked retryable.
- runSupervisedCommand gains stdin piping, because a design plan is far larger
  than Windows' 32767-character command line, and a configurable output cap,
  because the 64 KB default would truncate a project payload into malformed JSON.
- cli.ts probes the selected CLI at startup, so a missing tool fails immediately
  with a message naming it instead of ten minutes into a generation.

Verified end to end against both CLIs. Each passed the gate on the first pass with
no repair: claude-code 429s/18 files, codex 126s/11 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
universal Ready Ready Preview Aug 3, 2026 2:42am

@7shep
7shep merged commit 4cc5c22 into main Aug 3, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant