Skip to content

[pull] main from CopilotKit:main#727

Open
pull[bot] wants to merge 4514 commits into
bryanwills:mainfrom
CopilotKit:main
Open

[pull] main from CopilotKit:main#727
pull[bot] wants to merge 4514 commits into
bryanwills:mainfrom
CopilotKit:main

Conversation

@pull

@pull pull Bot commented May 2, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

@pull pull Bot locked and limited conversation to collaborators May 2, 2026
@pull pull Bot added ⤵️ pull merge-conflict Resolve conflicts manually labels May 2, 2026
jpr5 and others added 27 commits July 8, 2026 09:25
…nostics aren't dead code

Both entrypoints run under set -e. The tail `wait -n $AGENT_PID $NEXTJS_PID`
returns non-zero on the PRIMARY designed exit path (137 = size-gate/watchdog
SIGKILL of the agent tree, or an agent crash), so set -e aborted the script AT
that line — making EXIT_CODE=$?, the entire 'which process exited with code N'
diagnostic, and the final `exit $EXIT_CODE` dead code on exactly the
interesting exits. Capture the code with `EXIT_CODE=0; wait -n ... || EXIT_CODE=$?`
so the diagnostic and explicit exit run and preserve the exact code (incl. 137);
the container-restart path is unchanged.

Same class: langgraph's LANGGRAPH_SIZE_THRESHOLD_MB was used in
`[ "$DIR_SIZE_MB" -ge "$threshold" ]` with no numericity guard, so a
non-integer operator override made the test error and silently no-op the size
gate every cycle. Validate the threshold the same way DIR_SIZE_MB already is
(numeric case guard + 'size guard inactive' WARNING, then skip safely).
## Release monorepo v1.62.3

**Scope:** `monorepo` | **Bump:** `patch`

---

### How this release process works

1. **This PR was created automatically** by the "release / create-pr"
workflow.
   It bumped the `monorepo` packages to `1.62.3`
   and generated AI-enhanced release notes.

2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
   must pass before merging. This is the review gate.

3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.

4. **When this PR is merged**, the `release / publish` workflow
automatically:
   - Builds all packages
   - Publishes the `monorepo` packages to npm at version `1.62.3`
   - Creates git tag `monorepo/v1.62.3`
   - Creates a GitHub Release with the final release notes

### Before merging

- [ ] CI is green (tests, lint, types, build)
- [ ] Version bumps look correct
- [ ] Release notes are accurate (edit in Notion if a draft was created)

---

> **Do not merge until CI is fully green.** The full test suite runs
automatically on this PR.
…bpath

The package entry (@copilotkit/bot) re-exported runStateStoreConformance from
./testing/state-store-conformance, which imports vitest at module top-level.
An ESM re-export eagerly evaluates that module, so a bare
`import { createBot } from "@copilotkit/bot"` dragged vitest into every
consumer's runtime graph and threw ERR_MODULE_NOT_FOUND when vitest wasn't
installed (i.e. any production consumer).

- Drop the re-export from src/index.ts (entry is now vitest-free)
- Publish the conformance helper under the ./testing export subpath
- Declare vitest as an optional peerDependency (documents the /testing need)
- Update docs to import from @copilotkit/bot/testing

Names/behavior of the runtime API are unchanged; only the import path for the
test-only conformance helper moves.
run-demo.sh detaches everything except the Next.js dev server (docker
compose up -d, native Metal TEI via nohup/disown, then exec pnpm dev), so
Ctrl-C on the dev server leaves the docker stack and the host embedder
running. stop-demo.sh brings those leftovers down in one command.

Tears down, idempotently:
  - the Next.js dev server on :3000 (defensive; usually gone via Ctrl-C)
  - the docker compose stack (project banking-memory), containers only by
    default so a re-run reuses the built image + seeded data
  - the native Metal TEI on :7067 (Apple Silicon; the host process docker
    doesn't manage), SIGTERM then SIGKILL

Flags: --purge also drops volumes for a clean slate; --keep-tei leaves the
slow-to-warm embedder running when only bouncing the stack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bpath (#5875)

## Problem

`@copilotkit/bot`'s package entry re-exports `runStateStoreConformance`
from `./testing/state-store-conformance`, which does `import { describe,
it, expect, ... } from "vitest"` at module top-level. In ESM a static
re-export **eagerly evaluates** the re-exported module, so a plain:

```ts
import { createBot } from "@copilotkit/bot";
```

drags `vitest` into the consumer's runtime module graph and throws
`ERR_MODULE_NOT_FOUND: Cannot find package 'vitest'` for any consumer
that doesn't have vitest installed (i.e. every production consumer).
`vitest` is only a devDependency.

Surfaced while smoke-testing the package rename (OSS-438) — but it's a
pre-existing bug on `main`, independent of that rename.

## Fix

- **Drop the re-export from `src/index.ts`** → the package entry is now
vitest-free.
- **Publish the helper under a `./testing` subpath**
(`@copilotkit/bot/testing`) — test tooling lives off the runtime entry,
the standard pattern.
- **Declare `vitest` as an optional `peerDependency`** so consumers of
`/testing` get the right signal.
- **Docs** updated to `import { runStateStoreConformance } from
"@copilotkit/bot/testing"`.

Only the import path of the test-only conformance helper changes; the
runtime API is untouched.

## Verification
- Static import trace: the entry graph is 12 runtime modules, **none**
import vitest; every vitest importer is a `.test.js` (not in the graph)
or `testing/state-store-conformance.js` (only reachable via `/testing`).
- `@copilotkit/bot` builds; **143/143** tests pass; `publint` + `attw`
clean (the internal conformance test imports the helper by relative
path, unaffected).

## Coordination
Touches `packages/bot` on `main`. The OSS-438 rename PR (#5849) renames
this package to `@copilotkit/channels`; that PR re-derives from `main`
before merge, so it will absorb this fix automatically. If #5849 merges
first, this rebases onto `packages/channels` mechanically.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…5877)

## What

Adds `examples/showcases/banking/stop-demo.sh` — the teardown companion
to the existing `run-demo.sh`.

## Why

`run-demo.sh` detaches everything except the Next.js dev server:

- `docker compose up -d --wait` (detached stack)
- native Metal TEI via `nohup … & disown` (Apple Silicon only)
- `exec pnpm dev` (the only foreground process)

So Ctrl-C stops *only* the dev server and silently leaves the docker
stack (`banking-memory`) and the host embedder on `:7067` running. There
was no one-command way to bring those down. This script fills that gap
and mirrors `run-demo.sh`'s conventions (same `say`/`ok` helpers, same
header-comment style, idempotent).

## What it does

Tears down, idempotently, in order:

1. Next.js dev server on `:3000` (defensive — usually already gone via
Ctrl-C)
2. docker compose stack (project `banking-memory`), **containers only**
by default so a re-run of `run-demo.sh` reuses the built composite image
+ seeded Postgres
3. native Metal TEI on `:7067` (Apple Silicon; the host process docker
doesn't manage) — SIGTERM, then SIGKILL for anything that ignores it

## Flags

- `--purge` — also delete the docker volumes (postgres/redis/minio/tei
model cache) for a full clean-slate reset
- `--keep-tei` — leave the slow-to-warm native embedder running when
only bouncing the stack

## Testing

- `bash -n stop-demo.sh` — syntax clean
- `shellcheck stop-demo.sh` — clean, no warnings
- `./stop-demo.sh --help` renders the banner correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary

- Backport the website messaging from CopilotKit/website#398 into the
shell-docs Slack and Microsoft Teams frontend pages.
- Replace the stale waitlist/managed-only framing with "get early
access" copy that presents CopilotKit Enterprise Intelligence as the
self-hosted or cloud-hosted production layer around the open source Bot
SDK.
- Frame Slack and Teams as frontends for agents built on any harness or
framework, while reserving production-layer terminology for CopilotKit
Enterprise Intelligence.
- Address browser review annotations on both pages: remove filler in the
opener, avoid setup-heavy lead copy, use "open source" without a hyphen,
add the full CopilotKit Enterprise Intelligence name to the CTA titles,
and keep CTA telemetry surfaces intact.
- Update the shell-docs nav test expectation so it matches the current
root IA, where Threads lives under Build Chat UIs rather than the
generated Intelligence Platform section.

## Validation

- `npm run lint` from `showcase/shell-docs` (passes with existing
warnings)
- `npm run typecheck` from `showcase/shell-docs`
- `npm run test` from `showcase/shell-docs`
- `npm run build` from `showcase/shell-docs`

## Notes

- Hydrated Git LFS assets locally with `git lfs pull` so the shell-docs
public asset tests could read real PNG bytes.
…wrapped-PID kill through guarded tree-kill

CLASS 1 (guard silently disabled by a bad numeric override): add a reusable
_require_int validator and run it at startup over EVERY operator-overridable
numeric knob in both entrypoints (size threshold/interval, startup grace,
health-probe interval, strike limit). A non-integer/empty override now WARNs
and falls back to the documented default instead of breaking a sleep/loop/
arithmetic test. Closes instance #3 (LANGGRAPH_SIZE_CHECK_INTERVAL='60s'
killing the size-monitor loop on its first iteration).

CLASS 2 (wrapped-PID orphan + kill-0 footgun): route the cleanup() NEXTJS_PID
kill through _kill_agent_tree (it is process-sub-wrapped like the agent, so a
bare kill orphaned the real Next.js node server holding $PORT across redeploy).
Harden _kill_agent_tree and _agent_descendants to refuse a PID that is empty,
non-numeric, 0, or 1 (fail closed), making kill -9 0 / kill -9 1 structurally
impossible. Remove the ${AGENT_PID:-0} sentinel in the --check-size-once seam;
skip with a warning when AGENT_PID is unset instead of defaulting to 0.

Shared helper code kept byte-identical between the two entrypoints.
…ect 0 and leading-zero/octal)

