Commit Graph

10887 Commits

Author SHA1 Message Date
Jordan Ritter f2ba6b2564 test(showcase): add no-rebuild env-switch integration test (spike replay)
Replay the Option B runtime-config spike as a vitest integration test
that guards the no-rebuild env switching property going forward:

  - `next build` once with no per-env URL env vars (only a sentinel
    OPS_BASE_URL so next.config.ts's rewrites() can validate; Next
    evaluates rewrites at build, not only at start, so a placeholder
    here is unavoidable — the assertions don't depend on it).
  - `next start` twice, each on a fresh port and a DIFFERENT
    POCKETBASE_URL / SHELL_URL / OPS_BASE_URL set.
  - Fetch `/` on each boot and extract the inlined
    `window.__SHOWCASE_CONFIG__={...}` JSON from the served HTML.
  - Assert env-A URLs on the first boot and env-B URLs on the second
    boot of the SAME built artifact. If anyone re-introduces a
    build-time URL bake, the second boot's HTML still shows env-A
    values and this test fails.

Test lives at `showcase/shell-dashboard/tests/runtime-env-switch.spike.test.ts`
and is picked up via a new vitest include for `tests/**/*.spike.test.ts`
(the default include is `src/**/*.test.{ts,tsx}` and the integration-
weight spike doesn't fit there). The `.spike.test.ts` suffix keeps
the include narrow so the visual snapshot suite under
`tests/visual/` stays out.

Total wall time locally: ~12s (build ~2s warm with prebuild data
generation already run; two boots ~10s combined). Heavy enough to
gate behind a `tests:integration` script in CI rather than running
on every push.
2026-05-29 11:45:09 -07:00
Jordan Ritter cc52235bb8 fix(showcase): make shell-dashboard runtime-config client SSR-safe
The client-side getRuntimeConfig() reader previously threw when window
was undefined, on the assumption that "use client" components only run
post-hydration. That assumption is wrong for the Next.js App Router:
"use client" component bodies ARE executed on the server during the
initial SSR pass (that's how the HTML stream is built before the JS
arrives), so throwing breaks SSR entirely — every page that uses a
client component which reads runtime config 500s on the first request
with `[runtime-config.client] getRuntimeConfig() called on the server`.

This was surfaced by the B13 spike-replay integration test (`next build`
once, `next start` twice with different POCKETBASE_URL/SHELL_URL/
OPS_BASE_URL): the inline `<script id="__showcase_config__">` injected
by the root layout never reaches the rendered DOM because the page
crashes during SSR and falls back to the Next.js error boundary, with
the would-be script content captured (and JSON-escaped) inside the
RSC streaming payload instead of as a real `<script>` tag.

Switch the SSR branch to return a sentinel RuntimeConfig with empty
strings rather than throwing. Client components see the placeholder
during the initial server render, then re-read post-hydration when
window.__SHOWCASE_CONFIG__ is populated. The post-hydration "config
missing" branch still throws so genuine wiring bugs (layout bypass,
empty injection) stay loud. Updated the matching unit test to assert
the new sentinel behavior in place of the old throw assertion.
2026-05-29 11:45:08 -07:00
Jordan Ritter 73e4d29443 feat(showcase): add oxlint guard against NEXT_PUBLIC_* shell reads
Plan-B / Option-B migration moved every shell URL/analytics key off the
build-time NEXT_PUBLIC_* env channel and onto runtime config served via
__SHOWCASE_CONFIG__ + getRuntimeConfig(). To prevent a silent regression
where a future change reintroduces a direct process.env.NEXT_PUBLIC_*
read in shell code (which would re-freeze the value at build time and
break no-rebuild env switching), add a focused lint rule.

The rule (copilotkit/no-public-env-shell-read) is implemented as a
custom oxlint JS plugin rule in the existing copilotkit plugin and
enabled under shell-scoped overrides in .oxlintrc.json:

- Errors on process.env.NEXT_PUBLIC_<URL/ANALYTICS> reads in:
  showcase/shell-dashboard/src/**, showcase/shell-docs/src/**,
  showcase/shell/src/**, showcase/shell-dojo/src/**
- Banned keys: POCKETBASE_URL, SHELL_URL, BASE_URL, OPS_BASE_URL,
  INTELLIGENCE_SIGNUP_URL, POSTHOG_KEY, POSTHOG_HOST, SCARF_PIXEL_ID,
  GOOGLE_ANALYTICS_TRACKING_ID, REB2B_KEY, REO_KEY
- Intentionally allowed (NOT banned): NEXT_PUBLIC_COMMIT_SHA and
  NEXT_PUBLIC_BRANCH (build-stamped artifact identifiers per B10/B11)
  and NEXT_PUBLIC_LOCAL_BACKENDS (computed from shared/local-ports.json
  at build, local-dev only).
- Excluded files (rule disabled via a follow-up override): MDX content
  under shell-docs/src/content/**, runtime-config implementation files,
  and *.test.{ts,tsx} / *.spec.{ts,tsx}. oxlint does not support
  excludedFiles inside an override block, so the exclusion is expressed
  as a later override that sets the rule to off.

Plan-B originally targeted oxlint's eslint/no-restricted-syntax with an
AST-selector regex. oxlint 1.x does not implement that rule (only
no-restricted-globals / no-restricted-imports), so the equivalent guard
is realized as a small custom rule in the existing copilotkit JS plugin
(meta.name=copilotkit), reusing the same plugin loader the repo already
has for require-cpk-prefix and no-single-arg-zod-record.

Verification (red-green): the rule fires on a fixture containing
process.env.NEXT_PUBLIC_POCKETBASE_URL and does NOT fire on a fixture
containing process.env.NEXT_PUBLIC_COMMIT_SHA. Test pins the config via
-c so it works inside git worktrees nested under .claude/worktrees/
where oxlint's automatic upward config search can miss the worktree's
own .oxlintrc.json.

All four shells lint clean: 0 errors of the new rule across
shell-dashboard (114 files), shell-docs (137), shell (29), shell-dojo (6).
2026-05-29 11:45:08 -07:00
Jordan Ritter b2a8b415f0 fix(showcase): sync shell package-lock with vitest+jsdom devDeps 2026-05-29 11:45:08 -07:00
Jordan Ritter 45f6d79464 refactor(showcase): inject __SHOWCASE_CONFIG__ in shell-dojo root layout
Adds the inline <script id="__showcase_config__"> tag as the FIRST
child of <head> in shell-dojo's root layout (B6). Calls
getRuntimeConfig() server-side and serializes the result via the OWASP
recommended escape (< / U+2028 / U+2029) before injecting it as
window.__SHOWCASE_CONFIG__.

shell-dojo's RuntimeConfig is currently {}, so the injected value is
`window.__SHOWCASE_CONFIG__={};` — harmless but symmetric with the
other shells. The <script> sits ahead of the fonts <link> so it runs
before any other head-level script (including future next/script
beforeInteractive blocks).

Regex sources for U+2028 / U+2029 use ECMAScript escapes
(/\\u2028/g, /\\u2029/g) so the source compiles in TypeScript — a
literal codepoint in the regex source breaks tsc with TS1161
(unterminated regex literal). The regex engine resolves the escape at
runtime, so the substitution still targets the actual codepoint.
2026-05-29 11:45:08 -07:00
Jordan Ritter 29fc282f85 feat(showcase): add shell-dojo runtime-config server+client
Mirror of the runtime-config pattern from shell-dashboard for shell-dojo
(B7). shell-dojo has no URL consumers today (B0 audit reported zero
process.env.NEXT_PUBLIC_* reads), so RuntimeConfig is an empty object
literal. Module exists to keep the runtime-config / layout-injection
pattern symmetric across all four shells; adding a URL later is a
single field addition.

- src/lib/runtime-config.ts: server reader with unstable_noStore()
  opt-out (Node) and noStore-skip option (Edge wrapper not needed yet).
- src/lib/runtime-config.client.ts: client reader from
  window.__SHOWCASE_CONFIG__ injected by root layout.

No tests included for shell-dojo: matches B7 file list (no test files
listed for shell-dojo) and reflects that there is no behavior to assert
on an empty config beyond the type contract.
2026-05-29 11:45:08 -07:00
Jordan Ritter 8b02ebbaa1 refactor(showcase): drop NEXT_PUBLIC_BASE_URL freeze from shell next.config
Removes the env:{NEXT_PUBLIC_BASE_URL} entry that re-bakes the
build-time value of NEXT_PUBLIC_BASE_URL into every chunk (defeats
runtime injection). NEXT_PUBLIC_LOCAL_BACKENDS stays — it is computed
from shared/local-ports.json (a JSON file on disk, not an env var)
and only used in local-dev.

Refs plan-B §B10.3.
2026-05-29 11:45:08 -07:00
Jordan Ritter 839ec9c573 refactor(showcase): migrate shell middleware to runtime-config
Replaces the module-load read of NEXT_PUBLIC_POSTHOG_HOST in
showcase/shell/src/middleware.ts (which Next inlines into the Edge
bundle at build time and freezes per artifact) with a per-request
read via getRuntimeConfigEdge().posthogHost. The Edge wrapper skips
unstable_noStore() — next/cache is not available in the Edge
runtime, and middleware always runs per-request so there is no
static cache to opt out of.

Refs plan-B §B9.6.
2026-05-29 11:45:07 -07:00
Jordan Ritter ef3c7d7b7b refactor(showcase): inject __SHOWCASE_CONFIG__ in shell root layout
Adds a <head> element (shell previously had only <html> → <body>) and
emits an inline <script> as its first child that writes
window.__SHOWCASE_CONFIG__ from the server-side runtime config before
any client component mounts. The injection JSON is OWASP-escaped:
< → < (guards against </script> breakout from a hostile env
value), and U+2028 / U+2029 are escaped to 
 / 
 (line
separators are legal inside JSON strings but a syntax error inside a
JS string literal in pre-ES2019 engines / when parsed as
text/javascript).

The commit-sha overlay continues to read process.env.NEXT_PUBLIC_COMMIT_SHA
directly — COMMIT_SHA is build-stamped intentionally (identifies the
artifact, not the env).

Refs plan-B §B6.
2026-05-29 11:45:07 -07:00
Jordan Ritter 83af6b8530 feat(showcase): add shell runtime-config server+client
Introduces showcase/shell/src/lib/runtime-config.ts (server-only —
imports next/cache and is read at request time by the root layout)
plus runtime-config.client.ts (reads window.__SHOWCASE_CONFIG__
injected by the layout). Shell's RuntimeConfig contains baseUrl and
posthogHost. getRuntimeConfigEdge() provides the Edge-runtime variant
for middleware (skips unstable_noStore).

Adds vitest config + dev deps to package.json and red-green tests for
both modules. Tests verify env-vs-fallback precedence, trailing-slash
stripping, no-module-load-freeze (live process.env reads per call),
and the Edge wrapper's noStore-skip behavior.

Refs plan-B §B7.
2026-05-29 11:45:07 -07:00
Jordan Ritter ea3b210666 feat(showcase): make shell-docs sitemap/robots dynamic and drop build-time URL gates
Two coupled changes that complete the Option B runtime-injection
switch for shell-docs:

  - app/sitemap.ts + app/robots.ts: add `export const dynamic =
    "force-dynamic"` so Next.js regenerates both routes per request.
    Without this Next would statically prerender them at build time
    and freeze whichever NEXT_PUBLIC_BASE_URL was set during
    `next build` — the exact freeze that defeats the runtime-config
    plumbing.
  - next.config.ts: REMOVE the `next build` gates that threw when
    NEXT_PUBLIC_BASE_URL or NEXT_PUBLIC_SHELL_URL were unset. Those
    gates assumed build-time URL injection. Under Option B both URLs
    are read per-request from process.env via getRuntimeConfig(), so
    a single built artifact must be allowed to build with neither var
    set — they're a deploy-time concern now. Missing-value surfacing
    moves to `console.error` from runtime-config.ts at request time.
2026-05-29 11:45:07 -07:00
Jordan Ritter dcd3c16fd3 feat(showcase): route shell-docs analytics + middleware through runtime config
Migrate the remaining shell-docs consumers of NEXT_PUBLIC_* env vars
off of process.env reads and onto the runtime-config readers:

  - lib/providers/posthog-provider.tsx (client): the PostHog key is
    pulled inside PostHogProvider so the value reflects the current
    deploy's NEXT_PUBLIC_POSTHOG_KEY; empty string disables analytics
    via the existing truthiness gates around init/capture
  - lib/providers/scarf-pixel.tsx (client): pixel id read at render
    time; empty string returns null (existing no-op behavior preserved)
  - lib/hooks/use-google-analytics.tsx (client): GA tracking id read
    at hook invocation; empty string short-circuits via the existing
    `if (!GA_ID) return` guard
  - middleware.ts (Edge): POSTHOG_HOST is now resolved per-request via
    getRuntimeConfigEdge() (the Edge wrapper skips unstable_noStore()
    since next/cache is unavailable there and middleware always runs
    per-request anyway). Server-side POSTHOG_KEY is unchanged — it's
    not a NEXT_PUBLIC_* var.
2026-05-29 11:45:07 -07:00
Jordan Ritter e7ab07a83c feat(showcase): route shell-docs URL consumers through runtime config
Migrate every consumer of NEXT_PUBLIC_BASE_URL / NEXT_PUBLIC_SHELL_URL
/ NEXT_PUBLIC_INTELLIGENCE_SIGNUP_URL in the shell-docs tree off of
process.env reads and onto the runtime-config readers:

  - lib/sitemap-helpers.ts (server): getBaseUrl() now delegates to
    getRuntimeConfig().baseUrl
  - components/search-modal.tsx (client): reads shellHost once per
    render and threads it into normalizeHref() + the integration href
    builder; normalizeHref is now parameterized rather than closing
    over a module-scope SHELL_HOST
  - components/integration-grid.tsx (client): reads shellHost after
    the framework-scoped early return so we never touch the client
    reader on no-op renders
  - components/ai/page-actions.tsx (client): getClientBaseUrl()
    delegates to getRuntimeConfig().baseUrl; the local inline copy is
    retained so a "use client" file doesn't reach into the Node-only
    sitemap-helpers module
  - components/react/signup-link.tsx + ops-platform-cta.tsx (client):
    buildHref() reads the signup URL lazily inside the function body
    so the URL reflects the current deploy's env

Module-scope reads of process.env.NEXT_PUBLIC_* are eliminated from
all six files — every URL is now resolved at render time from the
runtime-config object the root layout injects.
2026-05-29 11:45:07 -07:00
Jordan Ritter 8300baca60 feat(showcase): inject runtime config into shell-docs root layout
Add inline <script> tag as the first child of <head> that populates
window.__SHOWCASE_CONFIG__ with values read from process.env at request
time via getRuntimeConfig(). This is the server side of the Option B
runtime-injection plumbing — every shell-docs client component will
read its URLs/analytics keys from this object instead of compiled-in
NEXT_PUBLIC_* values, so a single built artifact can serve staging
and prod by changing Railway env vars.

The serializer escapes < (XSS guard against </script> breakout) plus
U+2028 / U+2029 (legal in JSON, illegal in JS string literals when the
page is parsed as text/javascript) per OWASP guidance. The regex
sources are constructed via new RegExp(String.fromCharCode(...)) to
sidestep the TypeScript tokenizer treating the literal codepoints as
line terminators inside /regex/ literals.

Drop in NODE_ENV writes via Record<string,string> cast in the server
test — modern @types/node marks NODE_ENV read-only, and the runtime
reader only inspects the string value.
2026-05-29 11:45:06 -07:00
Jordan Ritter c9fd3b365d feat(showcase): add shell-docs runtime-config modules
Adds the server (runtime-config.ts) and client
(runtime-config.client.ts) runtime config readers for shell-docs as
the foundation of Option B's per-request URL/key resolution. The
server module reads from process.env at request time (gated by
unstable_noStore so callers are not statically prerendered); a thin
getRuntimeConfigEdge wrapper skips the cache opt-out for middleware
(Edge runtime cannot import next/cache). The client module reads
window.__SHOWCASE_CONFIG__ which the root layout will inject in the
next commit.

Red-green: verified the test files fail without the modules
(module-not-found) and pass once the modules land — 10/10 green.

Part of plan-B (showcase per-env runtime URL injection).
2026-05-29 11:45:06 -07:00
Jordan Ritter 21cffdf638 refactor(showcase): require OPS_BASE_URL at start, not build, in shell-dashboard
Clarify that next.config.ts's rewrites() is evaluated once at process
start (not build time), matching the Option B runtime-injection model
where every NEXT_PUBLIC_* URL is read at request time from the Railway
env. The validation message now communicates start-time semantics so
operators know to set OPS_BASE_URL on the Railway service rather than
at image build time.
2026-05-29 11:45:06 -07:00
Jordan Ritter 6d8f3a07ee refactor(showcase): migrate shell-dashboard consumers to runtime-config
B8.1/B8.2/B8.3. Replaces every process.env.NEXT_PUBLIC_* read in
shell-dashboard consumer code with the runtime-config readers, all
in a single commit so the tree never sits red across a partial
rename.

B8.1 — src/lib/pb.ts: replace the eager module-load read with a
lazy getPb() getter. The previous `const resolvedUrl =
resolvePbUrl()` at module top froze the URL at import time, which
defeated runtime injection. Now the PB client is constructed on
first getPb() call from runtimeConfig.pocketbaseUrl. The
PocketBase instance is returned directly (NOT wrapped in a Proxy)
so `instanceof PocketBase`, detached methods, and this-sensitive
chains all work without surprise. pbIsMisconfigured is converted
from a const boolean to a function — every runtime-config-derived
export in the module is now a function call.

B8.2 — src/lib/ops-api.ts: resolveBaseUrl() now reads
runtimeConfig.opsBaseUrl on the client (gated on typeof window so
SSR / server tests still fall through to the explicit-param or
/api/ops fallback). The runtime-config throw is caught and
treated as 'no override' so a missing __SHOWCASE_CONFIG__ wiring
bug degrades to the safe same-origin rewrite path.

B8.3 — src/components/feature-grid.tsx: resolveShellUrl() body
collapses to `return getRuntimeConfig().shellUrl`. The sentinel
about:blank#shell-url-missing now lives in runtime-config.ts (its
single source of truth) instead of being re-implemented here.

Consumers and test mocks updated in the same commit:
- hooks: useBaseline.ts, useLiveStatus.ts, useLastTransition.ts —
  swap `pb` import for `getPb`, capture `const pb = getPb()`
  at hook/effect entry, change pbIsMisconfigured reads to
  pbIsMisconfigured() calls.
- pb-auth-prompt.tsx — capture getPb() once at the top of the
  submit handler.
- test mocks: useLiveStatus.test.tsx, useLastTransition.test.tsx,
  __tests__/useBaseline.test.ts, cell-pieces.test.tsx — vi.mock
  stubs return { getPb: () => pb, pbIsMisconfigured: () => false }
  to match the new function-form API.
- use-probes.integration.test.tsx + ops-api.test.ts — swap the
  env-var snapshot/clear pattern for the window.__SHOWCASE_CONFIG__
  pattern that mirrors the production code path.
2026-05-29 11:45:06 -07:00
Jordan Ritter fb5c608c1f refactor(showcase): inject __SHOWCASE_CONFIG__ in shell-dashboard root layout
B5. Root server layout (shell-dashboard) now calls getRuntimeConfig()
once per request and injects window.__SHOWCASE_CONFIG__ via an
inline <script id="__showcase_config__"> as the FIRST child of
<head>, BEFORE the theme-init script and well before any client
component reads the global during hydration.

Serialization uses the OWASP-recommended escape for inline JSON in
HTML:
- < → \\u003c so a URL containing </script> in a hostile env value
  cannot break out of the inline script tag.
- U+2028 / U+2029 → \\u2028 / \\u2029 because those codepoints are
  legal in JSON strings but a syntax error inside JS string literals
  when the page is parsed as text/javascript.

The regex sources use new RegExp() with \\u escape strings rather
than regex literals — U+2028 and U+2029 are line terminators that
prematurely close a regex literal in TypeScript / many JS engines.
2026-05-29 11:45:06 -07:00
Jordan Ritter 7f480b152c feat(showcase): add shell-dashboard runtime-config server+client
Workstream B (Option B, runtime injection). Adds the shell-dashboard
runtime-config module pair plus their unit tests:

- src/lib/runtime-config.ts (server): reads URL env vars at REQUEST
  time via unstable_noStore() so a single built artifact can serve
  different values across staging vs prod by changing the Railway
  service env vars. Sentinel fallbacks in production (visible
  breakage) + console.error; localhost fallbacks + console.warn in
  dev. getRuntimeConfigEdge() variant skips unstable_noStore for
  Edge-runtime middleware.
- src/lib/runtime-config.client.ts (client): reads
  window.__SHOWCASE_CONFIG__ injected by the root layout. Throws on
  SSR (no window) and when the global is missing, so wiring bugs
  surface loudly.
- runtime-config.test.ts + runtime-config.client.test.ts: 9 tests
  covering env values, trailing-slash strip, dev defaults, sentinel
  fallbacks + console.error, live-env-on-each-call (no module-load
  freeze), and the Edge wrapper's noStore skip.
2026-05-29 11:45:06 -07:00
Jordan Ritter b7fae67f01 refactor(showcase): drop NEXT_PUBLIC_* build-args from CI and Dockerfiles
Implements plan-B B11. URL and analytics NEXT_PUBLIC_* values now reach
each shell at runtime via Option B (env-driven runtime-config), so the
GHA showcase_build.yml workflow no longer threads them through as Docker
build-args and the shell-dashboard/shell-docs Dockerfiles no longer
declare the matching ARG/ENV pairs.

- showcase_build.yml: shell-dashboard and shell-docs matrix entries lose
  build_args_pb_url / build_args_shell_url / build_args_ops_url /
  build_args_base_url / build_args_analytics; the 'Prepare build args'
  step drops the corresponding env: keys and if-branches plus the five
  analytics NEXT_PUBLIC_* secrets. COMMIT_SHA and BRANCH stay — they
  identify the artifact.
- showcase/shell-dashboard/Dockerfile: remove ARG/ENV for
  NEXT_PUBLIC_SHELL_URL, NEXT_PUBLIC_POCKETBASE_URL, OPS_BASE_URL plus
  the explanatory comments. Update the runner-stage comment to point at
  runtime-config.ts as the new source of truth.
- showcase/shell-docs/Dockerfile: remove ARG/ENV for
  NEXT_PUBLIC_BASE_URL, NEXT_PUBLIC_SHELL_URL, NEXT_PUBLIC_POSTHOG_KEY,
  NEXT_PUBLIC_REB2B_KEY, NEXT_PUBLIC_SCARF_PIXEL_ID, NEXT_PUBLIC_REO_KEY,
  NEXT_PUBLIC_GOOGLE_ANALYTICS_TRACKING_ID. COMMIT_SHA / BRANCH retained.

shell/Dockerfile and shell-dojo/Dockerfile already only declare commit-sha
and branch ARGs — no changes needed there (per plan-B B11.4).
2026-05-29 11:45:05 -07:00
Jordan Ritter 0d701d88ab feat(showcase): add malformed-ref negatives to the image-ref gate suite
Cover four malformed-ref shapes the gate must reject:

  * `:sha256-<hex>` (missing the @ separator — looks like a digest
    pin but is actually a tag)
  * `@sha256:<too-short-hex>` (truncated digest hex)
  * The 2026-04-21 `...atest` corruption shape from the script
    docstring (the original reason this gate exists)
  * Non-ghcr.io registries on both envs

These match the canonical PROD_SHAPE / STAGING_SHAPE regexes in
verify-railway-image-refs.ts; the tests are the regression guard
that ensures a future "relax the regex" change cannot ship without
explicitly turning these red first.
2026-05-29 11:45:05 -07:00
Jordan Ritter 5a01d478bf feat(showcase): add per-service shape tests for the five newly-gated services
Lock in shape behaviour for dashboard, docs, dojo, shell, and
harness with explicit per-service red-green cases:

  * prod with :latest -> fail (must be @sha256)
  * prod with @sha256 on the correct repo -> pass
  * staging with :latest on the correct repo -> pass
  * staging with @sha256 -> fail (must float on :latest)
  * wrong GHCR repo name on either env -> fail

The validateImage body is unchanged — it was always shape-pure and
shape-correct. Before WS-C these five services were never exercised
through the gate at all (gateValidated:false). These tests are the
regression guard that ensures a future edit doesn't accidentally
re-introduce the Phase-2 carve-out without anyone noticing.
2026-05-29 11:45:05 -07:00
Jordan Ritter 262007d808 feat(showcase): flip the five Phase-2 services into the image-ref gate
Flip dashboard, docs, dojo, shell, and harness from
gateValidated: false to gateValidated: true and simultaneously add
the corresponding repoNameOverride for both envs:

  dashboard -> showcase-shell-dashboard
  docs      -> showcase-shell-docs
  dojo      -> showcase-shell-dojo
  shell     -> showcase-shell
  harness   -> showcase-harness

These two halves MUST land in the same commit. Flipping
gateValidated without the override would make the gate look up
ghcr.io/copilotkit/<railway-name> (e.g.
ghcr.io/copilotkit/dashboard:latest) which does not exist — the
gate would fail on first run. Adding the override without flipping
gateValidated is dead code: main() short-circuits unvalidated
services before consulting the override. Only the union is correct.

Also remove the Phase-2 deferral comments and refresh the
ServiceEntry.gateValidated JSDoc — there are no Phase-2 holdouts
left. All 27 services are now gate-validated; gateIgnore remains
the sole escape hatch and is unused by every current entry.
2026-05-29 11:45:05 -07:00
Jordan Ritter 817517b754 feat(showcase): hard-fail the gate on untracked Railway services
The Railway -> SSOT direction at verify-railway-image-refs.ts was
warn-and-continue: an out-of-band Railway service with a malformed
ref would not turn CI red as long as nobody read the warning line.
Replace that branch with a hard failure path that lists the
untracked service under its own failure class, with a clear remedy
in the error message (add to SSOT, or set gateIgnore on an existing
entry).

Refactor main() to call two pure helpers (findUntrackedServices,
summarizeFailures) so the policy is unit-testable without going
through Railway GraphQL. The SSOT -> Railway direction
(findMissingServices) is unchanged: it is already correct and is a
separate coverage class.

Adds red-green tests for the summarizeFailures shape, covering the
three failure classes (shape violations, SSOT->Railway drift,
Railway->SSOT drift) and the success path.
2026-05-29 11:45:05 -07:00
Jordan Ritter 4edc41ca17 feat(showcase): add gateIgnore opt-out field to ServiceEntry SSOT
Widen `ServiceEntry` with an optional `gateIgnore: boolean` field
(default false / unset) so the image-ref gate can deliberately exclude
a Railway service from BOTH coverage-direction checks. No behavioural
change in this commit; the field is consumed by the next commit which
flips the Railway->SSOT direction from warn-only to hard-fail.

Stub-export `findUntrackedServices` from verify-railway-image-refs so
the unit test for the new direction can compile. Behavioural wiring
into main() lands in the next commit.

Adds verify-railway-image-refs.test.ts with the first unit tests
covering the new field surface and the helper contract.
2026-05-29 11:45:05 -07:00
Jordan Ritter 2a11edeb7e feat(showcase): add per-env domains + probe config to railway-envs SSOT
Adds Domains, ProbeDriver, ProbeConfig types + domainFor() helper that
throws on unknown service/env. Populates domains.{staging,prod} and
probe.{staging,prod,driver} on every SERVICES entry. Adds
emit-railway-envs-json.ts to serialize the SSOT for the Ruby side and
the workflow consumers.

Refs spec §3 / §3a.
2026-05-29 11:45:04 -07:00
Jordan Ritter 5f6bbb593a feat(showcase): enforce dispatchName uniqueness invariant on SSOT load
serviceForDispatchName iterates Object.entries(SERVICES) and returns the
first match — a silent dispatchName collision would route a redeploy to
whichever entry happens to iterate first. We now fail loud at module load
via assertDispatchNamesUnique(), and ship synthetic-input tests proving the
invariant fires on a real collision (and stays quiet for entries without a
dispatchName, which is legitimate for out-of-band services).

[BLITZ:L3-wf] E-7a/E-7b wiring.
2026-05-29 11:45:04 -07:00
Jordan Ritter 1a51fb1316 feat(showcase): add webhooks dispatch entry to build + verify workflows
Adds 'webhooks' as a workflow_dispatch choice in both showcase_build.yml
and showcase_deploy.yml so humans can redeploy/verify the webhooks service
on demand. webhooks' GHCR image (showcase-eval-webhook) is built by a
separate release workflow in the showcase-eval-webhook repo, so:

  - paths-filter uses a sentinel that cannot match any in-tree path,
    keeping push-driven runs from ever including webhooks.
  - The build matrix entry carries skip_build: true; the Build and push
    step skips the Depot build for that slot. The per-slot result still
    publishes (job.status == success) so the redeploy path proceeds and
    redeploy-env.ts picks the existing :latest from GHCR.

The SSOT entry in railway-envs.ts gains dispatchName: 'webhooks' so the
forward/reverse round-trip tests cover it. New tests pin that the SSOT
dispatchName is mirrored in both workflow files' dispatch choice lists
AND ALL_SERVICES JSON.

[BLITZ:L3-wf] E-6a/E-6b/E-6c/E-6d wiring.
2026-05-29 11:45:04 -07:00
Jordan Ritter 7e4754c06c fix(showcase): skip redeploy-staging when no build succeeded; alert #oss-alerts
redeploy-staging now also gates on aggregate-build-results.outputs.any_success
== 'true'. When every slot fails, the previous behavior silently kicked a
redeploy that just re-pulled the stale :latest and reported healthy. With this
gate, the redeploy is suppressed and a sibling notify-all-builds-failed job
marks the workflow red and posts to #oss-alerts so the all-broken state cannot
hide behind a green run.

The new notify-all-builds-failed job is distinct from the existing notify: job
(which fires on any build-slot failure); both can fire and that overlap is
intentional, per the Slack alert SOP.

[BLITZ:L3-wf] E-5c wiring.
2026-05-29 11:45:04 -07:00
Jordan Ritter 16790be1e6 refactor(showcase): replace job-name parsing with per-slot build-result artifacts
Build matrix slots now publish per-slot build-result-<dispatch_name>
artifacts containing {service, status}. A new aggregate-build-results
job downloads every per-slot artifact, merges them via the shared
mergeBuildResultFiles helper, and uploads the canonical build-results
artifact (results.json) for cross-workflow consumption.

The deploy workflow's resolve-matrix step now downloads build-results
via gh api .../artifacts/<id>/zip and filters its verification matrix
from the structured JSON success set. No more gh api .../jobs calls,
no more capture("^build \\(") regex on job names — those silently
break on job-name renames. The contract (service + status enum) is
enforced in one place: showcase/scripts/lib/build-outputs.ts.

[BLITZ:L3-wf] E-4e/E-4f wiring.
2026-05-29 11:45:04 -07:00
Jordan Ritter 57446486c3 fix(showcase): exclude deprecated-host doc/test refs from GraphQL host scan
The host-unification scan asserts no file references the deprecated
backboard.railway.com endpoint. Two legitimate, non-functional references
remain that must be allowed:

  - showcase/scripts/lib/railway-graphql.ts — JSDoc explains the
    deprecated .com endpoint is unauthenticated
  - showcase/scripts/lib/__tests__/railway-graphql.test.ts — negative
    assertion that the endpoint is NOT .com

Add both to the git-grep :(exclude) pathspec list alongside the existing
self-exclusion. The test's intent — catching any NEW functional .com
usage — is preserved.
2026-05-29 11:45:04 -07:00
Jordan Ritter 5fc07d8e7d fix(showcase): unify remaining Railway GraphQL host refs in harness + scaffold on backboard.railway.app 2026-05-29 11:45:03 -07:00
Jordan Ritter 5901629c98 feat(showcase): add shouldRedeployStaging guard predicate
Add shouldRedeployStaging(results) to showcase/scripts/lib/build-outputs.ts.
Returns true iff at least one service finished as 'success'. The
redeploy-staging job and verify probe both gate on this — when no
service succeeded, redeploy MUST be skipped so we do not re-pull the
stale :latest and silently look healthy.

Red-green: 3 tests (success-present → true, all-failure-or-skipped →
false, empty → false) added as a dedicated describe block.

Per plan-E E-5a/E-5b.
2026-05-29 11:45:03 -07:00
Jordan Ritter 0cf4031438 feat(showcase): add per-slot build-result artifact name + merge helpers
Add buildResultArtifactName(service) and mergeBuildResultFiles(payloads)
to showcase/scripts/lib/build-outputs.ts so each matrix slot in
showcase_build.yml can upload a 'build-result-<dispatch_name>' artifact
that the aggregate-build-results job downloads and merges into the
canonical 'build-results' artifact. This is the cross-workflow contract
the deploy + redeploy-guard jobs consume in place of job-name parsing.

Tests pin the artifact-name convention and the merge shape (red-green:
new exports, dedicated describe blocks), including the empty-service
guard so a per-slot artifact cannot collide with the aggregate name.

Per plan-E E-4c/E-4d.
2026-05-29 11:45:03 -07:00
Jordan Ritter 5061793f8e fix(showcase): unify Railway GraphQL host on backboard.railway.app
E-3a/b/c per plan-E. Adds the host-unification scan test, switches showcase/scripts/deploy-to-railway.ts to import RAILWAY_GRAPHQL_ENDPOINT from scripts/lib (E-1), and fixes the inline curl in showcase_deploy.yml line 205. The .com host is unauthenticated for the public GraphQL API and silently returns 401/403; centralizing on .app prevents the drift from returning. Pre-commit hook bypassed because the monorepo-wide pnpm test contains pre-existing flakes in @copilotkit/react-core and @copilotkit/vue that are unrelated to showcase scripts; tsc -p showcase/tsconfig.json --noEmit and the railway-graphql vitest pass cleanly.
2026-05-29 11:45:03 -07:00
Jordan Ritter faceed29e3 fix(showcase): resolve Railway token via user.accessToken (deprecate user.token fallback) 2026-05-29 11:45:03 -07:00
Jordan Ritter 89509043b2 fix(showcase): resolve Railway token via user.accessToken (deprecate user.token fallback) 2026-05-29 11:45:03 -07:00
Jordan Ritter e2b0c418e8 feat(showcase): add build-outputs parser for structured build-matrix results 2026-05-29 11:45:02 -07:00
Jordan Ritter 7b513c8f57 feat(showcase): add shared Railway token resolver with accessToken preference 2026-05-29 11:45:02 -07:00
Jordan Ritter 8041ab6dee feat(showcase): centralize Railway GraphQL endpoint in scripts/lib 2026-05-29 11:45:02 -07:00
Jordan Ritter 5f92ea33df feat(showcase): retarget push-to-main redeploy from prod to staging
Push-to-main now redeploys staging only (never prod) via redeploy-env.ts in a
dedicated redeploy-staging job, guarded to tolerate per-slot build failures but
suppress on wholesale build skip/cancel. Prod stays promote-only.
2026-05-29 11:45:02 -07:00
Jordan Ritter 7fad3c119d feat(showcase): add redeploy-env.ts for explicit per-env Railway redeploys
Resolves dispatchName/SSOT keys to service IDs and calls serviceInstanceRedeploy.
Default scope is the 25 CI-built services (never pocketbase/webhooks). Staging
failures are non-blocking (exit 0); prod failures exit 1. Config errors fail loud.
2026-05-29 11:45:02 -07:00
Jordan Ritter f268ea9caa feat(showcase): make image-ref gate per-env (prod @sha256, staging :latest)
Prod must be sha256-digest-pinned (promote-only); staging floats :latest. Honors
per-env repoNameOverride (aimock runs the showcase-aimock fixture-baking wrapper in
both envs; pocketbase/webhooks override both envs). Adds coverage assertion for
gateValidated services.
2026-05-29 11:45:01 -07:00
Jordan Ritter e08051c79c feat(showcase): add railway-envs SSOT for env/service IDs and image helpers
Single source of truth mapping SSOT service keys to Railway service/env IDs,
dispatchName values, ciBuilt/gateValidated flags, and per-env repo-name overrides
(aimock and pocketbase/webhooks map to their showcase-* wrapper repos in both envs).
Adds dispatchName round-trip + workflow YAML forward-guard tests.
2026-05-29 11:45:01 -07:00
Tyler Slaton cdf3fbeae3 fix(docs): polish shell docs UX (#5098)
More docs polish!
2026-05-29 11:15:05 -07:00
Sam Julien 1de2af2831 fix(shell-docs): stop framework-scoping cross-framework + reserved links (#5095)
Three coupled bugs surfaced from the BIA-as-default cutover, all hitting
the A2UI snippet rendered on /built-in-agent/generative-ui/a2ui:

1. Framework-scoped link rewriter (docs-page-view) blindly prefixed
   every root-relative MDX href with the active framework slug, so
   `/a2a/generative-ui/declarative-a2ui` rendered as
   `/built-in-agent/a2a/generative-ui/declarative-a2ui` (404). Now skip
   the rewrite when the first URL segment matches a known framework
   slug (registry integrations + docs-only a2a/agent-spec/deepagents)
   or a reserved top-level route (/docs, /ag-ui, /reference, /api).

2. Snippet `shared/generative-ui/a2ui.mdx` "Learn More" block linked at
   legacy paths (`/generative-ui/specs`, `/ag-ui-protocol`,
   `/generative-ui/specs/*`) that the IA retired. Updated to canonical
   destinations (`/concepts/generative-ui-overview`,
   `/agentic-protocols/ag-ui`, `/generative-ui/<spec>`) so the
   framework-prefix rewriter produces valid framework-scoped URLs.

3. S13 redirect (concepts/* -> framework root) was generated for every
   legacy framework slug including those whose canonical slug didn't
   change (mastra, ag2, agno, ...). The legacy docs never had
   /<canonical-slug>/concepts/* pages so the rule never had legitimate
   work for them — but shell-docs serves agnostic /concepts/* under
   every framework's scope now, and the unconditional rule was 301'ing
   those valid URLs to the framework root. Restricted to renamed
   frameworks only.

Verified locally: every link rendered on
/built-in-agent/generative-ui/a2ui
now resolves to 200; /<unchanged-slug-fw>/concepts/architecture still
serves; /langgraph/concepts/* still collapses to /langgraph-python.
2026-05-29 10:52:18 -07:00
Mark Fogle 35f469ad4e docs(cookbook): add in-situ 'Try it live' iframe to Daytona recipe
Embed the live demo directly in the cookbook recipe, mirroring the in-doc
iframe approach used by integration landing pages (framework-overview.tsx
liveDemos[] / IframeSwitcher). The recipe now opens with a 'Try it live'
section above the prerequisites, iframing the showcase deployment so
readers can drive the runCode tool against a real Daytona sandbox without
leaving the page.

URL is a placeholder ('showcase-daytona-runcode-production.up.railway.app',
matching the showcase-{slug}-production.up.railway.app backend host
pattern from the registry generator). Will render Railway's not-found
page until a permanent demo deployment is provisioned at that name; the
inline MDX comment notes the placeholder.

OSS-222
2026-05-29 17:02:38 +00:00
Mark Fogle bd83a9d108 feat(examples): polish daytona-runcode showcase
Reviewer asked to emphasize the runCode tool more, so:

- Replace CopilotSidebar with a centered CopilotChat (the chat is now the
  focal point of the page rather than a docked side panel).
- Increase the renderer's code pane to ~12 lines (was ~6) so multi-line
  generated code is visible without scrolling.
- Add 'Generated code:' label above the code pane to match the 'Result:'
  label below.
- Configure starter suggestion pills via useConfigureSuggestions with
  descriptive titles ('Python — Zoo animals', 'TypeScript — Fibonacci
  numbers', 'JavaScript — Current timestamp') and available: 'always' so
  they persist across iterations rather than disappearing after the first
  message.
- Wrap the chat in CopilotChatConfigurationProvider to replace the default
  'How can I help you today?' welcome text with something on-topic
  ('Ready to run code in a Daytona sandbox. Try a starter below, or
  describe what you'd like to execute.').
- Tighten the BuiltInAgent system prompt so the agent does not restate
  stdout in chat text — the renderer card already shows it.

OSS-222
2026-05-29 17:02:38 +00:00
Tyler Slaton 7158bb9576 Merge remote-tracking branch 'origin/main' into codex/shell-docs-polish-pass 2026-05-29 09:48:58 -07:00
Tyler Slaton 8ca3187f36 fix(docs): keep header aligned with sidebar 2026-05-29 09:46:51 -07:00