14240 Commits

Author SHA1 Message Date
Maxim 25817605cc fix(reskinnable-demo): put real data behind the banking report, and fix the six defects it was hiding (#6378)
Fixes the six defects a 12-agent review found in the banking skin, by
fixing the thing that caused most of them: the skin had **three
disagreeing answers to "what did we spend"**.

| Source | Total | Drove |
|---|---|---|
| `charges-data.ts` fixture (45 rows, Apr–Jun) | $632,806 | the Charges
page |
| `seed.json` ledger (4 rows, **Apr–May only**) | $30,089 | the report's
**charts** |
| static `policies[].spent` | $137,000 | the report's **KPI** |

Every one of those numbers appeared in the product, and they were never
reconciled.

## The headline defect

`SpendingTrendChart` substituted a hard-coded `[3200, 4100, 3600, 5200,
4800, 6400]` / Jan–Jun series whenever fewer than three distinct months
were present. Intended as an empty state — but the ledger spanned
exactly **two** months, so the fallback was the **default path**. The
report's "Spend over time" showed six invented figures, roughly 20×
smaller than the total printed directly above them, under a card whose
own docstring reads:

> Every number is computed here from the live ledger … so a report can
never quote a figure the app disagrees with.

Reproduced in the running app before fixing (`POST
/api/banking/v1/reports`, no agent needed), and there's a nasty
interaction worth knowing: attaching an invoice dates a synthetic
transaction *today*, supplying a third month, so **attaching an invoice
masked the bug**. A test written casually would sit in the masked state
and pass.

## The fix: one ledger

The 45 charges now live in `seed.json` as real transactions across
**Apr/May/Jun**, and the Charges page reads them over REST like every
other surface.

**Team and policy are now different axes.** A charge belongs to one of
seven org teams; a policy is one of three budget envelopes (Technology /
Go-to-Market / G&A) and several teams share one. They were a single
`ExpenseRole` enum — which is exactly why covering every team meant
choosing between a seven-slice donut and discarding real charges.
`ExpenseRole` still types a member's own team; `PolicyType` types the
envelopes, joined by `policyForTeam`.

**`policies[].spent` is derived from approved charges on every read.**
It could no longer disagree with the charts — and it also now *moves*:
approving a charge previously left `spent` untouched, so the budget
never reflected the approval and the over-limit gate kept comparing
against a stale figure. Verified live: approving a \$960 charge moved
`spent` by exactly \$960.

**Over-limit is derived-only.** A charge no longer *stores*
`over-limit`; the Charges badge resolves through `withOverLimit`, the
same rule the report uses.

## The six review findings

| | Defect | Fix |
|---|---|---|
| a1 | report charted fabricated spend | charts real months; explicit
empty state at zero |
| a2 | donut its own docs said would collapse | real \$533k base — the
\$900k invoice that hit **89%** now reaches **73%**; 89% would need
~\$2.8M |
| a3 | `report` matched inside "quarterly report" | anchored both sides,
whole words |
| a4 | `as Transaction` laundered a nullable `policyId` | cast removed;
compiler checks it |
| a5 | comment claimed additions "have no policyId" | corrected — three
lines from the code that sets it |
| a6 | `?sort=banana` lit the "active" tint | params validated; unknown
reads as unset |

All six existed **identically in `examples/showcases/banking/`** — they
came from the upstream PRs this skin replayed, not from the port. Scoped
to the skin per review; banking still carries them.

## The scripted demo is unchanged by construction

- the four demo-load-bearing transactions survive byte-identical
- over-limit is still **exactly 3 charges / \$30,000**
- AWS \$15,000 still derives over-limit for the teach-mode pill
- Delta Airlines is still the only Delta charge (the fixture's
near-duplicate "Delta Air Lines" became United Airlines)
- all four status badges still appear (`Amazon Business` is kept
pending, under its policy's headroom, so a plain **Pending** chip
survives)

The donut goes 44/40/16 → **48/27/24** and is relabelled "Spend by
policy", which is what it reads.

## Verification

```
nx build react-core,a2ui-renderer,core,runtime,shared   exit 0
tsc --noEmit                                            0 errors
vitest                                                  239 passed (was 215)
eslint                                                  0 problems
```

Plus live checks against a running server: 49 transactions over 3
months, derived `spent` tracking an approval, over-limit holding at
3/\$30,000.

The 24 new tests are confirmed **red** against the old code —
`parseSort("banana")` returned `"banana"`, `parseTop("-5")` returned
`-5`, the seed spanned two months, no row carried a team, and `spent`
was stored.

## Not in this PR

A "tool replay-guard sweep" (4 items, `navigateToPageAndPerform` and
three approval tools missing the resolved-state guard `showCharges` has)
and ~13 subject-neutral items are captured as follow-ups.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-05 02:07:14 +02:00
Maxim 8829633e29 Merge branch 'main' into fix/banking-skin-real-data 2026-08-05 02:00:42 +02:00
Maxim addbe2888d test(reskinnable-demo): lock in the single-ledger invariants
Two new suites, each written so it would have failed against the previous code
rather than merely describing the new behaviour:

- derived-spend: policies[].spent equals the sum of approved charges, excludes
  pending/flagged (so the over-limit check cannot double-count the charge it is
  gating), and agrees between policies() and findPolicy(). Plus the properties
  the demo depends on — at least three distinct months (the condition whose
  absence made the trend chart fabricate), AWS $15,000 still deriving
  over-limit for the teach-mode pill, exactly one Delta charge, and a team and
  category on every ledger row.
- charges-data: parseSort and parseTop reject the values that previously slipped
  through as valid, and toChargeRow's over-limit projection only applies to
  pending charges.

Confirmed red against the old code: parseSort("banana") returned "banana",
parseTop("-5") returned -5, the seed spanned two months, no row carried a team,
and spent was a stored field.

The four existing fixtures move from ExpenseRole to PolicyType for policy
`type`, following the team/policy split.
2026-08-05 00:40:55 +02:00
Maxim d7e7c8eaf4 fix(reskinnable-demo): stop the alert marker firing on ordinary notes
The regex that decides whether a transaction note gets a red-alert prefix was
anchored on the left only, so `report` matched inside "reporter" and "quarterly
report" and `disput` inside "disputation". A note reading "attached to the
quarterly report" was served a fraud marker.

Anchored on both sides and switched from stems to whole words, verified against
the four phrasings that previously false-positived.
2026-08-05 00:40:40 +02:00
Maxim 05390dc625 fix(reskinnable-demo): stop the banking report showing invented figures
Four defects in the report's charts, all reported by review and all
reproduced in the running app before fixing.

- SpendingTrendChart substituted a hard-coded [3200, 4100, 3600, 5200, 4800,
  6400] Jan-Jun series whenever fewer than three months were present. Intended
  as an empty state, it was the DEFAULT path: the seeded ledger spanned two
  months, so the report's "Spend over time" always showed six invented numbers
  — roughly 20x smaller than the total printed directly above them — under a
  card whose own contract says every number comes from the live ledger. It now
  charts whatever months exist, with a real empty state at zero.
- SpendBreakdownChart's docstring said the report must use SpendByTeamBars
  instead, "because an attached invoice can push one team to ~96% and a donut
  cannot survive that", while the report rendered the donut anyway. The warning
  was real but its cause was the thin ledger, not the chart: against the old
  $137,000 base a $900,000 invoice took one slice to 89%. Against the real
  ~$533,000 base the same invoice reaches 73%, and 89% would need ~$2.8M.
  Robust because the data is real, not because a floor was added to the arc.
- augmentForReport built its synthetic transactions behind an `as Transaction`
  cast that was hiding a real hole: `policyId` came from an `?.id` lookup, so it
  could be undefined where the field is a required string. The cast is gone and
  the compiler checks it. Additions also now resolve their model-authored team
  to a policy envelope through `policyForTeam` rather than comparing a team name
  to a policy name, with one "Unattributed" segment for unmappable names.
- A comment inside TopChargesChart claimed document-sourced charges "have no
  policyId" — three lines above the code that gives them one.

The donut column is relabelled "Spend by policy", which is what it reads.
2026-08-05 00:40:40 +02:00
Maxim 274d46ffcb feat(reskinnable-demo): put one real ledger behind the banking skin
The skin held three disagreeing answers to "what did we spend": a 45-row
Charges fixture ($632,806), a 4-row seeded ledger ($30,089 across two
months), and static policy totals ($137,000). Each surface read a different
one, so they drifted silently — and because the ledger spanned only two
months, the report's trend chart fell back to a hard-coded series and showed
invented figures under a card that promises live numbers.

Now there is one ledger. The 45 charges live in seed.json as real
transactions across Apr/May/Jun, and the Charges page reads them over REST
like every other surface.

- Splits team from policy. A charge belongs to one of seven org teams; a
  policy is one of three budget envelopes (Technology / Go-to-Market / G&A)
  and several teams share one. These were a single `ExpenseRole` enum, which
  is why the two axes read as one thing and why covering every team meant
  either a seven-slice donut or discarding real charges. `ExpenseRole` still
  types a member's own team; `PolicyType` types the envelopes, joined by
  `policyForTeam`.
- Derives `policies[].spent` from approved charges on every read, so it can
  no longer disagree with the charts. It also now MOVES: approving a charge
  previously left `spent` untouched, so the budget never reflected the
  approval and the over-limit gate kept comparing against a stale figure.
- Makes over-limit derived-only. A charge no longer stores "over-limit"; the
  Charges table resolves the badge through `withOverLimit`, the same rule the
  report uses, so the two cannot disagree.
- Validates the `?sort=` and `?top=` params. `?sort=banana` used to be cast
  straight to a SortKey and lit the control's "active" tint while the table
  silently sorted by the default; `?top=-5` reached `slice(0, -5)` and dropped
  the LAST five rows, inverting top-N.

The scripted demo is unchanged by construction: the four demo-load-bearing
transactions survive byte-identical, over-limit is still exactly three charges
totalling $30,000, AWS $15,000 still derives over-limit for the teach-mode
pill, and Delta Airlines is still the only Delta charge (the fixture's near
-duplicate "Delta Air Lines" became United Airlines).
2026-08-05 00:40:22 +02:00
Tyler Slaton c236e33978 chore: release monorepo v1.66.2 (#6376)
🚀 pkg-pr-new / build (push) Has been cancelled
## Release monorepo v1.66.2

**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.66.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 `monorepo` packages to npm at version `1.66.2`
   - Creates git tag `monorepo/v1.66.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.
v1.66.2
2026-08-04 14:58:22 -07:00
tylerslaton 53b772552f chore: release monorepo v1.66.2 2026-08-04 21:57:57 +00:00
Tyler Slaton cc9b74bdb1 chore: release channels v0.7.3 (#6375)
🚀 pkg-pr-new / build (push) Has been cancelled
## Release channels v0.7.3

**Scope:** `channels` | **Bump:** `patch`

---

### How this release process works

1. **This PR was created automatically** by the "release / create-pr"
workflow.
   It bumped the `channels` packages to `0.7.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 `channels` packages to npm at version `0.7.3`
   - Creates git tag `channels/v0.7.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.
channels/v0.7.3
2026-08-04 14:54:52 -07:00
tylerslaton b95a43e254 chore: release channels v0.7.3 2026-08-04 21:51:00 +00:00
Mark c1adb14c48 chore(showcase): pin opentelemetry-api<1.44 for pydantic-ai v1 (#6374)
`showcase/integrations/pydantic-ai` cannot be installed from scratch
today. A clean `pip install -r requirements.txt` produces an agent that
dies on import:

```
ModuleNotFoundError: No module named 'opentelemetry._events'
```

pydantic-ai 1.0.18 imports `opentelemetry._events`. That module was
removed in **opentelemetry-api 1.44.0** (last present in 1.43.0).
pydantic-ai declares an unbounded `opentelemetry-api>=1.28.0`, so a
fresh resolve picks 1.44.0 and the import fails.

## Why nothing was red

`Dockerfile:22-24` copies **only** `requirements.txt` before `pip
install`, so that layer's cache key depends on nothing else. The agent
image has been reusing a pip layer baked before 1.44.0 shipped
(2026-07-16) — including a successful build earlier today. The failure
was masked by Docker layer caching, not absent. Any change to
`requirements.txt`, a cache eviction, or a `--no-cache` build surfaces
it.

Note that this PR busts that cache by definition, so CI's image build is
a genuine fresh-resolve test of the fix rather than a cached pass.

## Verification

Run with `pip` in a clean 3.12 venv, matching how the Dockerfile
installs:

```
RED  (main)         opentelemetry-api 1.44.0 -> import pydantic_ai raises ModuleNotFoundError
GREEN (this branch) opentelemetry-api 1.43.0 -> import pydantic_ai + pydantic_ai.ag_ui OK
```

Only the ceiling is added; `opentelemetry-sdk` follows to 1.43.0 on its
own, so a second pin isn't needed. No resolution conflict with
`logfire>=4.10.0`.

Docker and the `--d6` probe stack aren't available in my environment, so
the mandatory value-test per `showcase/AGENTS.md` rule 4 has not been
run locally — CI's image build and the dojo cells are the real gate
here.

## Scope

One line plus a comment recording why it exists and when to remove it.
The comment matters: an undocumented pin is what caused the sibling
`starlette==0.45.3` rot in #6363, where a correct-when-written pin
outlived its reason and silently capped pydantic-ai a full major.

The ceiling should come off when this package moves to pydantic-ai v2,
which requires `opentelemetry-api>=1.28.0` without needing `_events`.
2026-08-04 14:07:28 -07:00
Tyler Slaton 224dfb0d8c fix(channels): reconnect after clean gateway close (#6371)
## Summary

- replace Phoenix's retained closed transport when a live managed
session receives an unexpected WebSocket close with code 1000
- reconnect and rejoin through the existing Phoenix channel lifecycle
- preserve intentional session disconnects as terminal
- cover the clean-close recovery path with a regression test

## Root cause

Phoenix 1.8.4 does not schedule its reconnect timer for close code 1000.
CopilotKit still transitioned the managed session to `reconnecting`, so
the runtime reported that Phoenix was retrying indefinitely even though
Phoenix never created another transport.

## Impact

A brief gateway interruption that cleanly closes an established socket
can now recover once the gateway is healthy, instead of leaving the
runtime stuck and repeatedly logging the managed-session-down warning.

## Verification

- `pnpm nx run @copilotkit/channels-intelligence:test` — 188 tests
passed
- `pnpm nx run @copilotkit/channels-intelligence:check-types` — passed
- `pnpm nx format:check
--files=packages/channels-intelligence/src/realtime-gateway.ts,packages/channels-intelligence/src/realtime-gateway.test.ts`
— passed
- repository pre-commit test, package validation, and lint gates —
passed
2026-08-04 13:55:27 -07:00
Tyler Slaton 7e30957976 fix(channels): reconnect after clean gateway close 2026-08-04 13:54:36 -07:00
Mark 80fa407978 chore(showcase): pin opentelemetry-api<1.44 for pydantic-ai v1
pydantic-ai 1.0.18 imports opentelemetry._events, removed in
opentelemetry-api 1.44.0. Its own floor is an unbounded
opentelemetry-api>=1.28.0, so a fresh resolve of this package's
requirements picks 1.44.0 and import pydantic_ai fails with
ModuleNotFoundError.

The agent image kept building because the Dockerfile copies only
requirements.txt before pip install, so that layer stayed cached from
before 1.44.0 shipped (2026-07-16). Any requirements change or cache
eviction would have surfaced it.

Verified with pip in a clean 3.12 venv, as the Dockerfile installs:
  before: opentelemetry-api 1.44.0, import pydantic_ai raises
  after:  opentelemetry-api 1.43.0, import pydantic_ai OK
2026-08-04 20:34:45 +00:00
Mike Ryan 3eaeeb19ce fix(channels): preserve Slack provider diagnostics (#6373)
## Summary

- serialize Slack Carousel cards through elements and validate Card and
Carousel payloads before provider calls
- keep bounded provider diagnostics from ChannelDeliveryError through
RUN_ERROR and ChannelCanonicalRunError
- log low-cardinality error fields without validation messages or
provider bodies
- move best-effort Slack status cleanup failures to debug logs

## Why

Invalid nested Slack Card payloads failed at the provider with no useful
path at the application boundary. This change catches known Card and
Carousel shape errors locally and preserves safe JSON pointers when
Slack rejects a payload.

## Compatibility

The new error details are optional. Existing consumers keep their
current behavior, so no protocol version bump is needed.

## Validation

- NX_DAEMON=false pnpm nx run-many -t test,check-types,build
--projects=@copilotkit/channels-core,@copilotkit/channels-intelligence,@copilotkit/channels-slack,@copilotkit/runtime
--skip-nx-cache
- pnpm lint
- pnpm check:channel-native-catalogs
- targeted oxfmt and oxlint checks
- git diff --check
- pre-commit test, publint, and attw checks for affected packages

Companion Intelligence PR:
https://github.com/CopilotKit/Intelligence/pull/759
2026-08-04 13:33:41 -07:00
Mark cdd605c672 docs(pydantic-ai): port integration docs and demos to Pydantic AI v2 (#6367)
This pull request was posted by Claude Code using claude-opus-5 on
behalf of David. David has not reviewed this diff line by line.

Closes https://github.com/CopilotKit/CopilotKit/issues/6363

`Agent.to_ag_ui()`, `AGUIApp` and the whole `pydantic_ai.ag_ui` module
were removed in Pydantic AI v2. The docs installed pydantic-ai unpinned,
so following the quickstart today gets 2.22.0 and fails twice: first at
resolution (`starlette==0.45.3` conflicts with the `>=0.46.2` the
`ag-ui` extra requires), then at `AttributeError`.

## What changed

**8 doc pages** under
`showcase/shell-docs/src/content/docs/integrations/pydantic-ai/`
(`quickstart.mdx`, `quickstart/pydantic-ai.mdx`,
`human-in-the-loop.mdx`, `human-in-the-loop/agent.mdx`,
`generative-ui/tool-rendering.mdx`, and the three `shared-state/`
pages):

- the agent is served from a Starlette route via
`AGUIAdapter.dispatch_request(request, agent=agent)`
- `StateDeps` imports move from `pydantic_ai.ag_ui` to `pydantic_ai.ui`
- install commands exact-pin `pydantic-ai-slim[ag-ui,openai]==2.22.0`
and `ag-ui-protocol==0.1.19`, matching the starter fleet, plus
`starlette>=0.46.2` since the snippets import Starlette directly

**Per-request deps.** Every stateful snippet builds `StateDeps` inside
the request handler:

```python
async def run_agent(request: Request) -> Response:
    return await AGUIAdapter.dispatch_request(
        request, agent=agent, deps=StateDeps(AgentState())
    )
```

`dispatch_request` validates the client's state into `deps.state`
(`pydantic_ai/ui/_adapter.py`, `run_stream_native`), so a module-level
instance shared across requests lets concurrent runs clobber each other.
The old `to_ag_ui(deps=...)` snippets all did this.

**`examples/canvas/pydantic-ai`** — `requirements.txt` pinned,
`agent/agent.py` ported, README corrected.

**`examples/showcases/pydantic-ai-todos`** — `pyproject.toml` pinned and
`uv.lock` regenerated (it was still resolving 1.0.10), `agent/main.py`
ported, `src/agent.py` and `src/tools.py` imports moved, README and
`src/app/api/copilotkit/route.ts` comments corrected.

**`skills/copilotkit-integrations`** — beyond the issue's file list:
`SKILL.md`, `sources.md` and `references/integrations/pydantic-ai.md`
also taught `to_ag_ui()`. Same rot, same fix.

## Verified by execution

The reason these docs rotted is that nothing runs them, so everything
below was actually run, not read.

- Both install commands were run verbatim in throwaway environments. `uv
add 'pydantic-ai-slim[ag-ui,openai]==2.22.0' 'ag-ui-protocol==0.1.19'
'starlette>=0.46.2' uvicorn` and the `pip install` equivalent both
resolve, landing pydantic-ai-slim 2.22.0, ag-ui-protocol 0.1.19,
starlette 1.3.1.
- Every ```python fence on the 8 doc pages was extracted, `exec`'d, and
driven with a real `RunAgentInput` POST through
`starlette.testclient.TestClient` with the model overridden to
`TestModel`. All 8 return 200 `text/event-stream` with a `RUN_STARTED`
... `RUN_FINISHED` sequence and no `RUN_ERROR`.
- The canvas agent was installed from its `requirements.txt` and driven
the same way: 200, SSE, `RUN_STARTED` ... `TOOL_CALL_*` ...
`STATE_SNAPSHOT` ... `RUN_FINISHED`.
- The todos agent was installed with `uv sync --frozen` from the
regenerated lock and driven the same way. Two sequential requests, one
seeding a todo and one sending empty state, each saw only their own
state, confirming the per-request deps actually isolate.

Not executed: the Next.js frontends and the docs site build (no
`node_modules` in this checkout). The TypeScript edits are comment-only.

## Deliberately out of scope

`showcase/integrations/pydantic-ai` is left on its v1 fleet pin. It is
418 files, 19 mounts and 190 e2e specs, and CopilotKit said they will
take it as https://github.com/CopilotKit/CopilotKit/issues/6364. The
dojo and the docs therefore diverge until that lands.

The CI guard from the issue's last acceptance criterion is not built
here. A proposal for it is posted on
https://github.com/CopilotKit/CopilotKit/issues/6363 for the team to
own.

Two pre-existing malformed code fences were fixed in passing, because
leaving them meant the ported snippets still would not run:
`quickstart/pydantic-ai.mdx` and
`shared-state/predictive-state-updates.mdx` each had TypeScript embedded
inside an unterminated ```python fence. The TypeScript now sits in its
own fence.

Overlaps with https://github.com/CopilotKit/CopilotKit/pull/6355, which
ports `examples/integrations/pydantic-ai`. No file overlap.
2026-08-04 12:58:07 -07:00
Mike Ryan 80370b5ecb fix(channels): preserve provider diagnostics 2026-08-04 12:33:17 -07:00
Mike Ryan ef531035e6 fix(channels-slack): validate card payloads locally 2026-08-04 12:31:46 -07:00
Mike Ryan 79080816cd fix(channels-slack): serialize carousel cards as elements 2026-08-04 12:06:16 -07:00
Tyler Slaton aae4c0bd5d chore: release channels v0.7.2 (#6372)
🚀 pkg-pr-new / build (push) Has been cancelled
## Release channels v0.7.2

**Scope:** `channels` | **Bump:** `patch`

---

### How this release process works

1. **This PR was created automatically** by the "release / create-pr"
workflow.
   It bumped the `channels` packages to `0.7.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 `channels` packages to npm at version `0.7.2`
   - Creates git tag `channels/v0.7.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.
channels/v0.7.2
2026-08-04 11:50:07 -07:00
tylerslaton 1f5c70da2e chore: release channels v0.7.2 2026-08-04 18:42:33 +00:00
Tyler Slaton f1b42e2e0a feat(skills): make setup-slack-channel drive the browser by default (refs OSS-705) (#6370)
## Summary

Retesting the Slack setup end to end found the skill **too cautious to
be useful**. It stopped at almost every step, where the earlier version
stopped only for passwords and got through — and the run was rescued by
the developer telling the agent outright to just control their browser,
which cut human involvement down to typing passwords.

A run that pauses at every control is slower than the manual path it
replaced. Three changes, all pulling the same direction.

## 1. Driving is now the default, not implicit

The skill said *"most of this workflow happens in a browser"* —
descriptive, and it never told the agent to **drive** that browser. It
now does, explicitly, and checks its own capability before Phase 0
rather than assuming either way.

## 2. When there is no browser, ask for one — per harness

Generic advice is useless here, because enabling browser control differs
by harness. The agent now works out which one it is in and names the
single applicable route: Claude Code and Codex each ship their own
support and enable it differently, most other harnesses take a general
browser-use MCP server such as Playwright MCP. **If it isn't sure, it
looks it up rather than guessing.**

It also names the payoff — driving turns this into typing three secrets,
the fallback is roughly fifteen manual browser steps — because that is
what turns a shrug into a yes. The step-by-step walkthrough survives
only for an explicit decline.

## 3. Consent is batched into one authorization

Phase 0 now takes **one** yes naming the whole sequence: the Slack app
from the wizard manifest, its install into a named workspace, the
Channel, the adapter attach, and the project key.

Phase 3 and `references/intelligence-channel.md` previously required
*"state what you are about to change, get an explicit yes"* for
**every** dashboard goal — and the reference said it twice, back to
back. That is the concrete source of the stop-at-every-step behaviour.
Reading the page before acting stays required; it is no longer a reason
to check in.

## What did not change

The secret boundary. The developer still types the bot token, signing
secret, and API key themselves, and those remain the only mandatory
stops alongside anything the authorization did not cover. Batching
consent must not batch away a password — that is called out in the text.

`version` bumped 1.0.0 → 1.1.0.

## Validation

- `tsx scripts/sync-plugin-skills.ts --check` → **plugin skill mirror in
sync**
- `scripts/__tests__/sync-plugin-skills.test.ts` → 10 tests passed
- `prettier --check` clean on both changed files

## Companion

The same shift landed on the hosted big prompt in
[CopilotKit/website#445](https://github.com/CopilotKit/website/pull/445)
— default to driving, one batched authorization, harness-aware
capability request. Both surfaces now say the same thing, which was the
point of keeping them in step.

Worth noting for reviewers: this takes Atai's side on *"where the skill
asks permission to proceed, just do it"*, which was only half-applied
before. It also reverses my own per-step confirmations from the first
pass on #445 — the dogfooding run is the reason.
2026-08-04 11:19:54 -07:00
Jerel John Velarde 7c5468e5d1 fix(skills): ask for the decisions a driving agent must not invent
Batching the Phase 0 authorization removed the per-goal confirmations that were
accidentally serving as decision points. Nothing then asked the developer for the
inputs the agent cannot legitimately choose, and Phase 1 still said "Enter a
Display name" in the imperative — so an autonomous run named the bot itself.

That name is the expensive one. The wizard derives the Channel Code from it, the
Code is what createChannel({ name }) declares and what the developer types as
/invite, and Slack bot names are workspace-wide — Phase 1 already warns that a
collision blocks the install. An agent that settles it has named someone's bot for
them and can fail the install doing it.

Phase 0 now gathers four decisions in one exchange before any browser opens: the
display name, the workspace, the test channel, and whether this is throwaway.
Phase 1 consumes the chosen name instead of inventing one, Phase 1's workspace step
uses the named workspace, and Phase 2's invite names the agreed channel and says
the developer runs it.

States the rule the whole design turns on: the decisions are inputs you cannot
invent, the authorization is permission you need once, and collapsing the second
does not license skipping the first.
2026-08-04 10:30:16 -07:00
Tyler Slaton 7193856d96 fix(runtime): ignore late lock renewal failures (#6369)
## Summary

- stop handling in-flight lock renewal failures after the run has
settled
- keep aborting when a renewal fails during an active run
- cover the completion-before-renewal race with a regression test

## Why

Intelligence releases the thread lock after it accepts a terminal run
event. A renewal that was already in flight can then return a 409.
Clearing the interval stops future renewals, but it does not cancel that
pending promise, so Runtime logged an error and called `abortRun()`
after the run had completed.

The lifecycle guard makes that late rejection a no-op. Active-run
renewal failures still follow the existing abort path.

## Testing

- `pnpm nx test @copilotkit/runtime` — 1,866 tests passed
- `pnpm nx run @copilotkit/runtime:check-types`
- `pnpm nx build @copilotkit/runtime`
- `pnpm exec oxlint
packages/runtime/src/v2/runtime/handlers/intelligence/run.ts
packages/runtime/src/v2/runtime/__tests__/intelligence-lock-heartbeat.test.ts`
- `pnpm exec oxfmt --check
packages/runtime/src/v2/runtime/handlers/intelligence/run.ts
packages/runtime/src/v2/runtime/__tests__/intelligence-lock-heartbeat.test.ts`

The pre-commit hook also passed affected tests, `publint`, and `attw`.
The repo-wide `pnpm check-format` still reports 25 unrelated files
already present on `main`; both changed files pass the focused format
check.
2026-08-04 10:30:11 -07:00
Jerel John Velarde ae0dbc2fd7 feat(skills): make setup-slack-channel drive the browser by default
Retesting the Slack setup end to end found the skill too cautious to be useful.
It stopped at almost every step, where the earlier version stopped only for
passwords and got through — and the run was rescued by the developer telling the
agent outright to just control their browser. A run that pauses at every control
is slower than the manual path it replaced.

Three changes, all pulling the same direction.

Driving the browser is now stated as the default rather than left implicit in
"most of this workflow happens in a browser". When the agent has no browser tool
it asks the developer to install one before starting, and names the route for the
harness it is actually running in — Claude Code and Codex enable this differently,
most other harnesses want a browser-use MCP server — with an instruction to look
it up rather than guess. The manual walkthrough survives only for an explicit
decline.

Consent is batched into one Phase 0 authorization naming the whole sequence: the
Slack app from the wizard manifest, its install, the Channel, the adapter attach,
and the API key. Phase 3 and the Intelligence reference previously required "state
what you are about to change, get an explicit yes" for every dashboard goal, and
the reference said it twice. Reading the page before acting stays required; it is
no longer a reason to stop.

The secret boundary is untouched: the developer still types the bot token, signing
secret, and API key themselves, and those remain the only mandatory stops
alongside anything the authorization did not cover.
2026-08-04 10:13:27 -07:00
David Sanchez 22108c0948 docs(pydantic-ai): constrain the direct dep, not the transitive one
Follows the maintainer's Correction #2 on issue 6363. An exact version in a
docs install command is the same rot as the starlette==0.45.3 pin it replaced:
it goes stale silently and nobody re-resolves prose. The 2.22.0 the docs shipped
was already a version behind current the day it was written.

- docs install lines use pydantic-ai-slim[ag-ui,openai]>=2,<3, which constrains
  the dep the pages actually care about and fails loudly at the v3 boundary
- ag-ui-protocol drops out of the docs lines entirely; no doc snippet imports
  ag_ui, so naming it there was the transitive-dep noise the correction is about
- starlette>=0.46.2 stays, because the v2 snippets import Starlette directly.
  A floor with no ceiling cannot force a downgrade, so it does not recreate the
  silent backtrack
- examples/showcases/pydantic-ai-todos moves to a range in pyproject.toml and
  relocks; the uv.lock is what reproduces
- examples/canvas/pydantic-ai keeps exact pins: it has no lockfile, so
  requirements.txt is its only reproducibility artifact

Smoke-tested the open question from the issue: starlette 1.x works on
pydantic-ai v2. All 8 doc pages pass on 2.23.0 + starlette 1.3.1 and on
2.23.0 + starlette 0.52.1, so Jordan's <1.0 guard can be dropped rather
than raised.
2026-08-04 12:08:20 -05:00
Tyler Slaton 86cd674c7c fix(runtime): ignore late lock renewal failures 2026-08-04 09:26:00 -07:00
Ben Taylor 8e59bfd16b feat(skills): add channels-setup pointing at the hosted onboarding guide (#6366)
## Summary

The Channels onboarding workflow is served at
**https://copilotkit.ai/channels-guide.md**, and every other entry point
now copies one line that points there — the docs surfaces in #6357, the
website strip in
[website#444](https://github.com/CopilotKit/website/pull/444), the
README in
[channels-sdk#15](https://github.com/CopilotKit/channels-sdk/pull/15).

Coding agents reached through a **skill** had no such pointer. They
matched `setup-slack-channel`, which is scoped to Slack, to the provider
half, and to an OpenTag checkout — so "help me get my agent into Teams"
landed on a workflow that does not cover it.

`channels-setup` closes that gap as a **pointer, not a copy**. The
workflow stays in one place and is corrected there, instead of becoming
a seventh surface that drifts against the CLI on its own schedule.

## Verifying the fetch is the substance of the file

A thin pointer has one non-obvious failure mode, and it is the reason
this skill is more than two sentences:

```
$ curl -sL -o /dev/null -w "%{http_code} %{content_type}\n" https://copilotkit.ai/channels-guide.md
200 text/html; charset=utf-8
$ curl -sL https://www.copilotkit.ai/channels-guide.md | grep -o "<title>[^<]*</title>"
<title>Page not found | CopilotKit | ...</title>
```

**A missing guide does not return 404.** The site answers unknown paths
with a "Page not found" HTML page under HTTP 200. An agent that keys on
the status code gets a 73KB marketing page, concludes the fetch
succeeded, and improvises channel setup from memory — which is exactly
what the guide's boundaries exist to prevent, and it fails in the most
expensive way available here: the project installs cleanly and answers
nothing.

So the skill checks the **body**: markdown rather than HTML, the guide's
`# Build and prove a CopilotKit Channels agent` H1, and five `## Phase`
headings. If any fail it stops and hands the user the URL, no matter
what the status code said.

## Notes

- **`RESERVED_LIFECYCLE_SLUGS` is not optional.** Standalone skills are
not generated from `packages/*/skills`, so without the entry
`sync-plugin-skills` treats the directory as an orphan and deletes it.
Test updated alongside, including the hard-coded `size` (10 → 11).
- **No existing skill is modified.** `setup-slack-channel` and
`copilotkit-channels` keep their descriptions and bodies. Three skills
now match channel work; if that proves too ambiguous in practice,
narrowing the other two's `description` frontmatter is a follow-up, not
a body rewrite.
- **No manifest change.** `plugin.json` and `marketplace.json` do not
enumerate skills individually.
- No changeset — `packages/**` is untouched.

## Blocked on

**The guide is not live yet.**
[website#444](https://github.com/CopilotKit/website/pull/444) is still
open, so the URL currently serves the 404 page shown above. This skill
is inert until that merges and deploys — at which point it works with no
further change here. The sentinel means the pre-deploy state is a clean
stop rather than a wrong answer, so merging early is safe; it just isn't
useful yet.

## Validation

- `npx vitest run scripts/__tests__/sync-plugin-skills.test.ts` — 10
passed
- `pnpm run check:plugin-skills` — `plugin skill mirror in sync`, exit
0; `skills/channels-setup/` survives the orphan pass
- `npx oxfmt --check` on both changed TS files — clean
- `npx oxlint scripts/` — 0 errors
- `SKILL.md` frontmatter parsed with js-yaml: `name=channels-setup`,
`version=1.0.0`, description 660 chars
- Soft-404 behaviour confirmed against production with the `curl`
commands above

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-04 11:03:01 -05:00
Mark 14a294f2d8 Port the Pydantic AI example to Pydantic AI v2 (#6355)
This pull request was posted by Claude Code using claude-opus-5 on
behalf of David. David has not reviewed this diff.

`examples/integrations/pydantic-ai` only runs on Pydantic AI **v1**.
`Agent.to_ag_ui()`, `AGUIApp`, and the `pydantic_ai.ag_ui` shim were all
removed in v2 (pydantic/pydantic-ai#5464, announced in
pydantic/pydantic-ai#5345). The example pins `pydantic-ai-slim==1.0.18`,
so anyone installing it against current Pydantic AI (2.22.0) fails at
import.

This ports it to v2.

## Changes

1. `agent/pyproject.toml` — `pydantic-ai-slim[ag-ui,openai]>=2.0.0`,
`ag-ui-protocol>=0.1.19`
2. `agent/src/agent.py` — `StateDeps` moved from `pydantic_ai.ag_ui` to
`pydantic_ai.ui`
3. `agent/src/main.py` — serve via `AGUIAdapter.dispatch_request` on a
Starlette route
4. `agent/uv.lock` — relocked (resolves `pydantic-ai-slim` 2.22.0,
`ag-ui-protocol` 0.1.19)

## One fix beyond the mechanical port

The old wiring built the app once around a single shared `StateDeps`
instance. `dispatch_request` mutates `deps.state` with the state the
client sends, so one shared instance lets state leak between threads,
channels and users — which matters more for Channels than it did for a
single browser tab. Each request now gets its own `replace(deps)` copy,
matching the pattern in [Pydantic AI's own AG-UI
examples](https://github.com/pydantic/pydantic-ai/blob/main/examples/pydantic_ai_examples/ag_ui/api/shared_state.py).

<details><summary>Verified end to end</summary>

`uv sync` + a request through the actual ASGI app (model overridden with
`TestModel` so no API call), with a Channels-shaped payload (`threadId`,
`state`, `forwardedProps`):

```
health: 200 {'status': 'ok'}
POST / -> 200 text/event-stream; charset=utf-8
event types: ['RUN_STARTED', 'TOOL_CALL_START', 'TOOL_CALL_END', 'TOOL_CALL_START', 'TOOL_CALL_ARGS',
 'TOOL_CALL_END', ..., 'TOOL_CALL_RESULT', 'STATE_SNAPSHOT', 'TOOL_CALL_RESULT', 'STATE_SNAPSHOT',
 'TEXT_MESSAGE_START', 'TEXT_MESSAGE_CONTENT', ..., 'TEXT_MESSAGE_END', 'RUN_FINISHED']
threadId echoed: slack-C123-thread-1
tool call: get_proverbs
tool call: add_proverbs
tool call: set_proverbs
tool call: get_weather
```

The emitted event set is exactly what `channels-slack`'s `RunRenderer`
subscribes to (`RUN_*`, `TEXT_MESSAGE_*`, `TOOL_CALL_*`), plus
`STATE_SNAPSHOT` which it ignores.

</details>

<details><summary>Note on <code>ag-ui-protocol</code>: 0.1.19, not
0.1.18</summary>

Not required by this port, but worth pinning forward: typed multimodal
input content (`ImageInputContent` &c.) landed in 0.1.15 and the
interrupt lifecycle in 0.1.19. We found that a Pydantic AI install below
0.1.15 rejects an inbound image attachment with a 422 rather than
skipping it — so a Channels gateway forwarding a Slack image needs the
newer floor. We're tracking that on our side.

</details>
2026-08-04 09:02:10 -07:00
David Sanchez d19bd0f81c docs(pydantic-ai): port integration docs and demos to Pydantic AI v2
Agent.to_ag_ui(), AGUIApp and the pydantic_ai.ag_ui module were removed in
Pydantic AI v2. The docs installed pydantic-ai unpinned, so anyone following
the quickstart got 2.22.0 and failed first at dependency resolution
(starlette==0.45.3 conflicts with the >=0.46.2 the ag-ui extra needs) and then
at AttributeError.

- 8 doc pages under showcase/shell-docs .../integrations/pydantic-ai serve the
  agent from a Starlette route via AGUIAdapter.dispatch_request
- StateDeps moves from pydantic_ai.ag_ui to pydantic_ai.ui
- stateful snippets build StateDeps per request; dispatch_request writes the
  client's state into deps.state, so a shared instance leaks state between users
- install commands exact-pin pydantic-ai-slim==2.22.0 and ag-ui-protocol==0.1.19
- examples/canvas/pydantic-ai and examples/showcases/pydantic-ai-todos ported
  and pinned, todos relocked
- skills/copilotkit-integrations reference updated to the same shape

showcase/integrations/pydantic-ai is deliberately untouched; it is tracked
separately.
2026-08-04 10:51:17 -05:00
Alem Tuzlak 7b5cc8ccf9 fix(channels): recover from transient gateway outages (#6347)
## What changed

- Mark initial gateway HTTP 5xx and transient transport failures as
retryable.
- Retry initial managed Channel activation with exponential backoff from
1 second to a 30-second cap until it connects or the manager stops.
- Preserve retry hints from `gateway_draining` join replies and retry
initial join timeouts.
- Keep HTTP 4xx and NXDOMAIN failures terminal.
- Back off established-session outage reminders from 30 seconds to a
15-minute cap while Phoenix continues reconnecting.

## Why

The OpenTag Railway runtime saw the gateway host return HTTP 502 during
an outage. Established Phoenix sessions keep retrying, but a runtime
that starts during the outage stops after its one initial connect
window. It cannot recover when the gateway comes back unless the process
restarts. Fixed 30-second reminder logs also flood long outages.

The gateway drain work now rejects new joins with a structured retryable
response. The client must preserve that response so the runtime can
retry instead of leaving the Channel in a terminal error state.

## Companion change

CopilotKit/OpenTag#25 keeps the Railway HTTP server alive while an
initial Channel retry is pending. OpenTag must consume a CopilotKit
release containing this PR before that companion change can recover by
itself.

## Validation

- `pnpm nx run-many -t test,check-types,build -p
@copilotkit/runtime,@copilotkit/channels-intelligence`
- `pnpm nx run-many -t publint,attw -p
@copilotkit/runtime,@copilotkit/channels-intelligence`
- pre-commit tests and package checks for all affected projects
- `pnpm exec oxfmt --check` on all five changed files
- `pnpm exec oxlint` on all five changed files
- `git diff --check`
2026-08-04 17:39:00 +02:00
David Sanchez 4be25161b1 fix(examples): pin pydantic-ai starter deps and build deps per request
Address review on CopilotKit/CopilotKit#6355:

- Pin `pydantic-ai-slim[ag-ui,openai]==2.22.0` and `ag-ui-protocol==0.1.19`,
  matching the starter fleet standard. Open-ended floors would pull a
  breaking major on release, and `docker/Dockerfile.agent:16` runs a bare
  `uv sync` that would silently re-resolve forward.
- Mirror the specifiers in `uv.lock`'s `requires-dist` so `uv sync --frozen`
  at `Dockerfile:42` keeps working. Resolved versions and hashes are
  unchanged; `uv lock` is a no-op.
- Construct `StateDeps(ProverbsState())` per request instead of
  `dataclasses.replace()`. `replace()` is a shallow copy, so the new deps
  point at the same state object; it is only safe today because `StateDeps`
  has one field that the adapter rebinds before every run. Any deps class
  with a second mutable field would silently share it.
2026-08-04 10:31:07 -05:00
Benjamin Taylor 4c2ba2d161 feat(skills): add channels-setup pointing at the hosted onboarding guide
The Channels onboarding workflow is served at
https://copilotkit.ai/channels-guide.md, and every other entry point now
copies one line that points there (#6357 for the docs surfaces). Coding
agents invoked through a skill had no such pointer, so they fell back to
setup-slack-channel, which is scoped to Slack, to the provider half, and to
an OpenTag checkout.

channels-setup is a pointer, not a copy. The workflow stays in one place and
is corrected there instead of drifting against the CLI on its own schedule.

Verifying the fetch is the substance of the file. The site answers unknown
paths with a "Page not found" HTML page under HTTP 200, so a status check
proves nothing: an agent that keys on the status code gets a marketing 404
and improvises the workflow from memory. The skill checks the body instead
-- markdown, the guide's H1, five Phase headings -- and stops if any fail.

Standalone skills are not generated from packages/*/skills, so the slug is
registered in RESERVED_LIFECYCLE_SLUGS. Without that entry the sync treats
the directory as an orphan and deletes it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 10:30:02 -05:00
Ben Taylor 397c8ce2e4 docs: route every Channels entry point at one onboarding skill (#6357)
## Visual

Docs `/slack` — the featured panel from #6356, now carrying the one-line
pointer.

| Desktop screenshot | Mobile screenshot |
|---|---|
| ![docs slack
desktop](https://github.com/CopilotKit/website/releases/download/channels-entry-points-2026-08-03/docs-slack-desktop-screenshot-7f14af56.png)
| ![docs slack
mobile](https://github.com/CopilotKit/website/releases/download/channels-entry-points-2026-08-03/docs-slack-mobile-screenshot-7f14af56.png)
|

| Desktop scroll | Mobile scroll |
|---|---|
| <video
src="https://github.com/CopilotKit/website/releases/download/channels-entry-points-2026-08-03/docs-slack-desktop-scroll-7f14af56.webm"
controls width="400"></video> | <video
src="https://github.com/CopilotKit/website/releases/download/channels-entry-points-2026-08-03/docs-slack-mobile-scroll-7f14af56.webm"
controls width="240"></video> |

Docs `/teams` — same component, Teams wording, proving the frontend
switch.

| Desktop screenshot | Desktop scroll |
|---|---|
| ![docs teams
desktop](https://github.com/CopilotKit/website/releases/download/channels-entry-points-2026-08-03/docs-teams-expanded-screenshot-7f14af56.png)
| ![docs
root](https://github.com/CopilotKit/website/releases/download/channels-entry-points-2026-08-03/docs-root-desktop-screenshot-7f14af56.png)
|

<sub>Artifacts are hosted on a CopilotKit/website release tag rather
than a new tag in this repo, to keep clear of the monorepo's release
automation.</sub>

---

## Summary

Builds on #6356 (merged in here, its featured-panel treatment kept) and
finishes the job it started.

#6356 made the Channels starter prompts **discoverable**. This makes
them **correct**, and makes every other road say the same thing.

The onboarding workflow was written out as prose on six surfaces across
three repos: the Channels overview page (Slack and Teams), the docs
landing activation strip, copilotkit.ai/channels, and the channels-sdk
README twice. They drifted apart, and each went stale against the CLI on
its own schedule. That drift is what the dogfooding feedback was
reporting:

- **"It didn't tell me which skills to install, so it dropped me into a
list of god knows how many."** Neither copy button named a skill.
- **"`npx copilotkit project select` didn't work — I had an old CLI."**
Only some surfaces pinned `@latest`.
- **An agent halted on `copilotkit channels`.** No such command exists
in the published CLI — verified against 4.5.1, whose `--help` has no
`channels` entry.
- **"Copy this prompt is super easy to miss."** The strongest prompts
were behind an accordion; the two visible buttons had invisible
payloads.

## What changed

- **`<ChannelsStartPrompt />`** — one shared entry point,
frontend-aware, wearing #6356's featured treatment: accent panel,
terminal mark, eyebrow, prominent copy action.
- **Both overview accordions are gone.** The accordion existed because
the payload was twenty lines. The payload is now one sentence, so hiding
it behind "Open & copy prompt" costs a click and buys nothing.
- **The activation strip emits the same pointer**, rendered on screen
rather than living only in a clipboard payload.
- **Both surfaces send `promptCopied` with a `surface` property**, so
the funnel can answer which road people take. The website emits its own
event name with the same property for the same reason.

The prompt every surface now emits:

```
Run `npx copilotkit@latest skills install --skill setup-slack-channel -y`, then follow that
skill to build your first CopilotKit Channels agent and connect it to Slack using CopilotKit's
built-in agent.
```

## Notes for reviewers

- **`-y` respects `--skill`** — verified empirically against
`copilotkit@4.5.1`: exactly one skill installs, no picker. The installer
detects the coding agent it runs inside, so `--agent` is omitted.
- **The panel renders exactly what the button copies.** Two earlier
shapes were wrong in instructive ways: a full monospace paragraph
wrapped like a rendering bug, and a code block with the ask underneath
read as a shell command with a footnote — which made a button labelled
"Copy prompt" look like it was lying.
- **The `featured` Accordion variant from #6356 stays in
`mdx-components.tsx`** as a shared opt-in capability, but its only two
consumers were the accordions removed here, so it is currently unused.
Worth a call: keep it, or drop it in a follow-up.
- **`setup-slack-channel` is the slug the combined onboarding prompt
will occupy** once it lands from channels-sdk. Its frontmatter is
currently scoped hard to the Slack provider half, so it needs rescoping
before this reads correctly for Teams.

Companion PRs: CopilotKit/website#444 and the channels-sdk README.

## Validation

- `npx vitest run` in `showcase/shell-docs` — 54 files, 372 tests passed
- `npx tsc --noEmit` — clean
- pre-commit lint and commitlint hooks passed
- Rendered and reviewed `/slack` locally; confirmed the copied string
matches the rendered one



## Update — impressions and a copy-only prompt

Since the artifacts above were first posted:

- **Impression event.** `docs.channels_activation_viewed` fires once per
surface on first intersection at 50%. Both docs entry points previously
emitted a copy event and nothing else, so the copy count had no
denominator. `surface` values now live in a shared
`CHANNELS_ACTIVATION_SURFACES` map, keeping the landing strip, the
overview panel, and copilotkit.ai/channels separable inside one funnel.
- **Guarded on `typeof IntersectionObserver`.** An impression is never
worth breaking a render for, and this repo's jsdom tests do not define
it.
- **The prompt is no longer rendered.** Per review, the copy button
carries the payload. The panel leads with the copy action directly under
the heading — the prompt is not on the page, so the button is the point
of the panel rather than trailing furniture — and the supporting line no
longer says "paste this" next to nothing.

Screenshots and videos above were re-captured after all of it.

## Validation (current)

- `npx vitest run` in `showcase/shell-docs` — 55 files, 376 tests passed
- `npx tsc --noEmit` — clean
- pre-commit lint and commitlint hooks passed
- Reviewed `/slack` and `/teams` at 1440×900 and iPhone 13

## Companion PRs

- CopilotKit/channels-sdk#15 — **merged**
- CopilotKit/website#444 — open


---

## Update — addresses both review blockers

### Blocking issue: the skill did not support the promised workflow —
resolved by removing the skill from the path

The pointer is now a fetch of one hosted file, not a skill install:

```
Read https://copilotkit.ai/channels-guide.md and help the user build their first channel
```

The guide lives at `public/channels-guide.md` in CopilotKit/website and
owns the whole workflow. It asks the developer which platform and which
agent framework they want, which is why the pointer passes neither.

That removes the mismatch at its root rather than narrowing the copy
around it. Naming the picker's channel and backend meant these pages
promised coverage on `setup-slack-channel`'s behalf, and that skill is
scoped to Slack, to the provider half, and to an OpenTag checkout — so
Teams pointed at a workflow that does not exist and the picker implied
nineteen backends it never claimed. A pointer that names nothing cannot
overpromise.

`CHANNELS_ONBOARDING_SKILL`, its install command, and the per-selection
prompt builders are gone; one constant serves every surface.

### Visual regression: disclosure restored, treatment re-tokenised

| Collapsed | Expanded |
|---|---|
|
![collapsed](https://github.com/CopilotKit/website/releases/download/channels-entry-points-2026-08-03/docs-slack-desktop-screenshot-7f14af56.png)
|
![expanded](https://github.com/CopilotKit/website/releases/download/channels-entry-points-2026-08-03/docs-slack-expanded-screenshot-7f14af56.png)
|

| Mobile | Teams variant |
|---|---|
|
![mobile](https://github.com/CopilotKit/website/releases/download/channels-entry-points-2026-08-03/docs-slack-mobile-screenshot-7f14af56.png)
|
![teams](https://github.com/CopilotKit/website/releases/download/channels-entry-points-2026-08-03/docs-teams-expanded-screenshot-7f14af56.png)
|

- **The expandable preview is back.** The panel is the shared featured
`<Accordion>` again — reused, not re-implemented — so the overview stays
compact and a reader can expand to read the exact prompt before copying.
- **Whitespace.** Collapsed height went from ~380px to ~110px with
`p-4`, matching every other docs panel. The page's own introduction is
visible without scrolling.
- **Colour.** You were right, and my "it is unchanged from #6356" reply
missed the point. `copilotkit-ui-theme` names *"purple accent bar or
stripe"* as a known wrong direction, and `copilotkit-branding` requires
accent to be restrained with gradients behind content rather than as the
contrast layer — the saturated `--accent` tile plus accent-mixed
gradient was both at once. It now matches the `OpsPlatformCTA` idiom:
`--bg-elevated`, `--border`, `--shadow-control`, accent only on a 20px
glyph and the hover state. Verified light and dark.

One deliberate deviation: I dropped the "READY-TO-USE STARTER PROMPT"
eyebrow on the low-noise rule, since the title says the same thing. Easy
to restore if you want it.

### Behavioural bug: fixed

`copyPrompt` shared one `try` with `posthog.capture`, so a throwing
client reported "Copy blocked" for a prompt already on the clipboard.
Capture now sits behind the same isolated helper
`ChannelsActivationStrip` uses. Two regression tests: capture throwing
after a resolved write still shows "Copied"; a rejected write still
shows "Copy blocked" and emits no copy event.

### Lockfile churn: dropped

Reverted to `origin/main`. It came from an `npm install` in the review
worktree; no dependency changed.

### Also added

`docs.channels_activation_prompt_expanded` — the disclosure is a funnel
step neither `viewed` nor `promptCopied` can see, so someone who never
opened the panel was indistinguishable from someone who opened it and
left.

## Validation (current)

- `npx vitest run` in `showcase/shell-docs` — 55 files, 376 tests passed
- `npx tsc --noEmit` — clean; pre-commit lint and commitlint passed
- Reviewed `/slack` and `/teams`, collapsed and expanded, light and
dark, at 1440×900 and iPhone 13
2026-08-04 10:20:01 -05:00
Tyler Slaton d92652dcdd chore: release monorepo v1.66.1 (#6365)
🚀 pkg-pr-new / build (push) Has been cancelled
## Release monorepo v1.66.1

**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.66.1`
   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.66.1`
   - Creates git tag `monorepo/v1.66.1`
   - 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.
v1.66.1
2026-08-04 08:03:11 -07:00
tylerslaton c69f7e96a5 chore: release monorepo v1.66.1 2026-08-04 14:59:52 +00:00
Benjamin Taylor a1d27ab017 feat(shell-docs): strip both Channels surfaces to one copy action
**Root page.** The channel and agent-backend pickers are gone, and the copy
action moves beside the supporting sentence, under the heading. The pickers had
stopped earning their place: the guide asks which platform and framework the
developer wants, so choosing here asked the same question twice and changed
nothing about what got copied. `ActivationSelect` and its option plumbing go with
them. The setup-guide link becomes one aside alongside OpenTag rather than a
per-selection route.

**Per-framework pages.** The accordion is gone. It existed to keep a twenty-line
prompt out of the way; the payload is now a single action, so a disclosure cost a
click and revealed nothing. The panel keeps the in-content idiom it shares with
`OpsPlatformCTA` — neutral surface, `--border`, accent on a small glyph — and the
button reads "Copy prompt" like every other surface.

`docs.channels_activation_prompt_expanded` retires with the disclosure that was
its only trigger.
2026-08-04 09:58:54 -05:00
Tyler Slaton d8eeb77aef chore: release channels v0.7.1 (#6361)
🚀 pkg-pr-new / build (push) Has been cancelled
## Release channels v0.7.1

**Scope:** `channels` | **Bump:** `patch`

---

### How this release process works

1. **This PR was created automatically** by the "release / create-pr"
workflow.
   It bumped the `channels` packages to `0.7.1`
   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 `channels` packages to npm at version `0.7.1`
   - Creates git tag `channels/v0.7.1`
   - 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.
channels/v0.7.1
2026-08-04 07:57:46 -07:00
tylerslaton f8f9e2a721 chore: release channels v0.7.1 2026-08-04 14:48:21 +00:00
Tyler Slaton dfe728d833 fix(channels): retry gateway drain joins 2026-08-04 07:34:36 -07:00
Tyler Slaton 82eaecdfda fix(channels): retry transient gateway activation 2026-08-04 07:34:36 -07:00
Tyler Slaton 503fc7c593 fix(channels): back off prolonged outage logs
Keep drop, give-up, and recovery logs immediate.

Space repeated still-down reminders during long gateway outages.
2026-08-04 07:34:36 -07:00
Benjamin Taylor 335b12a96a feat(shell-docs): point the Channels prompt at the hosted guide
Replaces the skill-install pointer with a fetch of one hosted file:

    Read https://copilotkit.ai/channels-guide.md and help the user build
    their first channel

The guide lives at `public/channels-guide.md` on the marketing site and owns the
whole workflow. It asks the developer which platform and which agent framework
they want, which is why this pointer passes neither.

That is what resolves the review's blocking issue rather than papering over it.
Interpolating the picker's channel and backend meant these pages promised
coverage on a skill's behalf — and the skill it named is scoped to Slack, to the
provider half, and to an OpenTag checkout, so the Teams road pointed at a
workflow that does not exist and the backend picker implied nineteen it never
claimed. A pointer that names nothing cannot overpromise, and the guide handles
selection itself.

Consequently `CHANNELS_ONBOARDING_SKILL`, its install command, and the
per-selection prompt builders are gone: one constant serves every surface.
Supporting copy that said the prompt "installs the onboarding skill" or offered
a "tailored" prompt is corrected — neither is true now.
2026-08-04 08:22:16 -05:00
Benjamin Taylor afef22d316 feat(shell-docs): restore the prompt disclosure and put it on the docs palette
Two changes from review.

**The disclosure is back.** The panel is the shared featured `<Accordion>`
again — reused, not restyled — so the overview stays compact when collapsed and
a reader can expand to read the exact prompt before copying it. Copy-only made
the payload opaque, which was the problem #6356 set out to fix. The component
still owns the Slack/Teams switch and the analytics; the container is markup.

**The treatment is now token-only.** The featured variant carried a saturated
`--accent` tile and an accent-mixed gradient. `copilotkit-ui-theme` names a
purple accent bar or stripe as a known wrong direction, and
`copilotkit-branding` scopes accent to restrained, atmospheric use with
gradients behind content rather than as the contrast layer — the old treatment
was both at once, on a docs `--accent` that resolves to violet. It now matches
the in-content panel idiom already in `OpsPlatformCTA`: `--bg-elevated`,
`--border`, `--shadow-control`, and accent carried only by a small glyph and the
hover state. Padding drops to `p-4` like every other docs panel, so a collapsed
prompt no longer pushes the page's own introduction below the fold. Verified in
light and dark.

Also adds `docs.channels_activation_prompt_expanded`. The disclosure is where a
funnel loses people and neither `viewed` nor `promptCopied` can see it: someone
who never opened the panel is indistinguishable from someone who opened it and
walked away.

The prompt inside wraps instead of scrolling. The docs' usual code block scrolls
horizontally, which is right for code and wrong here — it hid half the prompt
behind the overflow, defeating the point of letting people read it first.
2026-08-04 07:44:25 -05:00
Maxim bc80efb17f Reskinnable demo: a health-system knowledge desk with grounded retrieval and long-running approval gates (#6321)
Adds a skin to `examples/showcases/reskinnable-demo`: **Keel**
(`/keel`), the internal knowledge and operations desk of Harbor Point
Health, a fictional nine-hospital health system.

The product is one sentence: **an answer you can trust, that turns into
work that actually happens.**

## Why this skin

The shipped skins bracket the `Skin` contract — `airline` is in-memory
with four optional slots, `banking` is REST-backed with nearly all of
them, and #6302's `logistics` proves a server-enforced authority gate. A
further skin earns its place only by demonstrating something none of
them do. Keel introduces **two substrates the app has never had**:

1. **Grounded retrieval.** A server-side `search_knowledge` tool over a
nine-document policy corpus, returning cited passages. Every existing
tool in this app pushes client state *up* to the agent; this inverts the
flow — the agent reaches for something the user has not seen and brings
it back.
2. **Long-running stateful process.** Runs whose steps advance on a
900ms ticker and halt at human approval gates. Banking's HITL is a
single approval; airline's mutations are instantaneous. Neither has
duration — state that changes while you are not looking.

It is also the first skin with **parameterized routes**
(`knowledge/<docId>`, `runs/<runId>`), so `resolvePage` actually
destructures the segment array rather than using a flat lookup.

## The demo arc

| Beat | What happens |
| --- | --- |
| *"What's our policy on giving a contractor access to patient
records?"* | `search_knowledge` → grounded answer + `showSources` citing
POL-114 and POL-302 |
| Click a citation | Routes to
`/keel/knowledge/phi-access-policy#minimum-necessary`, scrolls,
highlights |
| *"Set that up for Priya Raman"* | `showPlaybook` → `startRun` HITL
plan preview (6 steps · 1 gate · 5 roles) |
| Run ticks | `RUN-1045` advances, then halts **blocked** on Privacy
Officer sign-off |
| Switch persona → *"approve it"* | `approveStep` card carrying the
policy citation as the reason the gate exists |

**The citation resolving into the real app is the load-bearing beat.**
Most AI-knowledge demos show a footnote; few make the footnote drive the
app.

## Two details that are load-bearing, not stylistic

**`showSources` cannot render a fabricated citation.** It accepts only
`(docId, sectionId)` pairs — never citation text — and resolves the ref,
heading, and snippet from the real corpus client-side. A citation the
model invents fails to resolve and is dropped, rather than rendering as
a convincing fake. Grounding is structural here, not prompt-dependent.

**The ticker cannot thrash the agent's context.** A 900ms timer plus a
`useAgentContext` readable is a token-cost hazard. The readables memoize
on a `summaryKey` derived only from `(runId, status, currentStepId)`
tuples — never from elapsed time — so context is rewritten on meaningful
transitions and never on a raw tick. The interval is also cleared
whenever nothing is running.

## What it exercises

- **In-memory substrate** — seed-backed `useData` with a pure reducer
(`tick` / `startRun` / `approveStep` / `rejectStep` / `cancelRun`) and a
stale-approval race guard.
- **Identity triad** — `RuntimeProviders` (persona above the provider) +
`useRuntimeProperties` + a server-safe `identifyUser`, scoping memory
per persona. A gate is actionable only when its `approverRole` matches
the active persona; otherwise it renders "waiting on <role>".
- **a2ui canvas** — an ops report from a deterministic op-builder; no
figures travel in the ops, every number binds client-side.
- **OGUI sandbox** — four functions projecting to allowlisted DTOs.
- **Theme** — deep pine on warm neutrals at `--radius: 0.5rem`, the
tightest in the app, with `--brand-violet` re-valued as the amber
awaiting-approval accent (the shared vocabulary has no warning token,
and a skin may not invent token names). Pure token re-valuing; no
component edits, no `globals.css` change. Dark-capable via the
`--nw-dark-capable` opt-in.

Deliberately omits `chatHeaderActions` and `onSuggestionSelect` (they
exist in banking only to serve its PDF-attachment beat) and `Providers`
(nothing needs to mount below the provider).

## The most valuable file in the diff

`src/skins/keel/data/seed.test.ts` asserts that **every one of the 23
playbook-step `policyRef`s resolves to a real `docId` + `sectionId`** in
the corpus. That invariant is what fuses the two substrates — a dangling
reference means an approval card cites a document that does not exist.

It has already earned its keep: it caught a citation to `POL-208
§documentation` that did not exist (`documentation` belongs to POL-121),
which had been written into the design spec and propagated into the
implementation plan before any code was generated.

That test was also **mutation-tested** rather than trusted: deliberately
breaking one `policyRef` turns it red, so its green result reflects a
real check and not one of its fallback paths silently no-opping.

## Verification

Keel's own suite: **16 test files / 105 tests** (`npx vitest run
src/skins/keel`). Whole app after merging `main`: **38 files / 215
tests** — the larger number includes logistics' 65 tests, which arrived
with the merge and are not this PR's. `pnpm lint` 0 issues · `npx tsc
--noEmit -p .` exit 0 · `pnpm build` compiles.

`main` is merged in (resolving the append-only registry conflict
predicted below), so the branch is mergeable and all four skins —
banking, airline, logistics, keel — are registered under the same id in
both the client and server registries.

**No CI job exercises this app, so green checks here are not evidence.**
`.github/workflows/static_quality.yml` carries `paths-ignore:
["examples/**"]` on both its `push` and `pull_request` triggers, and no
workflow in the repo references `reskinnable-demo` at all. The passing
checks on this PR are unrelated legacy example demos plus binary/config
checks. The local gates above are the only real signal — worth knowing
before reading a green tick as coverage.

**Not verified: the chat agent itself.** No API key was available, so
gen-UI, both HITL cards, the citation deep-link, the ticker's live
advance, persona switching, dark mode, and OGUI are unexercised at
runtime. A live smoke test of those paths is the main thing a reviewer
should add — it is also where the `pointer-events-auto` fix and the
context-churn guard would actually be observed working.

## Review pass

The skin went through a four-round unbiased review (16 reviewers per
round, non-leading prompt) plus a fix cycle per finding. Findings landed
as six thematic commits on top of the feature commits:

| Commit | Closes |
| --- | --- |
| `fix(keel): make engine mutations honest and single-path` | Lost
updates from non-functional `setRuns`, a stale render-time read, gates
that could enter `awaiting_approval` without `startedAt`, a rejector
mislabelled as approver, and a mutation reporting success for a commit
that did not happen |
| `fix(keel): make retrieval deterministic and citations grounded` |
Locale-dependent tiebreak ordering (breaking the documented
byte-identical guarantee), order-dependent transitive synonym expansion,
phrase synonyms matching across word boundaries, duplicate citation
keys, and a second citation into the same document silently failing to
re-scroll |
| `fix(keel): guard prototype access on untrusted lookup keys` |
`/keel/constructor` and friends resolving `Object.prototype` members
past a `?? null` guard and being handed to React as a component; same
class in the persona lookup, where the key arrives from client-forwarded
properties |
| `fix(keel): pin date/time formatting and fix in-chat interactivity` |
Hydration mismatches from ambient-locale formatting, and policy links
dead in chat because CopilotKit paints `useComponent` renders with
`pointer-events: none` |
| `fix(keel): anchor seeded runs to a per-call now` | Seed timestamps
anchored to fixed calendar dates while the app compares them to
`Date.now()` — a ~6.7-day cycle time poisoning the median KPI, and the
mid-flight ticking run arriving already resolved on remount |
| `fix(keel): harden report surface, catalog, and remaining chrome` |
Duplicate a2ui component ids colliding React keys, and a failed report
surface latching so the canvas never self-healed |

Each fix carries a red-green proof (test written, source broken to
confirm red, restored to confirm green). Two were structural rather than
per-instance: the prototype-safe lookup uses a `Map` so inherited keys
cannot exist, and a single `ChatSurface` wrapper now carries
`pointer-events-auto` so exactly one such literal remains in the skin.

**Caveat worth stating plainly: the final six fixes did not themselves
get an unbiased review round.** The review was stopped deliberately
after round four. Earlier rounds are the calibration — round one's 16
fixes introduced 3 regressions (~19%), round three's introduced zero —
so the risk is real but bounded, and it is concentrated in these six
commits rather than spread across the diff.

## Relationship to #6302

**Resolved: #6302 (`logistics` / Meridian) has merged, and keel landed
second.** The predicted conflict materialised exactly as described —
both PRs append to the same two registry lines — and is resolved
keep-both in the merge commit on this branch. Worth noting that until
that merge, GitHub created *no* CI runs for this PR at all: it does not
evaluate `pull_request` workflows for a branch whose mergeable state is
`CONFLICTING`, because it cannot compute the merge ref those events run
against. A conflicting PR therefore looks like "CI hasn't started"
rather than "CI failed".

Worth deciding before either merges whether these ship as two skins or
Keel's retrieval half folds into Meridian. Keel adds grounded retrieval,
duration, and parameterized routes; it also re-proves approval gates,
maximal contract coverage, and an a2ui canvas that Meridian already
demonstrates. Keel's canvas (`canvas-surface.tsx` + `ops-report.ts`) was
deliberately built as cleanly separable scope for exactly this reason —
nothing imports it except the agent and the skin object.

## Known follow-ups (non-blocking)

- In the `adverse-event` playbook, both the `rca-decision` and `close`
steps cite `POL-208 §root-cause-analysis`. POL-208 has no section that
fits "Document and close" better, so this is the least-bad mapping
rather than a clean one; a new corpus section would resolve it properly.
- `seed.test.ts`'s live-corpus check retains two fallback paths (`catch
{ return }` and `if (!docs) return`) that would let it pass silently if
the corpus import ever broke. It genuinely runs today (verified by
mutation), but the escape hatches are a latent hazard worth removing now
that the corpus is guaranteed present.

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

https://claude.ai/code/session_01FkMi54kqfNwAoUB3M3CBp3
2026-08-04 14:20:38 +02:00
Benjamin Taylor 4d5fd3a25e fix(shell-docs): stop a throwing analytics client from faking a copy failure
The clipboard write and the capture call shared one try block, so a PostHog
client that threw reported "Copy blocked" for a prompt already sitting on the
clipboard. Only the write decides what the reader is told; capture moves behind
the same isolated helper the activation strip already uses, and the impression
observer uses it too instead of its own inline catch.

Two regression tests: capture throwing after a resolved write still shows
"Copied", and a rejected write still shows "Copy blocked" without emitting a
copy event.

Also drops 16 lines of `dev: true` lockfile churn picked up from an `npm install`
in the review worktree — no dependency actually changed.
2026-08-04 07:17:31 -05:00
Maxim 0b1b37e0af Merge origin/main into feat/reskinnable-demo-keel-skin
Resolves the append-only registry conflict this PR's description predicted:
#6302 (logistics / Meridian) landed first and took the same two lines, so
keel takes the trivial conflict. Both edits are additive, so the resolution
is keep-both in both registries — logistics first (it landed first), keel
second.

Why the merge was necessary rather than optional: GitHub does not create
pull_request workflow runs for a PR whose mergeable state is CONFLICTING,
because it cannot compute the merge ref those events run against. The push
of the review fixes therefore produced ZERO CI runs — verified over 12
minutes of polling the Actions API by head SHA, and corroborated by Actions
being healthy repo-wide at the same time. Not a timing artifact, and not a
path filter: test_e2e-legacy-v1 and auto_merge_showcases both trigger on
pull_request with paths: examples/**, and synchronize is a default activity
type.

Scope of the drift: main is 127 commits ahead of the merge-base, but within
reskinnable-demo it added only logistics' own files (48 skin + 17 API route
+ 2 reskin-skill docs) plus +7 append-only lines across exactly the two
registry files. No globals.css change, no skin-contract.ts change, no
skins-config.ts change — so the shared token vocabulary and the frozen Skin
contract are untouched, and keel's isolation claim still holds against a
four-skin registry.

All four skins are now registered under the same id in both registries
(banking, airline, logistics, keel), which is the invariant the two-registry
split exists to maintain.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkMi54kqfNwAoUB3M3CBp3
2026-08-04 13:52:24 +02:00
Maxim c01b637bdc fix(keel): harden report surface, catalog, and remaining chrome
Remaining fixes across the ops-report builder, the a2ui canvas surface,
the catalog, agent wiring, suggestions, and the knowledge/playbooks pages:
correct the report surface output and its catalog registration so the
canvas renders the intended report, and tighten the surrounding chrome so
these entry points behave consistently. Tests cover the report builder and
canvas surface.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkMi54kqfNwAoUB3M3CBp3
2026-08-04 04:46:21 +02:00
Maxim 28adff5dbf fix(keel): anchor seeded runs to a per-call now
Seed data used a fixed or module-load timestamp, so seeded run timelines
drifted out of a plausible window as time passed and could not be
reproduced deterministically in tests. This anchors every seeded run
relative to a `now` passed in at seed time, keeping the demo's timeline
coherent whenever it is generated and making the seed testable. A test
pins the relative anchoring.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkMi54kqfNwAoUB3M3CBp3
2026-08-04 04:46:13 +02:00
Maxim 5bbe483073 fix(keel): pin date/time formatting and fix in-chat interactivity
Dates and times were formatted with the runtime's ambient locale and
timezone, so server and client rendered different strings and React
hydration mismatched; several call sites also built ad-hoc formatters that
drifted from one another. This centralizes formatting behind pinned
locale/timezone formatters — one per format — so output is stable across
environments. It also fixes the ChatSurface pointer-events boundary so
interactive controls rendered inside the chat transcript (playbook,
approval, and run cards) actually receive clicks. Tests cover the pinned
formatters.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkMi54kqfNwAoUB3M3CBp3
2026-08-04 04:46:05 +02:00