The _require_int validator in the langgraph-typescript and strands-typescript
entrypoints accepted '0' and leading-zero/octal forms like '010'/'08'. Operator
typos on any numeric knob then broke a guard:
- SIZE_THRESHOLD_MB=0 kills the agent on cycle 1 (instant restart loop)
- HEALTH_STRIKE_LIMIT=0 kills on first probe miss
- SIZE_CHECK_INTERVAL=0 / HEALTH_CHECK_INTERVAL=0 busy-spin on 'while sleep 0'
- '010' is read as OCTAL (8) in arithmetic; '08'/'09' abort under set -e

Tighten the predicate to accept only a positive integer with no leading zero
([1-9][0-9]*). Invalid values keep the existing fail-safe behavior: WARN and
fall back to the documented default. Helper stays byte-identical across both
files.
… review findings

Correctness:
- C1 create-bot: start() is now idempotent — a second start() no longer
  re-resolves the backend / rebuilds Transcripts+Telemetry+ActionRegistry or
  re-connects adapters (which would wipe MemoryStore state and double-bind real
  adapters). stop() clears the flag so start→stop→start is still a real restart.
- S1 bot-slack ingress: a threaded reply that @-mentions the bot is now skipped
  (app_mention handles it) so the managed path no longer double-responds. Matches
  both the plain <@U…> and labeled <@U…|handle> mention forms.
- S2 runtime: CopilotSseRuntime throws if `bots` is passed without intelligence
  instead of silently dropping them (guards a JS/as-any caller past the type).
- S3 bot-intelligence: startManagedBots rolls back — stops already-started bots —
  when a later bot fails to start, instead of leaking listeners/connections.

Lower:
- S4 ingress: stripMentions handles the labeled <@U…|handle> form; DM turns strip
  mentions too (parity with app_mention/thread_reply).
