Root cause: gpt-4o-mini occasionally answered "Show me the sales
dashboard with metrics and a revenue chart" with a prose preamble
or clarifying question instead of the JSON spec the `@json-render`
renderer expects, which made the frontend fall back to
CopilotChatAssistantMessage (plain text) rather than a MetricCard +
BarChart tree. The single-component prompts (Revenue by category,
Expense trend) were terse enough that the model usually complied,
but the multi-component Sales Dashboard prompt was where it drifted.
Fix:
- Pass response_format={"type": "json_object"} via model_kwargs so
the OpenAI response is guaranteed to be a single JSON object,
removing any possibility of a plain-text fallback.
- Tighten the system prompt so every user turn is answered with JSON
(no clarifying questions, no prose) and components are referenced
by their exact case-sensitive names.
- Extend the Sales Dashboard worked example to include two MetricCards
plus a BarChart so the model has a concrete multi-component pattern
to mirror - the round-1 children forwarding in the registry wrapper
already supports arbitrarily many siblings under the root MetricCard.
The agent system prompt instructed the LLM to return <ui>...</ui> XML markup,
but the frontend renderer uses useJsonParser(message.content, kit.schema)
which expects a JSON object matching createUiKit(...).schema:
{
"ui": [
{ "metric": { "props": { "label": "...", "value": "..." } } },
{ "pieChart": { "props": { "title": "...", "data": "[{...}]" } } },
...
]
}
The XML syntax in useUiKit({ examples }) is hashbrowns prompt DSL for when
hashbrown drives the LLM directly (e.g. useUiChat); because this demo drives
the LLM via langgraph, the agent itself must emit the JSON wire format. With
XML content, useJsonParser returned { value: undefined } and the renderer
bailed to null, leaving the chat with only a loading indicator.
Rewrote the system prompt to produce the JSON envelope and updated the
renderer top-of-file docstring to match. Prop schemas tracked from the
current kit (metric: label+value, pieChart/barChart: title+data-as-JSON-string,
dealCard: title+stage+value, Markdown: children).
The multimodal demo failed three ways:
1. Try-with-sample-image rendered a broken thumbnail because the sample
PNG on disk is a Git LFS pointer stub when LFS is not pulled at build
time. The browser base64-encoded the 130-byte text stub, fed it into
the chat as a valid-looking image/png, and CopilotChat rendered it
as a broken <img>.
2. Try-with-sample-pdf attached in the composer but the agent said
"no image/document attached". Same broken-image pipeline also hid the
real symptom: the published @ag-ui/langgraph converter (0.0.x) only
understands the legacy { type: binary, mimeType, data | url }
AG-UI content-part shape. The modern { type: image | document,
source: {...} } parts CopilotChat emits are silently filtered out of
the LangChain message stream, so the LangGraph agent never saw them.
3. Manual drag-and-drop of an image failed for the same reason as #2.
Fixes:
- sample-attachment-buttons.tsx: reject Git LFS pointer stubs and
validate PNG/PDF magic bytes on the client before feeding them to the
attachment queue. Surfaces an actionable error pointing at
git-lfs-pull instead of silently producing a broken thumbnail.
- page.tsx: install an onRunInitialized subscriber on the active agent
that rewrites outgoing user-message image / document / audio / video
parts to the legacy binary shape the runtime converter preserves.
Everything else (CopilotChat UI, useAttachments upload pipeline,
paperclip + drag-drop + paste paths) is untouched; we only retarget
the wire format so the LangGraph side receives the attachment.
- multimodal_agent.py: after the rewrite, attachments arrive in the
agent as LangChain image_url content parts (data URLs) regardless of
upstream modality. Route on MIME instead of the AG-UI part type:
image/* forwards to GPT-4o natively, application/pdf is flattened to
text via pypdf. The modern document shape is still handled for
forward-compat when the runtime is upgraded.
Verified: classifier helpers pass inline unit checks; tsc --noEmit
introduces no new type errors relative to HEAD (module-resolution
errors for @copilotkit/shared and lucide-react are pre-existing on this
branch and unrelated to the multimodal demo).
## Summary
On integration profile pages (e.g. `/integrations/langgraph-python`),
the `cli-start` demo entry — which has a `command` field instead of a
`route` — was being rendered inside the Live Demos grid. Clicking it
opened `DemoDrawer` with an undefined `demoRoute`, so the iframe tried
to load `${backend_url}undefined`.
This PR:
- Splits `integration.demos` into `liveDemos` (runnable, have `route`)
and `commandDemos` (CLI-only, have `command`).
- Filters command-only entries out of the Live Demos grid.
- Adds a new **Get Started** section above Live Demos that renders each
`commandDemos` entry as a description + copy-able `<code>` block
(mirrors how `shell-dashboard` already handles these via `CommandCell`).
- Makes `Demo.route` optional and adds `Demo.command?: string` on the
`Demo` type.
Only `langgraph-python` currently has a command-only demo, so this is
the only profile page that changes visibly.
## Test plan
- [x] Ran `showcase/shell` dev server, visited
`/integrations/langgraph-python`, confirmed:
- New "Get Started" card shows `npx copilotkit@latest init --framework
langgraph-python` with a working Copy button.
- "Live Demos" grid no longer contains the broken "CLI Start Command"
tile.
- Other integrations (no `command` demos) render unchanged — Live Demos
still present, no empty "Get Started" section.
- [x] Pre-commit hooks (lint-fix, check-binaries,
test-and-check-packages — 1149 tests, commitlint) all pass.
Starting the /demos/auth cell unauthenticated meant the initial `/info`
handshake from CopilotKit returned 401 before any React error handler
could attach, crashing the page on load. Flip the default so the demo
mounts already signed in — the chat is immediately usable — and expose
Sign out / Sign in as the toggle for exercising the 401 path.
- use-demo-auth: default state flipped from unauthed to authed so the
page mounts with a valid Bearer header.
- page.tsx: add a ChatErrorBoundary around <CopilotChat /> that catches
any render-time throw from chat internals after signing out and shows
a friendly in-page message instead of white-screening. The boundary
resets on auth transitions so signing back in restores a live chat.
- auth-banner: rename Authenticate -> Sign in; status copy updated to
Signed in / Signed out to match the inverted default.
- tests/e2e/auth.spec.ts: flip assertions — page loads authenticated,
sign out triggers 401 on next send with banner still mounted, and
sign in clears the error and restores successful sends.
- qa/auth.md: refreshed checklist for the authenticated-default flow.
V1 CopilotRuntime wrapper silently drops `transcriptionService`, so the
composer never saw the mic button on /voice. Write it onto the V2 runtime
instance directly, and wrap the OpenAI service with a guard that throws an
"api key missing" error when OPENAI_API_KEY is absent (mapped by the
runtime error categorizer to AUTH_FAILED / 401) instead of the opaque 503
users were hitting.
Clicking any suggestion in the byoc-json-render demo crashed with
"useVisibility must be used within a VisibilityProvider". Renderer from
@json-render/react relies on StateProvider, VisibilityProvider,
ValidationProvider, and ActionProvider contexts that its internal
ElementRenderer consumes, but the assistant-message slot was rendering
Renderer bare.
Wrap it in JSONUIProvider (which composes all four required providers)
so streamed spec output renders without a context crash.
Also pass children through the MetricCard catalog wrapper. The agent's
Sales Dashboard worked example uses a MetricCard as the root with a
BarChart nested in its children array; previously that chart was
silently dropped by the registry wrapper, which only rendered the
MetricCard itself.
## Problem
Follow-up to #4251 (voice route lazy-init). The voice fix unblocked the
build far enough to hit the **next** failure:
\`\`\`
Error occurred prerendering page "/demos/byoc-hashbrown"
Error: Example prompt has 5 errors:
Prop 'trend' is not defined on <metric>
Prop 'data' on <pieChart>: Expected an array at: <root>
Prop 'data' on <barChart>: Expected an array at: <root>
Prop 'assignee' is not defined on <dealCard>
Prop 'dueDate' is not defined on <dealCard>
\`\`\`
Wave 4a was ported from the starter, which used an older
\`@hashbrownai/core\` where:
- Optional props used \`.optional()\`.
- Array props accepted stringified JSON in example JSX attributes.
\`@hashbrownai/core@0.5.0-beta.4\` dropped \`.optional()\` (props
omitted from the schema are treated as not-present) **and** tightened
example-prompt validation to reject strings against array schemas. Wave
4a already adapted the schema for \`.optional()\` removal (see the
comment on MetricCard in hashbrown-renderer.tsx) but the \`examples\`
prompt still carried the old-style attributes.
## Fix
- Wrap \`PieChart\` + \`BarChart\` with \`PieChartWithStringData\` /
\`BarChartWithStringData\` that accept \`data: string\`, JSON-parse it,
and render the real chart. Defensive: renders nothing if the parse fails
mid-stream (hashbrown feeds partial tokens during \`useJsonParser\`
streaming).
- Change the hashbrown schema on chart \`data\` from
\`s.streaming.array(...)\` to \`s.string(...)\`.
- Drop the non-schema example props (\`trend\` on \`<metric>\`,
\`assignee\` + \`dueDate\` on \`<dealCard>\`).
This unblocks Railway deploys once more. Modeling \`data\` as a string
also matches how the LLM streams it (JSON tokens through
\`useJsonParser\`).
## Why this blocks everything
Same as #4251: while \`npm run build\` fails on the langgraph-python
container, no new image can be pushed to Railway, and the full showcase
deploy pipeline stalls.
## Test plan
- [ ] Validate Showcase CI passes
- [ ] Container builds locally (or in \`showcase_deploy.yml\`) with
schema-validated examples
- [ ] Post-merge: \`showcase_deploy.yml\` run for langgraph-python
succeeds
- [ ] Post-deploy: \`/demos/byoc-hashbrown\` prompt streams JSON that
hits the new string-\`data\` schema; charts render via the wrapper
Wrap PieChart + BarChart with string-accepting wrappers that JSON-parse the
data prop, and change the hashbrown schema from s.streaming.array(...) to
s.string(). Drop the non-schema example props (trend on metric, assignee
+ dueDate on dealCard).
Wave 4a was ported from the starter, which used an older hashbrown where:
- optional props were declared via .optional()
- array props accepted stringified JSON in example JSX attributes
@hashbrownai/core@0.5.0-beta.4 dropped .optional() (props omitted from the
schema are treated as not-present) AND tightened example-prompt validation
to reject strings against array schemas. The build failed at prerender:
Error: Example prompt has 5 errors:
Prop 'trend' is not defined on <metric>
Prop 'data' on <pieChart>: Expected an array at: <root>
Prop 'data' on <barChart>: Expected an array at: <root>
Prop 'assignee' is not defined on <dealCard>
Prop 'dueDate' is not defined on <dealCard>
Modeling data as a string matches how the LLM streams it anyway (JSON
tokens through useJsonParser), and the wrappers are defensive about
partial tokens — they render nothing if the parse fails mid-stream.
## Problem
Every \`showcase-langgraph-python\` deploy has been **failing** since
#4224 (voice demo) merged:
\`\`\`
[Error: Failed to collect page data for /api/copilotkit-voice]
Missing credentials. Please pass an \`apiKey\`, or set the
\`OPENAI_API_KEY\` environment variable.
Error: failed to solve: process "/bin/sh -c npm run build" did not
complete successfully: exit code: 1
\`\`\`
Root cause: the voice route was instantiating \`new OpenAI({ apiKey:
process.env.OPENAI_API_KEY })\` at module scope. Next.js runs route
modules at build time to collect page/route data, and the Docker build
context does **not** have \`OPENAI_API_KEY\` set (only the running
container does), so the SDK's constructor throws and the build fails.
## Fix
Wrap the \`OpenAI\` + \`TranscriptionServiceOpenAI\` +
\`CopilotRuntime\` construction in a \`getRuntime()\` function that runs
on the first request. Cached after first call so there's no per-request
cost.
Affects only \`/api/copilotkit-voice\`. Runtime behavior is unchanged;
this is a pure build-time-safety fix. The comment on the original code
already said "construct lazily" — the code just didn't match the
comment.
## Why this blocks everything
Railway auto-deploys on every push to \`main\` under \`showcase/**\`.
While this route fails \`npm run build\`, the langgraph-python container
can't be pushed to Railway, which means:
- Voice, multimodal, auth, agent-config, byoc-hashbrown,
byoc-json-render demos all ship nothing new to Railway — every merge
since #4224 has been failing the deploy.
- The dashboard at \`dashboard.showcase.copilotkit.ai\` won't reflect
any of Waves 2b-4b until this lands.
## Test plan
- [ ] Validate Showcase CI passes
- [ ] Container builds locally with \`docker build\` (or CI build job)
when \`OPENAI_API_KEY\` is unset in the build context
- [ ] Post-merge: \`showcase_deploy.yml\` run for \`langgraph-python\`
succeeds
- [ ] Post-deploy: \`/api/copilotkit-voice\` responds normally
(first-request init still works)
Review feedback from #4196:
- `[slug]/[demo]/page.tsx` constructed `${backend_url}${demo.route}`
without a null check, so command-only demos (which have no `route`)
rendered an iframe pointing at `${backend_url}undefined`. Now builds
the src only when `demo.route` exists and renders a 'no live preview'
panel otherwise, mirroring the Get Started section on the profile
page. Also replaces the `any`-typed state with proper `Demo` and
`Integration` types imported from `@/lib/registry`.
- `[slug]/[demo]/preview/page.tsx` had the same bug — already typed
but TypeScript doesn't catch template-literal coercion of undefined.
Now bails with a command-focused message before concatenating.
- `profile-client.tsx` no longer duplicates `Demo`/`Integration`
interfaces — deleted the local copies and imports from
`@/lib/registry`. copyDemoCommand's catch now logs the failure so a
double-failure (no clipboard API + blocked prompt) is diagnosable.
Comment above the live-demos section updated from 'Demos' to
'Live Demos' to match the rendered heading.
The voice route was instantiating OpenAI() at module scope, which Next.js
triggers during 'collect page data' at build time. The Docker build context
does NOT have OPENAI_API_KEY set — only the running container does — so
every showcase-langgraph-python deploy has been failing since the voice
demo merged (#4224):
Error: Missing credentials. Please pass an `apiKey`, or set the
`OPENAI_API_KEY` environment variable.
[Error: Failed to collect page data for /api/copilotkit-voice]
Wrap the OpenAI + TranscriptionService + CopilotRuntime construction in
a getRuntime() function that runs on first request. Cached after first
call so there's no per-request cost in production.
This unblocks the full showcase deploy pipeline, not just voice.
## Summary
Follow-up to #4249 (langgraph-python waves 2b–4b consolidation). Adds
the three bundled sample assets that the voice and multimodal demos
reference but had left as placeholders:
| Path | Size | Notes |
|---|---:|---|
| `showcase/packages/langgraph-python/public/demo-audio/sample.wav` | 87
KB | 16 kHz mono, ~2.7 s. Windows TTS clip saying "What is the weather
in Tokyo?" — pairs with the existing `weather` aimock fixture so the
voice demo's "Play sample" → transcribe → send → WeatherCard flow works
end-to-end. |
| `showcase/packages/langgraph-python/public/demo-files/sample.png` | 10
KB | CopilotKit logo (lifted from
`examples/showcases/mcp-demo/public/copilotkit-logo-light.png`). |
| `showcase/packages/langgraph-python/public/demo-files/sample.pdf` |
2.4 KB | One-page CopilotKit quickstart excerpt. Contains the word
"CopilotKit" nine times, matching the multimodal E2E spec's
case-insensitive substring assertion on the agent's PDF summary. |
## Why it's a separate PR
The wave PRs shipped placeholders in those directories (README stubs) so
CI could pass without binaries committed alongside 6 feature branches
running in parallel. This PR lands the three binaries in one tiny
commit, no other code changes.
## Test plan
- [ ] `check-binaries` pre-commit hook passes (all three files are well
under the 1 MB cap)
- [ ] Post-deploy: voice demo's "Play sample" button populates the input
with a transcript containing "weather" / "Tokyo"
- [ ] Post-deploy: multimodal demo's "Try with sample image" and "Try
with sample PDF" buttons produce the expected attachment chips + agent
responses referencing CopilotKit
Add *.wav to .gitattributes so audio assets follow the same LFS policy
as the existing *.png / *.pdf / *.gif / *.jpg / *.jpeg / *.mp4 / *.webm
rules. Re-stage sample.wav so the committed object becomes an LFS
pointer instead of a raw 87 KB blob.
- public/demo-audio/sample.wav (87KB, 16kHz mono, ~2.7s) — TTS "What is the weather in Tokyo?" for the voice demo's "Play sample" button
- public/demo-files/sample.png (10KB) — CopilotKit logo for the multimodal demo's "Try with sample image"
- public/demo-files/sample.pdf (2.4KB, one page, contains "CopilotKit" 9x) — quickstart excerpt for the multimodal demo's "Try with sample PDF"
Makes both demos fully self-contained end-to-end; the "Try with sample X" buttons now work without the user providing their own files.
## Summary
Consolidates 5 independent langgraph-python wave PRs into one branch to
avoid a sequential rebase chain. Wave 2a (voice) already merged via
#4224; this PR carries the remaining five:
- **Wave 2b — multimodal** (supersedes #4225): `<CopilotChat
attachments>` with image + PDF (inline base64), dedicated vision-capable
`multimodal_agent`
- **Wave 3a — auth** (supersedes #4226): `onRequest` bearer-token gate
with an error-first UX showing both 401 and authenticated states
- **Wave 3b — agent-config** (supersedes #4230): forward typed `{tone,
expertise, responseLength}` via `<CopilotKit properties>` to a
dynamic-system-prompt agent
- **Wave 4a — byoc-hashbrown** (supersedes #4229): port starter's
hashbrown renderer into a single-mode demo with
`@hashbrownai/{core,react}`
- **Wave 4b — byoc-json-render** (supersedes #4227): integrate
`@json-render/{core,react}` with a Zod-validated catalog
One consolidated PR means one rebase, one CI wait, one review, one merge
— rather than 5 sequential rebase-fests on shared files (manifest.yaml /
constraints.yaml / docs-links.json / regenerated catalog + registry +
test counts).
## Merge structure
Sequential merge commits from `origin/main` (post-#4224), branches
merged in order: 2b → 3a → 3b → 4a → 4b. Each conflict
(manifest/constraints/docs-links appends, agent-id appends in
langgraph.json, test-count bumps) resolved with union semantics. Final
test counts bumped once to 38 features/demos and 37/1/0
wired/stub/unshipped for LGP, 182/1/480 overall.
## Closes
Once this lands, the following PRs can be closed as superseded:
- #4225
- #4226
- #4227
- #4229
- #4230
## Test plan
- [ ] `nx run showcase-scripts:test` passes (generate-registry +
generate-catalog counts match 38 LGP demos)
- [ ] `nx run showcase-ops:test` passes
- [ ] `pnpm exec tsc --noEmit` clean in `packages/langgraph-python`
- [ ] `python -m pytest src/agents/test_agent_config_agent.py -v` passes
(from Wave 3b)
- [ ] Post-deploy: run each wave's Playwright spec 3× against Railway
- [ ] Post-deploy: walk the langgraph-python column on
dashboard.showcase.copilotkit.ai — all 5 new cells green
## Outstanding (post-merge)
- Sample binaries for voice + multimodal demos — see the voice PR for
voice's `sample.wav` expectation; multimodal needs `sample.png`
(CopilotKit logo) + `sample.pdf` (one-page CopilotKit doc excerpt).
Drop-in commands documented in the team thread on Slack.
- Python `forwardedProps` → `config.configurable` mapping for Wave 3b —
flagged in that wave's PR; agent falls through to defaults cleanly
today, but the dropdowns won't visibly change agent behavior until a
middleware lands.
- V1 NextJS adapter gap surfaced by Wave 3a —
`copilotRuntimeNextJSAppRouterEndpoint` silently drops the `hooks`
option. Wave 3a works around by calling `createCopilotRuntimeHandler`
directly. Worth a core fix.
## Summary
Wave 2a — wire the existing `@copilotkit/voice` package into a
`langgraph-python` `/demos/voice` route with a dedicated runtime that
mounts `TranscriptionServiceOpenAI`.
- Dedicated runtime route `/api/copilotkit-voice` with
`transcriptionService: new TranscriptionServiceOpenAI(...)` — the only
route in this showcase that advertises `audioFileTranscriptionEnabled:
true`, so the mic button only appears in this demo.
- Demo page at `/demos/voice` with `<CopilotChat />` plus a
`<SampleAudioButton />` that bypasses the mic: fetches a bundled
`/demo-audio/sample.wav`, POSTs the single-route transcribe envelope to
`/api/copilotkit-voice`, and writes the transcribed text into the chat
textarea (native setter + synthetic input event keeps React state in
sync).
- Sample audio file path wired (`public/demo-audio/sample.wav` — binary
to be added by user, see Concerns).
- QA checklist (`qa/voice.md`) + Playwright E2E spec
(`tests/e2e/voice.spec.ts`) authored.
- `manifest.yaml` + per-shell `registry.json` / `demo-content.json` +
`docs-links.json` wired.
- `voice` was already allowlisted in `showcase/shared/constraints.yaml`
— no change there.
- Small pre-existing pre-commit hook tidy:
`showcase/shell-docs/src/data/demo-content.json` and
`showcase/shell-dojo/src/data/demo-content.json` are already >1 MB on
main; added them to `check-binaries.sh`'s allowlist alongside the
existing `shell` one so any commit that regenerates the bundle (like
this one) can land.
Out of scope (per spec): realtime voice, TTS, continuous dialog, custom
mic UI. Those are later waves.
## Test plan
- [x] `pnpm exec tsc --noEmit` — new files clean (pre-existing errors in
beautiful-chat / headless-complete are untouched baseline).
- [x] `showcase-scripts:test` — no new failures vs. main. Remaining
Windows-specific CLI-subprocess failures are a baseline (same count on
origin/main detached HEAD run); CI on Linux is the source of truth.
- [ ] Sample audio binary added by user (pending —
`public/demo-audio/.gitkeep` + README placeholder committed; path is
wired).
- [ ] Playwright `voice.spec.ts` runs green 3x against Railway
(post-deploy verification).
- [ ] Dashboard walk — `voice` row green for `langgraph-python`
(post-deploy).
## Concerns
- `public/demo-audio/sample.wav` binary must be produced and committed
by the user before the demo works end-to-end. The path is wired (demo
page, sample button, QA, E2E); the file is currently a placeholder
directory.
- E2E stabilization deferred to post-deploy. The new `/demos/voice`
doesn't exist on Railway yet, so the spec was authored (structural
selectors, permissive weather/tokyo regex on transcription) but not
executed — "3x green against Railway" happens once the service redeploys
with this PR.
Re-run `generate-starters.ts` for langgraph-python to emit the
byoc_json_render_agent.py starter agent and register it in langgraph.json
(post-merge drift-check was failing because the starter hadn't been
regenerated after adding the byoc-json-render demo).
Bump generate-catalog.test LGP-feature-count assertions 32->33 and
wired-totals 176->177 / 31->32, unshipped 486->485 / 6->5 to match the
new manifest.
Regenerate langgraph-python starter so agent_config_agent is included
(drift-check fails without this). Bump generate-catalog.test.ts counts
to match wiring of agent-config: LGP wired 31→32, LGP unshipped 6→5,
total wired 176→177, total unshipped 486→485.
Resolves conflicts:
- manifest.yaml / constraints.yaml / docs-links.json: keep both byoc-json-render
and main's re-wired open-gen-ui + open-gen-ui-advanced entries
- scripts/hooks/check-binaries.sh: keep both shell-docs and shell-dojo
demo-content exclusions (identical lines, different ordering)
- pnpm-lock.yaml (langgraph-python): regenerated via pnpm install
- shell/shell-dojo/shell-docs data JSONs: accept main's deletion (now
regenerated by nx targets), then regenerate via scripts/generate-registry.ts
and scripts/bundle-demo-content.ts
Bump generate-registry.test expected feature/demo count 32 -> 33 to account
for the new byoc-json-render demo.
Resolves textual conflicts in manifest.yaml and docs-links.json (keeps
both open-gen-ui* from main and agent-config from this branch). Accepts
main's deletion of generated shell*/src/data/*.json (now gitignored).
Bumps langgraph-python feature/demo count test from 32 to 33.
Resolved conflicts:
- showcase/packages/langgraph-python/manifest.yaml: kept auth (HEAD) +
open-gen-ui / open-gen-ui-advanced (main) in both features and demos.
- showcase/packages/langgraph-python/docs-links.json: kept auth entry
from HEAD alongside updated subagents shell_docs_path.
- showcase/shared/constraints.yaml: kept auth + open-gen-ui* entries.
- Deleted shell/shell-dojo/shell-docs generated JSONs per main (now
generated at build, no longer committed).
Bumped langgraph-python feature/demo count 32 -> 33 in generate-registry
test to reflect the added auth demo.
## Summary
Fixes the repeating "Starter Deployed Smoke Test Failed — 0 failure(s) —
job-level error" alerts in #oss-alerts.
**Root cause:** `integration-smoke.spec.ts` imports `registry.json`,
which is now gitignored (generated at build time, removed from tracking
in PR #4236). The CI workflow didn't run the generator before tests, so
the import fails with "Cannot find module."
**Fix:** Add a `generate-registry` step before the Playwright test run.
## Test plan
- [ ] Next scheduled run (or manual dispatch) passes without "Cannot
find module" error
- [ ] #oss-alerts stops receiving "job-level error" messages
The test at integration-smoke.spec.ts:21 imports registry.json, which
is gitignored (generated at build time). After PR #4236 removed it
from tracking, every CI run fails with "Cannot find module
'../../shell/src/data/registry.json'" — producing the repeating
"Starter Deployed Smoke Test Failed — 0 failure(s) — job-level error"
Slack alerts in #oss-alerts.
Adds a generate-registry step before the Playwright test run.