## What
Completes the cvdiag observability subsystem so a single flapping
showcase run is **fully attributable end-to-end** — and makes every
probe reproducible on demand. Four fixes, all proven against the real
surface (live PocketBase writer-role, real `next` build, driver vitest):
1. **Cross-layer `test_id` join (the keystone).** Both sides were using
different ids: the backend minted its own UUIDv7, and the probe
re-minted a random UUIDv7 when its forwarded `X-Test-Id`
(`d6-<slug>-<runId>`) wasn't a UUIDv7. Now both run the same
`sanitizeJoinTestId` over the forwarded id → probe.* and backend.* rows
for one run **share `test_id`** (the join key per `schema.ts`); each
side keeps its own `trace_id`/`span`.
2. **Probe failure classifier.** `probe.exit` now stamps `outcome=err` +
`failure_classifier` (`sse-missing`/`dom-missing`/`text-unstable`) from
`waitForTurnComplete`'s reason, instead of `outcome=ok` on every run —
so reds are labeled, not inferred.
3. **Backend boundaries.** (Confirmed already on main — full 11-boundary
set incl `request.ingress`/`sse.first_byte`/`llm.call.*`.)
4. **On-demand trigger for EVERY probe.** New `POST
/api/runs/:family/trigger` (OPS-token-gated, rate-limited) fires any
fleet/D6 probe via the existing job-producer enqueue; in-process probes
keep their route. No more waiting on the hourly cron to reproduce a
flap.
## Why
cvdiag was live and persisting, but couldn't yet say "backend stall vs
frontend race" for a single red because probe and backend rows couldn't
be joined, reds weren't labeled, and D6 wasn't on-demand reproducible.
These close those gaps.
## Verification
- harness `tsc --noEmit` clean · cvdiag + drivers vitest **665 passed /
31 files** (join + classifier + boundary + trigger together) · 4
fix-test files 151 passed
- codegen `--check` + `cvdiag-stage-ts --check` in-sync (schema.json + 4
staged copies regenerated, not hand-merged)
- real Docker `next build` of built-in-agent succeeded (backend boundary
+ writer code bundles)
- live-PB writer-role RED→GREEN for the join; driver RED→GREEN for the
classifier; enqueue-assertion RED→GREEN for the trigger
## Follow-ups (not in this PR)
- Python dual-process (:8123) backend→PB shipping gap (python
integrations emit zero backend rows despite the auth fix)
- liveness-probe 400 noise → `outcome=info` + plumb real `model_id`
- Java/.NET backend writer-role auth
- break the run-view⇄control-plane import cycle the trigger routes
around
🤖 Generated with [Claude Code](https://claude.com/claude-code)
schema.json regenerated from the merged canonical schema.ts (failure_classifier
probe.exit additions UNION backend request.ingress/sse.first_byte/llm.call.*
boundaries + test_id adoption). Per-integration staged schema.ts copies
re-derived via 'showcase cvdiag-stage-ts' so codegen --check and stage --check
are both in sync. No hand-merge of generated artifacts.
## Summary
- `upload-artifact`'s `!**/node_modules/**` filters are post-walk: the
action still descends into every `node_modules` and stats every file
(~6M with pnpm's `.pnpm/` symlink farm) before applying negations. That
enumeration is the actual bottleneck — `Upload workspace` runs 10+
minutes even with the filters added in #5044.
- Replace the filtered upload with: `rm -rf` the heavy dirs
(`node_modules`, `.nx`, `.turbo`, `.next`), `tar -czf /tmp/workspace.tgz
.`, upload that single file. Publish job `tar -xzf`'s it after download
and continues unchanged.
- Applied symmetrically to `prerelease.yml` and `publish-release.yml`.
## Measured impact
Verified on a dry-run dispatch of `release / pre` against this branch
([run
26531785850](https://github.com/CopilotKit/CopilotKit/actions/runs/26531785850)):
| Step | Before (run 26529550757) | After (this PR) |
| ------------------------------- | ------------------------ |
--------------- |
| Upload workspace | ~800s (cancelled) | **3s** |
| Pack workspace | — | 9s |
| Download workspace | — | 1s |
| Unpack workspace | — | 1s |
| **Total artifact round-trip** | **~800s** | **14s** |
- `Upload workspace` step alone: **~99.6% reduction (~267× faster)**.
- Full pack/upload/download/unpack pipeline vs the prior single upload:
**~98% reduction (~57× faster)**.
The 800s baseline is from a cancelled run, so both numbers are
conservative.
## Test plan
- [x] Dispatch `release / pre` against this branch with `dry_run=true`
- [x] Confirm `Upload workspace` completes in seconds instead of 10+ min
- [x] Confirm publish job `Unpack workspace` restores the tree and `pnpm
install` succeeds
- [x] Confirm dry-run publish step exits clean (no missing files from
the tarball round-trip)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## What
A new Cookbook recipe — **Build an agentic app on Angular + Google ADK**
— covering the non-obvious production gotchas when you wire an Angular
frontend to a Google ADK agent over AG-UI, with optional CopilotKit
Intelligence threads and memory. Written in the existing cookbook house
style (symptom → cause → fix callouts), and it links out to the Angular
and ADK quickstarts rather than re-teaching setup.
## Why
The Angular + ADK combination has a handful of correctness issues that
only surface in a real, multi-user, governed app, and they aren't
covered by the per-piece quickstarts:
- One agent, one store (a second composer must not create its own store)
- Scope the user via the **run body**, not an HTTP header (a header lags
by one in-session switch)
- Never reconfigure the runtime mid-submit (it recreates the agent store
and drops the message)
- Governance is server-side; the per-request allow-list rides the run
body
- Choose a capable model, and degrade gracefully when the Intelligence
platform is absent
The recipe is **model-flexible**: it frames ADK as running Gemini by
default but supporting any model ADK supports, consistent with the ADK
quickstart.
## Changes
- **New** `cookbook/angular-adk-agentic-app.mdx` — the recipe
- `cookbook/meta.json` — register in nav
- `cookbook/index.mdx` — add the index card (uses
`/logos/google-adk.svg`)
- `src/lib/sidebar-icon.tsx` — add `custom/google-adk` sidebar icon
entry
- `frontends/angular.mdx` — bidirectional cross-link into the recipe
- `src/lib/__tests__/docs-render.test.ts` — update the cookbook nav test
for the new page
## Test plan
Run from `showcase/shell-docs`:
- `npm run typecheck` — passes
- `npm run lint` — passes (only pre-existing warnings; none in changed
files)
- `npm run test` — the cookbook-nav test passes. Two
`public-assets.test.ts` cases fail **only locally** because git-LFS PNGs
are unmaterialized in a fresh worktree (the tests assert assets are not
LFS pointer stubs); they are unrelated to this change and pass in CI.
- `npm run build` — passes; all 211 static pages generate, including
`/cookbook` and `/cookbook/[...slug]`.
Verified in the browser at `localhost:3003`: recipe renders with correct
callout styling, code highlighting, populated TOC, sidebar entry, and
index card.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<img width="1026" height="586" alt="image"
src="https://github.com/user-attachments/assets/4c8d61ef-d74b-40fa-9e54-807750daed24"
/>
Two PR checks were red on the new integration:
- check-config-files: add strands-typescript/next.config.ts to the build-
config allowlist.
- Validate Showcase: update the new-integration guard pins that intentionally
trip when an integration is added — BORN_IN_SHOWCASE 6→7, calculator
_from-feature-parity count 18→19, catalog cross-join 874→920 / total_cells
855→900 / docs_only 19→20 (46 features × 20 integrations), and the aimock
substring-shadow ceiling 132→133 (+1 from the strands-typescript calculator
fixture).
Also drop the premature deploy wiring: strands-typescript is removed from
showcase_build.yml (matrix + path filter + ALL_SERVICES) because it has no
Railway service yet (deployed: false) and the railway-envs SSOT test requires
a real service entry. It re-enters the deploy pipeline when the Railway
service is provisioned (external setup per INTEGRATION-CHECKLIST).
Bring the TypeScript AWS Strands integration to parity with the Python
strands sibling now that the @ag-ui/aws-strands TS adapter is confirmed to
support the same feature surface (per its examples/server):
- Restore A2UI: the declarative-gen-ui + a2ui-fixed-schema demos, their
routes, qa, specs, the @copilotkit/a2ui-renderer dep, beautiful-chat's
A2UI catalog, and the manifest entries (generative_ui / features / demos /
a2ui_pattern). manifest now matches strands-python feature-for-feature.
- Header forwarding: attach `x-aimock-context: strands-typescript` as a
static defaultHeader on the OpenAI client (model-factory + sub-agent
client) — the TS analog of the Python integration's _header_forwarding
shim — so aimock matches this integration's fixtures.
- aimock fixtures: add d6/strands-typescript + d4/strands-typescript
(ported from the Python sibling, context retargeted).
- playwright.config: X-AIMock-Context → strands-typescript.
Note: the raw tests/e2e Playwright suite is flaky and not a CI merge gate
(demo e2e / `/eval` D5 are comment-triggered, not required) — it fails the
same specs for strands-python too. The auto-gates (build, validate-
constraints, oxlint/oxfmt, unit) are green.
A catalog on <CopilotKit a2ui={{ catalog }}> is now enough to use A2UI
end to end. Previously developers also had to set a2ui.injectA2UITool: true
on the runtime.
The provider forwards an a2uiCatalogAvailable signal per run whenever it has
a catalog (and renders surfaces locally). handle-run reads that signal and
hands it to configureAgentForRequest, which enables A2UIMiddleware and defaults
injectA2UITool to true. An explicit injectA2UITool value (including false) and
an explicit a2ui.enabled: false are always respected via ?? / short-circuit, so
this only fills the default and never overrides a deeper opt-out.
## What
Fixes a production defect in the cvdiag observability subsystem (#5591):
**backend emitters never persisted to PocketBase.** The `cvdiag_events`
createRule requires an authenticated `cvdiag_api_keys` record with
`role="writer"`, but:
- The **Python** writer sent a custom `X-Cvdiag-Writer-Key` header and
never authenticated → every CREATE 403'd and was silently dropped by the
never-throw daemon.
- The **TS** backends had a *type-only* PB-writer seam — the concrete
writer lived in `harness/` and was never bundled into the deployed
Next.js runtime → `pbWriter` undefined → flush was a no-op.
So backend boundaries (`request.ingress`, `llm.call.*`, `sse.*`,
`error.caught`) emitted to stdout but never reached the DB. (The
probe/harness side already persists — it auths as superuser.)
**Why #5591's CR didn't catch it:** the M2 persistence tests
authenticated as **superuser**, which the migration itself notes
bypasses *all* collection rules — so the real writer-role auth path was
never exercised.
## Fix
- **Python** (`cvdiag_pb_writer.py`): `auth-with-password` against
`cvdiag_api_keys` (identity `cvdiag-writer@keys.local`, password =
`CVDIAG_WRITER_KEY`) → cache Bearer token → `Authorization: Bearer` on
CREATE; re-auth on token expiry; auth failure degrades to no-op
(never-throw). Identity overridable via `CVDIAG_WRITER_IDENTITY`.
- **TS** (`harness/src/cvdiag/pb-writer-fetch.ts`, staged into the 4
integrations): a plain-`fetch` writer-role writer (no SDK, bundles
cleanly in Next), injected by `withCvdiagBackend` only when
`CVDIAG_PB_URL` is set.
The writer credential already exists — the migration seeds the `writer`
record — so no provisioning needed.
## Verification (against a LIVE PocketBase, authenticating as the writer
role — not superuser)
- Python: live-PB writer-role test — header-only → **0 rows**; after fix
→ **row persists**; wrong password → **0 rows, daemon alive**. `_shared`
pytest 16/16.
- TS: live-PB writer-role tests — 0 rows → rows persist; stale-token
re-auth lands; wrong-key/no-url no-op. harness cvdiag vitest 255/255.
Real Docker `next build` of built-in-agent succeeds (writer bundles).
- `cvdiag-stage-ts --check` in-sync.
## Follow-up (not in this PR)
Java/.NET backends have the same auth gap; they're not the flap-heavy
integrations and will be fixed before enabling their columns.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Replace the placeholder logo with the official Agent Development Kit mark
from google/adk-python (assets/agent-development-kit.png), committed as an
LFS PNG like the other recipe logos (Daytona, Arcade). Point the card and
sidebar at /logos/google-adk.png and revert the unused google-adk.svg back
to its original state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
public/logos/google-adk.svg was a placeholder (a grey tile with the text
"Go"), like the other letter-stub SVGs in that directory. Replace it with
the real ADK glyph, reusing the vector paths from the `AdkIcon` component
(src/components/icons/framework-icons.tsx) on a light tile so it renders
on both light and dark surfaces. Only the new cookbook recipe references
this file, so no other page is affected. Monochrome for now; design can
recolor to official ADK colors at approval.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ADK adapter (ag_ui_adk) builds session state from `dict(input.state)`
plus `input.context` (under `_ag_ui_context`); it does not mirror
`forwarded_props` into session state (it only reads it for the
`injectA2UITool` flag). So a user id sent via CopilotKit `properties`
(-> forwardedProps) never reaches `tool_context.state`, and the documented
scoping silently failed — the exact bug class the section warns about.
Carry the user id as agent context via `connectAgentContext` (or shared
agent state) instead, and read it from `tool_context.state["_ag_ui_context"]`
(or directly from state). Reconcile the contradictory forwardedProps/state
lines and fix the coding-agent prompt to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a cookbook recipe covering the non-obvious production gotchas when
wiring an Angular frontend to a Google ADK agent over AG-UI, with optional
CopilotKit Intelligence threads and memory: one agent store, run-body user
scoping (not a header), never reconfiguring the runtime mid-submit,
server-side governance, model selection, and graceful platform degradation.
- New recipe at cookbook/angular-adk-agentic-app.mdx
- Register in cookbook nav (meta.json) and add an index card
- Add a custom/google-adk sidebar icon entry
- Cross-link from frontends/angular
- Update the cookbook nav test for the new page
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## What
Adds **cvdiag** — a permanent, always-available observability subsystem
for the showcase, built to diagnose the red↔green cell flap on the
staging dashboard and to make that diagnosis a dashboard query rather
than a multi-day forensic hunt in the future.
Captures the full request path with `X-Test-Id` correlation across
**probe → backend → aimock → edge**, across every integration
(TypeScript, Python, Java/spring-ai, .NET):
- Per-language backend emitters (canonical + staged/compile-linked
mirrors), all sharing one schema (`schema.json`, closed-world
`additionalProperties:false`).
- CREATE-only writes to two new PocketBase collections: `cvdiag_events`
and `cvdiag_raw_byte_samples` (additive migrations — no existing data
touched).
- An 8-class flap classifier mapping to the observed failure signatures
(`sse-missing` / `text-unstable` / `dom-missing`).
- DEBUG-tier raw-byte capture (secret-scrubbed) and HMAC-guarded A/B
edge-interference detection.
## Why
The runId flap-fix (`cdc1e90e`, 2026-06-09) did **not** fully resolve
the flap — it was still observed 2026-06-19. cvdiag exists so the
*remaining* cause is observed live with full correlation instead of
inferred.
## Safety / enablement
- **Inert by default.** With `CVDIAG_BACKEND_EMITTER` unset the
subsystem performs zero host mutation (no logging-config changes, no
threads/tasks, no stdout) — verified by
`test_cvdiag_inert_when_disabled`. **To accumulate data, set
`CVDIAG_BACKEND_EMITTER=1` on the showcase services.**
- All per-language scrubbers match the canonical `scrubSecrets`
(sk-/base64url, Bearer, colon-less URL userinfo, size-guard) — verified
with real toolchains (vitest / mvn / dotnet).
- Merged latest `main` (only conflict: a clean `.csproj` include union).
## Verification
- harness `tsc --noEmit` ✓ · `src/cvdiag` vitest 251/251 ✓ ·
`cvdiag-stage-ts --check` in-sync ✓
- Java MessageScrubber 17/17 (mvn) ✓ · .NET CvdiagBackend 5/5 (dotnet
sdk:9.0) ✓ · Python emitters 93/93 (3.12) ✓
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The inspector's owned thread store (created when useThreads() isn't mounted)
initialized its context with empty headers, so its /threads requests omitted
the headers configured on <CopilotKit> (e.g. X-CSRF, auth). This produced
HTTP 403 in environments that enforce CSRF/auth checks.
Source the headers from core.headers at store creation, and re-apply them via
onHeadersChanged so the owned store stays authorized when headers are updated
at runtime, mirroring how useThreads() keeps its context in sync.
## Summary
- Rework Angular, React Native, Vue, Slack, and Teams frontend docs to
center on Copilot Runtime instead of direct AG-UI agent endpoints
- Add local runtime setup examples, package install updates, and
troubleshooting guidance for runtime auth and connectivity
- Update language, prerequisites, and next-step links to match the new
runtime-based quickstarts
## Testing
- Not run (not requested)
## What does this PR do?
Adds a Cookbook recipe and a runnable showcase that give CopilotKit's
Built-in Agent
**authenticated tools** through [Arcade](https://www.arcade.dev). The
agent calls OAuth-backed
tools (Gmail send/list, Google News search), and Arcade's one-time
authorization step renders
as a generative-UI **Connect** card in the chat. Approve once, and the
agent completes the action.
Included:
- **Docs:** `showcase/shell-docs/src/content/docs/cookbook/arcade.mdx`,
plus the
`cookbook/meta.json` entry and an `index.mdx` card.
- **Showcase app:** `examples/showcases/arcade-tools/` (Next.js App
Router). Three Arcade tools
on a Built-in Agent, the Connect-card flow, and a no-keys `/mock`
preview of the cards.
Design notes:
- Single-route runtime: `createCopilotRuntimeHandler({ mode:
"single-route" })` paired with
`<CopilotKit useSingleEndpoint />`.
- Auth and identity fail closed in production by default (a per-request
user id is required and
the runtime is gated), with the multi-user path documented. Credentials
are vaulted by Arcade
and never reach the model.
## Related PRs and Issues
- Proposal: #5513
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked
Made with [Cursor](https://cursor.com)
## Summary
Fixes#5535.
`CopilotKitCore.setHeaders` was typed `Record<string, string>`, so there
was no type-safe way to clear a header like `Authorization` on logout.
`null` was a TS error, and an empty string leaves the header present
with a blank value.
## Change
- Widen `setHeaders` to `Record<string, string | null | undefined>` and
drop any `null`/`undefined` entry. A shared `normalizeHeaders` helper
enforces the same string-only invariant at both write paths (the
constructor and `setHeaders`).
- `setHeaders` stays a full overwrite, so clearing one header while
keeping the rest uses the spread pattern:
```ts
copilotkit.setHeaders({ ...copilotkit.headers, Authorization: token ?
`Bearer ${token}` : null });
```
- Update the `react-core` `AuthTokenSync` skill example to show the
logout/clear path, and warn that a header must not be managed via both
the `headers` prop and imperative `setHeaders` (the provider re-applies
its prop-derived headers as a full overwrite whenever its inputs
change).
## Tests
Added `packages/core` coverage: drop `null`/`undefined` keys,
empty-string preservation, overwrite-not-merge semantics, single-header
clear via spread, `onHeadersChanged` notification, and propagation to
local and remote (`ProxiedCopilotRuntimeAgent`) agents.
## Notes
- No API break: `Record<string, string>` is assignable to the widened
type, so existing callers are unaffected.
- The second commit syncs the plugin manifest version (`plugin.json` +
`marketplace.json` `plugins[0].version`) to `1.60.2` via `pnpm
sync:plugin-skills`. This drift pre-existed on `main` and surfaced in CI
only because this PR touches skill files; it is unrelated to the fix.
Adds a minimal Angular quick-start guide and wires Angular into the docs
**frontend picker** introduced in #5586. Connects directly to an AG-UI
agent via `HttpAgent`, no runtime required.
Closes
[OSS-252](https://linear.app/copilotkit/issue/OSS-252/add-angular-quick-start-guide).
### Angular in the frontend picker

- Guide lives at `content/docs/frontends/angular.mdx`, served at
`/angular` like the other frontends
- Registered `angular` in `frontend-options` (+ `SiAngular` logo),
`search-hrefs`, the search-index pages, and the typed reference-slug
record
- Not flagged early access (parity with Vue / React Native)
### Verified end-to-end
Built a fresh Angular 21 app, followed the guide verbatim, and got a
live streamed reply. shell-docs typecheck and the frontend unit tests
(8/8) pass.

Key specifics from that validation: install **`@copilotkit/angular`**
(the deprecated `@copilotkitnext/angular` is renamed), no TS7016
workaround needed (the package ships types), and pin `@angular/cli@21`
since `@latest` (v22) is outside the `19-21` peer range.
> Reference-docs link points at `/reference` for now; it should switch
to `/reference/angular` once the Angular reference PR (#5585) lands.
> The in-repo package is still `@copilotkitnext/angular`; the
`@copilotkit/angular` rename only landed on npm 2026-06-18. Renaming in
source is a separate follow-up.
_Screenshots live on the `assets/oss-252-angular-quickstart` branch to
keep this diff scoped._
## Summary
Every `showcase_promote.yml` dispatch has failed at the
`resolve-targets` job's "Generate SSOT artifact" step since 2026-06-18
(last green run `27790968900`), blocking ALL promotes including the
team's regular `shell-docs` promote.
**Root cause:** `emit-railway-envs-json.ts` (`oxfmtCanonical`, ~L212)
does `execFileSync(<repo-root>/node_modules/.bin/oxfmt, ...)`. The
`resolve-targets` step's `npm ci` runs in `showcase/scripts` only and
never installs the repo-root oxfmt binary → `spawnSync
.../node_modules/.bin/oxfmt ENOENT` → exit 1.
**Fix (approach a — explicit opt-out, fail-loud-correct):** The JSON
emitted on the `resolve-targets` / `promote` path is **ephemeral** —
`resolve-promote-targets.sh` parses it with jq and `bin/railway` reads
it in-memory to pick the promote target; it is **never committed**, so
oxfmt-canonical formatting is irrelevant there. Added an explicit
`EMIT_SKIP_OXFMT=1` env opt-out that returns the raw `JSON.stringify`
form, and set it on both ephemeral workflow steps. The
**committed-artifact path stays unchanged**: oxfmt is REQUIRED and fails
loud if absent (the committed `railway-envs.generated.json` must stay
oxfmt-canonical or `static_quality.yml`'s `oxfmt --check` auto-format
bot fires). This is opt-IN-to-skip, never silent-on-absence — the
fail-loud-discipline-correct shape.
Approach (b) (read the committed JSON) was rejected to preserve the
existing regenerate-from-SSOT-at-runtime guarantee; approach (c)
(install root oxfmt in that job) is heavier and unnecessary for an
ephemeral consumer.
## RED-GREEN proof (real failure surface, oxfmt binary absent)
**RED — reproduce the production failure** (`npm ci` in
`showcase/scripts` only; repo-root oxfmt absent, exactly as CI's
resolve-targets):
```
$ npx tsx emit-railway-envs-json.ts
Error: spawnSync /private/tmp/cpk-oxfmt/node_modules/.bin/oxfmt ENOENT
at oxfmtCanonical (.../emit-railway-envs-json.ts:212:5)
at serialize (.../emit-railway-envs-json.ts:221:10)
at main (.../emit-railway-envs-json.ts:241:16)
EXIT=1
```
**GREEN — ephemeral path now works** (oxfmt still absent):
```
$ EMIT_SKIP_OXFMT=1 npx tsx emit-railway-envs-json.ts --out=/tmp/ephemeral-out.json
wrote /tmp/ephemeral-out.json
EXIT=0
$ jq '.services | length' /tmp/ephemeral-out.json → 41
$ jq -r '.services[]|select(.probe.prod==true)|.name' ... → aimock, dashboard, docs, ...
$ jq '.closure.services | length' ... → 39
```
And the actual downstream consumer parses it and resolves the team's
`shell-docs` promote:
```
$ INPUT=shell-docs GENERATED=/tmp/ephemeral-out.json ... resolve-promote-targets.sh
services_csv=docs
closure_csv=pocketbase,dashboard,harness,docs
closure_plan=0:pocketbase,1:dashboard,1:harness,2:docs
EXIT=0
```
**GREEN — committed path still canonical AND still fail-loud** (oxfmt
PRESENT, default path, no env var):
```
$ npx tsx emit-railway-envs-json.ts # regenerate committed artifact
wrote .../railway-envs.generated.json
$ git diff --quiet showcase/scripts/railway-envs.generated.json; echo $? → 0 (byte-identical)
$ npx tsx emit-railway-envs-json.ts --check → railway-envs.generated.json is up to date. EXIT=0
```
Committed/default path STILL fails loud when oxfmt absent (NOT
weakened):
```
# oxfmt removed, NO env var:
$ npx tsx emit-railway-envs-json.ts --out=/tmp/x.json
Error: spawnSync .../node_modules/.bin/oxfmt ENOENT (exit 1, nothing written)
```
## Call-site enumeration
- `showcase_promote.yml` **resolve-targets** — `EMIT_SKIP_OXFMT=1` (this
fix). ✓
- `showcase_promote.yml` **promote** — `EMIT_SKIP_OXFMT=1` (this fix;
same ephemeral regen). ✓
- `static_quality.yml` committed-artifact `--check` — unset → oxfmt
REQUIRED, canonical guarantee preserved. ✓
- `resolve-verify-matrix.ts` (`showcase_deploy.yml`) — invokes the
emitter only `if (!existsSync(SSOT_JSON))`; the checkout always carries
the committed JSON, so the default (oxfmt) path is correct and
unchanged. ✓
## Test plan
- [x] RED reproduced on the real surface (ENOENT at
oxfmtCanonical←serialize←main).
- [x] GREEN: ephemeral path emits valid JSON + downstream resolve script
works.
- [x] GREEN: committed path regenerates byte-identical (empty git diff,
`--check` passes).
- [x] GREEN: committed/default path still fails loud when oxfmt absent.
- [x] 2 new `EMIT_SKIP_OXFMT` unit tests; full showcase/scripts suite
2037 passing.
- [x] Pre-push: oxfmt --check, oxlint, actionlint all clean on changed
files.
- [ ] CI green.
The showcase_promote.yml resolve-targets job runs
`emit-railway-envs-json.ts`, which shells out to the repo-root
`node_modules/.bin/oxfmt` to produce oxfmt-canonical JSON. That job's
`npm ci` runs in showcase/scripts only and never installs the root oxfmt
binary, so every promote dispatch died at "Generate SSOT artifact" with
`spawnSync .../node_modules/.bin/oxfmt ENOENT` (exit 1) — blocking ALL
promotes, including the team's regular shell-docs promote, since the last
green run on 2026-06-18.
The emitted JSON on the resolve-targets / promote path is EPHEMERAL: it
is parsed in-memory by jq (resolve-promote-targets.sh) and bin/railway to
pick the promote target and is NEVER committed, so oxfmt-canonical
formatting is irrelevant there. Add an explicit `EMIT_SKIP_OXFMT=1`
opt-out that returns the raw `JSON.stringify` form, and set it on both
ephemeral workflow steps.
The DEFAULT (committed-artifact) path is unchanged: oxfmt stays REQUIRED
and fails loud if the binary is absent, because the committed
railway-envs.generated.json must stay oxfmt-canonical or CI's
static_quality.yml `oxfmt --check` auto-format bot fires on the drift.
This is opt-IN-to-skip, never silent-on-absence.
Call sites of emit-railway-envs-json.ts:
- showcase_promote.yml resolve-targets — EMIT_SKIP_OXFMT=1 (this fix).
- showcase_promote.yml promote — EMIT_SKIP_OXFMT=1 (this fix).
- static_quality.yml committed-artifact `--check` — unset, oxfmt required.
- resolve-verify-matrix.ts (showcase_deploy.yml) — only invokes the
emitter when the committed JSON is absent; the checkout always has it,
so the default (oxfmt) path is correct and unchanged.
Tests: 2037 showcase/scripts tests pass; 2 new EMIT_SKIP_OXFMT unit tests
assert the skip path emits valid (raw) JSON; the existing oxfmt-canonical
golden tests still gate the committed path.
> **DRAFT / WIP — not reviewed, not ready to merge.** Checkpoint per
request. The mandatory 7-agent cr-loop + CI-green gate runs before this
leaves draft. LGP and ADK ship together in this PR.
## Problem (a false-D6 in both directions)
Declarative A2UI demos wire `a2ui.injectA2UITool: true`, so the response
is a rendered `render_a2ui` surface with **no assistant text bubble**.
The D6 conversation-runner's turn-completion gate required the assistant
**text** to stabilize — so on a working declarative demo the run
finished and the dashboard painted, but text never settled →
`waitForTurnComplete` timed out (`reason=text-unstable`) **before the
render assertion ran**.
Result: `langgraph-python:declarative-gen-ui` (the gold standard)
reported **false-RED while rendering correctly** (all 4 pills verified
live on staging), while `google-adk` reported **false-GREEN**.
## Fix
Opt-in `ConversationTurn.completeOnMount` (set only by
`d5-gen-ui-declarative.ts`). For those turns the text-stability
completion conjunct is **replaced** by a surface-mount predicate:
run-finished (sseOk) + a new assistant bubble + the expected declarative
testids **newly mounting**. A non-rendering surface now yields a new
`surface-missing` failure reason (truthful RED). Text-based demos are
byte-for-byte unchanged (opt-in, per-turn).
## Proof (both directions, live D6)
- RED (before): `text-unstable` timeout; dashboard text painted.
- GREEN (after): passes in ~5s; `buildDeclarativeAssertion` actually
runs and verifies testids mount for all 4 pills.
- INTEGRITY: forced a broken render (renamed testids) → test goes
**red** (`surface-missing`). Not "always green now."
- Unit: 89/89 + 3 new (green-on-mount, red-on-surface-missing).
## Scope: LGP + ADK (ship together) — both truthful GREEN
- **LGP** gold-standard cell: false-RED → truthful GREEN (all 4 pills
assert).
- **ADK** realignment: **test-only, complete.** The shared-script fix
auto-applies; verified all 4 ADK pills truthfully GREEN (each surface
mounts from baseline 0 via surface-mount completion). Prior false-green
closed; **no ADK backend gap**.
## Out of scope (someone else's problem)
This shared-script change re-evaluates **every** declarative-gen-ui cell
truthfully. Integrations beyond LGP/ADK that don't actually render will
flip to **truthful RED** — e.g. `langgraph-typescript` pill 2
(team-performance `declarative-data-table` doesn't mount). Those are
real per-demo render gaps for their owners; **not fixed here.**
## Follow-up (not in this PR)
The `render_a2ui` call returns a ~5.9 MB SSE for a ~2 KB surface (LGT
worse) — a separate runtime amplification concern in the
`injectA2UITool:true` middleware path.
## Before ready/merge
- [x] ADK empirical verdict — all 4 pills truthful GREEN, realignment
test-only, no backend gap
- [ ] mandatory cr-loop → zero findings
- [ ] CI green
setHeaders typed headers as Record<string, string>, so there was no
type-safe way to clear a header (e.g. Authorization on logout) — passing
an empty string left the header present with a blank value.
Widen the signature to Record<string, string | null | undefined> and drop
any entry whose value is null/undefined. setHeaders remains a full overwrite,
so clearing one header while keeping the rest is the spread pattern:
setHeaders({ ...copilotkit.headers, Authorization: null }). A shared
normalizeHeaders helper enforces the same string-only invariant at both
write paths (constructor and setHeaders).
Update the react-core AuthTokenSync skill example to show the logout/clear
path and warn that a header must not be managed via both the headers prop and
imperative setHeaders (the provider re-applies prop-derived headers as a full
overwrite when its inputs change). Also update the setHeaders reference
signature docs. Tests cover null/undefined stripping, empty-string
preservation, overwrite-not-merge semantics, single-header clear via spread,
subscriber notification, and propagation to local and remote
(ProxiedCopilotRuntimeAgent) agents.
Fixes#5535
Opts each declarative-gen-ui pill into the new `completeOnMount` turn
completion so these surface-rendering demos are gated on their expected
declarative testids mounting rather than assistant-text stability.