- S5 bot-intelligence: bot-name uniqueness is now case-insensitive.
- S6 runtime: fail fast at construction when a declared bot has no name (full
  shape/uniqueness validation stays at the activation seam — assertValidBotNames —
  because it can't cross into this CJS package from pure-ESM bot-intelligence).
- S7 bot-intelligence: buildActivationMetadata throws on a nameless bot instead of
  silently filtering it out of the activation set.
- S8 bot-intelligence: startManagedBots warns on an empty bots array.
- M1 intelligence-adapter: the per-turn egress seq Map entry is deleted after each
  turn so it can't grow unbounded over a long-running bot.
- M2 intelligence-adapter: an inbound file that fails to fetch degrades to a
  fail-visible text note instead of being silently dropped from model context.
- I2 contracts: dropped the now-dead `duplicate_skipped` RenderAccepted value
  (Intelligence returns duplicate_accepted or a 409 conflict).

Changelog (C2/C3, intended behavior after moving init into start()):
- bot.transcripts now throws before start() (was a concrete property).
- telemetry `oss.bot.configured` now fires at start() rather than construction, so
  a constructed-but-never-started bot no longer emits it.

Not addressed here (cross-repo, tracked on the Intelligence side):
- I1 realtime render-event kind:"file" clause on the gateway validator.
- I3 lease-token fencing on the render-accept path.
…pk-drawer-reserved-width [ENT-1051]

Read grid-template-columns' first track from var(--cpk-drawer-reserved-width, 320px)
so when the drawer collapses on desktop (it sets the var to 0) the reserved
column collapses and the chat reclaims the space — instead of leaving an empty
placeholder column. Mobile (single-column) is unchanged.
…er on desktop [ENT-1051]

The floating launcher/collapsed cluster is fixed at the top-left corner. Below
1024px it always shows (already cleared via max-lg:pl-24); on desktop it appears
only when the drawer is COLLAPSED. Drive the header's left padding off
--cpk-drawer-reserved-width (0px when collapsed, 320px default otherwise) so the
logo starts at ~6rem when collapsed and pl-6 when expanded — no overlap. No-op
on current packages (var never set → stays pl-6).
…[ENT-1051]

The 7px/16px launcher inset was tuned for the mobile off-canvas launcher; on
desktop it leaked onto the collapsed cluster. Move it into the mobile media
query so desktop-collapse uses the element's own 24px gutter default.
… match the drawer [ENT-1051]

- Mobile header: max-lg:pb-0 -> pb-4 so chat content clears the fixed launcher/
  toggle strip instead of butting right under it (no boundary).
- Chat/App ModeToggle: rounded-full -> rounded-[4px] container + rounded-[2px]
  buttons, matching the drawer's 4px radius cap so the header controls are
  visually consistent.
…her left matches right gutter, logo/toggle on one line [ENT-1051]

- ModeToggle: move left (right-[72px]) so the top-right inspector FAB no longer
  covers the App segment; grow to 46px (lg:min-h) + center on the logo line
  (top-6) to match the launcher; keep the 4px corners.
- Launcher: left gutter -> 16px to match the right-side controls' inset.
- Logo: pt-7 so it centers on the same line as the launcher + toggle.
- ModeToggle: one style on both breakpoints (top-4/right-4 = 16px gutter,
  46px min-height, 4px corners); symmetric p-1.5 + fixed 20px button leading
  so the selected pill has an even gap on all four sides (was tight L/R vs T/B).
- Launcher: uniform 16px gutter (top + left) on both breakpoints so it mirrors
  the toggle; drop the mobile-only 7px override.
- Logo: centered on the launcher/toggle middle line (pt-[23px]); wordmark
  padding normalized so its height matches on both breakpoints.
- Inspector FAB: sits beneath the toggle, gap = the 16px top gutter (one rule,
  no media query, since the toggle is identical across breakpoints).

Net: launcher, logo, toggle share center-y; launcher + toggle are both 46px;
the FAB tucks under the toggle with a matching gap; the selected toggle pill is
evenly inset.
The 1.62.3 release publishes the CopilotThreadsDrawer redesign (web-components +
react-core wrapper) and the stateless /suggest feature. Bump the 15 integration
examples that consume the drawer from 1.62.2 -> 1.62.3 (package.json + lockfiles)
so they pick up the released packages alongside this branch's example CSS.

Validated: langgraph-js runs on the published 1.62.3 (no local links) — the
redesigned drawer renders (New Conversation, Recent Conversations, filter funnel,
desktop collapse toggle, per-row kebab), threads are licensed, and a real agent
message round-trips.
…ads list scrolls internally [ENT-1051] (post-release follow-up) (#5828)

## What

Applies the threads-drawer grid fix to all 15 integration examples: adds
`grid-template-rows: minmax(0, 1fr)` to each example's `.layout` so the
drawer's threads list scrolls **internally** (pinned header + New
Conversation) instead of the whole page growing to content height.

Without this, `height: 100dvh` on a grid whose rows aren't bounded lets
the row size to content, so the list can't scroll within the drawer and
the delete-confirm dialog centers against a content-tall root.

## Why this is a separate PR

These are **example** changes that consume **published** `@copilotkit/*`
packages. They were pulled out of the drawer-redesign PR (#5823) so that
PR stays scoped to the packages. This one lands **after** the redesigned
drawer is released.

## ⛔ Blocked / TODO before marking ready

- [ ] Drawer redesign PR #5823 merged
- [ ] Lockstep release cut (web-components + react-core + vue + angular)
- [ ] Bump each example's `@copilotkit/*` dependency to the new
published versions **in this PR** (currently only the CSS is here)
- [ ] Re-verify one example end-to-end against the released packages

## Testing

- Grid fix verified live during the redesign work (langgraph-js against
a hosted Intelligence backend): threads list scrolls internally, header
+ New Conversation pinned, delete-confirm renders correctly.
- Example dependency bumps + a fresh end-to-end pass will be
added/redone here once the release is published.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…ce-delivered events (OSS-360/361) (#5761)

## Summary

Lets the `@copilotkit/bot` SDK run from **Intelligence-delivered
events** without a second programming model, and adds the runtime `bots`
declaration API. A managed event (delivered by Intelligence) runs the
*same* customer handlers, tools, context, commands, Bot UI, and agents
as local/custom adapters — the managed path is "just another
`PlatformAdapter`," fed by injected transports.

This is the **OSS / SDK slice** of the Hosted Managed Bots work. The
credentialed transports (Realtime Gateway, Connector Outbox) and the
frozen shared contracts live elsewhere (see *Out of scope*); this PR
ships the seams they plug into, fully runnable headless.

Relates to **OSS-360** (runtime bots API), **OSS-361** (run the SDK from
Intelligence events), **OSS-363** (Slack render/codec reuse).

## What's in here

- **`intelligenceAdapter()` bridge** (`@internal`, not publicly
documented) — implements `PlatformAdapter` over two injected transports:
`DeliverySource` (inbound) + `EgressSink` (outbound). Ingress →
`onTurn`/`onCommand`/`onInteraction`/`onThreadStarted`/`onReaction`; ack
on success / nack on throw (at-least-once). Egress emits generic
operations carrying `BotNode[]` IR with **deterministic ids**
(`turnId:seq`, reset per turn) so a redelivered turn reproduces the same
ids for the Connector Outbox to dedupe. Idempotency lives at egress, so
the managed path skips ingress dedup (`skipIngressDedup`) — a redelivery
re-runs rather than being dropped.
- **Runtime `bots` API** — `new CopilotRuntime({ intelligence, bots })`,
accepted by TypeScript **only when `intelligence` is configured**
(discriminated union). `createBot({ name })`; `startManagedBots()`
validates names (required, identifier-style, unique — fail-loud), builds
activation metadata, and wires each bot to its resolved transport.
- **`PlatformCodec` seam** + Slack egress codec (`slackCodec`) composing
the existing pure `renderSlackMessage`, so IR→native rendering is shared
(no Bolt/creds) instead of duplicated.
- **Backwards-compatible SDK foundations**: `bot.addAdapter()` +
optional `adapters`, deferred backend resolution at `start()` with
`stateStore`-provider precedence (+ multi-provider warning),
`bot.transcripts` throws pre-start, optional
`eventId`/`turnId`/`deliveryId` on ingress + handler context. Existing
`createBot` callers and every `PlatformAdapter` implementer are
unaffected.
- **In-memory transports + fixture tests** — the full dispatch path
(envelope in → handler runs → egress op out) runs with zero
Slack/Intelligence/network.

## Out of scope (external / separate tickets)

- **Realtime Gateway + Connector Outbox transports** — implemented in
the closed-source repo against the `DeliverySource`/`EgressSink`
interfaces shipped here.
- **Shared contracts freeze (OSS-377)** — consumed here via a minimal,
isolated placeholder (`managed/contracts.ts`, marked `TODO(OSS-377)`);
swaps in via one import change.
- **OSS-363 ingress normalization** — the egress codec is done;
extracting the pure Slack event→neutral mapping out of the Bolt listener
(so local + Intelligence ingress share it) is the remaining, higher-risk
half and is left to that ticket (`TODO(OSS-363)`).

## Testing

TDD throughout (RED→GREEN per behavior). New: managed adapter
dispatch/ack-nack/ids/run-renderer/exclusivity, all-kinds routing, name
validation + metadata + lifecycle, runtime `bots` option, Slack codec.
Full suites green: `bot` 147, `bot-slack` 256, `runtime` 1574. All
builds typecheck (`bot`/`bot-slack`/`bot-discord`/`runtime`);
oxlint/oxfmt clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Unstale #5814 onto main after the #5761 stack landed. Conflict:
- intelligence-adapter.ts: kept #5814's interaction dispatch + supportsBlockingChoice
  alongside main's #5761 review fixes (seq.delete cleanup, file fetch-failure note).
- Drop the resurrected inline buildContentParts method from the adapter; the
  turn path now uses the extracted ./content-parts.js helper (#5814's refactor).
- Port main's fail-visible file-fetch behavior into content-parts.ts so a file
  that can't be retrieved becomes a short note in both the live-turn and
  history-seeding paths (they share the helper).
- Update the state-store conformance import to the @copilotkit/bot/testing
  subpath (main moved it there to keep vitest out of consumers' runtime graph).
…restart actually fires (#5874)

## What & why

Two showcase agent containers (**langgraph-typescript**,
**strands-typescript**) could enter a *running-but-dead* state: Railway
showed the service `● Online` while `/api/health` returned **HTTP 502**.
This took down all 36 LGT dashboard cells on staging **and** prod
(prod's `.langgraph_api` had crossed the 200 MB size-watchdog
threshold).

**Root cause:** the agent is launched via process substitution (`... &>
>(awk …) &`), so `$AGENT_PID` (`=$!`) is the **wrapper subshell**, not
the real `npm`→`node` server that holds the port. Every watchdog/cleanup
did a bare `kill -9 $AGENT_PID`, which reaped only the subshell and
**orphaned the real server** (reparented to PID 1, still bound to the
port). The watchdog's "kill agent → container restart → boot-purge"
contract therefore never fired: the frontend kept proxying to a dead
agent → 502 forever.

## Fixes (each with local red-green on the real entrypoint in
`node:22-slim`)

1. **cleanup() EXIT trap** → routes through `_kill_agent_tree` (was
orphaning the agent on every SIGTERM/redeploy).
2. **`_kill_agent_tree`** → `/proc`-based tree-kill with a bounded
re-scan (root killed last) so mid-walk forks can't escape; refuses PID ≤
1 (fail-closed).
3. **size-watchdog** hardened against non-numeric `du` and transient
errors (no silent gate-disable, no permanent loop death).
4. **strands health-watchdog** → 180 s startup-grace window (parity with
langgraph); the now-effective kill would otherwise loop a slow cold
start.
5. **`wait -n` under `set -e`** → capture exit code so the restart
diagnostic isn't dead code on the primary (137) path.
6. **structural:** one `_require_int` validator over *every*
operator-overridable numeric knob (fail-safe to default), and **every**
wrapped-PID kill (incl. `NEXTJS_PID`) routed through the guarded
tree-kill; dangerous `${AGENT_PID:-0}` sentinel removed.
7. **`_require_int`** requires a positive integer (rejects `0` and
leading-zero/octal).

## Incident status
Staging **and** prod LGT were restored immediately via redeploy
(boot-purge cleared the oversized state) — both `/api/health` → 200.
This PR stops the recurrence.

## Review
Converged through a 5-round unbiased review-fix loop (1 + 4
confirmation), zero mandatory findings at close, all load-bearing guards
independently re-verified. `bash -n` + shellcheck (`-S warning`) clean;
170/170 shell bats pass.

## Follow-up (tracked, separate PR — non-load-bearing)
`_require_int` upper-bound clamp (LOW arith-overflow, needs a 20+-digit
value); a stale `cleanup()` comment; size-guard unarmed during the
startup-grace window; SIZE_PID trap-registration micro-window;
diagnostic label on near-simultaneous exit; cosmetic log nits; startup
readiness `sleep 3`+`kill -0` probes the wrapper subshell; no dedicated
Next.js frontend watchdog.
…hannels* (OSS-438)

Renames the Bots SDK to the Channels SDK. Names only — no behavior change.

- 8 packages @copilotkit/bot* -> @copilotkit/channels* (git mv dirs, names,
  workspace: cross-deps). Now includes @copilotkit/bot-intelligence ->
  @copilotkit/channels-intelligence (landed on main via #5761; unpublished, so
  renamed fresh with the family).
- release.config.json scope keys + versionSource; ReleaseScope union;
  canary/stable-release/publish-release scope dropdowns; verify script
- examples/slack (Kite) + examples/teams: deps, jsxImportSource, imports
- showcase/shell-docs: content dirs docs/bots->docs/channels and
  reference/bot->reference/channels, nav registry, redirects

createBot and other API names unchanged. Old @copilotkit/bot* to be deprecated
after the new packages publish (bot-intelligence was never published).

Re-derived onto latest main (was conflicting after #5761 landed).

Refs OSS-438
An unknown delivery kind previously fell through the dispatch switch as a
silent no-op (dispatch acks on resolve, so it would ack an unhandled delivery
as processed) and mapDeliveryToEnvelope coerced it into an empty turn. Add
matching `never` exhaustiveness guards to both so a future wire kind throws
instead of being silently swallowed.
…agent_descendants

The /proc PPID walk forked an awk process for every entry in the process
table on every scan pass. Replace the `echo "${stat##*) }" | awk '{print $2}'`
pipeline with the `read` builtin, which word-splits the post-comm remainder
("STATE PPID PGRP …") on IFS and captures the 2nd field with no subprocess.
Byte-identical across the langgraph-typescript and strands-typescript
entrypoints. Non-behavioral; bash -n + shellcheck --severity=warning clean.
jpr5 and others added 30 commits July 13, 2026 17:36
…pure-ESM boot

The harness runs as pure Node ESM (package.json "type":"module", built
with tsc moduleResolution:"bundler" which preserves extensionless import
specifiers at emit, launched via node dist/orchestrator.js). Under pure
Node ESM, relative import specifiers must carry the .js extension — a
convention the harness already honors everywhere (79/79 relative imports
in orchestrator.ts end in .js).

The relocated shared/cell-model fold broke that convention: cell-model.ts,
live-status.ts, staleness.ts, and the equivalence fixtures/test imported
sibling modules extensionless ("./live-status", "./staleness", etc). tsc,
vitest, and tsx all resolve those fine, so it built and tested green — but
at container boot node threw ERR_MODULE_NOT_FOUND on
dist/shared/cell-model/live-status and crash-looped the orchestrator,
breaking the staging auto-deploy.

Add the .js extension to every offending relative import to match the
harness convention. Minimal fix — no tsconfig change.
…t crash-loops

CI missed the extensionless-import regression because tsc (bundler
resolution), vitest, and tsx all resolve extensionless relative
specifiers fine — no existing step ever ran the real node dist module
graph, which is what the container actually does at boot.

Add a boot-smoke to the Validate Showcase job (already gated on
showcase/harness/**): after building the harness dist, load
dist/orchestrator.js via a node import() and fail hard on
ERR_MODULE_NOT_FOUND. A later runtime error from missing env/PocketBase
is expected and passes — only a module-resolution failure reddens the
build. Verified red-green: the guard exits 1 on the pre-fix
extensionless imports and 0 once the .js extensions are added.
…odule-resolution error class

The boot-smoke step only treated ERR_MODULE_NOT_FOUND as failure, so other
module-resolution regressions (ERR_UNSUPPORTED_DIR_IMPORT,
ERR_PACKAGE_PATH_NOT_EXPORTED, ERR_UNKNOWN_FILE_EXTENSION,
ERR_INVALID_MODULE_SPECIFIER) were swallowed as BOOT_OK/exit 0 — the very
class of bug this gate exists to catch could slip through. It also had no
process.exit(0) on the success/expected-error paths and no bounded timeout,
so a future open handle at import time could hang node -e to the job's
25-minute ceiling.

- Broaden the failure condition to a MODULE_RESOLUTION_CODES set (any of the
  five codes => BOOT_FAIL, exit 1). Non-module-resolution runtime errors
  (e.g. the HARNESS_ROLE env guard, no such code) stay BOOT_OK/exit 0.
- Add explicit process.exit(0) on both the success and expected-error paths.
- Wrap the node invocation in `timeout 120s` (non-zero on timeout => step
  fails) and add step-level timeout-minutes: 5.

Red-green proof (extracted guard logic vs synthetic modules): current logic
passes ERR_UNSUPPORTED_DIR_IMPORT / ERR_PACKAGE_PATH_NOT_EXPORTED at exit 0
(RED gap); hardened logic fails all five codes at exit 1, keeps benign
runtime error at exit 0, and against the real built dist/orchestrator.js
reports BOOT_OK and exits promptly (779ms, no hang).
…egress fix)

Add an env-scoped `internalDomain` (showcase-aimock.railway.internal) to the
aimock SSOT entry in both envs and emit it as `internalDomains` in the
generated JSON. Railway bills public *.up.railway.app traffic as egress even
intra-project, while *.railway.internal private networking is free and
env-scoped. aimock is ~89% of showcase egress; routing the ~20 demo backends'
LLM traffic at the private host over http://showcase-aimock.railway.internal:4010
eliminates the billed path. The public `domain` is retained for health probes.

Serviceref host resolution + assertions to follow in subsequent commits on
this branch.
ssot_target_host now prefers the env-scoped internalDomains host over the
public domains host, so the Stage-2 (U5) serviceRef assertion expects demo
backends' OPENAI_BASE_URL/etc. to point at
http://showcase-aimock.railway.internal:4010 (free intra-env networking)
rather than the billed public egress host. Non-aimock targets (no
internalDomains) fall back to their public host unchanged.

Red-green: reverting the resolver makes the three new U5 tests fail (public
host asserted); restoring makes them pass. Full Ruby spec suite green (184
runs, 715 assertions, 0 failures).
…alert text

The aimock-wiring probe matches on hostname, so it needs no code change for
the private-networking migration. Add a discriminating test pair proving the
internal host (http://showcase-aimock.railway.internal:4010, with :4010 port
and /v1 suffix) resolves green while a demo still on the public egress host
goes red. Update the aimock-wiring-drift.yml Fix text to point operators at
the private host instead of the public production URL.
…e classifier

The boot-smoke gate classified pass/fail using only the top-level `e.code`.
A module-resolution error that arrives WRAPPED — nested in `e.cause`
(possibly a chain), bundled inside an `AggregateError` (`e.errors[]`), or
rethrown without preserving `.code` at the top — showed no code to the
`MODULE_RESOLUTION_CODES.has(e.code)` check and was misclassified as
BOOT_OK, defeating the gate.

Add a `collectErrorCodes` helper that gathers every code reachable from
the thrown error: the error itself, its cause chain (recursively), and any
AggregateError members (recursively), with a depth cap to bound cause
cycles. If ANY collected code is a module-resolution code -> BOOT_FAIL /
exit 1. Purely additive to the FAIL set: direct top-level codes still
redden, and a benign non-resolution runtime error (e.g. the
`HARNESS_ROLE must be set` guard, which carries no such code anywhere)
still passes as BOOT_OK / exit 0. The `timeout 120s` wrapper,
`timeout-minutes: 5`, and success/expected-error `process.exit(0)` are
unchanged.

Local red-green (classifier extracted to a temp file, driven against
synthetic errors):
- RED (top-level-only): wrapped cause -> BOOT_OK exit 0 (swallowed);
  AggregateError member -> BOOT_OK exit 0 (swallowed).
- GREEN (hardened): wrapped -> exit 1; aggregate -> exit 1; direct
  ERR_MODULE_NOT_FOUND -> still exit 1; benign ERR_INVALID_ARG_TYPE and
  HARNESS_ROLE error -> BOOT_OK exit 0; real built dist/orchestrator.js ->
  BOOT_OK exit 0 in <200ms (prompt exit, no hang).
…r fails

The boot-smoke previously failed only when the thrown error carried a
module-RESOLUTION code (ERR_MODULE_NOT_FOUND + siblings, walked through
the cause chain / AggregateError members) and passed everything else.
That defaults-to-pass on module-EVALUATION crashes — a top-level throw,
an await-rejection, a bad named binding, or a SyntaxError — none of which
carry a resolution code, so a real boot-crashing regression of that class
would ship green.

The smoke runs `node -e "import('./dist/orchestrator.js')"` with
process.argv[1] UNSET, so bootFleet() (the env/PocketBase validation that
legitimately throws) never runs — only the module graph is linked and
evaluated. A clean build therefore loads with no thrown error, so ANY
error thrown by import() here is a boot regression and must fail the gate.

Now: any rejection -> BOOT_FAIL / exit 1 (resolution AND
evaluation/link/binding/syntax/top-level-throw). Successful load ->
BOOT_OK / exit 0. The collectErrorCodes cause/AggregateError walk is
retained ONLY to label the failure ("module-resolution failure" vs
"boot/evaluation failure") — both exit 1, richer diagnostics preserved.
Kept process.exit(0) on success, the timeout 120s wrapper, and
timeout-minutes: 5 (a hang still fails).

No-false-red proof: built the real harness dist and ran the strict guard
against the real dist/orchestrator.js under node -e (argv[1] unset) —
BOOT_OK, exit 0, ~0.28s, no hang, confirming a clean graph loads without
throwing and the strict guard does not false-red real CI.
…ts (#5952)

## What broke

The showcase control-plane harness crash-looped at boot on `origin/main`
HEAD, breaking the staging auto-deploy:

```
BOOT_ERR ERR_MODULE_NOT_FOUND Cannot find module '.../dist/shared/cell-model/live-status' imported from .../dist/shared/cell-model/cell-model.js
```

## Root cause

The harness ships as **pure Node ESM**:
- `showcase/harness/package.json` has `"type": "module"`
- build is `tsc -p tsconfig.build.json` with `moduleResolution:
"bundler"`, which **preserves extensionless import specifiers at emit**
(it does not rewrite `./live-status` → `./live-status.js`)
- the Docker `CMD` and the `start` script both run `node
dist/orchestrator.js`

Under pure Node ESM, relative import specifiers **must** carry the `.js`
extension. The harness already honors this convention everywhere —
`orchestrator.ts` has 79 relative imports, 79/79 ending in `.js`.

The relocated `showcase/harness/src/shared/cell-model/` fold broke the
convention: `cell-model.ts`, `live-status.ts`, `staleness.ts`, and the
equivalence fixtures/test imported their siblings extensionless
(`"./live-status"`, `"./staleness"`, etc). `tsc`, `vitest`, and `tsx`
all resolve extensionless specifiers fine, so it built green and passed
every test — then `node dist/orchestrator.js` threw
`ERR_MODULE_NOT_FOUND` at boot and crash-looped.

## The fix

Add the `.js` extension to every offending relative import in the
cell-model fold, matching the harness convention. **No tsconfig change**
(deliberately not switching to `nodenext`) — the minimal,
convention-matching fix is the extensions.

Files fixed (7 relative import specifiers across 5 files):
- `cell-model.ts` — 4 specifiers (`./live-status` ×3, `./staleness` ×2
counting the re-export)
- `live-status.ts` — `./format-ts`, `./staleness`
- `staleness.ts` — `./live-status`
- `cell-model.equivalence-fixtures.ts` — `./live-status` ×2,
`./cell-model`, `./staleness`
- `cell-model.equivalence.test.ts` — `./cell-model`,
`./cell-model.equivalence-fixtures`

The shell-dashboard re-export shims
(`showcase/shell-dashboard/src/lib/{cell-model,live-status,staleness,format-ts}.ts`)
were intentionally **left unchanged**: that package is a Next.js build
(not `node dist`) and uses extensionless relative imports as its own
convention, including reaching into harness `src`. Adding `.js` there
would break it.

## Local RED / GREEN proof

Same `node dist/orchestrator.js` boot probe, before and after the fix.

**RED** (unmodified branch code, after `pnpm --filter
@copilotkit/showcase-harness build`):
```
BOOT_ERR ERR_MODULE_NOT_FOUND Cannot find module '/…/showcase/harness/dist/shared/cell-model/live-status' imported from /…/showcase/harness/dist/shared/cell-model/cell-model.js
```

**GREEN** (after the fix, rebuilt):
```
BOOT_OK
```
The orchestrator module graph now loads fully — `ERR_MODULE_NOT_FOUND`
is gone.

## Docker boot result

Built the harness image locally from repo root (`docker build -f
showcase/harness/Dockerfile -t harness-esm-test .`) — build succeeded
(exit 0). Ran the container (`docker run harness-esm-test`, no env
supplied). It did **not** crash-loop on module resolution: the full ESM
module graph loaded and boot reached `bootFleet` in `orchestrator.js`,
then exited cleanly on the expected missing-env application error — **no
`ERR_MODULE_NOT_FOUND`**:

```
{"level":"error","msg":"showcase-harness.boot-failed","err":"HARNESS_ROLE must be set to one of: control-plane, worker (got: <unset>). ...","stack":"Error: HARNESS_ROLE must be set ...
    at resolveFleetRoleConfig (file:///app/dist/fleet/role-config.js:72:15)
    at bootFleet (file:///app/dist/orchestrator.js:3680:20)
    at file:///app/dist/orchestrator.js:3762:5
    at ModuleJob.run (node:internal/modules/esm/module_job:343:25)"}
```

Every `dist/` module resolved (note
`file:///app/dist/fleet/role-config.js` and
`file:///app/dist/orchestrator.js`); the crash was the intended
env-validation guard, exactly the "later runtime error is acceptable"
case. On pre-fix code this same container would have thrown
`ERR_MODULE_NOT_FOUND` at import time and crash-looped.

## CI regression guard

CI missed this because `tsc` (bundler resolution), `vitest`, and `tsx`
all resolve extensionless specifiers — **no existing CI step ever ran
the real `node dist` module graph** that the container boots.

Added a **harness ESM boot-smoke** step to the `Showcase: Validate` job
(already gated on `showcase/harness/**`, a required PR check): after
building the harness dist, it loads `dist/orchestrator.js` via `node
import()` and fails hard on `ERR_MODULE_NOT_FOUND`. A later runtime
error from missing env/PocketBase is expected and passes — only a
module-resolution failure reddens the build.

**Guard red-green** (the guard's own proof, run locally against both
code states):
- **pre-fix** (extensionless imports): `BOOT_FAIL: ERR_MODULE_NOT_FOUND
…/dist/shared/cell-model/live-status` → exit **1** ✅ caught
- **post-fix** (with `.js`): `BOOT_OK: orchestrator module graph loaded`
→ exit **0** ✅ passes

## Verification

- `pnpm typecheck` / `tsc --noEmit`: clean
- `cell-model.equivalence.test.ts`: 7/7 pass
- lefthook lint + commitlint: pass on both commits

Do not merge — merge is user-gated after code review.
…egress fix) (#5953)

## Summary

Routes the ~20 showcase demo backends to **aimock** (the record/replay
LLM proxy) over Railway **private networking** (`*.railway.internal`)
instead of aimock's **public** `*.up.railway.app` host.

Railway bills traffic to a public domain as **egress even
intra-project**, while `*.railway.internal` private networking is
**free** and **env-scoped**. The 240-concurrent-browser harness fleet
drives every demo continuously, so every LLM SSE stream from aimock back
to a demo backend is currently billed egress.

- aimock ≈ **89% of showcase egress**, ≈ **92% of the 13TB→78TB/mo
increase**.
- Estimated impact: avoids the ≈ **$602/mo → $3,856/mo** growth on the
aimock path.

## Change (config-only, reversible; SSOT-driven)

1. **SSOT** (`showcase/scripts/railway-envs.ts`): add an env-scoped
`internalDomain: "showcase-aimock.railway.internal"` to the aimock entry
in **both** envs. The public `domain` is **kept** (health probes /
external reachability).
2. **Emitter** (`showcase/scripts/emit-railway-envs-json.ts`): emit
`internalDomains` (additive, after `domains`) into the generated JSON.
Every non-aimock service keeps its frozen shape.
3. **Generated JSON** regenerated (oxfmt-canonical; 4-line additive
diff, only the aimock entry).
4. **Promote preflight** (`showcase/bin/railway`): `ssot_target_host`
now **prefers** the private `internalDomains[env]` over the public
`domains[env]`, so the Stage-2 (U5) serviceRef assertion requires demo
backends' `OPENAI_BASE_URL`/etc. to point at the private host.
Non-aimock targets (no `internalDomains`) fall back to their public host
unchanged.
5. **Harness** wiring probe needs **no code change** (it matches on
hostname); added a discriminating test pair + updated the drift-alert
Fix text to the private host.

**Target:** aimock binds `0.0.0.0:4010` (per
`showcase/aimock/RAILWAY.md`); demo backends resolve to
`http://showcase-aimock.railway.internal:4010`.

Deployed env vars, both envs (before → after):

| key | before (public, billed egress) | after (private, free) |
|---|---|---|
| `OPENAI_BASE_URL` | `https://<aimock>.up.railway.app/v1` |
`http://showcase-aimock.railway.internal:4010/v1` |
| `ANTHROPIC_BASE_URL` | `https://<aimock>.up.railway.app` |
`http://showcase-aimock.railway.internal:4010` |
| `GOOGLE_GEMINI_BASE_URL` | `https://<aimock>.up.railway.app` |
`http://showcase-aimock.railway.internal:4010` |
| `AIMOCK_URL` | `https://<aimock>.up.railway.app` |
`http://showcase-aimock.railway.internal:4010` |

`<aimock>` = `aimock-staging` (staging) / `showcase-aimock-production`
(prod). `railway.internal` is env-scoped, so staging demos reach the
staging aimock and prod demos reach prod aimock automatically — the same
private DNS name in both envs.

---

## Red-green proof (verbatim)

### RED — live staging today (billed public egress)

Deployed `showcase-langgraph-fastapi` (staging, service `06cccb5c-…`)
via Railway `variables(...)` GraphQL:

```
OPENAI_BASE_URL        = https://aimock-staging.up.railway.app/v1
ANTHROPIC_BASE_URL     = https://aimock-staging.up.railway.app
GOOGLE_GEMINI_BASE_URL = https://aimock-staging.up.railway.app
AIMOCK_URL             = https://aimock-staging.up.railway.app
```

Pre-fix generated JSON aimock entry — **no** `internalDomains`:

```json
{ "domains": { "staging": "aimock-staging.up.railway.app",
               "prod": "showcase-aimock-production.up.railway.app" },
  "internalDomains": "ABSENT" }
```

### RED — Ruby U5 serviceref resolver, with the resolver reverted to
public-only

The three new U5 tests FAIL when `ssot_target_host` returns the public
host:

```
7 runs, 15 assertions, 3 failures
1) test_serviceref_prod_pointing_at_public_aimock_host_refuses:
   expected REFUSE for prod serviceRef on the public egress host, got []
2) test_serviceref_prod_pointing_at_private_aimock_passes:
   prod private aimock ref must not REFUSE, got ["REFUSE: §5.2 (showcase-ag2): prod
   OPENAI_BASE_URL="http://showcase-aimock.railway.internal:4010/v1" does NOT point at
   aimock's env-LOCAL prod host "showcase-aimock-production.up.railway.app" ..."]
3) test_ssot_target_host_prefers_internal_over_public:
   expected "showcase-aimock.railway.internal",
   actual "showcase-aimock-production.up.railway.app"
```

### GREEN — after the fix

Post-fix generated JSON aimock entry:

```json
{ "domains": { "staging": "aimock-staging.up.railway.app",
               "prod": "showcase-aimock-production.up.railway.app" },
  "internalDomains": { "staging": "showcase-aimock.railway.internal",
                       "prod": "showcase-aimock.railway.internal" } }
```

Ruby U5 serviceref tests (fixed resolver — prefers `internalDomains`):

```
7 runs, 20 assertions, 0 failures, 0 errors, 0 skips
```

Full Ruby spec suite:

```
184 runs, 715 assertions, 0 failures, 0 errors, 0 skips
```

Harness aimock-wiring probe (hostname-match; internal host with `:4010`
+ `/v1` → green, demo still on public host while harness on private →
red):

```
src/probes/aimock-wiring.test.ts        29 passed  (was 27; +2 new: internal-host green, public-host drift red)
src/probes/drivers/aimock-wiring.test.ts 16 passed
src/rules/rule-loader.test.ts           61 passed  (aimock-wiring-drift.yml parses after Fix-text update)
renderer + render-red-tick + orchestrator 157 passed  (no alert-text snapshot broke)
```

Scripts test suite (emitter golden + everything): `2147 passed, 7
skipped` (one pre-existing `/tmp` lockfile flake in
`integration-smoke-registry.test.ts`, green on rerun after clearing the
stale lock). `emit --check` idempotent + oxfmt-canonical. Harness `tsc
--noEmit`: clean.

### GREEN — live infra confirmation

- aimock **staging** deployment status = `SUCCESS` (running), binds
`0.0.0.0:4010` — so `showcase-aimock.railway.internal:4010` resolves to
a live listener for any peer in the staging env.
- aimock serving LLM-shaped responses on `:4010`: `GET /health` → `200`;
`GET /v1/models` → `200` `{gpt-4o, gpt-4o-mini}`.

## What was vs wasn't live-validated

**Validated live:** the RED (deployed staging vars still on the public
egress host); aimock staging is deployed/running and serving on `:4010`;
the full unit/wiring/promote-preflight test surface passes with the new
internal-host values.

**NOT live-validated in-session:** the in-Railway-network DNS resolution
of `showcase-aimock.railway.internal:4010` from a peer service, and a
full staging deploy that flips the four keys + redeploys a demo backend.
Reason: the in-network vantage needs `railway ssh` (requires registering
a persistent account SSH key — a stateful, human-gated change I declined
to make unsupervised) or a staging deploy (the local Railway access
token was expired; the CLI refreshed it for read/GraphQL but a deploy is
a separate gated action). Railway private networking
(`*.railway.internal`) is a standard platform feature; the local
`docker-compose.local.yml` already runs the identical
`http://aimock:4010` internal-host pattern, and the wiring probe's
hostname match is exercised by the new tests. The staging deploy +
in-network curl is the first step of the rollout plan below and must be
run before prod.

## Irreducible egress remains

This does **not** zero showcase egress. Still billed: real browse users
hitting the public demo/shell domains; and aimock in **record mode**
proxying to real providers (the outbound prompt to
OpenAI/Anthropic/Google still bills).

## Rollout plan (reversible config change, staging-first, user-gated)

1. Land this branch (SSOT + generated JSON + assertions).
2. **Staging first:** set the four keys on staging demo backends +
`AIMOCK_URL` on the harness to
`http://showcase-aimock.railway.internal:4010` (`/v1` on
`OPENAI_BASE_URL`); redeploy one demo backend + aimock; from inside a
staging service curl
`http://showcase-aimock.railway.internal:4010/health` (expect 200) and
run a real demo LLM turn / aimock-wiring probe (expect green); confirm
the aimock egress path stops accruing
(`usage(measurements:[NETWORK_TX_GB])`).
3. **User-gated** promote to prod (staging→prod), same key flip.
4. **Rollback** = flip the keys back to the public host (no code revert
needed).

## Follow-ups (out of scope — do NOT bundle)

- Fleet right-sizing (240-concurrent-browser harness).
- `OPENAI_API_KEY` consolidation.

---

Draft — do not merge. Do not deploy to prod.
…ose CI gap

PR #5952 (9a8cf61) added explicit `.js` extensions to the relative imports
inside the harness's shared cell-model fold
(showcase/harness/src/shared/cell-model/{cell-model,live-status,staleness}.ts)
— REQUIRED for the harness's pure-Node-ESM runtime and correct as-is.

But the dashboard re-exports that fold via shims
(showcase/shell-dashboard/src/lib/{cell-model,live-status,staleness,format-ts}.ts
`export * from "../../../harness/src/shared/cell-model/*"`), pulling the fold
into the dashboard's `next build`. `export *` does not rewrite the fold's
INTERNAL `.js` edges, and the dashboard's empty next.config.ts had no
extensionAlias, so webpack resolved `./live-status.js` literally, found only
the `.ts` source, and failed:

    Module not found: Can't resolve './live-status.js'
    Module not found: Can't resolve './staleness.js'
    Module not found: Can't resolve './format-ts.js'
    > Build failed because of webpack errors

Two-part fix (one coherent subject):

1. Resolution: add `webpack.resolve.extensionAlias` to
   showcase/shell-dashboard/next.config.ts so `.js`/`.mjs` specifiers resolve
   to `.ts`/`.tsx`/`.mts` sources — the bundler complement to TS NodeNext's
   `.js`-import convention. Covers the `next build` (webpack) path CI uses.
   The harness fold's `.js` imports are left untouched (they are correct).

2. CI gap: the dashboard build did not run on #5952 because the build matrix
   is path-filtered and #5952 only touched `showcase/harness/**`, which
   selects `showcase_harness` but not `shell_dashboard`. Add
   `showcase/harness/src/shared/**` to the `shell_dashboard` paths-filter so
   any change to the shared fold the dashboard compiles in also selects the
   dashboard build — a fold change can never again ship an unbuilt dashboard.

Local red-green proof:
- RED (main, before fix): `next build` in showcase/shell-dashboard emitted the
  4 fold-resolve errors above.
- GREEN (after extensionAlias): same build → 0 fold-resolve errors; the fold
  resolves. Remaining `@/data/*.json` errors are the prebuild-generated files
  (generate-registry/probe-docs) skipped in the local repro, produced in CI's
  Docker build — unrelated to this fix.
…ose CI gap (#5955)

## What broke

PR #5952 (`9a8cf615`) added explicit `.js` extensions to the relative
imports inside the harness's shared cell-model fold
(`showcase/harness/src/shared/cell-model/{cell-model,live-status,staleness}.ts`).
Those `.js` extensions are **REQUIRED** for the harness's pure-Node-ESM
runtime and are **correct** — this PR does not revert them.

The problem: the dashboard re-exports that fold via re-export shims
(`showcase/shell-dashboard/src/lib/{cell-model,live-status,staleness,format-ts}.ts`,
each `export * from "../../../harness/src/shared/cell-model/*"`), which
pulls the fold **into the dashboard's `next build`**. `export *` does
not rewrite the fold's *internal* `.js` edges, and the dashboard's
`next.config.ts` was empty (no `extensionAlias`), so webpack resolved
`./live-status.js` **literally**, found only the `.ts` source, and
failed:

```
../harness/src/shared/cell-model/cell-model.ts
Module not found: Can't resolve './live-status.js'
Module not found: Can't resolve './staleness.js'
../harness/src/shared/cell-model/live-status.ts
Module not found: Can't resolve './format-ts.js'
> Build failed because of webpack errors
```

First-red at `9a8cf615`; reproduced in CI run `29306712559`
(shell-dashboard build job).

## The fix (two parts, one coherent subject)

**1. Resolution** — `showcase/shell-dashboard/next.config.ts`: add a
webpack `resolve.extensionAlias` so `.js`/`.mjs` specifiers resolve to
`.ts`/`.tsx`/`.mts` sources. This is the standard bundler complement to
TypeScript NodeNext's `.js`-import convention, and it applies to the
`next build` (webpack) path CI uses. A shim-only fix does **not** work —
`export *` doesn't intercept the fold's internal `.js` edges; the alias
in the dashboard build is the correct layer. The harness fold `.js`
imports are **left untouched**.

**2. CI gap** — `.github/workflows/showcase_build.yml`: the dashboard
build didn't run on #5952 because the build matrix is path-filtered and
#5952 only touched `showcase/harness/**`, which selects
`showcase_harness` but **not** `shell_dashboard`. Added
`showcase/harness/src/shared/**` to the `shell_dashboard`
`dorny/paths-filter` set. Now any change to the shared fold the
dashboard compiles in also selects the dashboard build — a fold change
can never again ship an unbuilt dashboard. (Verified only
`shell-dashboard` consumes this fold, so the gate is scoped precisely.)

## Local red-green proof

**RED** (latest main, before the `next.config.ts` fix) — from
`showcase/shell-dashboard`, `next build`:
```
../harness/src/shared/cell-model/cell-model.ts
Module not found: Can't resolve './live-status.js'
Module not found: Can't resolve './staleness.js'
../harness/src/shared/cell-model/live-status.ts
Module not found: Can't resolve './format-ts.js'
Module not found: Can't resolve './staleness.js'
> Build failed because of webpack errors
```
(4 fold-resolve errors.)

**GREEN** (after the `extensionAlias` fix) — same `next build`:
```
(0 fold-resolve errors — the fold resolves)
```
The only remaining `Module not found` errors are
`@/data/{catalog,registry,docs-status}.json`, which are generated by the
dashboard's `prebuild` scripts (`generate-registry.ts` /
`probe-docs.ts`) that were skipped in the local repro. CI's Docker build
runs `prebuild` first, so those files exist there — unrelated to this
fix.

## CI-gap trace

`dorny/paths-filter` emits `changes` as the JSON array of filter keys
whose patterns matched. A change to
`showcase/harness/src/shared/cell-model/live-status.ts` now matches both
`showcase_harness` (via `showcase/harness/**`) **and** `shell_dashboard`
(via the new `showcase/harness/src/shared/**`), so the matrix `select`
(`$dispatch == "" and ($changes | index($fk) != null)`) includes the
`shell-dashboard` slot. Gap closed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…d failures

Two pre-existing silent-failure gaps in the showcase build/deploy/notify
pipeline (surfaced in code review):

1. Green-but-zero-redeploy: the redeploy-staging job computes the redeploy
   set as (build matrix ∩ build-success). This job only runs when
   any_success=='true', so an EMPTY intersection means builds succeeded but
   none maps to a matrix dispatch_name — a dispatch_name/service contract
   skew. The old code emitted an empty services= and exited 0, going GREEN
   while redeploying nothing. Now it fails loud with a diagnostic naming both
   sides of the skew. The legitimate nothing-changed/nothing-succeeded no-ops
   stay guarded at the job level, so they are unaffected.

2. Starter-failure-invisible: the notify job's needs omitted build-starters,
   so a failed starter image build produced no Slack alert and no PR comment.
   Added detect-starter-changes + build-starters to notify.needs, and gave
   build-starters a per-slot build-result artifact mirroring the main build
   matrix (distinct starter-build-result-* prefix so it never pollutes the
   showcase aggregator's build-result-* set).
…d failures (#5956)

## Two silent-failure gaps in the showcase build/deploy/notify pipeline

These are **pre-existing** silent-failure holes surfaced in code review
(not
caused by any recent PR). This PR fixes the two load-bearing ones.

### 1. Green-but-zero-redeploy (silent "we thought we shipped but
didn't")

The `redeploy-staging` job computes the redeploy set as the intersection
of the
build matrix and the build-success set. This job **only runs when
`aggregate-build-results.outputs.any_success == 'true'`** (job-level
`if:`
guard). So if that intersection comes back **EMPTY**, it does NOT mean
"nothing
to deploy" — it means at least one slot built successfully yet none of
those
successes maps back to a matrix `dispatch_name`. That's a
`dispatch_name`↔
`service` contract skew (the aggregator's `service` values and the
matrix's
`dispatch_name` values drifted apart).

The old code emitted `services=` (empty) and exited 0 → the build went
**GREEN
while redeploying NOTHING**, silently.

**Fix:** on an empty intersection in this any_success-guaranteed step,
fail loud
(`::error::` + `exit 1`) with a diagnostic naming both sides of the
skew.
The legitimate "nothing changed / nothing succeeded" no-op paths are
guarded at
the **job level** (`has_changes=='true' && any_success=='true'`), so the
fixed
step never runs there — no false-red.

### 2. Starter build failures had no alert surface (invisible failures)

The `notify` job's `needs` (and its `if: failure()`) omitted
`detect-starter-changes` and `build-starters`, and `build-starters`
wrote no
per-slot build-result artifact. So a **failed starter image build
produced NO
Slack alert and NO PR comment** — it shipped silently.

**Fix:**
- Added `detect-starter-changes` + `build-starters` to `notify.needs` so
`if: failure()` sees a starter build failure → Slack alert + PR comment.
- Gave `build-starters` a per-slot build-result artifact **mirroring the
main
  `build` matrix** (same `{service,status}` shape, `cancelled→skipped`
  normalization, `if: always()`, `if-no-files-found: error`), using a
  **distinct `starter-build-result-*` prefix** so it never matches the
aggregator's `build-result-*` download pattern (starters must not
pollute the
  showcase redeploy set keyed by `dispatch_name`).

### Red / Green

**Finding #1** — extracted the step's shell/jq logic and drove it with
synthetic
inputs:

RED (pre-fix), any_success=true + empty intersection:
```
No services in matrix ∩ success-set — skipping redeploy.
Computed services CSV (matrix ∩ build-success):
EXIT=0        # $GITHUB_OUTPUT: services=   -> silent pass, redeploys NOTHING
```
GREEN (post-fix), same inputs:
```
::error::Build succeeded (any_success=true) but matrix ∩ success-set is EMPTY — dispatch_name/service contract skew; nothing would be redeployed.
Successful build service values: ["shell-RENAMED","mastra-RENAMED"]
Scheduled matrix dispatch_name values: ["shell","mastra"]
EXIT=1        # fails loud
```
No-regression: non-empty intersection → `EXIT=0 ; services=shell`. The
nothing-changed/nothing-succeeded paths are skipped at the job level
(never
reach the step) → no false-red.

**Finding #2** — modeled `if: failure()` (fires iff any `needs` job
result is
`failure`):
```
BEFORE (starters NOT in needs), starter=failure -> notify fires = False  (INVISIBLE, the bug)
AFTER  (starters IN needs),     starter=failure -> notify fires = True   (FIXED)
AFTER no-regression, starters=skipped, all green -> notify fires = False (quiet)
```

### Validation
- `python3 yaml.safe_load` parses OK.
- `actionlint`: only pre-existing findings remain (matrix jq SC2086 +
the known
`depot-ubuntu-24.04-4` runner-label warning); no new errors in edited
regions.
- `yamllint`: only pre-existing line-length/document-start/truthy
warnings.

### Scope
Touches **only** `.github/workflows/showcase_build.yml`, and only these
two
concerns. Does NOT touch the `shell_dashboard` paths-filter region (PR
#5955's
domain), nor the other backlog debt (false-root-cause comment,
double-alert,
check-lockfile guard). Self-contained; not stacked on #5955.
… cell-model fold

Turbopack has no resolve.extensionAlias parity (Next #82945), so
'next dev --turbopack' can't resolve the shared cell-model fold's
.js->.ts specifiers and fails with Can't resolve './live-status.js'.
The extensionAlias in next.config.ts (added in #5955) is honoured by
webpack, which next build already uses. Drop --turbopack from the dev
script so dev runs on webpack too and resolves the fold.

Deploy path unaffected: the Dockerfile builds with 'next build' (webpack)
and serves with 'next start' -- the dev script is never in the build or
runtime path.
… cell-model fold (#5959)

## Problem

`pnpm dev` on the shell-dashboard fails to start the fold-importing
routes:

```
Module not found: Can't resolve './live-status.js'
Module not found: Can't resolve './format-ts.js'
Module not found: Can't resolve './staleness.js'
GET / 500
```

The dashboard re-exports the shared **cell-model fold** from the harness
(`showcase/harness/src/shared/cell-model/*.ts`). Those fold files are
authored for the harness's pure-Node-ESM runtime, so their internal
relative imports carry explicit `.js` extensions (e.g. `import {
formatTs } from "./format-ts.js"`) even though they exist on disk as
`.ts`. `export *` does not rewrite those internal edges.

`next build` (webpack) already resolves this via the
`resolve.extensionAlias` in `next.config.ts` (added in #5955), which
tells webpack to try the TS sources for a `.js` specifier. But the `dev`
script forced Turbopack (`next dev --turbopack`), and **Turbopack has no
`resolve.extensionAlias` parity** ([Next
#82945](vercel/next.js#82945)) — so dev
couldn't resolve the fold.

## Fix

Drop `--turbopack` from the `dev` script. On Next 15.5.x, `--turbopack`
is an explicit opt-in flag (there is no `--webpack` opt-out); plain
`next dev` runs **webpack**, which honours the existing
`extensionAlias`.

```diff
- "dev": "next dev --turbopack --port 3002",
+ "dev": "next dev --port 3002",
```

`build` is unchanged (`next build` = webpack). A note in
`next.config.ts` explains why dev uses webpack.

**Tradeoff:** dev loses Turbopack's faster HMR and falls back to
webpack-speed dev until Turbopack ships `extensionAlias` parity
(#82945), at which point dev can switch back.

## Red / Green (empirical, this branch)

**Empirical proof of which bundler each command runs** (Next 15.5.15
startup banner):
- `next dev --turbopack` → `▲ Next.js 15.5.15 (Turbopack)`
- `next dev` → `▲ Next.js 15.5.15` (no "(Turbopack)" = webpack)

**RED** — `next dev --turbopack`, `GET /`:
```
▲ Next.js 15.5.15 (Turbopack)
Module not found: Can't resolve './format-ts.js'
Module not found: Can't resolve './live-status.js'
Module not found: Can't resolve './staleness.js'
GET / 500 in 3691ms
```

**GREEN** — `next dev` (webpack), `GET /`:
```
▲ Next.js 15.5.15
✓ Ready in 1124ms
✓ Compiled / in 1948ms (704 modules)
GET / 200 in 2844ms
```
No `Can't resolve`.

**No regression** — `next build` (webpack) still resolves the fold:
```
Creating an optimized production build ...
✓ Compiled successfully in 3.1s
```
(The fold compiles clean under webpack via the unchanged
`extensionAlias`.)

## Deploy path is UNAFFECTED

The `dev` script is never in the build or runtime path.
`showcase/shell-dashboard/Dockerfile` builds with `npx next build`
(webpack) and serves with `npx next start`. This change touches only
local `pnpm dev` — the built/deployed dashboard image is byte-for-byte
identical.
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/setup-node](https://redirect.github.com/actions/setup-node) |
action | major | `v6.4.0` → `v7.0.0` |
| [ruby/setup-ruby](https://redirect.github.com/ruby/setup-ruby) |
action | minor | `v1.316.0` → `v1.317.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/592) for more information.

---

### Release Notes

<details>
<summary>actions/setup-node (actions/setup-node)</summary>

###
[`v7.0.0`](https://redirect.github.com/actions/setup-node/compare/v6.5.0...v7.0.0)

[Compare
Source](https://redirect.github.com/actions/setup-node/compare/v7.0.0...v7.0.0)

###
[`v7`](https://redirect.github.com/actions/setup-node/compare/v6.5.0...v7.0.0)

[Compare
Source](https://redirect.github.com/actions/setup-node/compare/v6.5.0...v7.0.0)

###
[`v6.5.0`](https://redirect.github.com/actions/setup-node/compare/v6.4.0...v6.5.0)

[Compare
Source](https://redirect.github.com/actions/setup-node/compare/v6.4.0...v6.5.0)

</details>

<details>
<summary>ruby/setup-ruby (ruby/setup-ruby)</summary>

###
[`v1.317.0`](https://redirect.github.com/ruby/setup-ruby/releases/tag/v1.317.0)

[Compare
Source](https://redirect.github.com/ruby/setup-ruby/compare/v1.316.0...v1.317.0)

##### What's Changed

- Add ruby-4.0.6 by
[@&#8203;ruby-builder-bot](https://redirect.github.com/ruby-builder-bot)
in [#&#8203;928](https://redirect.github.com/ruby/setup-ruby/pull/928)

**Full Changelog**:
<ruby/setup-ruby@v1.316.0...v1.317.0>

</details>

---

### Configuration

📅 **Schedule**: (in timezone America/Los_Angeles)

- Branch creation
  - "before 9am every weekday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/CopilotKit/CopilotKit).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNTkuMiIsInVwZGF0ZWRJblZlciI6IjQzLjI1OS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
…#5812) (#5885)

## Summary

Pressing **Stop** while an assistant message is streaming
(CopilotRuntime + `HttpAgent` proxy) crashed the chat with:

```
Cannot send event type 'TEXT_MESSAGE_END': The run has already errored with 'RUN_ERROR'. No further events can be sent.
```

Root cause: `finalizeRunEvents` appended a trailing `TEXT_MESSAGE_END`
**after** the `RUN_ERROR` that the aborted agent had already emitted.

Fixes #5812.

## Root cause

When the upstream agent (e.g. pydantic-ai's `AGUIAdapter`) is aborted
mid-stream it emits a live `RUN_ERROR` while a text message is still
open — it does **not** close the message first. All runners
(`in-memory`, `intelligence`, `sqlite`) stream `finalizeRunEvents`'
output *after* everything the agent already emitted, so the appended
closer landed past the terminal:

| | outgoing event order |
|---|---|
| **Before** | `… TEXT_MESSAGE_CONTENT → RUN_ERROR → TEXT_MESSAGE_END` ❌
verifier throws |
| **After** | `… TEXT_MESSAGE_CONTENT → RUN_ERROR` ✅ terminal closes the
message client-side |

Per the AG-UI invariant: at most one terminal event per run, and no
sub-events after it. I confirmed against the real `@ag-ui/client`
`verifyEvents` (the verifier the browser runs) that a terminal arriving
with a message still open is valid — the terminal implicitly closes it.

## Fix

`finalizeRunEvents` (in `@copilotkit/shared`) now returns early and
appends **nothing** when the stream already contains a terminal event
(`RUN_FINISHED` or `RUN_ERROR`). The abrupt-end path (no terminal →
close open streams + synthesize a terminal, in the correct order) is
unchanged. No API/signature change; the in-memory, intelligence, and
sqlite runners all inherit the fix.

## Testing

RED→GREEN verified — each new/updated assertion was confirmed to fail
against the pre-fix code:

- **`finalize-events.test.ts`** — terminal-present appends nothing
(parametrized over `RUN_FINISHED` and `RUN_ERROR`) + a named #5812 case.
- **`in-memory-runner.test.ts`** — end-to-end mid-stream-stop
regression: a fake `HttpAgent`-style agent is stopped between
`TEXT_MESSAGE_START` and `TEXT_MESSAGE_END`; asserts no events follow
`RUN_ERROR` **and** that the collected stream passes `verifyEvents`
(before the fix this threw the exact browser error).
- **`intelligence-runner.test.ts`** — corrected a pre-existing assertion
that had encoded the buggy post-terminal `TEXT_MESSAGE_END`.

Green: full `@copilotkit/runtime` suite, `@copilotkit/sqlite-runner`,
`@copilotkit/shared`, `check-types`, `oxlint` (0 errors), and build.

## Reviewer notes

- The behavior change is a single early-return in `finalize-events.ts`;
the `terminalEventMissing` guards simplify away because they're only
reachable when no terminal exists.
- Diff is +204/−55 across 4 files, the bulk of it tests.
Flip the 4 TS a2ui builders (shared/typescript + mastra,
claude-sdk-typescript, langgraph-typescript) from the legacy flat
operation shape to v0.9 nested (createSurface / updateComponents /
updateDataModel), matching the Python builder and what A2UI consumers
process. Flat ops were never processed as valid nested operations, so
the surface schema and components were never applied.

Also align the empty-data guard to Python's `if data:` semantics (empty
object -> no updateDataModel), add a v0.9 parity guard test to all 4
test files, and add 12 gen-ui-a2ui-fixed aimock fixtures.
The shared TS builder exported `_design_a2ui_surface` / `DESIGN_A2UI_SURFACE_TOOL_SCHEMA`
while the Python builder, all integration copies, and agent_server.ts use `render_a2ui`
/ `RENDER_A2UI_TOOL_SCHEMA`. Align the shared source to that source-of-truth name; this
also unbreaks the re-export in shared/typescript/tools/index.ts.
… guard

Restore the 12 Python integration tools/ dirs to symlinks into
shared/python/tools. They had eroded to real, drifting copies via an
accidental stage_shared() leak (commit 534cd1e) — the structural root
cause of showcase divergence bugs. Symlinking re-establishes the single
source of truth; content is identical to shared (only render_a2ui naming
and the shared roll_dice/sanitize additions are adopted).

Add showcase/AGENTS.md documenting the 4 iron rules and the single-source
symlink mechanism, plus a validate-shared-symlinks CI guard (shrink-only
baseline) that fails on any NEW erosion.
…hon/TS parity) (#5971)

## Summary

The TypeScript A2UI operation builders
(`buildA2uiOperationsFromToolCall`) emitted the **legacy flat**
operation shape (`{ type: "create_surface", surfaceId }`). A2UI
consumers process operations by their **nested** `createSurface` /
`updateComponents` / `updateDataModel` keys — a flat op is never
processed as a valid nested operation, so the surface's schema and
components are never applied and the UI renders nothing (the
`generate_a2ui` / `render_a2ui` path).

Python was fixed to the v0.9 nested shape long ago (#4792, #5832); the
TypeScript builders were **born flat and never fixed** — a Python/TS
parity gap with no guard. This aligns the TS side and adds a guard so
the two can't silently drift again.

## Changes

- **4 TS builders → v0.9 nested** (byte-identical):
`shared/typescript/tools` + integrations `mastra`,
`claude-sdk-typescript`, `langgraph-typescript`.
- **Empty-data parity fix**: TS `if (data)` treated `{}` as truthy and
emitted a spurious `updateDataModel` op; Python `if data:` does not. Now
guarded to match Python (empty object → no `updateDataModel`). Our
mastra fixture records `"data": {}`, so this is exercised directly.
- **v0.9 parity guard test** in all 4 test files (asserts nested keys,
no flat `type`).
- **12 `gen-ui-a2ui-fixed` aimock fixtures** for the fixed-schema a2ui
demo.

## Red–green evidence

- Empty-data: pre-fix builder emits 3 ops on `data:{}` →
`toHaveLength(2)` **FAILS (red)**; fixed builder emits 2 → **passes
(green)**.
- Parity guard: flat shape → `.type` present / nested keys absent →
**red**; nested → **green**.

## Validation (please read — what CI does and doesn't cover)

CI **does** run `check-types`, `format`/`oxlint`, `Validate Showcase`,
and `build-check` on the changed integrations (mastra,
langgraph-typescript, claude-sdk-typescript) — these catch TS/build
breaks. But the unit-test workflow has `paths-ignore: showcase/**`, so
the **showcase vitest suites where the parity guard and aimock-fixtures
tests live are NOT run in CI**. Those were validated **locally**:

- `aimock-fixtures`: **837 passed** (all 12 new fixtures valid).
- Parity guard + empty-data red–green: **verified**
(`showcase/shared/typescript` vitest).
- `tsc --noEmit --strict`: **clean** on all 4 builders.
- **mastra Playwright screenshot**: the `render_a2ui` flow renders the
flight card (SFO→JFK, Flight Details, $289) with the nested ops.

## Real-surface confirmation (bin/showcase test --direct)

Proven on the live probe, not just unit tests:

- **RED** (old flat builder): `d6:mastra a2ui-fixed-schema` → `1 failed
— [data-testid="a2ui-fixed-card"] failed to mount within 60000ms`.
- **GREEN** (this branch, rebuilt image): `d6:mastra a2ui-fixed-schema`
→ `✓ green, 1 passed` — the card mounts and renders (SFO→JFK, UNITED,
$289, "Book flight").
- **≥3 cells `--direct` GREEN**: `mastra` ✓, `langgraph-typescript` ✓,
`pydantic-ai` ✓.
- **Frontend parity**: mastra ≡ langgraph-python render the same flight
card (byte-identical frontend; only expected agent-specific
tool-row/ordering differences).

## Iron-rule adherence

| Rule | Evidence |
|------|----------|
| Identical tests | One shared harness probe (`d5-gen-ui-a2ui-fixed`)
measures the feature across all integrations; no per-integration test
copies added. |
| Near-identical frontends | mastra ≡ langgraph-python frontend
(byte-identical); visual parity confirmed on the rendered card. |
| Minimal backends | Change is the minimal flat→nested + empty-data
guard; 4 TS builder copies byte-identical. |
| Per-integration fixtures | 12 `gen-ui-a2ui-fixed.json`, one per slug,
context-keyed. |

Note: the *single-source symlink* restoration (the
`shared-tools/`/`tools/` symlinks that eroded to real files repo-wide)
is handled in a **companion structural PR**, plus a `showcase/AGENTS.md`
documenting the iron rules and a CI check that fails on future erosion.

## Known follow-ups (out of scope here)

1. **`render_a2ui` naming drift (pre-existing):** the *shared* builder
exports `_design_a2ui_surface` while the 3 integrations + Python use
`render_a2ui`; the shared test asserts `render_a2ui` and is red on
`main` today. Not run by CI. A one-line rename aligns shared to the
source of truth and greens the file — happy to fold it in if wanted.
2. **Integration test copies aren't executed** by their vitest `include`
globs (only `shared/typescript` runs its copy) — add a cross-copy
byte-identity CI check.
3. **Full `_sanitize_a2ui_components` parity** — TS forwards components
raw; Python sanitizes (drops entries missing id/component, unstringifies
Gemini JSON-string arrays).
4. **Showcase tests are CI-excluded** (`paths-ignore: showcase/**`) — no
automated gate for showcase unit tests.
… guard (#5975)

## Summary

Companion structural PR to #5971. It fixes the **root cause** behind the
a2ui divergence #5971 patched: the showcase's single-source symlinks
eroded to real, drifting copies.

`showcase/integrations/*/tools`, `*/shared-tools`, `*/_shared` are meant
to be **symlinks into `showcase/shared/...`** — `stage_shared()`
dereferences them for the Docker build, `restore_symlinks()` restores
them. An **accidental `stage_shared()` leak** (commit `534cd1efa7`, PR
#4449 "D5 all-green") committed the dereferenced real files instead of
restoring the symlinks. Once they were real files, they drifted — which
is exactly how the a2ui `render_a2ui` vs `_design_a2ui_surface` split
(fixed in #5971) arose.

## Changes

- **Restore 12 Python `tools/` dirs to symlinks** →
`../../shared/python/tools` (ag2, agno, claude-sdk-python, crewai-crews,
google-adk, langgraph-fastapi, langgraph-python, langroid, llamaindex,
ms-agent-python, pydantic-ai, strands). Content is byte-adopted from
shared — verified no load-bearing per-integration code is lost (only
`render_a2ui` naming + shared `roll_dice`/sanitize additions).
Integrations' intentional internal-planner names (llamaindex,
ms-agent-python) live in `src/`, not `tools/`, and are untouched.
- **`showcase/AGENTS.md` (+ `CLAUDE.md`, root pointers,
INTEGRATION-CHECKLIST section)** — canonical statement of the 4 iron
rules (identical tests, near-identical frontends, minimal backends,
per-integration fixtures) + the single-source symlink mechanism ("edit
the shared source only; a real file there is a bug"). These were
previously written down nowhere.
- **`validate-shared-symlinks` CI guard** — fails on any NEW erosion
(real dir where a symlink belongs), with a shrink-only baseline that
tightens to fully-enforcing as symlinks are restored. Mirrors the
existing `validate-*` ratchet pattern.

## Scope / independence

- **No overlap with #5971** — this PR touches nothing under
`showcase/shared/typescript/` and does not modify the 3 TS integration
`shared-tools/` dirs (verified: empty file-set intersection). Mergeable
independently.
- Build-safe: `stage_shared()` correctly dereferences the restored
symlinks (targets resolve within the build context);
`restore_symlinks()` recreates them post-build.

## Verified

- `validate-shared-symlinks` test suite: 7/7 pass; validator EXIT 0 (no
new erosion).
- Reviewed by a full panel (correctness, content-integrity, build/CI,
docs, scope, silent-failure, simplicity) — zero mandatory findings.

## Follow-ups (deliberately out of scope)

1. **3 TS `shared-tools` dirs** (mastra, claude-sdk-typescript,
langgraph-typescript) remain real (baselined) — symlink them in a
follow-up **after #5971 merges**, to avoid overlapping its TS edits.
2. **Guard hardening**: validate the symlink *target* (not just that
it's a symlink), fail-loud on a malformed baseline, and code-enforce the
shrink-only ratchet. (This PR's guard catches the real-file erosion —
the actual failure mode; these are robustness extras.)
3. Pre-existing `shared/python` a2ui test failures (#5971-adjacent) and
a couple of stale doc line-refs, noted during review.

Companion: #5971.
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [ruby/setup-ruby](https://redirect.github.com/ruby/setup-ruby) |
action | minor | `v1.317.0` → `v1.318.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/592) for more information.

---

### Release Notes

<details>
<summary>ruby/setup-ruby (ruby/setup-ruby)</summary>

###
[`v1.318.0`](https://redirect.github.com/ruby/setup-ruby/releases/tag/v1.318.0)

[Compare
Source](https://redirect.github.com/ruby/setup-ruby/compare/v1.317.0...v1.318.0)

#### What's Changed

- Update CRuby releases on Windows by
[@&#8203;ruby-builder-bot](https://redirect.github.com/ruby-builder-bot)
in [#&#8203;929](https://redirect.github.com/ruby/setup-ruby/pull/929)

**Full Changelog**:
<ruby/setup-ruby@v1.317.0...v1.318.0>

</details>

---

### Configuration

📅 **Schedule**: (in timezone America/Los_Angeles)

- Branch creation
  - "before 9am every weekday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/CopilotKit/CopilotKit).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNTkuMiIsInVwZGF0ZWRJblZlciI6IjQzLjI1OS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

⤵️ pull merge-conflict Resolve conflicts manually

Projects

None yet

Development

Successfully merging this pull request may close these issues.