Commit Graph

13191 Commits

Author SHA1 Message Date
Tyler Slaton 3fb24ef403 fix(shell-docs): address docs review edge cases 2026-06-16 10:08:54 -07:00
Benjamin Taylor 6618d92f72 ci(test-unit): scope build+test to nx affected packages
Run `test / unit` over only the packages affected since the base instead
of building + testing every package 3× across the Node 20/22/24 matrix on
every PR.

- fetch-depth: 0 so affected has a merge-base to diff against.
- Derive NX_BASE/NX_HEAD: PR → merge-base with the base branch tip; push →
  github.event.before with a HEAD~1 fallback.
- Select packages via `nx show projects --affected --projects='packages/**'`
  fed to run-many (the `nx affected` run form ignores --projects and pulls
  in downstream examples/storybook — hence the show-projects → run-many split).
- workflow_dispatch still runs all packages (manual/full run).
- Editing this workflow can't surface as an affected package, so a change to
  test_unit.yml in the range now forces a full all-packages run — this keeps
  the build/test path exercised on the PR that changes it.
- GitHub context passed via env: (not inline ${{ }}) to satisfy zizmor;
  NX_VERBOSE_LOGGING forced off for the JSON-parsing step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 12:08:18 -05:00
Tyler Slaton 88588223af fix(react-core): add dark mode styling for default tool rendering (#5455)
## Summary
- Update the default tool-call renderer to use dark-theme-aware classes
for the card, header, status badge, and detail panels
- Add coverage for the dark theme styling path in unit tests
- Add a Storybook example that demonstrates the default tool renderer on
a dark CopilotKit surface

## Testing
- Added unit tests for the renderer’s dark-theme class output and
expanded details state
- Not run (not requested)
2026-06-16 10:04:54 -07:00
Tyler Slaton 06cee3f727 fix(vue): adjust pin-to-send spacer in both directions to keep the user message pinned (#5423)
## What does this PR do?

Ports the `usePinToSend` spacer fix from #5386 to the Vue package.
`packages/vue/src/v2/hooks/use-pin-to-send.ts` is a documented 1:1
parity port of the React hook and retained the shrink-only
`ResizeObserver`: when content below the anchored user message loses
height (suggestions swapping in, input container resizing after
streaming), the spacer could not grow back, so total scrollable height
dropped and the pinned message shifted — the same defect class reported
in #5355.

- Lets the spacer adjust in both directions so total scrollable space
below the pinned bubble stays constant.
- Mirrors the test change from #5386 1:1 in the Vue suite (red/green:
the updated assertion fails on the shrink-only implementation, passes
with the fix).
- Updates the `PARITY.md` row and hook JSDoc that documented the old
shrink-only semantics.

Vue has no scroll-to-bottom button, so the scroll-listener half of #5386
does not apply here.

## Related PRs and Issues

- Follow-up to #5386
- Relates to #5355 (Vue side of the same spacer defect)

## Tests

- `nx test @copilotkit/vue` — 1001 passed (94 files), including the
mirrored `adjusts spacer as content height changes to keep the user
message pinned` case
- `nx build @copilotkit/vue` — green (dts build)
- `oxfmt --check` clean on touched files
- Pre-existing on main, untouched by this diff: 176 package-wide lint
errors (identical count on clean main) and
`@copilotkit/core:check-types` failures

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-16 10:03:43 -07:00
Jordan Ritter 06112f4c36 refactor(showcase): rename Wired → BE (Agent) across dashboard taxonomies (#5498)
## Summary

Renames the display label \"Wired\" to **\"BE (Agent)\"** across both
taxonomies in the showcase dashboard, unifying the two overloaded
meanings (live-probe runtime health + catalog integration build state)
under one user-visible name. Mirrors the shape of #5473's `E2E (Demo)` →
`UI (Frontend)` rename — same atomic, label-only refactor.

## Why

The word \"Wired\" was overloaded across the dashboard:
- The **live-probe L1 row** in `level-strip` rendered \"Wired\" for
runtime backend health (the agent is up and responding).
- The **catalog integration-status** column in
`stats-bar`/`adaptive-stats-bar`/`coverage-bar`/`filter-chips` rendered
\"Wired\" for build state (the integration is built and shipped).

Both refer to the same underlying thing — the backend agent integration
— at different lifecycle stages. Calling both **\"BE (Agent)\"** makes
the dashboard internally consistent and aligns with the parallel \"UI
(Frontend)\" label introduced in #5473.

## Scope

- Visible label flip across `level-strip.tsx`, `packages-section.tsx`
legend (incl. `W=Wired` → `B=BE (Agent)` mnemonic), `stats-bar.tsx`,
`adaptive-stats-bar.tsx`, `coverage-bar.tsx`, `filter-chips.tsx`.
- Internal counter renames: `pctWired`/`totalWired` →
`pctBeAgent`/`totalBeAgent` (`coverage-bar.tsx`, `cells-view.tsx`,
`parity-view.tsx`).
- Test assertion + comment updates in `level-strip.test.tsx`,
`packages-section.test.tsx`.
- Slugifier fix in `stats-bar.tsx` testid generator —
`label.toLowerCase()` on `\"BE (Agent)\"` previously produced `stat-be
(agent)` (spaces + parens). Now slugifies to `stat-be-agent`.
Incidentally also fixes the pre-existing `stat-max depth` →
`stat-max-depth`. Surfaced and fixed during cr-loop Round 1.

## Stable contracts preserved (no behavior change)

- PocketBase row keys `agent:<slug>`
- `LiveDimension` union value `\"agent\"`
- `Status` enum string values `\"wired\" | \"stub\" | \"unshipped\" |
\"unsupported\"` (persisted catalog data; only display labels flip)
- Filter chip `id: \"wired\"` (filter state key joining to
`cell-matrix.tsx`)
- `catalog.metadata.wired` data field reads
- Harness driver `kind`/emission contracts
- `data-testid=\"coverage-segment-wired\"`

## Commits (4)

- \`ef40bd7c\` live-probe agent dimension Wired → BE (Agent)
- \`bfebac65\` catalog status Wired display → BE (Agent)
- \`f316d055\` rename internal Wired counters to BeAgent
- \`80b526d6\` slugify stats-bar testids (CR fix)

## Verification

- typecheck (\`tsc --noEmit\`): clean
- lint (next build inline): clean
- tests (\`vitest run\`): 1089 pass / 1 skip / 0 fail across 63 files
- build (\`next build\`): clean
- oxfmt: clean
- NUL-byte scan: empty
- cr-loop converged at Round 2 with Procedure 3 audit returning 0
PROMOTE_TO_A items

## Follow-up backlog

Round 1 + Round 2 surfaced ~30 pre-existing structural findings in
adjacent code (not subject-scope for this rename). Grouped into 8
follow-up PR subjects: fail-loud sweep, catalog view derivation
extraction, derivePackageDepth gate consistency, parity tier counting
hardening, coverage-bar tooltip/rounding, cells-view stats useMemo, test
hardening, Tailwind opacity-on-CSS-var. None block this rename.
2026-06-16 09:58:23 -07:00
wuyangfan a76b2eff69 docs: apply LangGraph link fixes to shell-docs (canonical source)
The top-level docs/ folder is retired; the live docs are sourced from
showcase/shell-docs. Move the same three link fixes there and revert the
no-op edits to docs/:

- configurable.mdx: graph-api -> use-graph-api (#add-runtime-configuration)
- configurable.mdx: drop malformed %23 double-anchor on the schema link
- step-2-langgraph-agent.mdx: studio.langchain.com -> docs.langchain.com/.../studio

Closes #3190.
2026-06-16 09:56:53 -07:00
Martha Schumann 38e987f7e4 fix(docs): validate showcase docs model names 2026-06-16 09:55:14 -07:00
Tyler Slaton 0df97a830b fix(examples): bump integrations to @copilotkit 1.60.1 for @ag-ui fetch fix (#5457)
## Summary

Fixes the integration examples whose chat never responds and logs this
on **every agent run**:

```
TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
  (agent_run_failed_event){ source: onRunFailed, agentId: default }
```

The fix is a dependency bump that lands every integration demo on
`@copilotkit/* 1.60.1` + `@ag-ui/client 0.0.57`.

## Root cause

`@ag-ui/client@0.0.56` (and `0.0.55`/`0.0.53`) `HttpAgent` stores the
global `fetch` **unbound** and later calls it as a method:

```ts
// buggy
this.fetch = config.fetch ?? fetch;
run(input) { return runHttpRequest(() => this.fetch(this.url, this.requestInit(input))); }
```

`this.fetch(...)` invokes native `fetch` with the agent instance as the
receiver instead of `window`. Native `fetch` is brand-checked and throws
`Illegal invocation`. It fires on the client run path
(`ProxiedCopilotRuntimeAgent.run` → `#runViaHttp`) in **both dev and
prod** — not environment-specific.

Fixed upstream in `@ag-ui/client@0.0.57`:

```ts
this.fetch = config.fetch ?? ((url, requestInit) => fetch(url, requestInit));
```

`@copilotkit/* 1.60.1` (published) bumped its pin to
`@ag-ui/client@0.0.57`. Examples still on `1.60.0` ship the buggy client
— which is why deployments fail.

## What changed (all `examples/integrations/*`)

- **`@copilotkit/* 1.60.0 → 1.60.1`** across every integration demo. The
`_parity` manifest requires all demos to track the north-star
(`langgraph-python`) `@copilotkit` version, so they move in lockstep — a
partial bump fails `parity-check`.
- **Starter-fleet apps force a single `@ag-ui` tree via `overrides`**
(`@ag-ui/client|core|encoder|proto`). Bumped that single-tree pin
**`0.0.53`/`0.0.55` → `0.0.57`** so they actually carry the fetch fix
instead of a `1.60.1 ↔ 0.0.55` skew. Adapter packages (`@ag-ui/crewai`,
`@ag-ui/mastra`, `@ag-ui/llamaindex`, `@ag-ui/a2a`, middlewares) are
intentionally left at their own versions.
- Lockfiles regenerated; all resolve `@ag-ui/client 0.0.57`.

## Verification

- [x] Reproduced `Illegal invocation` locally on `1.60.0` (dev + prod
build)
- [x] Verified `langgraph-python` on `1.60.1`: agent run completes, todo
created, **0 console errors**
- [x] All lockfiles resolve `@ag-ui/client 0.0.57`
- [x] `pnpm parity:check` passes locally (0 errors)
- [ ] CI `parity-check` + per-integration `smoke-starter` green
- [ ] Redeploy affected examples (e.g. Render) and confirm chat works
2026-06-16 09:48:09 -07:00
Jordan Ritter 80b526d68b refactor(showcase): slugify stats-bar testids to handle multi-word labels
The testid generator in stats-bar.tsx used `label.toLowerCase()` directly,
producing fragile selectors with spaces and parens for compound labels.
After this PR's rename of "Wired" to "BE (Agent)", the testid became
`stat-be (agent)` — a CSS-hostile selector. A pre-existing
`stat-max depth` (from "Max Depth") had the same problem but predated
this PR.

Fix: replace with a proper slugifier:
  label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "")

Resulting testids:
  "BE (Agent)"   -> stat-be-agent   (was stat-be (agent), broken)
  "Max Depth"    -> stat-max-depth  (was stat-max depth, pre-existing fix)
  "Stub"         -> stat-stub       (unchanged)
  "Unshipped"    -> stat-unshipped  (unchanged)
  "Unsupported"  -> stat-unsupported (unchanged)
  "Regressions"  -> stat-regressions (unchanged)
  "Failures"     -> stat-failures   (unchanged)

Call-site enumeration receipt (mandatory):
  rg 'stat-wired|stat-be|stat-max|stat-stub|stat-unshipped|stat-unsupported|stat-regressions|stat-failures' showcase/shell-dashboard/
    -> no matches (zero hardcoded references anywhere)
  rg 'data-testid=.stat-|"stat-|`stat-' showcase/shell-dashboard/
    -> only one hit: stats-bar.tsx:29 (the generator itself)
  rg 'stat-' showcase/shell-dashboard/tests/
    -> no matches (no Playwright/visual tests depend on these testids)

No test updates required.

Verification:
  npm run typecheck   -> clean
  npm test            -> 1088 pass / 1 pre-existing unrelated failure
                         (useLiveStatus.test.tsx R5 F5.2 — does not
                         reference stats-bar, exists on PR HEAD)
  npm run build       -> clean (Next.js lint inline; no lint script)
  NUL byte sweep      -> empty
2026-06-16 09:40:49 -07:00
Austin Merrick d88245199f docs: testing that previews are up (#5431) 2026-06-16 09:38:39 -07:00
Jordan Ritter 9491b89340 fix(showcase): disjoint catchall userMessages + content-asserting probes (supersedes #5465)
Closed PR #5465 introduced cross-fixture leakage by stripping the
toolName discriminator on shared {userMessage, context} keys: with
aimock's alphabetical first-match-wins ordering,
tool-rendering-custom-catchall.json sorts before
tool-rendering-default-catchall.json, so default-catchall page requests
were served the custom file's content. The probe was structurally blind
because it asserted only DOM testids (copilot-tool-render +
data-tool-name=get_weather) — both fixtures emit get_weather, so the
testid signal passed regardless of which fixture won.

Real LGP-gold pattern is disjoint userMessages between default and
custom catchall fixtures (NOT shared keys discriminated by toolName).
This PR ports the LGP-gold pattern to the other 17 integrations and
adds page-text content assertions to both d5 catchall probes so this
class of regression can't recur silently:

- Probe prompts disjoint between default-catchall and custom-catchall
- Fixture userMessages updated to match the new disjoint prompts
- Page-text content assertions in both d5 catchall probes
  (default negatively asserts the custom-catchall leak phrase;
   custom positively asserts it)

Local gold-standard red-green proof captured:
- Step A: live staging Playwright baseline (RED-OF-RECORD)
- Step B: local control-plane on origin/main reproduces structural
  fragility (probes pass at testid level, custom fixture wins on
  default's userMessage path)
- Step C: local control-plane on this branch — both catchall probes
  GREEN with the new content-asserting assertions
    tool-rendering-default-catchall: pass=true (5443ms)
    tool-rendering-custom-catchall:  pass=true (8956ms)
                                     cross-tool signature passed

LGP (langgraph-python) was already disjoint; it remains untouched.
2026-06-16 09:37:51 -07:00
Maxim f9bcf11295 fix(examples): sync langgraph-js agent lockfile to @langchain/core 1.1.49
Keeps package-lock.json in sync with the core bump pulled in from main
(#5489) so the agent lockfile is not stale after the rebase.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 18:29:30 +02:00
Maxim a144323d35 fix(examples): regenerate integration lockfiles for npm ci
The previous lockfiles were written with `npm install --package-lock-only`,
which left out optional transitive deps (e.g. @emnapi/wasi-threads). The
Docker smoke-starter build runs strict `npm ci`, which then failed with
"package.json and package-lock.json not in sync" (crewai-crews, 9s fail).

Regenerated each lockfile with a full `npm install --ignore-scripts` so the
tree is complete. Verified `npm ci` now succeeds (crewai-crews, mastra) and
all resolve @ag-ui/client 0.0.57.
2026-06-16 18:27:38 +02:00
Maxim cef4fab9d7 fix(examples): bump remaining integrations to 1.60.1 + @ag-ui 0.0.57
Follow-up to the @copilotkit 1.60.1 bump so the integration-demo parity
check passes. The north-star (langgraph-python) is now on 1.60.1, and the
_parity manifest requires every examples/integrations/* demo to track the
same @copilotkit version — so all integrations must move together.

- @copilotkit/* 1.60.0 -> 1.60.1 across the remaining integrations.
- Starter-fleet apps force a single @ag-ui tree via `overrides`
  (@ag-ui/client|core|encoder|proto). Bump that single-tree pin
  0.0.53/0.0.55 -> 0.0.57 so they actually carry the HttpAgent fetch fix
  (Illegal invocation) instead of a 1.60.1<->0.0.55 skew. Adapter packages
  (@ag-ui/crewai, @ag-ui/mastra, @ag-ui/llamaindex, @ag-ui/a2a, middlewares)
  are left untouched.
- Lockfiles regenerated; all resolve @ag-ui/client 0.0.57.

Per-integration smoke-starter CI validates each starter against 0.0.57.
2026-06-16 18:27:37 +02:00
Maxim 0363017503 fix(examples): bump @copilotkit to 1.60.1 for @ag-ui/client fetch fix
The chat never responds and the browser console shows
"TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation"
on every agent run (onRunFailed, agentId: default).

Root cause is in @ag-ui/client@0.0.56's HttpAgent: it stores the global
fetch unbound (`this.fetch = config.fetch ?? fetch`) and later invokes it
as `this.fetch(...)`, so native fetch runs with the agent instance as its
receiver instead of `window`. Native fetch is brand-checked and throws
"Illegal invocation". It surfaces in both dev and prod.

Fixed upstream in @ag-ui/client@0.0.57 (`config.fetch ?? ((url, init) =>
fetch(url, init))`), which shipped in @copilotkit/* 1.60.1. Bumping these
examples 1.60.0 -> 1.60.1 pulls 0.0.57 transitively; lockfiles regenerated.

Scope: only the examples that take @ag-ui transitively from react-core are
bumped here. Other integration examples force-pin @ag-ui via `overrides`
to 0.0.53/0.0.55, where 1.60.1 would create a version skew; those need a
per-integration bump and are intentionally left out of this change.
2026-06-16 18:27:37 +02:00
Maxim e9f1f21ddb fix(examples): bump langgraph-js agent @langchain/core to 1.1.49 (v6 uuid export) (#5489)
## Summary

The `smoke-starter (langgraph-js)` CI job is failing on `main` (and on
every open PR, e.g. #5457). The agent container crashes on startup:

```
SyntaxError: The requested module '@langchain/core/utils/uuid' does not provide an export named 'v6'
```

## Root cause

- `examples/integrations/langgraph-js/docker/Dockerfile.agent` copies
only `package.json` (not the lockfile) and runs `npm install`, so
transitive deps resolve fresh at build time.
- `@langchain/langgraph@1.3.0` →
`@langchain/langgraph-checkpoint@^1.0.2`.
- Checkpoint `1.1.1` (published 2026-06-12) started importing `v6` and
raised its peer to `@langchain/core@^1.1.48`.
- The agent pinned `@langchain/core@1.1.44`, which predates the `v6`
export → import crash.

Verified that `@langchain/core@1.1.44` does not export `v6` from
`utils/uuid` while `1.1.49` does, and a clean `npm install` with core
`1.1.49` dedupes to checkpoint `1.1.1` + core `1.1.49` and imports `v6`
successfully.

## Change

Bump `@langchain/core` `1.1.44` → `1.1.49` in the langgraph-js agent.
The npm `package-lock.json` is intentionally left untouched — no CI job
consumes it (the Docker agent build runs `npm install` against
`package.json`, per the Dockerfile's own "Mirrors the user experience"
comment).

## Test plan

- [ ] `smoke-starter (langgraph-js)` passes on this PR
2026-06-16 18:24:26 +02:00
Jordan Ritter f316d055fb refactor(showcase): rename internal Wired counters to BeAgent
Renames the internal display-counter variables that fed the
stats-bar / coverage-bar "Wired" labels to match the new "BE (Agent)"
taxonomy: totalWired → totalBeAgent in cells-view.tsx and
parity-view.tsx, pctWired → pctBeAgent in coverage-bar.tsx. These
are local render-time aggregates, not part of any persisted
contract.

The status enum literal "wired" (the .filter((c) => c.status ===
"wired") guard, the coverage-segment-wired test ID, and the
wiredByCategory Map's local name) is intentionally preserved — those
all key directly off the persisted catalog Status enum and changing
them would expand scope into the catalog contract.
2026-06-16 09:23:00 -07:00
Jordan Ritter bfebac6565 refactor(showcase): relabel catalog status Wired display → BE (Agent)
Unifies the catalog integration status display label with the same
"BE (Agent)" taxonomy as the live-probe agent dimension. The
underlying Status enum literal ("wired"), the catalog.metadata.wired
data field, and the filter chip id ("wired" → cell-matrix.tsx:311
status === "wired" filter key) are all PRESERVED — they are
persisted catalog data + filter state contracts that must not move.
Only the user-facing display label on stats-bar, adaptive-stats-bar,
and the filter chip flips.

This collapses the two distinct dashboard "Wired" surfaces (the L1
live-probe dimension and the per-cell build-state count) under one
unified label, matching the user-confirmed Path B.
2026-06-16 09:22:41 -07:00
Jordan Ritter ef40bd7cfb refactor(showcase): relabel live-probe agent dimension Wired → BE (Agent)
Unifies the L1 "agent" live-probe display label with the taxonomy
convention established by #5473 (UI (Frontend), E2E, CV, D6 — layer
descriptor in parentheses). The dimension name stays "agent" in code
(PocketBase row keys agent:<slug>, LiveDimension union, keyFor
lookups are all unchanged stable contracts) — only the visible label
changes.

Updates the level-strip L1 badge label and the packages-section
L1-L4 header legend (W → B, "Wired" → "BE (Agent)") so the
level-strip's ToneChip first-letter abbreviation matches the legend
key. Test assertions covering the rendered letter, the legend text,
and the degraded-tone test's local variable follow suit.
2026-06-16 09:22:31 -07:00
Varun Nuthalapati 5d67341881 docs(react-ui): clarify all modalities in attachments prop examples 2026-06-16 09:17:55 -07:00
Maxim c82e3b40e2 fix(examples): bump langgraph-js agent @langchain/core to 1.1.49 for v6 uuid export
The langgraph-js smoke-starter job crashes on agent startup with
"'@langchain/core/utils/uuid' does not provide an export named 'v6'".

@langchain/langgraph@1.3.0 depends on @langchain/langgraph-checkpoint@^1.0.2.
Checkpoint 1.1.1 (published 2026-06-12) imports v6 and raised its peer to
@langchain/core@^1.1.48. The agent pinned core 1.1.44, which predates the v6
export. The Docker agent build runs `npm install` against package.json (no
lockfile), so it floats to checkpoint 1.1.1 against the too-old core.

Bumping core to 1.1.49 restores the v6 export and satisfies the peer range.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 18:11:21 +02:00
Alem Tuzlak d305dc6ef2 fix(examples): regenerate standalone slack lockfile for @copilotkit/bot* 0.0.2 (#5481)
## What

Regenerates the **standalone** `examples/slack/pnpm-lock.yaml` so its
`@copilotkit/bot*` deps match `package.json` (`~0.0.2`).

## Why (the actual Railway failure)

`examples/slack` commits its own standalone `pnpm-lock.yaml`, which
Railway frozen-installs when building with
`rootDirectory=/examples/slack`. PR #5478 fixed the **root** workspace
lockfile but not this standalone one, so it still recorded
`@copilotkit/bot*` at `~0.0.1` while `package.json` requires `~0.0.2`:

```
ERR_PNPM_OUTDATED_LOCKFILE  Cannot install with "frozen-lockfile" because
pnpm-lock.yaml is not up to date with <ROOT>/package.json
  specifiers in the lockfile ({"@copilotkit/bot":"~0.0.1",...})
  don't match specs in package.json ({"@copilotkit/bot":"~0.0.2",...})
```

## How

Regenerated with the repo's pinned pnpm (10.33.4, via corepack) in
standalone mode — matching how the file was originally produced and how
Railway builds it:

```
pnpm -C examples/slack install --ignore-workspace --lockfile-only
```

Result: importer specifiers are now `~0.0.2` resolving to the published
`0.0.2`; the `@ai-sdk/mcp: 1.0.21` override is preserved.

> Note: regenerating with pnpm 11 dropped the `@ai-sdk/mcp` override
(pnpm 11 ignores `package.json` `pnpm.overrides` in `--ignore-workspace`
mode), so this was generated with the pinned 10.33.4 specifically.

## Verified
`CI=true pnpm -C examples/slack install --ignore-workspace
--frozen-lockfile` → **"Lockfile is up to date"** (exit 0). Full
pre-commit suite + commitlint green. Only
`examples/slack/pnpm-lock.yaml` changed (+103/−32).
2026-06-16 17:09:45 +02:00
Alem Tuzlak 7c884b931b fix(examples): regenerate standalone slack lockfile for @copilotkit/bot* 0.0.2
examples/slack ships a standalone pnpm-lock.yaml (used by Railway when building
with rootDirectory=/examples/slack). PR #5478 fixed the root workspace lockfile
but not this one, so it still pinned @copilotkit/bot* at ~0.0.1 while
package.json now requires ~0.0.2 — pnpm install --frozen-lockfile failed with
ERR_PNPM_OUTDATED_LOCKFILE on deploy.

Regenerate it with the repo's pinned pnpm (10.33.4) via
pnpm -C examples/slack install --ignore-workspace --lockfile-only: importer
specifiers are now ~0.0.2 resolving to the published 0.0.2, the @ai-sdk/mcp
override is preserved. Verified: CI=true frozen install reports "Lockfile is
up to date" (exit 0).
2026-06-16 17:01:26 +02:00
Alem Tuzlak eea1c861c5 docs(examples): note monorepo deploy watch-paths for slack example (#5479)
## What

A small docs touch to `examples/slack/README.md` that (a) documents the
monorepo-deploy watch-path gotcha and (b) **touches a watched path so
Railway redeploys the slack services off latest `main`**.

## Why

PR #5478 fixed the example's lockfile so a standalone deploy resolves
the published `@copilotkit/bot*` `0.0.2` — but that fix only changed
**repo-root** files (`pnpm-lock.yaml`, `package.json`, `.npmrc`). The
`kite-slack-bot` / `runtime` Railway services watch `examples/slack/**`,
so they reported "watched paths not modified" and never picked up the
fix. This commit changes a file under `examples/slack/**`, so merging it
triggers a rebuild of latest `main` (which carries #5478's fix).

The added note also tells future deployers to include the repo-root
`pnpm-lock.yaml` / `package.json` in their Railway watch paths so this
doesn't recur.

Docs-only; no code change.
2026-06-16 16:10:35 +02:00
Alem Tuzlak d6d487ba47 docs(examples): note monorepo deploy watch-paths for slack example
Document that the slack example consumes the published @copilotkit/bot*
packages and that deploys (e.g. Railway) must include the repo-root
pnpm-lock.yaml / package.json in their watch paths, so a dependency bump
triggers a rebuild and a frozen install doesn't fail on an out-of-date
lockfile. Touches examples/slack/** to trigger the Railway redeploy of the
current main (which now carries the deployable-lockfile fix).
2026-06-16 16:01:18 +02:00
Alem Tuzlak a7688dd35e fix(examples): make slack example lockfile deployable (drop workspace override) (#5478)
## What

Fixes the Railway deploy of `examples/slack` failing with
`ERR_PNPM_OUTDATED_LOCKFILE`.

## Why it broke

PR #5476 bumped the example's `@copilotkit/bot*` deps to `~0.0.2` but
the lockfile didn't change — because the root `pnpm.overrides`
(`@copilotkit/bot* → workspace:*`, added for local dev) pinned those
deps to **workspace links** for every importer. So the committed
lockfile recorded the example's bot deps as `~0.0.1` /
`link:../../packages/bot`, which:
- frozen-install validates fine **inside** the monorepo (the override
masks it), but
- a **standalone Railway deploy** frozen-installs only `examples/slack`,
sees `package.json ~0.0.2` vs lockfile `~0.0.1`, and can't resolve
`link:` paths → crash.

## The fix

Now that `bot`/`bot-slack`/`bot-ui` are published at `0.0.2`:
- **Drop the three `@copilotkit/bot*` overrides** from root
`package.json`. Workspace packages still link each other via
`workspace:~`; only the **example** switches to consuming the published
versions — the correct model for a deployable demo.
- **Regenerate `pnpm-lock.yaml`** — the example's importer now records
`specifier: ~0.0.2` resolving to the **registry** `0.0.2` (not a
`link:`), so a standalone frozen install resolves cleanly.
- **Add `@copilotkit/bot*` to `minimum-release-age-exclude`** in
`.npmrc` (matching the existing `@ag-ui/*` entries) so the
freshly-published `0.0.2` resolves past the 24h supply-chain gate.

## Verified
- `pnpm install --frozen-lockfile` → "Lockfile is up to date"; the
registry `0.0.2` packages download cleanly.
- Example importer: `specifier: ~0.0.2`, `version: 0.0.2(...)`
(registry, no `link:`).
- Full pre-commit suite + commitlint green.

## Trade-off
Local monorepo dev of the example now consumes the **published** `0.0.2`
rather than live worktree source. To iterate on the bot packages against
the example locally, temporarily set the example's bot deps to
`workspace:*` (or re-add the overrides) — don't commit that.
2026-06-16 15:30:29 +02:00
Alem Tuzlak 761ae8caec fix(examples): make slack example lockfile deployable (drop workspace override)
The root pnpm.overrides pinned @copilotkit/bot* to workspace:* for every
importer, so the committed lockfile resolved the slack example's bot deps to
workspace links. A standalone deploy (Railway) frozen-installs only the example
and can't resolve those, failing with ERR_PNPM_OUTDATED_LOCKFILE (lockfile
specifiers ~0.0.1 vs package.json ~0.0.2, and link: refs that don't exist
outside the monorepo).

Now that bot/bot-slack/bot-ui are published at 0.0.2, drop the overrides so the
example resolves the published ~0.0.2 from the registry, and regenerate the
lockfile (importer specifiers now ~0.0.2, versions resolve to registry 0.0.2 —
deployable). Add @copilotkit/bot* to minimum-release-age-exclude (matching the
@ag-ui/* entries) so the freshly published 0.0.2 resolves past the 24h gate.

Workspace packages still link each other via workspace:~; only the example
switches to published versions (the correct model for a deployable demo).
2026-06-16 15:16:01 +02:00
Alem Tuzlak 5f1d771cbe chore(examples): bump slack example to @copilotkit/bot* 0.0.2 (#5476)
## What

Bumps the `examples/slack` dependency ranges for the bot packages from
`~0.0.1` to `~0.0.2`, now that `@copilotkit/bot`,
`@copilotkit/bot-slack`, and `@copilotkit/bot-ui` are all published at
**0.0.2** (the agent-native assistant pane + native streaming release).

```diff
-    "@copilotkit/bot": "~0.0.1",
-    "@copilotkit/bot-slack": "~0.0.1",
-    "@copilotkit/bot-ui": "~0.0.1",
+    "@copilotkit/bot": "~0.0.2",
+    "@copilotkit/bot-slack": "~0.0.2",
+    "@copilotkit/bot-ui": "~0.0.2",
```

## Why

A standalone/deployed install of the example (outside the monorepo)
resolves these from npm. At `~0.0.1` it would pull `bot-slack@0.0.2` but
could still resolve `bot`/`bot-ui@0.0.1`, which lack the engine surface
the pane code calls at runtime (`sink.onThreadStarted`,
`thread.setSuggestedPrompts/setTitle`, the new capability flags) — so
the assistant pane would crash. Pinning all three to `~0.0.2` keeps the
trio aligned.

## Notes

- **Lockfile unchanged**: local monorepo installs already resolve these
to the workspace copies via the root `pnpm.overrides` (`@copilotkit/bot*
→ workspace:*`), so this range bump only affects deployed/standalone
installs.
- No code changes — `package.json` only.
2026-06-16 14:33:09 +02:00
Alem Tuzlak ba159ce8e1 chore(examples): bump slack example to @copilotkit/bot* 0.0.2
@copilotkit/bot, @copilotkit/bot-slack, and @copilotkit/bot-ui are published at
0.0.2 (agent-native assistant pane + native streaming). Point the slack
example's dependency ranges at ~0.0.2 so a deployed/standalone install pulls
the new packages. Local monorepo installs already use the workspace copies via
root pnpm.overrides, so the lockfile is unchanged.
2026-06-16 14:22:50 +02:00
Alem Tuzlak 7ed4a44447 chore: release bot v0.0.2 (#5475)
## Release bot v0.0.2

**Scope:** `bot` | **Bump:** `patch`

---

### How this release process works

1. **This PR was created automatically** by the "release / create-pr"
workflow.
   It bumped the `bot` packages to `0.0.2`
   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 `bot` packages to npm at version `0.0.2`
   - Creates git tag `bot/v0.0.2`
   - 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.
bot/v0.0.2
2026-06-16 13:58:23 +02:00
AlemTuzlak 035e7cf9c9 chore: release bot v0.0.2 2026-06-16 11:43:12 +00:00
Alem Tuzlak 72203fbcba chore: release bot-slack v0.0.2 (#5466)
## Release bot-slack v0.0.2

**Scope:** `bot-slack` | **Bump:** `patch`

---

### How this release process works

1. **This PR was created automatically** by the "release / create-pr"
workflow.
   It bumped the `bot-slack` packages to `0.0.2`
   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 `bot-slack` packages to npm at version `0.0.2`
   - Creates git tag `bot-slack/v0.0.2`
   - 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.
bot-slack/v0.0.2
2026-06-16 13:26:27 +02:00
Jordan Ritter e941049d3c fix(harness): cascade fallback to whole-bubble-minus-toolbar for tool-only responses (Class B) (#5474)
## Summary

Adds a **Class B cascade fallback** in `readCascadeStateLast` so the
harness settle gate can resolve text for **single-bubble tool-only
responses** (recharts SVG, gen-UI cards, A2UI render-only output). These
bubbles have ALL scoped text selectors empty but carry substantial
rendered content in non-cascade children. The current cascade-pollution
guard correctly returns `null` for arbitrary-index reads
(`readCascadeState` / `findAssistantBubbleAt`) — but for the LAST
bubble, by `RUN_FINISHED` the content is mounted and stable, so we can
safely fall back to `bubble.textContent` MINUS the assistant-toolbar's
textContent (the suffix the toolbar contributes).

This is the residual fix on top of PR #5472 (Class A: multi-bubble
last-bubble-has-prose) and #5473 (probe taxonomy cleanup). The ~200-red
plateau on PocketBase after #5472 landed is consistent with Class B
affecting the long tail of tool-render-only demos
(ms-agent-python:beautiful-chat-*, plus other tool-render-only
Class-B-shaped demos).

## Class B failure mode (live-confirmed)

Probed against
`https://showcase-ms-agent-python-staging.up.railway.app/demos/beautiful-chat`
with the prompt _"Show me a bar chart of monthly sales for Q1 2026."_:

- `count = 1` (single canonical bubble)
- `bubble.querySelector('[data-message-content]')` -> empty string
- `bubble.querySelector('.cpk:prose')` -> empty string
- `bubble.querySelector('.prose')` -> empty string
- `bubble.textContent` -> **351 chars** of real content (chart title,
axis labels, animation styles, query metadata)
- Current `readCascadeStateLast` returns `text=null` -> settle gate
spins on `text-unstable` until timeout (RED).

## Red-green proof (verbatim from /tmp/cr/class-b-red-green.mjs)

The probe runs TWO in-browser readers against the same DOM snapshot —
the OLD (current production) and the NEW (with fallback). Run live
against staging, **before any code changes** in this PR:

```
[probe] navigating to https://showcase-ms-agent-python-staging.up.railway.app/demos/beautiful-chat
[probe] locating chat input
[probe] sending prompt: Show me a bar chart of monthly sales for Q1 2026.
[probe] waiting 20000ms for response to render
[probe] result: {
  "count": 1,
  "oldText": null,
  "newText": "query_dataquery:\"monthly sales for Q1 2026\"\n        @keyframes barSlideIn {\n          from { transform: translateY(40px); opacity: 0; }\n          20% { opacity: 1; }\n          to { transform: translateY(0); opacity: 1; }\n        }\n      Monthly Sales for Q1 2026Breakdown of income generated each month in Q1 2026.JanuaryFebruaryMarch0255075100January",
  "oldLen": null,
  "newLen": 351,
  "newPreview": "query_dataquery:\"monthly sales for Q1 2026\"\n        @keyframes barSlideIn {\n          from { transform: translateY(40px); opacity: 0; }\n          20% { opacity: 1; }\n          to { transform: translat"
}
[probe] verdict: {
  "red": true,
  "green": true,
  "count": 1
}
[probe] RED-GREEN confirmed: old=null, new=non-empty.
```

- **RED**: `oldText = null`, `oldLen = null` — matches current
production behavior; settle gate cannot resolve text.
- **GREEN**: `newText` is 351 chars of stable rendered content; settle
gate can lock in `text-stable`.

## The fix

In `showcase/harness/src/probes/helpers/assistant-message-count.ts`,
inside `readCascadeStateLast`'s per-tier loop — AFTER the existing
scoped-selector cascade exhausts without a non-empty match and BEFORE
returning `{count, text: null}`:

1. Read `bubble.textContent` (whole bubble).
2. Read
`bubble.querySelector('[data-testid="copilot-assistant-toolbar"]').textContent`
if present.
3. If the toolbar's text is a **trailing suffix** of the whole-bubble
text (which it canonically is — toolbar is a leaf sibling of the prose
div), strip it.
4. If what remains is non-empty after trim -> return `{count, text:
<trimmed>}`.
5. Otherwise (no toolbar, no content, or only-toolbar text) -> return
`{count, text: null}` to keep polling.

The same fallback is NOT applied to `findAssistantBubbleAt` /
`readCascadeState` because those address arbitrary indices and
intermediate bubbles in multi-step responses can transiently carry empty
scoped text while the NEXT bubble is mounting. Reading whole-bubble at
intermediate indices would re-introduce the cross-bubble text flap PR
#5462 was designed to prevent. The LAST bubble in a multi-step turn is
the agent's terminal output, mounted by `RUN_FINISHED`.

## Why the fallback is safe (toolbar-suffix invariant)

In the canonical CopilotKit assistant bubble
(`CopilotChatAssistantMessage.tsx`), the toolbar is the **last child**
of the bubble, mounted as a leaf sibling of the prose/markdown wrapper.
Its `textContent` is therefore the trailing slice of
`bubble.textContent`. Stripping it by suffix-match yields the message
content. When the suffix-match fails (defensive — e.g. the DOM shape
changes), we return `bubble.textContent` unchanged rather than mangle
it. When the toolbar isn't present (headless / custom-composer), we
return whole text. When even whole-text is empty, we return `null`
(don't lock the settle gate on a placeholder).

## Tests

- `assistant-message-count.test.ts`: 9 new test cases under a new
`describe("readCascadeStateLast")` block. Pins:
  - non-empty scoped text path unchanged (no fallback when scoped works)
- Class B fallback: scoped empty + whole-bubble non-empty + toolbar
suffix -> strip suffix, return content
  - Class B fallback works without a toolbar (returns whole text)
  - defensive: toolbar text NOT a suffix -> return whole text unchanged
  - everything empty -> return `text:null` (settle gate keeps polling)
  - only-toolbar text -> return `text:null` after strip
  - multi-bubble: reads the LAST bubble, not the first
  - no tier matches -> `{count:0, text:null}`
  - `evaluate()` throws -> swallowed -> `{count:0, text:null}`
- Full harness suite: **2769 / 2769 passing across 129 files** (`pnpm
exec vitest run` in `showcase/harness`).
- TypeScript: `pnpm exec tsc --noEmit` clean.
- Lint: `oxlint` 0 warnings, 0 errors.
- Format: `oxfmt --check` clean.

## Out-of-scope (untouched)

- `conversation-runner.ts` (the settle-gate logic stays — the cascade
does the right thing now).
- `sse-interceptor.ts`, `init-scripts.ts`, `d6-all-pills.ts`.
- `findAssistantBubbleAt` / `readCascadeState` (intentionally retain the
pollution guard — they address arbitrary indices).
- `resolveBubbleTextFromSelectors` (pure sibling of
`findAssistantBubbleAt`, intentionally retains pollution-guard
semantics).

## Verification plan after merge

1. Watch CI green on the PR.
2. Promote `showcase-ms-agent-python` and other tool-render-only
services.
3. Observe PocketBase D6 red counts: expect the ~200-red plateau to drop
further toward baseline.
4. If any demo regresses, revert `readCascadeStateLast` alone.

## Files changed

- `showcase/harness/src/probes/helpers/assistant-message-count.ts`
(production fix)
- `showcase/harness/src/probes/helpers/assistant-message-count.test.ts`
(9 new tests)
2026-06-16 04:22:21 -07:00
Jordan Ritter 72520de43d fix(harness): cascade last-bubble whole-bubble-minus-toolbar fallback for tool-only responses 2026-06-16 04:05:49 -07:00
Jordan Ritter f7779f90bf refactor(showcase): probe taxonomy cleanup — drop Smoke, rename E2E (Demo) → UI (Frontend) (#5473)
## Summary

The smoke probe was the same HTTP contract as `/health` on the same
service (200-OK JSON body), so every tick paid two HTTP calls for the
same liveness signal. This PR:

- Drops the `/smoke` GET probe and the `smoke:<slug>` primary
`ProbeResult`. The driver now emits `health:<slug>` as the primary and
`agent:<slug>` via writer side-emit (half the per-tick cost).
- Renames the dashboard's D3 / e2e row label from `E2E (Demo)` to `UI
(Frontend)` (and the short cell badge from `E2E` to `UI`) — the row was
already about "the demo page renders in a browser", not E2E in the
traditional sense.
- Drops the `Smoke` row from the cell drilldown (CellState.smoke field
is retained for back-compat so historical rows still parse).
- Adds an explicit `Health` row to the legend (was implicit).

The driver's registry `kind` stays `"smoke"` so existing YAML configs +
orchestrator family wiring keep routing to this driver — the emission
key (`health:<slug>`) is the taxonomy contract that matters. The
underlying probe key (`e2e:<slug>/<feature>`) is preserved on PocketBase
so historical rows render correctly during the rename window.

## RED proof (BEFORE)

`smoke:<slug>` primary emission in `liveness.ts` (origin/main):

```
18: *   1. `smoke:<slug>`  — the RETURN VALUE of `run()`. The invoker runs
56: * `smoke:<slug>`/`health:<slug>`/`agent:<slug>` triple the dashboard
241:        input.mode === "discovery" ? `smoke:${slug}` : input.key;
```

`E2E (Demo)` / `Smoke` strings in dashboard render code (origin/main):

```
cell-drilldown.tsx:38: * Trip)" (chat+tools round-trip), D3/e2e = "E2E (Demo)" (the demo page loads
cell-drilldown.tsx:49:  { key: "e2e", label: "E2E (Demo)" },
cell-drilldown.tsx:52:  { key: "smoke", label: "Smoke" },
cell-pieces.tsx:382:        name="E2E"
unified-cell.tsx:231:      <TestBadge name="E2E" level={model.d3} />
adaptive-legend.tsx:59:        E2E (Demo): demo page loads and round-trips in a browser
```

## GREEN proof (AFTER)

Primary emission rewritten — driver returns `health:<slug>` and no
`smoke:<slug>` ProbeResult is ever emitted:

```
33: * to `/smoke` and emitted a `smoke:<slug>` primary result, but that probe
230:  // `kind: "smoke"` is the registry/family identifier the orchestrator
236:  kind: "smoke",        // registry-key back-compat; no smoke ProbeResult is emitted
367:  if (input.mode === "discovery") return `health:${slug}`;
368:  if (input.key.startsWith("smoke:")) {
369:    return `health:${input.key.slice("smoke:".length)}`;
```

New labels in dashboard:

```
cell-drilldown.tsx:60:  { key: "e2e", label: "UI (Frontend)" },
cell-pieces.tsx:382:        name="UI"
unified-cell.tsx:231:      <TestBadge name="UI" level={model.d3} />
adaptive-legend.tsx:65:        UI (Frontend): demo page renders in browser (Playwright)
```

Confirmation that the old labels are gone from render code:

```
$ grep -rn 'E2E (Demo)\|"Smoke"' cell-drilldown.tsx cell-pieces.tsx unified-cell.tsx adaptive-legend.tsx
(no matches — labels removed)
```

## Test results

- `pnpm exec vitest run` in `showcase/shell-dashboard`: **63 files, 1089
tests passed, 1 skipped**
- `pnpm exec vitest run` in `showcase/harness`: **129 files, 2760 tests
passed**
- `pnpm exec tsc --noEmit` in both: clean

Liveness driver tests include a regression guard (`never emits a
smoke:<slug> key`) so the smoke contract cannot be re-introduced
silently.

## Test plan

- [x] Dashboard vitest green (1089/1089)
- [x] Harness vitest green (2760/2760)
- [x] tsc --noEmit green in both packages
- [x] Visual cross-check: drilldown renders 6 rows (no Smoke), e2e row
labelled `UI (Frontend)`; cell strip shows `UI` badge in place of `E2E`;
legend shows explicit `Health` row + `UI (Frontend)` D3 line
2026-06-16 01:57:15 -07:00
Jordan Ritter 53f021afe8 refactor(showcase): probe taxonomy cleanup — drop Smoke, E2E (Demo) → UI (Frontend)
The smoke probe was the same HTTP contract as /health on the same
service (200-OK JSON body), so every tick paid two HTTP calls for the
same liveness signal. Drop the /smoke GET + the smoke:<slug> primary
ProbeResult; the driver now emits health:<slug> as the primary and
agent:<slug> via writer side-emit (half the per-tick cost).

The driver's registry kind stays "smoke" so existing YAML configs and
orchestrator family wiring keep routing to this driver — the
emission key (health:<slug>) is the taxonomy contract that matters.

Dashboard:
- Drilldown: D3/e2e row labelled "UI (Frontend)" (was "E2E (Demo)");
  Smoke row dropped (CellState.smoke field retained for back-compat).
- Cell badges: short label "UI" (was "E2E") in cell-pieces + unified-cell.
- Legend: D3 = "UI (Frontend): demo page renders in browser (Playwright)"
  plus an explicit Health row at the top.

Tests updated to assert new labels:
- cell-drilldown.test.tsx: 6 dimensions (no Smoke); UI (Frontend) label.
- cell-drilldown.lazy-signal.test.tsx: drilldown-badge-ui--frontend- testid.
- cell-pieces.test.tsx + .signal-degrade.test.tsx: badge name "UI".
- unified-cell.test.tsx: mock-badge-UI.
- overlay-selector-integration.test.tsx: "UI" in place of "E2E".
- dashboard-color-matrix.test.tsx: badge: "UI" case names.
- liveness.test.ts: two-call contract (health + agent), regression guard
  asserting no smoke:<slug> ever emitted.

Underlying probe key (e2e:<slug>/<feature>) preserved on PocketBase so
historical rows render correctly during the rename window.
2026-06-16 01:48:41 -07:00
Jordan Ritter 665dd5aae6 fix(harness): waitForTurnComplete reads LAST bubble (multi-step agent regression from #5462) (#5472)
## Summary

**URGENT hotfix.** Staging is widely red for D5/D6 multi-step demos.
Root cause confirmed via live Playwright DOM inspection on staging
(langgraph-typescript : beautiful-chat : pie chart prompt).

PR #5462's defect-2 fix uses `bubbleIndex = turnIndex - 1`, which
assumes one assistant bubble per turn. Multi-step agents (LangGraph,
Mastra, CrewAI, llama-index, ag2, etc.) emit 2-3+ bubbles per turn —
tool-call bubble + tool-render bubble + final-text bubble. The
strict-index read lands on an intermediate **tool-call** bubble whose
scoped-text selectors (`.cpk:prose`, `[data-message-content]`, `.prose`,
`p`) are **empty** (tool-call content lives in a sibling card div
outside the scoped cascade). The cascade-pollution guard correctly
returns `null` → the text-stable conjunct never converges →
`reason=text-unstable` timeout across virtually every multi-step demo
(beautiful-chat-*, gen-ui-*, shared-state-*, agentic-chat on most
backends).

## Live RED-GREEN proof (staging)

Standalone Playwright probe against
`showcase-langgraph-typescript-staging.up.railway.app/demos/beautiful-chat`,
prompt = `"pie chart of revenue distribution by category from the sample
sales data"`, captured BOTH the OLD-logic read (`bubbleIndex = turnIndex
- 1 = 0`) AND the NEW-logic read (`bubbleIndex = count - 1 = 2`) in ONE
atomic page evaluate after the turn settled:

```
TOTAL BUBBLE COUNT FOR TURN 1: 3  (true multi-step — tool-call + tool-render + final-text)
FINAL TIER: [data-testid="copilot-assistant-message"]

OLD LOGIC (bubbleIndex = turnIndex - 1 = 0):
  count = 3
  text  = null
  reason = all scoped selectors empty           ← would time out: reason=text-unstable

NEW LOGIC (bubbleIndex = count - 1 = 2):
  count = 3
  text  = "Here is the pie chart showing the revenue distribution by category from the sample sales data."
  textLen = 94                                  ← gate settles cleanly
```

RED ≠ GREEN; new logic returns 94 chars of stable final-text where old
logic returns `null`. The bug and the fix are proven on real staging
DOM.

### Live DOM evidence
LangGraph TS beautiful-chat, prompt = "pie chart of revenue distribution
by category…":
```
count = 3 bubbles in [data-testid="copilot-assistant-message"]
bubble[0]: tool-call "query_data"   — `.cpk:prose` exists but EMPTY (content in sibling <div class="my-1.5">)
bubble[1]: pie-chart card           — `.cpk:prose` exists but EMPTY (content in sibling styled card div)
bubble[2]: final text "Here is the pie chart..." — `.cpk:prose` has 94 chars + toolbar mounted
```

The old pre-#5462 runner read `list[last]` (always the last bubble —
always bubble[2] above). The PR moved to `turnIndex - 1`, which is
bubble[0] (the tool-call) for any multi-step turn → broken.

## Fix

Read the LAST bubble in the matched cascade tier (`count - 1`) instead
of a fixed strict index. Defect-2 protection (don't read a leftover
bubble from a prior turn) is preserved via an explicit **pre-submit
baselineCount sentinel snapshot** in the runner — the gate now requires
`count > baselineCount` rather than `count > turnIndex - 1`. This
correctly rejects stale prior-turn bubbles **without assuming 1 bubble =
1 turn**.

### Changes
- `assistant-message-count.ts`: add `readCascadeStateLast(page)` — same
atomic single-evaluate contract as `readCascadeState`, but resolves the
bubble index internally as `count - 1`.
- `conversation-runner.ts`:
- Snapshot `baselineCount = countAssistantMessages(page)` BEFORE
`sendTurnMessage()` each turn.
- Pass through to `waitForTurnComplete` via new `baselineCount` option
(defaults to `turnIndex - 1` to preserve unit-test fake compatibility).
- `waitForTurnComplete` reads via `readCascadeStateLast` and gates on
`domOk = count > baselineCount`.
  - Cold-start retry re-snapshots `baselineCount` after `page.reload()`.
- Returned `bubbleIndex` in the success ctx = `count - 1` (matches the
bubble whose text settled the gate).
- `dom-missing` reason classifier updated to `countFinal <=
baselineCount`.

## Why this doesn't regress defect-2

The original defect-2 (un-turn-scoped bubble selection) leaked a later
turn's bubble into THIS turn's assertions because
`readLastAssistantText` read `list[last]` GLOBALLY with no per-turn
baseline. The new fix re-introduces the "read last" semantics but adds
the **per-turn pre-submit baseline count snapshot** — the gate only
advances once a NEW bubble has appeared (`count > baselineCount`), so we
cannot read a leftover from a prior turn, and the "last" we return is
the last NEW bubble (which for multi-step turns is the final-text bubble
whose scoped text actually settles).

## Test plan

- [x] **Live RED-GREEN staging probe** (see above) — confirms OLD logic
= null and NEW logic = 94-char final-text on a real 3-bubble multi-step
turn.
- [x] All 72 `conversation-runner.test.ts` +
`assistant-message-count.test.ts` unit tests pass (default
`baselineCount = turnIndex - 1` keeps existing count-progression scripts
working).
- [x] Full harness vitest suite: 2758/2758 green across 129 test files.
- [x] `tsc --noEmit` clean on `showcase/harness`.
- [x] `oxfmt --check` clean on changed files.
- [x] `oxlint` clean on changed files.
- [ ] Staging D5/D6 sweep after merge confirms multi-step demos
(beautiful-chat, gen-ui, shared-state, langgraph/mastra/crewai
agentic-chat) recover from `text-unstable` red.

## Scope

Strictly the two files above. No unrelated changes. No refactors.
2026-06-16 01:12:10 -07:00
Jordan Ritter 88e77f1403 fix(showcase): allow harness to boot locally without SHARED_SECRET (unblock local verify) (#5471)
## Problem

PR #5458 (`c81b361f1` — *fix(showcase/harness): register
/webhooks/deploy on CP path + fail-loud on missing SHARED_SECRET*) added
a fail-loud gate to `loadWebhookSecrets` in
`showcase/harness/src/orchestrator.ts` that refuses harness boot in any
deployable mode (`NODE_ENV !== "test"`) without `SHARED_SECRET` (or
`SHARED_SECRET_PREV`) set, because `POST /webhooks/deploy` is only
registered when `webhookSecrets.length > 0` (gate at
`showcase/harness/src/http/server.ts:119`).

The local `showcase/docker-compose.local.yml` harness services inherit
`NODE_ENV=production` from the image and do not set `SHARED_SECRET` (and
should not — that secret only matters for the Showcase: Verify Deploy
webhook flow on Railway). As a result, the harness crash-loops on every
local `bin/showcase test --d5/--d6` run:

```
FATAL-CONFIG: SHARED_SECRET (or SHARED_SECRET_PREV) is required — refusing to boot in any deployable mode … Set SHARED_SECRET (or SHARED_SECRET_PREV) in the env, or set NODE_ENV=test / HARNESS_ALLOW_NO_SECRET=1 for local dev. Current NODE_ENV=production.
```

…which blocks **all** local D5/D6 verify.

## Fix

Add `HARNESS_ALLOW_NO_SECRET=1` (the documented local-dev escape hatch —
explicitly named in the FATAL-CONFIG message itself, and the dedicated
env var checked by `loadWebhookSecrets` at `orchestrator.ts:190`) to
both harness services in `showcase/docker-compose.local.yml`:

- `harness-control-plane`
- `harness-pool-worker`

Inline comments explain the rationale and pin the relevant source
locations (the gate at `src/http/server.ts:119` and `loadWebhookSecrets`
in `src/orchestrator.ts`).

Diff: 17 lines added (comments + 2 env vars), no other files touched.

## Prod impact: NONE

Railway sets `SHARED_SECRET` explicitly via env on every harness
service, so `loadWebhookSecrets` sees a real secret, registers `POST
/webhooks/deploy`, and **never reads `HARNESS_ALLOW_NO_SECRET`**. The
escape hatch only takes effect when both secrets are absent. This change
only affects the local docker-compose stack.

## Local verification

Brought up the slot-1 isolated stack with the fix applied via
`SHOWCASE_ISO_SLOT=3 ./bin/showcase test langgraph-typescript --d6
--verbose --isolate` and observed clean boot:

```
showcase-iso1-harness                 Up 12 seconds (healthy)
showcase-iso1-langgraph-typescript    Up 12 seconds (healthy)
showcase-iso1-harness-pool-worker-1   Up 12 seconds (healthy)
showcase-iso1-dashboard               Up 13 seconds (healthy)
showcase-iso1-aimock                  Up 18 seconds (healthy)
showcase-iso1-pocketbase              Up 18 seconds (healthy)
```

Harness boot logs confirm:

- `fleet.role-selected` + `fleet.control-plane.started` +
`showcase-harness.fleet.control-plane.boot` (port 8080) — control-plane
up
- `webhook auth disabled — neither SHARED_SECRET nor SHARED_SECRET_PREV
is set` with `escapeHatch:true` at **warn** level — the expected log
produced by `loadWebhookSecrets` when the escape hatch fires
- `worker.registered` + `fleet.worker.boot` — worker self-registered
- `worker.claimed jobId=… probeKey=d6:langgraph-typescript` — real D6
job picked up and run end-to-end

NO `FATAL-CONFIG`, NO restart loop.

(The D6 cell may still be RED on this branch — that is a pre-existing
issue, unrelated to this infra fix. The point of this PR is "harness
boots and runs a job at all locally", which it now does.)

## References

- PR #5458 (`c81b361f1`): the fail-loud gate this PR re-enables an
escape for
- `showcase/harness/src/orchestrator.ts` `loadWebhookSecrets` (~line
180-220): the predicate that reads `HARNESS_ALLOW_NO_SECRET`
- `showcase/harness/src/http/server.ts:119`: the route-registration gate
2026-06-16 01:07:44 -07:00
Jordan Ritter d145bb1708 fix(harness): waitForTurnComplete reads LAST bubble in matched tier
PR #5462's defect-2 fix assumed 1 bubble per turn (bubbleIndex =
turnIndex - 1). Multi-step agents (LangGraph, Mastra, CrewAI) emit
2-3+ bubbles per turn — tool-call + tool-render + final-text — so
the strict-index read lands on an intermediate tool-call bubble
whose scoped-text selectors (`.cpk:prose`, `[data-message-content]`,
`p`) are EMPTY (its content lives in a sibling card div outside the
scoped cascade). The cascade-pollution guard returns null forever
and the text-stable conjunct times out with `reason=text-unstable`
across virtually every multi-step demo (beautiful-chat-*, gen-ui-*,
shared-state-*, agentic-chat on LangGraph/Mastra/CrewAI/etc.).

Live DOM evidence from staging (langgraph-typescript :
beautiful-chat : pie chart prompt):
  count = 3 bubbles
  bubble[0]: tool-call "query_data" — `.cpk:prose` empty
  bubble[1]: pie chart card — `.cpk:prose` empty
  bubble[2]: final text "Here is the pie chart..." — 90 chars

Fix: read the LAST bubble in the matched cascade tier (count - 1)
instead of a fixed strict index. Defect-2 protection (don't read a
leftover bubble from a prior turn) is preserved via an explicit
pre-submit `baselineCount` sentinel snapshot in the runner; the
gate now requires `count > baselineCount` rather than
`count > turnIndex - 1`, which correctly rejects stale bubbles
without assuming 1 bubble = 1 turn.

Changes:
  - assistant-message-count.ts: add `readCascadeStateLast(page)` —
    same atomic single-evaluate contract as `readCascadeState` but
    resolves the bubble index internally as `count - 1`.
  - conversation-runner.ts: snapshot `baselineCount` before
    sendTurnMessage; pass through to `waitForTurnComplete` via new
    `baselineCount` option (defaults to `turnIndex - 1` for unit-
    test fakes); `waitForTurnComplete` reads via
    `readCascadeStateLast` and gates on `count > baselineCount`.
    Cold-start retry re-snapshots `baselineCount` after page.reload.

Test plan:
  - All 72 conversation-runner + assistant-message-count unit tests
    pass (default baselineCount preserves count-progression scripts).
  - Full harness suite 2758/2758 green, tsc clean, oxfmt/oxlint
    clean on changed files.
2026-06-16 01:02:52 -07:00
Jordan Ritter 48ef506abf fix(showcase): allow harness to boot locally without SHARED_SECRET
PR #5458 (c81b361f1) added a fail-loud gate that refuses harness boot
in any deployable mode (NODE_ENV != "test") without SHARED_SECRET or
SHARED_SECRET_PREV: POST /webhooks/deploy is only registered when
webhookSecrets.length > 0 (src/http/server.ts:119 +
loadWebhookSecrets in src/orchestrator.ts). The local docker-compose
stack inherits NODE_ENV=production from the harness image and does
not (and should not) set SHARED_SECRET, so every local D5/D6 verify
run via bin/showcase test --d5/--d6 was crashing the harness in a
restart loop with FATAL-CONFIG.

Fix: add HARNESS_ALLOW_NO_SECRET=1 (the documented local-dev escape
hatch — explicitly referenced in the FATAL-CONFIG message itself) to
both harness services in showcase/docker-compose.local.yml. Inline
comments explain the rationale and pin the relevant source locations.

Prod impact: NONE. Railway sets SHARED_SECRET explicitly via env on
every harness service, so loadWebhookSecrets sees a real secret,
registers POST /webhooks/deploy, and never reads
HARNESS_ALLOW_NO_SECRET. This change only affects the local
docker-compose stack.

Verified locally: showcase-iso1-harness boots cleanly (Up healthy),
the expected warn-level webhook-auth-bypass log fires
(escapeHatch:true), worker registers, scheduler starts, and a real
d6:langgraph-typescript job claims successfully — confirming the
gate fires only in deployable contexts.
2026-06-16 01:00:22 -07:00
Jordan Ritter 7cc579a58d chore: drop dead .changeset/ debris and workflow path filters (#5470)
## Summary

The repo migrated off `@changesets/*` to conventional-commit-driven
releases. This chore PR removes the leftover debris:

- **11 stale `.changeset/*.md` files** (the entire `.changeset/`
directory)
- **4 dead `paths:` filter lines** in two e2e workflows

## Migration context

Releases are driven by `scripts/release/prepare-release.ts` /
`scripts/release/lib/changes.ts::getChangesSummary`, which reads `git
log <lastTag>..HEAD` commit subjects. It never touches `.changeset/`.

Verification — none of the changesets tooling is wired up anymore:

- No `.changeset/config.json`
- No `@changesets/*` in any `package.json` (root or workspace)
- No npm scripts reference `changeset`

The 11 `.changeset/*.md` files describe changes that have either already
shipped (via commit subjects in prior releases) or will ship in the next
release (via the current commit subjects in the `v1.60.1..main` window).
The files are inert with respect to releases — most are 2+ months old;
the newest (`slack-agent-native-apis.md`) was added recently by a
contributor who didn't know about the migration.

## What's removed

`.changeset/` (11 files):

- `angular-21-install-and-types.md`
- `angular-disable-license-watermark.md`
- `bump-license-verifier-ent-251.md`
- `debug-mode.md`
- `empty-mails-applaud.md`
- `ent-314-thread-connect-ux.md`
- `ent-658-generated-thread-tool-roundtrip.md`
- `five-avocados-visit.md`
- `fix-thread-switch-state-reset.md`
- `little-pears-tell.md`
- `slack-agent-native-apis.md`

Workflow `paths:` filters (4 lines across 2 files):

- `.github/workflows/test_e2e-dojo.yml` — `push.paths`,
`pull_request.paths`, and the `dorny/paths-filter` `ts:` filter
- `.github/workflows/test_e2e-legacy-v1.yml` — `push.paths`

Nothing to fire on after the directory is gone.

## Test plan

- [ ] CI green on this PR
2026-06-16 00:28:03 -07:00
Jordan Ritter 5afa55f067 chore: drop dead .changeset/ debris and workflow path filters
The repo migrated off @changesets/* to conventional-commit-driven releases.
scripts/release/lib/changes.ts::getChangesSummary reads `git log <lastTag>..HEAD`
commit subjects and never touches .changeset/. No .changeset/config.json,
no @changesets/* in any package.json, no npm scripts reference it.

The 11 .changeset/*.md files describe changes that have either already
shipped (via commit subjects in prior releases) or will ship in the next
release (via the current commit subjects in the v1.60.1..main window) —
the .changeset/ files are inert.

Also removes the dead workflow `paths:` filters in
test_e2e-dojo.yml and test_e2e-legacy-v1.yml that re-fired e2e on
.changeset/ changes — nothing to fire on after the directory is gone.
2026-06-16 00:25:05 -07:00
Jordan Ritter f8b14f1d6a fix(react-core): await runAgent in useInterrupt::resolve and propagate to demo-local hooks (#5461)
## Summary

Convert `useInterrupt::resolve` and the demo-local
`useHeadlessInterrupt::resolve` callbacks from fire-and-forget into
`async` + `return await copilotkit.runAgent(...)`, so callers receive a
Promise that settles when the resume run settles.

## Mechanism

Pre-fix: `resolve(...)` called `copilotkit.runAgent({...})` without
`await` and without `return` — the arrow returned `undefined`. The
showcase harness DOM-settle check on the assistant confirmation bubble
timed out because consumers had no handle to sequence against the resume
run's settle.

Post-fix:
- Framework `useInterrupt::resolve` is `async`, `return await`s
`runAgent`, wraps in try/catch + `setPendingEvent(null)` +
`console.error` + rethrow on rejection.
- `onRunFailed` now also `setPendingEvent(null)` — symmetric with
`onRunStartedEvent`, prevents stuck popups on run failure.
- `InterruptHandlerProps.resolve` / `InterruptRenderProps.resolve` typed
`() => Promise<RunAgentResult>` (was `() => void`).
- The same fix-pattern applied to 13 demo-local `useHeadlessInterrupt`
implementations across integrations: ag2, agno, claude-sdk-typescript,
crewai-crews, langgraph-{fastapi,python,typescript}, langroid,
llamaindex, mastra, pydantic-ai, spring-ai, strands.

## Verification

- **Unit:** new tests `resolve returns a Promise that settles when
runAgent settles (RESUME-PATH)` + `resolve rejects when runAgent
rejects, logs the failure, and clears pending (RESUME-PATH-REJECT)`.
Red-then-green on baseline. 22/22 react-core hook tests passing.
- **Backplane (end-to-end):** showcase control plane on langgraph-python
via `./bin/showcase test langgraph-python:gen-ui-interrupt --d5 --direct
--verbose` (GREEN, 70.1s, was RED-RESUME-PATH baseline) and
`:interrupt-headless --d5 --direct --verbose` (GREEN, 7.7s, was
RED-RESUME-PATH baseline). Built Docker artifact + aimock D6 fixture
replay — staging-equivalent path.

## CR

4 review rounds × 7 unbiased agents = 28 agent-runs. 2 Procedure 3
audits (3 promotions in round 1 → Fix C + D; 0 promotions in round 2).
Convergence achieved per agent-confirmed `bucket (a) empty` + Procedure
3 zero `PROMOTE_TO_A`.

## Out of scope (deferred follow-ups)

- **No manifest unquarantine.** A prior version of this branch flipped
`interrupt-headless` and `gen-ui-interrupt` from
`not_supported_features` to `features` across 12 manifests, but Phase 4
backplane validation across 8 PATCHED integrations × 2 demos surfaced
(a) `langgraph-fastapi:gen-ui-interrupt` still RED-RESUME-PATH because
the framework patch doesn't propagate from workspace to the
integration's Docker image without a published `@copilotkit/react-core`
release, and (b) a separate popup-mount RED-RUNTIME-OTHER cluster across
6 integrations that's pre-existing rot unrelated to this PR. Manifest
unquarantine + `@copilotkit/react-core` version bump are deferred to a
follow-up `copilotkit-release` PR after this fix lands.
- **No version bump.** Release follows in a separate PR.
- **Bucket (c) follow-up backlog** (audit verdicts: 11 STAY_IN_C):
  - JSON.parse guarding in showcase demos (pre-existing, all 13)
- Type-design quirks in `InterruptHandlerProps.result` (non-nullable in
type, nullable in runtime)
  - `renderInChat` dynamic toggle leak (pre-existing edge case)
- JSDoc clarity ("symmetric with onRunFailed" wording, `@typeParam
TRenderInChat`, `event.value` "any" → "unknown")
  - StrictMode test coverage gap
- 13× duplicate `useHeadlessInterrupt` → bucket (d): consolidate into a
shared module in a future PR

## Files changed

16 files, +486/-154:
- `packages/react-core/src/v2/hooks/use-interrupt.tsx`
- `packages/react-core/src/v2/hooks/__tests__/use-interrupt.test.tsx`
- `packages/react-core/src/v2/types/interrupt.ts`
- 13×
`showcase/integrations/<integ>/src/app/demos/interrupt-headless/page.tsx`

## Test plan

- [x] react-core unit tests (22/22 passing including new RESUME-PATH +
RESUME-PATH-REJECT regression tests)
- [x] Backplane D5 probe on langgraph-python (gen-ui-interrupt +
interrupt-headless GREEN end-to-end via the showcase control plane)
- [ ] CI green on this PR
2026-06-16 00:08:56 -07:00
tylerslaton 4564e6205d chore: release bot-slack v0.0.2 2026-06-16 05:12:14 +00:00
Tyler Slaton 431d5baae0 feat(bot-slack): agent-native Slack APIs — assistant pane + native streaming (default-on) (#5447)
## What

Activates Slack's agent-grade APIs as the **default** experience for
`@copilotkit/bot-slack`, with zero config and safe degradation.
Implements the Notion spec *"Slack Agent APIs in bot-slack — assistant
pane + native streaming by default"* (Rev 2).

### Assistant pane ("Agents & AI Apps")
- Opening the pane greets the user + shows tappable prompt chips; each
pane conversation is its own thread (replies stay in-thread, instead of
leaking to a merged flat DM).
- While the agent runs, native composer status
(`assistant.threads.setStatus`: "is thinking…", "is using `tool`…")
replaces placeholder/`🔧` messages.
- Pane threads are auto-titled from the first message.
- Customize via the new `assistant` option; `assistant: false` disables
pane handling. Apps without the Agents toggle behave exactly as before
(pane machinery dormant).

### Native streaming
- Replies stream via `chat.startStream` / `appendStream` / `stopStream`
(raw markdown — real tables / fenced code render natively) wherever a
thread exists.
- Flat DMs and workspaces without the streaming API fall back to the
legacy `chat.update` transport **automatically** — the first
`startStream` failure marks the workspace legacy and replays via legacy.
Opting in can never break a bot. `streaming: "legacy"` forces the old
transport.

### Portable engine surface (`@copilotkit/bot`)
- `bot.onThreadStarted` lifecycle handler + `IncomingThreadStart` sink
event.
- Capability-gated `thread.setSuggestedPrompts` / `thread.setTitle` (the
shipped `postFile` gating pattern).
- Two `SurfaceCapabilities` flags + optional `PlatformAdapter` methods.
All degrade gracefully on surfaces without support.

## Files
- **New:** `bot-slack/src/assistant.ts` (Bolt `Assistant` middleware ⇄
engine sink), `bot-slack/src/native-stream.ts` (`NativeMessageStream`,
same `append/finish` contract as `MessageStream`, legacy fallback).
- **Engine:** `platform-adapter.ts`, `thread.ts`, `create-bot.ts`,
`index.ts` (+ `bot-ui` `Thread` type).
- **Adapter:** `adapter.ts` (options/capabilities/stream
branch/methods), `slack-listener.ts` (one-line no-double-delivery
guard), `event-renderer.ts` (pane status mode + native text transport).
- **Example/docs:** `examples/slack` manifest (`assistant_view` + scope
+ events) & dev-ex, package READMEs/ARCHITECTURE, `slack.mdx`.
- **Hooks:** `chore(hooks)` makes the `lint-fix` script
double-quote-free (it was breaking `sh -c "…"` on Windows/lefthook
2.1.1).

## Tests / verification (CI-runnable)
- New unit tests: `native-stream` (cadence, 12k continuation, fence
re-open, first- **and** continuation-`startStream` fallback),
`assistant` (defaults-before-onThreadStarted ordering, thread-scoped
turn, auto-title), listener no-double-delivery, renderer pane-status
mode, engine capability gating.
- Verified locally: `@copilotkit/bot` 31 tests, `@copilotkit/bot-slack`
199 tests, `bot-ui` tests; `tsc` clean (bot + bot-slack); build clean;
oxlint/oxfmt clean; publint/attw pass.

## ⚠️ E2E not covered here
The §8 end-to-end flows need a **real Slack workspace** with the Agents
toggle (open pane → chips → streamed reply w/ live status; channel
mention → native markdown; kill `startStream` → legacy fallback;
non-agent app → shipped behavior). `examples/slack` is the vehicle.
Three items remain to confirm against a live workspace (spec §7 spikes):
the `isAssistantThread` runtime detection, the exact `assistant_view`
manifest acceptance, and `stopStream` finalize tolerance.

Spec:
https://app.notion.com/p/copilotkit/Slack-Agent-APIs-in-bot-slack-assistant-pane-native-streaming-by-default-spec-37b3aa38185281f5948dc6a665064d04
2026-06-15 22:10:17 -07:00
Jordan Ritter c88d687456 fix(showcase/harness): bubble-race elimination — 4 defects + atomic readCascadeState + cold-start retry + SSE counter (#5462)
## Summary

Fixes 4 bubble-race defects in the CopilotKit showcase e2e harness's
conversation runner that caused flaky turn-completion detection on
slow-streaming and cold-start integrations.

**Defects fixed (RED → GREEN):**
- **Defect 1** (fast-replay): turn settled on count, not text/SSE →
false-positive settle
- **Defect 2** (multi-turn flicker): un-turn-scoped bubble selection via
`list[last]` → cross-turn leak
- **Defect 3** (cascade blindness): diagnostic cascade picked wrong tier
→ wrong bubble text
- **Defect 4** (boot-time baseline staleness): pre-paint/stale bubble at
boot poisoned turn-1 settle

**Core changes:**
- `waitForTurnComplete` 3-conjunct primitive (SSE counter + DOM-at-index
+ text-quiet for settleMs)
- Atomic `readCascadeState` helper — single `page.evaluate` returns
`{count, text}` from one cascade tier (prevents within-poll cross-tier
inconsistency)
- Probe contract: `assertions(page, ctx: {bubbleIndex, text})` —
turn-scoped bubble retrieval (replaces `list[last]`)
- SSE interceptor — `__hk_runsFinished` counter via CDP + page-side
fetch wrap; idempotent attach/detach + handle-cache invalidation on stop
+ `framenavigated` reset (cold-start retry compatible)
- Cold-start retry — single bounded retry on first-attempt banner;
reload + re-resolve chat input + settleMs floor honoring shared turn
deadline
- Banner fast-fail — baseline banner snapshot + differs-from-baseline
2-poll debounce; in-poll `BannerVisibleError` translates to
`AssistantErroredError`
- Pre-paint placeholder env adapter for defect-4 repro infrastructure

**Defect repro tests:** real-browser tests in
`test/integration/bubble-race-repro-defect-{1,2,3,4}.test.ts` plus
mechanism-GREEN tests and `wait-for-turn-complete.test.ts` (3-conjunct
classification matrix), `probe-contract.test.ts` (ctx bridge),
`sse-counter.test.ts` (counter increment + framenavigated reset +
handle-cache invalidation).

## Test plan

- [x] Full harness vitest: 2742/2742 passing
- [x] Defect-1, defect-2, defect-4 RED tests verified GREEN after fix
- [x] Mechanism-GREEN tests pin internal contracts (cascade pollution
guard, SSE counter atomicity, init-script navigation re-entry, etc.)
- [x] cr-loop converged across 10 rounds (25→6→6→6→4→2→0 bucket-(a)
findings) + Procedure 3 audit clean
- [x] `oxfmt --check` clean across all 31 changed TS files
- [x] `tsc -p tsconfig.build.json` clean
- [ ] CI green on this PR
- [ ] Manual smoke: `bin/showcase test --d5
langgraph-python:agentic-chat` post-merge

## Follow-up debt (deferred from cr-loop; not subject-scope)

Detected by cr-loop reviewers but out of this PR's scope:
- `d6-all-pills.ts` deploy-churn NSF aggregation — features in
`notSupportedFeatures` lose `incapable` classification during
deploy-churn skip
- `d6-all-pills.ts` `joinAimockJournal` slug fallback under D6
concurrency — can pick wrong-feature entry when aimock doesn't echo
`x-diag-run-id`
- `d6-all-pills.ts` `BrowserDisconnectedError` sentinel is constructed
but never matched in `runFeature` catch — gets bucketed as
`driver-error`
- `d5-tool-rendering-default-catchall.ts` Path B narration
false-positive (substring matches in assistant prose)
- HITL registry side-effect tests use manual fallback registration that
masks real registration failures
- `installPrePaintFromEnv` and `attachSseInterceptor` are re-registered
on every `page.goto` — accumulate init scripts; harmless today via
in-script idempotency guards

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-15 20:14:51 -07:00
Jordan Ritter 1ef6a0960b test(harness): update d5-* + d6-all-pills unit tests to new probe + cascade contract
- d5-* unit tests: update page fakes / assertion-call shapes to pass
  the ctx bridge ({bubbleIndex, text}) instead of querying the live
  bubble list
- d6-all-pills.test.ts: update page-fake dispatch + cascade-state
  expectations to match the atomic readCascadeState return shape
  ({count, text} or null) — picks up the cross-tier consistency
  guarantee in the unit layer
2026-06-15 18:37:26 -07:00
Jordan Ritter cd2af0f9fd test(harness): conversation-runner unit coverage — banner debounce, cold-start retry, skipFill, error-banner shapes
Comprehensive unit coverage for:
- preFill + fillAndVerifySend retry behavior
- baseline banner snapshot + differs-from-baseline debounce matrix
  (stable banner = no fail; changed banner = fast-fail after 2 polls)
- cold-start retry cases (first-attempt banner → reload + retry;
  retry-then-banner → translatedErr; retry exhausts deadline)
- skipFill / skipSend short-circuit paths
- ErrorBannerReadResult union shape variants (present/absent/unknown)
2026-06-15 18:37:26 -07:00
Jordan Ritter 3d49d4c38f test(harness): bubble-race integration tests + defect reproductions
- bubble-race-repro.ts: shared driver harness for repro tests
- bubble-race-repro-defect-{1,2,3,4}.test.ts: real-browser RED tests
  that fail without the fix and pass with it
    - Defect 1: fast-replay false-positive settle on count
    - Defect 2: multi-turn flicker via list[last]
    - Defect 3: cascade-tier blindness
    - Defect 4: boot-time baseline staleness / pre-paint placeholder
- bubble-race-mechanisms.test.ts: GREEN tests pinning internal
  contracts (cascade-pollution guard, atomic readCascadeState,
  init-script idempotency)
- wait-for-turn-complete.test.ts: 3-conjunct classification matrix
- probe-contract.test.ts: ctx-bridge shape
- sse-counter.test.ts: counter increment + framenavigated reset +
  handle-cache invalidation
2026-06-15 18:37:26 -07:00
Jordan Ritter cf7efbda95 fix(harness): d6 driver — installPrePaintFromEnv + attachSseInterceptor wiring + helper-based captureDiagnostics
- d6-all-pills.ts: wire installPrePaintFromEnv and
  attachSseInterceptor PRE-goto in both defaultLauncher and pooled
  launcher paths, so first-paint state is deterministic and the SSE
  counter is armed before any page navigation
- captureDiagnostics rewired through the Node-side helper
  (readCascadeState) — single page.evaluate per call, no
  within-snapshot tier drift
- Consume BUBBLE_RACE_MESSAGES override for defect-4 repro flows
2026-06-15 18:37:25 -07:00