13420 Commits

Author SHA1 Message Date
Jordan Ritter 105a5a9d65 fix(showcase): aimock-wiring probe now covers the harness fleet (harness/harness-workers)
The aimock-wiring drift probe excluded `harness` and `harness-workers`,
so when their aimock pointers drifted to the billed PUBLIC
`*.up.railway.app` host instead of `showcase-aimock.railway.internal:4010`
the probe never flagged it (~$657/mo egress on staging).

Un-exclude the harness fleet and verify it as aimock consumers via a new
AIMOCK_CONSUMER_SERVICES set. Consumers use HARNESS_FLEET_CANDIDATE_ENV_VARS
(the standard OPENAI/ANTHROPIC/GEMINI candidates plus AIMOCK_URL), because
`harness` exposes ONLY AIMOCK_URL as its aimock pointer. AIMOCK_URL is scoped
to the harness-fleet path only, so a regular backend's stray AIMOCK_URL can't
mask a missing real base-URL pointer. Pure-infra services (aimock/shell/
dashboard/docs/dojo/pocketbase/webhooks) stay excluded.
2026-07-19 20:14:38 -07:00
Ben Taylor 0035a7e387 docs(mastra): clarify that interrupts are not supported, redirect to tool-based HITL (#5895)
## Summary

Fixes FAC-64: Mastra Interrupts docs example fails on missing agentId
and suspendPayload guard

This PR rewrites the Mastra interrupt documentation to correctly reflect
that **Mastra does not support native interrupt flow**. The framework
lacks LangGraph-style `interrupt()` primitives and does not emit AG-UI
interrupt events.

## Changes

### 📝 Documentation Updates

1. **`interrupt-flow.mdx`**: Completely rewritten to:
- Add prominent warning callout that Mastra doesn't support interrupts
   - Explain why the interrupt pattern doesn't work with Mastra
   - Provide working alternative using `useHumanInTheLoop`
- Include comparison table between interrupt-based and tool-based
approaches
   - Redirect users to the tool-based HITL guide

2. **`index.mdx`**: Updated to:
   - Mark interrupt-based approach as "Not Supported"
   - Mark tool-based approach as "Supported" (the working pattern)
   - Reorder cards to prioritize the working approach

## Rationale

The original docs documented `useInterrupt` with examples that would:
- Fail with "Agent 'default' not found" (missing `agentId` parameter)
- Fail with "Cannot destructure property 'action'" (incorrect payload
access)
- Silently fail (hook listens for events Mastra never emits)

Research revealed:
- `showcase/integrations/mastra/manifest.yaml` explicitly lists
`gen-ui-interrupt` under `not_supported_features`
- The actual working demo uses `useHumanInTheLoop`, not `useInterrupt`
- Comments in the code confirm: "This framework has no LangGraph-style
`interrupt()` primitive"

## Migration Path

Users following the old docs can now:
1. See clear warning that interrupts aren't supported
2. Learn the correct `useHumanInTheLoop` pattern
3. Follow link to complete tool-based HITL guide with working examples

## Testing

- ✅ Documentation changes only (no runtime code affected)
- ✅ Verified redirect links work correctly
- ✅ Checked against actual working implementation in
`showcase/integrations/mastra/src/app/demos/gen-ui-interrupt/page.tsx`

## Related

- Linear: FAC-64
- QA Report: Documented three specific runtime errors from the broken
examples
- Research: Identified Option B (rewrite for useHumanInTheLoop) as the
correct approach
2026-07-19 22:14:20 -05:00
Jordan Ritter 18a279867a fix(starters): add python-multipart to agno starter (fixes startup crash)
The agno starter's Dockerfile installs Python deps via 'uv pip install --system -e .', resolving from pyproject.toml (not uv.lock). agno>=1.7.8 now resolves to 2.7.4, which dropped python-multipart as a hard dependency. AgentOS registers a FastAPI form-data route, so the app raised 'RuntimeError: Form data requires python-multipart' at import time, crash-looping starter-agno. Pin python-multipart>=0.0.20 directly so it installs regardless of agno's transitive deps.
2026-07-19 20:12:38 -07:00
Benjamin Taylor f51afb672f chore: remove changeset file (repo does not use changesets)
The .changeset/ tooling is not used by this repo; dead debris was removed
from main in 5afa55f. Drop the stray changeset added by this PR so it doesn't
re-introduce the directory.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 21:12:39 -05:00
Jordan Ritter ca67cd013f fix(showcase): aimock-wiring probe now covers starter-* services (#6048)
## What this fixes

The aimock-wiring probe (`showcase/harness/src/probes/aimock-wiring.ts`)
has an `isExcluded()` helper that short-circuited every `starter-*`
service out of the checked universe:

```ts
if (name.startsWith("starter-")) return true;
```

The comment justified it by claiming starters are "categorically NOT
wired through aimock (per product decision)." That comment is stale and
wrong. All 12 `starter-*` services route `OPENAI_BASE_URL` /
`ANTHROPIC_BASE_URL` / `GOOGLE_GEMINI_BASE_URL` / `AIMOCK_URL` through
aimock exactly like the `showcase-*` backends. So the probe was silently
skipping wiring verification for a third of the roster.

This PR removes the `starter-` prefix skip so the probe checks starters
like any other LLM-calling service, and corrects the two stale comments
(the `isExcluded` body and the `EXCLUDE_SERVICES` NOTE block). The
`EXCLUDE_SERVICES` set itself is untouched — it lists only pure infra
(aimock/shell/dashboard/docs/dojo/pocketbase/harness/harness-workers/webhooks),
and starters were never in it.

## Why now / ordering

We're flipping the 12 starters' live base-URL vars from aimock's public
URL to its internal URL (a companion live-ops change, not in this PR).
Once that flip lands, the probe should be watching those services. This
PR makes it do so.

**This must land AFTER the live starter vars are flipped to the internal
URL.** If it merges first, the probe will immediately flag the 12
starters as drifted against their pre-existing public-URL values (a real
mismatch until the flip happens). Sequence: flip vars → merge this.

## The probe is read-only

Including 12 more services adds only read-only Railway variable reads.
The verification path is pure string comparison:

- `aimockWiringProbe.run` calls `listServices()` then, per service,
`getServiceEnv(name)` (a Railway GraphQL `variables(...)` query).
- `pointsAtAimock` compares the parsed hostname of each candidate env
var (`CANDIDATE_ENV_VARS`) against the aimock URL's hostname.
- No HTTP request is made to the service itself; no LLM traffic is
generated. Adding starters just means 12 more variable lookups.

## Red → Green proof

Ran the probe's unit tests
(`showcase/harness/src/probes/aimock-wiring.test.ts`), which exercise
the real `isExcluded` logic through the exported `aimockWiringProbe.run`
surface. Added one focused test and updated three that asserted the old
(wrong) starters-excluded behavior.

**RED** — with the `starter-` short-circuit still in place, the tests
asserting starters are checked fail:

```
$ npx vitest run src/probes/aimock-wiring.test.ts

 FAIL  ... > checks starters like any LLM backend (NOT excluded); infra stays excluded
 FAIL  ... > excludes harness-workers (infra) but checks bare starter-* services
 FAIL  ... > checks starters alongside showcase-* backends (both in the checked universe)
 FAIL  ... > full live roster: the 20 showcase-* backends AND 12 starters are checked, 9 infra excluded

 Test Files  1 failed (1)
      Tests  4 failed | 29 passed (33)
```

Example assertion diff (roster test): expected 32 unwired (20 backends +
12 starters), received only 19 — the 12 `starter-*` entries were being
excluded.

**GREEN** — after removing the short-circuit, the same test file passes:

```
$ npx vitest run src/probes/aimock-wiring.test.ts

 Test Files  1 passed (1)
      Tests  33 passed (33)
```

## Follow-up: probe-correctness fixes (c1 / c2 / c5)

Now that the probe covers all 32 services (20 `showcase-*` + 12
`starter-*`), a cr-loop audit surfaced two coupled correctness holes
that let real drift go undetected, plus a stale comment. All three are
in `aimock-wiring.ts` / `aimock-wiring.test.ts`.

- **c1 — confirmed mismatch beats sealed.** Old `pointsAtAimock`
returned `sealed` whenever any candidate was the sealed sentinel and
none was a confirmed match — even if a *sibling* candidate was set to a
confirmed non-aimock host (e.g. real `api.openai.com`). That masked
provable drift. The precedence is now explicit: **match >
mismatch(confirmed) > sealed > mismatch(all-missing)**. A candidate that
is set, non-sentinel, non-empty and does not match the aimock target is
confirmed drift and wins over a sealed sibling. Purely-sealed (no
confirmed match/mismatch) still → `sealed`; all-missing still →
`mismatch`.
- **c2 — host + port matching (was host-only).** Internal aimock serves
**only on `:4010`**, so a service on the right host but the
wrong/missing port is drift, not "wired". Matching now compares host
**and** effective port. Default ports collapse (`http://h` ≡
`http://h:80`, `https://h` ≡ `https://h:443`) via the protocol default
on both sides. The expected port is **derived from the configured
`aimockUrl`** (no port hardcoded) — see `extractHostPort`.
- **c5 — comment fix (no logic change).** The
`isExcluded`/`EXCLUDE_SERVICES` doc block said the matcher "strips" a
leading `showcase-`; it actually **prepends** `showcase-` to the bare
name and tests that form. Corrected to describe the prepend.

### Blast-radius check (host+port change hits all 32 live services)

Confirmed the probe's expected `aimockUrl` carries `:4010` before making
matching port-aware. The live cron reads it from `env.AIMOCK_URL`
(`orchestrator.ts` `buildCronProbeResolver` /
`probes/drivers/aimock-wiring.ts`). Live production value: `AIMOCK_URL =
http://showcase-aimock.railway.internal:4010` — carries `:4010`,
matching the 32 services' `:4010` base URLs. Port-aware matching
therefore keeps prod green.

Note (out of scope): the **staging** harness `AIMOCK_URL` is still the
public host `https://aimock-staging.up.railway.app`, while staging
services point at the internal `:4010` host. That is already a hostname
mismatch under the *current* (host-only) code, so staging is unaffected
by this change — but it is a live staging config drift worth flipping to
the internal host separately.

### Red → Green proof (c1 / c2)

New/adjusted tests exercise the real `pointsAtAimock` through
`aimockWiringProbe.run`.

**RED** — against the pre-fix (host-only, sealed-wins) code:

```
$ npx vitest run src/probes/aimock-wiring.test.ts -t "c2 wrong-port|c2 missing-port|c1 confirmed-mismatch"

 FAIL  ... > c2 wrong-port: correct aimock host but a DIFFERENT port ...  expected 'green' to be 'red'
 FAIL  ... > c2 missing-port: correct aimock host but NO port (:80) ...   expected 'green' to be 'red'
 FAIL  ... > c1 confirmed-mismatch beats sealed ...                       expected 'green' to be 'red'

 Tests  3 failed | 1 passed | 34 skipped (38)
```

**GREEN** — after the c1/c2 fixes, the same tests (plus correct-port,
purely-sealed, and the strengthened default-port collapse test) pass,
and the full file is green:

```
$ npx vitest run src/probes/aimock-wiring.test.ts
 Test Files  1 passed (1)
      Tests  38 passed (38)
```

### LIVE value-test (shared-mechanism change)

Dumped all 32 non-infra services' base-URL vars from **live production**
Railway (read-only `railway variables --json`), then ran the real
`aimockWiringProbe.run` with the new code and the production `aimockUrl`
(`http://showcase-aimock.railway.internal:4010`):

```
state: green
RESULT: 32/32 wired   (unwired: 0, sealed: 0, errored: 0)
```

No false reds — the port-aware + mismatch-beats-sealed verdict
classifies every live production service as wired.

## Local quality (CI-relevant, this path)

- `npm run typecheck` (tsc --noEmit) — clean, exit 0
- `npm run build` (tsc -p tsconfig.build.json) — clean, exit 0
- `oxlint` / `oxfmt --check` on both changed files — clean
- `npm test` (full harness vitest) — **3308 passed**, 18 skipped. Two
pre-existing failures unrelated to this change, both reproducible on
`origin/main` (neither references aimock-wiring; a transient timing-only
flake in `probe-invoker.test.ts` under full-suite parallel load passes
67/67 in isolation):
- `d0-gone-predicate.test.ts` — reads the gitignored generated
`showcase/shell/src/data/registry.json`, which isn't present in a fresh
worktree (needs the shell generate step; present in CI/dev).
- `d5-mapping-drift.test.ts` — parses
`shell-dashboard/src/lib/live-status.ts` for `CATALOG_TO_D5_KEY`, but
that file is now a re-export barrel (the symbol moved to
`harness/src/shared/cell-model/live-status.ts`); the test's hardcoded
path is stale on `origin/main`. Not touched by this PR.

Neither failing file is related to the aimock-wiring probe.

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

https://claude.ai/code/session_01QCLub2Vb5Y56cPttSzkip1
2026-07-18 16:57:53 -07:00
Jordan Ritter 88138aefe0 docs(showcase): clarify aimock-wiring unwired bucket and extractHostPort null-return comments 2026-07-18 16:44:22 -07:00
Jordan Ritter a9c11f4627 test(showcase): lock aimock-wiring host+port precedence, refresh stale comments
Non-behavioral cleanup + coverage pass for the aimock-wiring probe:

- Correct comments that still described the old hostname-only matching or
  referenced removed helpers (normalizeUrl / extractHostname) to reflect the
  current host+port matching via extractHostPort, in both the probe and its test.
- Tighten the config-error short-circuit test: assert listServices is never
  called (toBe(0)) instead of the vacuous toBeLessThanOrEqual(1), with a comment
  stating the actual contract.
- Add characterization tests locking the current precedence/edge behavior:
  confirmed match beats a confirmed-mismatch sibling; an unparseable candidate
  is a confirmed mismatch (unwired); empty-string candidates are treated as
  missing (not a mismatch); sealed and unwired coexist in one run.

No runtime behavior changes.
2026-07-18 16:41:54 -07:00
Jordan Ritter b2602cac04 fix(showcase): port-aware + confirmed-mismatch-beats-sealed in aimock-wiring probe
Coupled correctness fixes so real starter/backend drift can't hide now that
the probe covers all 32 services (20 showcase-* + 12 starter-*):

- c1: a CONFIRMED mismatch (a candidate set to a non-aimock host) now wins
  over a sealed sibling. Previously a var pointing at real api.openai.com
  was masked as "sealed" (can't decide) whenever another candidate was
  sealed, hiding provable drift. Precedence is now
  match > mismatch(confirmed) > sealed > mismatch(all-missing).
- c2: matching compares host AND effective port, not host alone. Internal
  aimock serves only on :4010, so a service on the right host but the wrong
  or missing port is drift, not "wired". Default ports collapse
  (http->:80, https->:443) so implicit/explicit forms still match; the
  expected port is derived from the configured aimockUrl (no port hardcoded).
- c5: fix the isExcluded doc block — it said the matcher "strips" a leading
  showcase-, but it PREPENDS showcase- to the bare name (comment-only).

Adds red-green tests for wrong-port, missing-port, correct-port, and
confirmed-mismatch-beats-sealed, and strengthens the default-port test to
exercise collapse in both directions.
2026-07-18 16:41:54 -07:00
Jordan Ritter 167ae3ad1b fix(showcase): aimock-wiring probe now covers starter-* services
The isExcluded() helper short-circuited every starter-* service out of the
probe on the (stale) rationale that starters are not wired through aimock.
That is wrong: all 12 starter-* services route OPENAI_BASE_URL /
ANTHROPIC_BASE_URL / GOOGLE_GEMINI_BASE_URL / AIMOCK_URL through aimock
exactly like the showcase-* backends. Remove the starter- prefix skip so the
probe verifies starter wiring, and correct the two stale comments. Infra
services in EXCLUDE_SERVICES are untouched.
2026-07-18 16:41:53 -07:00
Jordan Ritter 090c080581 fix(showcase): langroid declarative gen-ui → two-stage A2UI north-star (D6 turn-1 surface-missing) (#6058)
## What

Brings the **langroid** `declarative-gen-ui` D6 cell to sibling parity
with the two-stage dynamic-schema A2UI north-star (google-adk /
strands). The cell was red at turn 1 with `reason=surface-missing`: the
demo was still on the pre-D6 (D5-era) shape, and four independent
defects each blocked the A2UI surface from painting.

Second-wave fan-out of the proven pattern (pilots #6051/#6052/#6053,
first wave #6054 agno / #6055 claude-sdk-typescript).

## Root cause (four defects, each verified against a live isolated
stack)

1. **Stale suggestions + fixtures.** `suggestions.ts` still offered the
old D5 pills; the aimock fixture only mocked those. The D6 driver sends
the four current business-question prompts. Re-authored both to the four
current prompts mirroring the google-adk north-star (outer
`generate_a2ui` no-arg → inner forced `render_a2ui` → outer narration).
2. **Required `context` on the outer tool.** `GenerateA2UITool.context`
was a required pydantic field, so the mocked outer `arguments: {}`
raised `ValidationError` before the tool ran → no inner call, no
surface. Made optional (default `""`) to match the no-arg sibling tools.
3. **Legacy functions API hid the inner tool from aimock's matcher.**
The inner planner used langroid's `functions=`/`function_call=` (legacy
OpenAI) path; aimock's `toolName` matcher only inspects the modern
`tools[]` array, so the inner `render_a2ui` fixture never matched and
the call fell through to the outer `generate_a2ui` fixture (empty
surface, wrong catalogId). Switched the inner call to the modern
`tools=`/`tool_choice=` API. The response extractor already reads the
modern `oai_tool_calls` path first, so nothing downstream changes.
4. **Inner call could not be discriminated per pill.** langroid has no
framework middleware to forward the run's conversation into the inner
call (unlike `ag_ui_adk` / `ag_ui_strands`), so its inner user message
was a fixed generic string across all four pills. Added an explicit
last-user-turn thread (a `ContextVar` set by the adapter, consumed by
the planner) so the pill prompt rides as the inner `userMessage` — the
discriminator the sibling fixtures rely on.

**Renderer/catalog parity:** added the missing `declarative-info-row`
testid on InfoRow (turn 4) and a full `DataTable` definition + renderer
(`declarative-data-table`, turn 2), plus `trendValue` on Metric. Added
`sales-context.ts` (byte-identical dataset + composition rules to the
strands/google-adk siblings) and wired it via `chat.tsx`.

Backend family mirrored: **google-adk / strands** — outer
`generate_a2ui` (no args) + inner forced `render_a2ui`,
`declarative-gen-ui-catalog`.

## Red-green proof (isolated control-plane, `--isolate --rebuild`, slot
16)

**RED (pre-fix, 3 runs):**
```
✗ d6:langroid/gen-ui-declarative red — state=red
  waitForTurnComplete: turn 1 did not complete within 90000ms (reason=surface-missing)
```
First diagnosis (direct backend SSE): outer `generate_a2ui` → `{"error":
"Tool generate_a2ui failed: ValidationError"}`. After fixing that: outer
succeeded but emitted `catalogId: copilotkit://app-dashboard-catalog`
with `components: []` (inner never matched — legacy functions API).
After the tools-API + threading fix:

**GREEN (post-fix, 2 runs):**
```
✓ d6:langroid/gen-ui-declarative green
  1 passed
```
aimock journal confirms all four pills' outer `generate_a2ui` + inner
`render_a2ui` (tool_choice forced) calls return **200** and emit
`declarative-gen-ui-catalog` surfaces.

Per-turn backend SSE verified:
- turn 1 (sales-dashboard): 4 × Metric + PieChart + BarChart
- turn 2 (team-performance): DataTable + BarChart
- turn 4 (top-account): 7 × InfoRow + PieChart

## Visual

`langroid-turn1-sales-dashboard.png` — live Playwright render
(X-AIMock-Context: langroid): 4 metric tiles + revenue-by-region pie +
monthly-revenue bar. Turns 2–4 surface renders are asserted and pass in
the authoritative D6 run (conjunctive per-turn testid checks) and
confirmed via backend SSE above.

## Notes

- Draft: not for merge/promote (user-gated).
- A concurrent `ms-agent-harness-dotnet` fan-out agent was hitting the
shared local aimock during manual browser capture (interleaved 404s in
the journal); it does not affect the isolated D6 result, which is the
binding proof.

---

**CI-driven follow-ups (in this same commit):**
- Updated `integrations/langroid/tests/python/test_generate_a2ui.py` to
assert the modern `tools=`/`tool_choice=` kwargs (was pinning the legacy
`functions=`/`function_call=` API this fix intentionally replaced). 78
passed / 1 skipped locally.
- Scoped the four inner `render_a2ui` fixtures to `context: langroid`
(langroid forwards `x-aimock-context` to the inner planner call, unlike
`ag_ui_adk`/`ag_ui_strands`) so they don't collide in the shared scope
with the sibling integrations' identical inner keys — keeps the
`aimock-fixtures` exact-duplicate ceiling at 297 (no bump). Full
`aimock-fixtures.test.ts` suite: 837 passed. D6 re-run after this
change: still green 4/4.
2026-07-18 16:20:05 -07:00
Jordan Ritter 04aefd9dcd fix(showcase): repair ms-agent-dotnet D6 gen-ui-declarative (surface-missing) (#6057)
## Summary

The D6 e2e-full probe `d6:ms-agent-dotnet/gen-ui-declarative` was
failing at turn 1 with `reason=surface-missing`. Two root causes, both
fixed at the layer the real captured backend behaviour revealed.

### Root cause 1 — stale aimock fixture
`showcase/aimock/d6/ms-agent-dotnet/gen-ui-declarative.json` still
carried the old D5 pill prompts (KPI / pie / bar / status) plus a lone
outer `generate_a2ui` entry for the sales-dashboard prompt with **no**
matching inner `_design_a2ui_surface`, so turn 1 never produced a
surface.

Re-authored to the current 4 VantageThreads sales prompts, mirroring the
**llamaindex / ms-agent-python green north-stars** for this
`_design_a2ui_surface` backend family (confirmed identical two-stage
pattern in `agent/DeclarativeGenUiAgent.cs` +
`agent/A2uiSecondaryToolCaller.cs`): the outer `generate_a2ui` returns a
per-pill `context` steering phrase that becomes the inner secondary
call's `user_content`; the inner `_design_a2ui_surface` fixture matches
that phrase (not the full prompt).

**ms-agent-dotnet–specific discriminator.** Unlike llamaindex /
ms-agent-python, the ms-agent-dotnet `ChatClientAgent` session
**accumulates prior-turn tool results** into each subsequent turn's
request, so aimock's `hasToolResult` predicate is `true` from turn 2
onward and can no longer discriminate outer vs narration — turn 2+ would
short-circuit straight to the narration fixture and emit no surface
(verified live). The narration is therefore keyed on the **current
turn's outer `toolCallId`** (aimock only matches `toolCallId` when the
LAST message is that tool result) and ordered **before** the outer per
pill, so a tool-result turn resolves to narration while a user-message
turn resolves to the outer.

### Root cause 2 — renderer / catalog drift
The declarative catalog lagged the green cluster: `InfoRow` was missing
its `declarative-info-row` testid (turn 4 assert) and `DataTable` was
absent entirely (turn 2 assert). Added the testid and the `DataTable`
renderer + definition, matching the green cluster.

## Red → Green proof (real control-plane surface)

`./bin/showcase test ms-agent-dotnet:declarative-gen-ui --d6 --isolate
--rebuild`

| | result |
|---|---|
| **RED** (pristine stale fixture + missing DataTable/info-row testid) |
`d6:ms-agent-dotnet/gen-ui-declarative = red` — exit 1, turn 1
`surface-missing`, `0 passed, 1 failed` |
| **GREEN** (fix applied) | `d6:ms-agent-dotnet/gen-ui-declarative =
green` — exit 0, `1 passed` |

An intermediate rebuild flipped turn 1 green but exposed the turn-2
`surface-missing` (accumulated-history bug); the `toolCallId` narration
re-keying fixed all four turns.

## Visual verification

Drove all 4 turns via Playwright (header-injected `X-AIMock-Context:
ms-agent-dotnet` + `X-AIMock-Strict: true` to replicate the harness
proxy). Confirmed real painted surfaces:
- **turn 1 sales-dashboard**: 4 KPI metrics ($4.2M revenue, 186
customers, 31% win rate, $22.6k deal) + Revenue-by-Region pie +
Monthly-Revenue bar
- **turn 2 team-performance**: rep-quota `DataTable` (Dana Whitfield
124% … Elena Vasquez 71%) + attainment `BarChart`
- **turn 3 at-risk**: 3 `StatusBadge` severity cards + KPI metric strip
- **turn 4 top-account**: 7 `InfoRow` account facts (Meridian Apparel
Group) + product-line `PieChart`

Screenshots under
`~/.local/share/copilotkit/cr/2ndwave-shots/msdotnet-turn{1..4}-full.png`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-18 16:20:01 -07:00
Jordan Ritter 92d0fab67e fix(showcase): repair ms-agent-harness-dotnet D6 gen-ui-declarative (surface-missing) (#6056)
## Summary

The D6 e2e-full probe `d6:ms-agent-harness-dotnet/gen-ui-declarative`
was failing at turn 1 with `reason=surface-missing`. Three layers, each
fixed at the layer the real captured request revealed. Part of the
second-wave declarative fan-out (siblings #6051–#6055 / #6053
ms-agent-python).

### Root cause 1 — stale aimock fixture
`showcase/aimock/d6/ms-agent-harness-dotnet/gen-ui-declarative.json`
still carried the old D5 pill set (KPI / pie / bar / status prompts)
with inner `_design_a2ui_surface` entries keyed on those stale prompts.
The current driver sends four VantageThreads sales prompts, so turn 1
("Show me my sales dashboard for this quarter.") had **no matching inner
surface** — the backend looped `generate_a2ui` to its invocation limit
and the frontend painted the stale KPI catch-all instead of the sales
dashboard. Re-authored to the four current prompts, mirroring the
**llamaindex green north-star** for this `_design_a2ui_surface`
two-stage backend family (ms-agent-harness-dotnet's inner tool is
`_design_a2ui_surface`, per `agent/DeclarativeGenUiAgent.cs` +
`agent/A2uiSecondaryToolCaller.cs`).

### Root cause 2 — `hasToolResult` breaks the interleaved thread
This is where the dotnet family diverges from the #6053 ms-agent-python
template. ms-agent-python starts a fresh session per turn; the **.NET
harness backend threads the FULL interleaved conversation**.
`hasToolResult` is a thread-global predicate (see
`showcase/GOTCHAS.md`), so once turn 1 leaves a `role:"tool"` message in
the thread, every later pill's outer `generate_a2ui` call sees
`hasToolResult:true` and matches the **narration** fixture instead of
emitting the tool call → surface-missing on turns 2–4. (Reproduced live:
turn 2's outer call, replayed with turn-1 history, returned the
narration string instead of `generate_a2ui`.) Switched to the sanctioned
interleaved-safe pattern: **narration keyed on this pill's outer
`toolCallId`** (ordered before the outer), **outer keyed on
`userMessage` only**. Verified all four pills resolve correctly through
the full interleaved thread against live aimock.

### Root cause 3 — renderer / catalog drift
`renderers.tsx` and `definitions.ts` lagged the green cluster — missing
the `DataTable` and `InfoRow` components (the `declarative-data-table` /
`declarative-info-row` testids that turns 2 and 4 assert), plus
`Metric.trendValue` and the Row/Column/Text gap overrides. Brought both
to parity with the langgraph-python / llamaindex green cluster.

## Red → Green proof (real control-plane surface)

`SHOWCASE_ISO_SLOT=18 ./bin/showcase test
ms-agent-harness-dotnet:declarative-gen-ui --d6 --isolate --rebuild`

| | result |
|---|---|
| **RED** (pristine stale fixture + drifted renderers) | `state=red`,
exit 1 — turn 1 `waitForTurnComplete: turn 1 did not complete within
90000ms (reason=surface-missing, runsFinished=1, count=41)`; body showed
the stale "Quarterly KPIs / $1.24M / SIGNUPS 8,420" surface +
`generate_a2ui` looping |
| **GREEN** (fix applied) | `state=green`, exit 0 — `1 passed` |

## Visual verification

Drove all 4 turns via Playwright with network-level route injection of
`x-aimock-context: ms-agent-harness-dotnet` (replicating the
harness/production proxy). Confirmed real painted surfaces:
- **turn 1 sales-dashboard**: 4 KPI metrics ($4.2M revenue, 186
customers, 31% win rate, $22.6k deal) + Revenue-by-Region donut +
Monthly-Revenue bar
- **turn 2 rep-quota**: rep-attainment `DataTable` (Dana Whitfield 124%
… Elena Vasquez 71%) + quota-attainment `BarChart`
- **turn 3 at-risk**: 3 severity `StatusBadge` cards (Northwind /
Cascadia / Atlas) + KPI metric strip
- **turn 4 top-account**: 7 `InfoRow` account facts (Meridian Apparel
Group) + product-line `PieChart`

Screenshots under
`~/.local/share/copilotkit/cr/2ndwave-shots/msharness-turn{1..4}-*.png`.
2026-07-18 16:19:57 -07:00
Jordan Ritter c13415f743 fix(showcase): repair claude-sdk-typescript D6 gen-ui-declarative (surface-missing) (#6055)
## What

Repairs the `claude-sdk-typescript` D6 `gen-ui-declarative` cell, which
was **red on turn-1 surface-missing**. The aimock fixture carried stale
D5-era prompts and only a partial turn-1 outer entry, so the two-stage
Anthropic A2UI flow (outer `generate_a2ui` → secondary `render_a2ui` →
narration) never painted a surface for the four current sales-analyst
pills.

Fan-out of the proven pattern from the pilots (#6051 claude-sdk-python,
#6052 mastra, #6053 ms-agent-python). This is the Anthropic two-stage
family — `injectA2UITool: false`, backend owns `generate_a2ui` +
secondary `render_a2ui`.

## Root cause

`aimock/d6/claude-sdk-typescript/gen-ui-declarative.json` had the old D5
prompts (`Show me a quick KPI dashboard` / `pie chart of sales by
region` / `bar chart of quarterly revenue` / `status report on system
health`) plus one lone turn-1 outer entry for the new sales prompt. The
driver's four current prompts had no complete triads, so aimock returned
`STRICT: No fixture matched` and no surface mounted.

## Fix (3 layers)

1. **Fixture re-author** — 12 fixtures (4 pills × {outer
`generate_a2ui`, inner `render_a2ui`, narration}) in the two-stage
shape, mirroring the #6051 sibling + google-adk data. Render payloads
mount the per-pill catalog components the driver asserts: Metric×4 + Pie
+ Bar (sales-dashboard); DataTable + Bar (team-performance); Metric×3 +
StatusBadge×3 (at-risk); InfoRow + Pie (top-account). Render payloads
are byte-identical to #6051.

**Ordering/matcher fix vs a naive python mirror:** the CSTS runtime
accumulates full conversation history across pills, so on turns 2-4 the
outer `generate_a2ui` call carries prior pills' tool results and a
`hasToolResult:false` matcher never fires. Each pill triad is ordered
**narration (`toolCallId`) first** so it claims the last-role:tool
calls, and the outer matcher drops `hasToolResult` and gates on
`userMessage` + `toolName generate_a2ui` (last-role:user). Verified live
via the aimock journal.

2. **InfoRow testid** — add `data-testid="declarative-info-row"` to the
InfoRow renderer (turn-4 top-account parity; CSTS was missed by #6050).

3. **Suggestions refresh** — `suggestions.ts` had stale D5-era pill
labels that emitted unmatched prompts (live 404 banner). Now the four
sales-analyst pills, matching google-adk.

## Red → green proof (control-plane, slot 30, `--isolate --rebuild`)

**RED (origin/main):**
```
✗ d6:claude-sdk-typescript/gen-ui-declarative red — state=red
[aimock] STRICT: No fixture matched for POST /v1/messages   (×6)
```

**GREEN (fixed):**
```
✓ d6:claude-sdk-typescript/gen-ui-declarative green — 1 passed
```
aimock journal after the green run: **all 12 calls returned 200, zero
503, zero no-match** across all 4 turns.

## Visual proof (Playwright, `x-aimock-context: claude-sdk-typescript`)

Drove all 4 pills live; per-turn DOM testid counts (no fixture error on
any turn):

| Turn | Pill | Newly-mounted testids |
|---|---|---|
| 1 | sales-dashboard | metric=4, pie=1, bar=1 |
| 2 | team-performance | data-table=1, bar +1 (→2) |
| 3 | at-risk | status-badge=3, metric +3 (→7) |
| 4 | top-account | **info-row=7**, pie +1 (→2) |

Screenshots:
`~/.local/share/copilotkit/cr/2ndwave-shots/csts-turn{1..4}-*.png`.

## Unit tests
- `scripts/__tests__/aimock-fixtures.test.ts`: 837 passed (validates the
new fixture shape).
- `harness/src/probes/scripts/d5-gen-ui-declarative.test.ts`: 31 passed.

## Notes
- Worktree tsc module-not-found / TS2322-ButtonProps noise is benign
symlink noise; trust the PR's real `check-types` CI check.
- Local red-green ran on slot 30 (slot 8 and several low slots were held
by concurrent isolate stacks).
2026-07-18 16:19:54 -07:00
Jordan Ritter 29ed44d913 fix(showcase): flip agno gen-ui-declarative D6 cell green (4-turn sales flow + DataTable/InfoRow parity) (#6054)
## What

Flips the `d6:agno/gen-ui-declarative` cell from **red (turn-1
dom-missing) → green**. Fan-out of the proven 2nd-wave declarative fix
pattern (pilots #6051 claude-sdk-python, #6052 mastra, #6053
ms-agent-python).

## Root cause (verified at the request level)

agno's declarative-gen-ui shipped a **stale D5 aimock fixture** keyed on
the old prompts (KPI dashboard / pie chart of sales by region / bar
chart / status report), while the current D6 driver sends the OSS-136
sales prompts. Captured from the aimock journal on a RED run: agno's
OUTER agent hit aimock with `tools=[generate_a2ui]`, `userMessage="Show
me my sales dashboard for this quarter."`, `x-aimock-strict:true`,
`context=agno` — the stale fixture matched **none**, aimock returned 503
(strict), the outer agent never emitted `generate_a2ui`, no surface
rendered → turn-1 dom-missing → `state=red`.

## Backend family + north-star

agno uses the plain **`render_a2ui` two-stage** family
(`src/agents/a2ui_dynamic_agent.py`): an OUTER `generate_a2ui(context:
str)` tool, then a forced-`render_a2ui` secondary call, then narration.
Mirrored the **google-adk** green north-star (same VantageThreads
surfaces + `declarative-gen-ui-catalog`; `render_a2ui` args copied
verbatim).

Key agno-specific wrinkle: the inner secondary call's **user message is
hardcoded and identical across all four pills** ("Generate a dynamic
A2UI dashboard based on the conversation."), so the inner `render_a2ui`
fixtures cannot key on `userMessage`. They discriminate on
`toolName:render_a2ui` + `context:agno` + a `systemMessage` substring
equal to the per-pill context phrase the outer injects ("Conversation
context:\n<context>"). aimock's CLI server uses substring matching, so
this works; verified live against the journal.

## Fix layers

- **Fixture** (`aimock/d6/agno/gen-ui-declarative.json`): 4 sales
prompts × 3 calls (outer/inner/narration) = 12 fixtures.
- **Renderers** (`.../a2ui/renderers.tsx`): `declarative-info-row`
testid on InfoRow (turn 4) + new `DataTable` renderer with
`declarative-data-table` testid (turn 2), mirroring google-adk.
- **Definitions** (`.../a2ui/definitions.ts`): `DataTable` schema,
`Metric.trendValue`, `z.unknown()` PrimaryButton action, refreshed
descriptions.
- **Backend** (`a2ui_dynamic_agent.py`): sales-analyst system prompt for
live-mode steering.
- **Test** (`scripts/__tests__/aimock-fixtures.test.ts`):
`KNOWN_DUPLICATE_CEILING` 297→300 (+3) — the 4 inner render fixtures
collapse to one `toolName=render_a2ui` matchKey (matchKey omits
systemMessage/context) but aimock's router disambiguates them at
runtime.

### Bug caught during green

First green attempt still red with a client-side exception: adding
`Row`/`Column`/`Text` to `myDefinitions` **without matching renderers**
(agno relies on `includeBasicCatalog:true` for those) made
`createCatalog` produce a definition set wider than its renderer set →
render crash. Fixed by not declaring Row/Column/Text in definitions.

## Local RED → GREEN proof (isolated D6 slots)

**RED** (control-plane, stale fixture):
```
✗ d6:agno/gen-ui-declarative red — state=red
```

**GREEN** (control-plane, after fix):
```
✓ d6:agno/gen-ui-declarative green
1 passed
```

**GREEN** (`--direct`, per-turn DOM assertions — authoritative):
```
turn 1/4 — assertions passed   (sales-dashboard: metric×4 + pie + bar)
turn 2/4 — assertions passed   (team-performance: data-table + bar)
turn 3/4 — assertions passed   (at-risk: status-badge×3 + metric×3)
turn 4/4 — assertions passed   (top-account: info-row + pie)
state=green, 1 passed
```

GREEN aimock journal: 12 requests = 4× OUTER `generate_a2ui` + 4× INNER
`render_a2ui` + 4× narration, all matched.

**Live Playwright visual** (all 4 surfaces painted, testids counted):
turn1 metric×4/pie/bar, turn2 data-table×1/bar, turn3
status-badge×3/metric, turn4 info-row×7/pie.

`aimock-fixtures.test.ts`: 837 passed after the ceiling bump.
2026-07-18 16:19:50 -07:00
Jordan Ritter 3d8eff5e70 fix(showcase): repair ms-agent-python D6 gen-ui-declarative (surface-missing) (#6053)
## Summary

The D6 e2e-full probe `d6:ms-agent-python/gen-ui-declarative` was
failing at turn 1 with `reason=surface-missing`. Two independent root
causes, both fixed at the layer the real captured request revealed.

### Root cause 1 — stale aimock fixture
`showcase/aimock/d6/ms-agent-python/gen-ui-declarative.json` still
carried the old D5 pill prompts (KPI / pie / bar / status) plus a lone
outer `generate_a2ui` entry for the current sales-dashboard prompt with
**no** matching inner `_design_a2ui_surface` and **no** narration. The
backend looped `generate_a2ui` to its invocation limit and
`RUN_FINISHED` was blocked while the tool call stayed active (`Cannot
send 'RUN_FINISHED' while tool calls are still active`).

Re-authored to the current 4 VantageThreads sales prompts, mirroring the
**llamaindex green north-star** for this backend shape
(ms-agent-python's inner tool is `_design_a2ui_surface`, not
google-adk's `render_a2ui`):
- outer `generate_a2ui` returns a `context` steering phrase — the
ms-agent-framework session does not surface the latest user message to
the secondary LLM, so the phrase becomes the inner call's
`user_content`;
- the inner `_design_a2ui_surface` fixture matches that phrase (not the
full prompt);
- `hasToolResult` discriminates outer (false) vs narration (true).

This also eliminates the stale `render-a2ui.json` "KPI dashboard"
catch-all collision that was rendering the wrong (KPI) surface for the
sales prompt.

### Root cause 2 — renderer / catalog drift
`renderers.tsx` and `definitions.ts` for ms-agent-python's
declarative-gen-ui lagged the green cluster — missing the `DataTable`
and `InfoRow` components (the `declarative-data-table` /
`declarative-info-row` testids that turns 2 and 4 assert), plus
`Metric.trendValue` and the `Row`/`Column`/`Text` gap overrides. Brought
both files to parity with the langgraph-python / google-adk green
cluster.

## Red → Green proof (real control-plane surface)

`SHOWCASE_ISO_SLOT=11 ./bin/showcase test
ms-agent-python:declarative-gen-ui --d6 --isolate`

| | result |
|---|---|
| **RED** (pristine fixture + stale renderers) |
`d6:ms-agent-python/gen-ui-declarative = red` — exit 1, turn 1
`surface-missing` |
| **GREEN** (fix applied) | `d6:ms-agent-python/gen-ui-declarative =
green` — exit 0, `1 passed` |

## Visual verification

Drove all 4 turns via Playwright (header-injected `x-aimock-context:
ms-agent-python` to replicate the harness/production proxy). Confirmed
real painted surfaces:
- **turn 1 sales-dashboard**: 4 KPI metrics ($4.2M revenue, 186
customers, 31% win rate, $22.6k deal) + Revenue-by-Region pie +
Monthly-Revenue bar
- **turn 2 team-performance**: rep-quota `DataTable` + attainment
`BarChart`
- **turn 3 at-risk**: 3 `StatusBadge` severity cards + KPI metric strip
- **turn 4 top-account**: 7 `InfoRow` account facts + product-line
`PieChart`

Screenshots captured under
`~/.local/share/copilotkit/cr/2ndwave-shots/mspy-turn{1..4}-*.png`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-18 16:19:46 -07:00
Jordan Ritter c01bde3759 fix(showcase): flip mastra gen-ui-declarative D6 cell green (#6052)
## What

Flips the `d6:mastra/gen-ui-declarative` showcase cell from **red** to
**green**.

The cell failed turn 1 with `reason=surface-missing` (the dom-missing
family). Reading the real failure surface (aimock logs + mastra
container logs + harness worker logs) exposed **three** distinct
defects, each fixed at its own layer.

## Root cause (empirically confirmed)

1. **Stale aimock fixture.** `aimock/d6/mastra/gen-ui-declarative.json`
carried the OLD D5 pill prompts (`"Show me a quick KPI dashboard"`,
`"pie chart of sales by region"`, ...) and the stale inner tool name
`_design_a2ui_surface`. The current D6 driver sends 4 different prompts,
so aimock matched **0** fixtures:
   ```
[aimock] STRICT: No fixture matched for POST /v1/responses (x2 = the
two-stage flow's outer + inner calls)
   ```
   Nothing emitted → no render → `surface-missing`.

2. **Mastra outer-tool arg schema (mastra-specific).** Unlike the green
`google-adk` peer whose outer `generate_a2ui` takes `{}`, mastra's
`generateA2uiTool` (`integrations/mastra/src/mastra/tools/index.ts`) has
an `inputSchema` that **requires a `messages` array**. After
re-authoring the fixture to the green shape (which emits `generate_a2ui`
with `{}`), the mastra runtime rejected it:
   ```
Tool input validation failed for generate_a2ui — messages: Required.
Provided arguments: {}
   ```
The outer tool never executed → no `a2ui_operations` container →
`surface-missing` (narration bubble rendered, but no A2UI surface).

3. **Renderer testid parity.** The D6 probe DOM-asserts
`declarative-data-table` (turn 2) and `declarative-info-row` (turn 4).
Mastra's `renderers.tsx` had **no DataTable renderer at all** and its
InfoRow renderer **lacked the `data-testid`**; `definitions.ts` had no
DataTable definition. Turns 2 and 4 could never satisfy their
assertions.

## Fix (3 files)

- **`aimock/d6/mastra/gen-ui-declarative.json`** — re-authored to the
current 4 driver prompts + the green two-stage shape (outer
`generate_a2ui` + inner forced `render_a2ui`, `context: "mastra"`,
`catalogId: "declarative-gen-ui-catalog"`, per-pill narration). Each
outer `generate_a2ui` call now carries a valid `messages` array (mastra
schema requirement).
- **`integrations/mastra/.../a2ui/definitions.ts`** — added the
`DataTable` catalog definition (mirrors the green `google-adk` peer).
- **`integrations/mastra/.../a2ui/renderers.tsx`** — added
`data-testid="declarative-info-row"` to the InfoRow renderer, and added
a `DataTable` renderer carrying `data-testid="declarative-data-table"`.

## Red → Green proof (real control-plane surface, slot 13, `--rebuild`)

Command: `SHOWCASE_ISO_SLOT=13 ./bin/showcase test
mastra:declarative-gen-ui --d6 --isolate --rebuild`

**RED (pristine main):**
```
✗ d6:mastra/gen-ui-declarative red — state=red
turn 1 did not complete within 90000ms (reason=surface-missing)
```

**GREEN (fixed):**
```
✓ d6:mastra/gen-ui-declarative green
1 passed
[conversation-runner] turn 1/4 — assertions passed   (metric x4, pie, bar; baseline 0)
[conversation-runner] turn 2/4 — assertions passed   (data-table NEW, bar)
[conversation-runner] turn 3/4 — assertions passed   (status-badge x3, metric x3)
[conversation-runner] turn 4/4 — assertions passed   (info-row NEW, pie)
[conversation-runner] conversation completed successfully { turnsCompleted: 4 }
```

## Visual evidence

Drove the live cell through all 4 turns via Playwright (route-level
`x-aimock-context` injection) and screenshotted each painted surface —
all real renders, no error states:

- **turn 1** sales-dashboard: 4 KPI metric tiles + donut PieChart
(Revenue by Region) + BarChart (Monthly Revenue)
- **turn 2** team-performance: DataTable (Rep attainment: Dana Whitfield
124%, ...) — the new renderer
- **turn 3** at-risk: 3 severity cards each with a StatusBadge + 3 KPI
metric tiles
- **turn 4** top-account: Card of InfoRow facts (Owner/Region/ARR/...) +
PieChart — the new testid

Note: `google-adk` remains the only D6 declarative fixture already on
the current prompts; the other integrations (`langgraph-typescript`,
etc.) still carry the same stale-fixture shape and are a follow-up wave.
2026-07-18 16:19:43 -07:00
Jordan Ritter 17663f90e1 fix(showcase): complete claude-sdk-python declarative gen-ui D6 (4-turn sales flow + DataTable/InfoRow parity) (#6051)
## What

Fixes the `claude-sdk-python` **gen-ui-declarative** D6 showcase cell,
which was red with `reason=done-signal-missing`.

## Root cause

The aimock fixture
(`showcase/aimock/d6/claude-sdk-python/gen-ui-declarative.json`) still
carried the **legacy D5 pills** (KPI dashboard / pie / bar / status
report) plus a single stray hero `generate_a2ui` entry. The current D6
driver uses the **4-prompt sales-analyst flow**:

1. "Show me my sales dashboard for this quarter."
2. "How are our sales reps performing against quota?"
3. "Are any accounts or pipeline deals at risk this quarter?"
4. "Pull up the details on our biggest account."

None of those turns had matching fixtures. The backend
(`src/agents/a2ui_dynamic.py`) runs a two-stage flow — an outer
`generate_a2ui` call, then a secondary `render_a2ui` call — and both hit
aimock's `/v1/messages` in STRICT mode, which 404'd (`No fixture
matched`). An unmatched turn never emits the expected render, so the run
never produced the done signal for that turn.

Two additional **renderer/definition parity gaps** on claude-sdk-python
(present on google-adk, absent here) blocked full green:
- Turn 2 asserts `declarative-data-table` — the `DataTable` catalog
component (definition + renderer) did not exist.
- Turn 4 asserts `declarative-info-row` — the `InfoRow` renderer was
missing that testid.

## Fix

- **Fixture** re-authored into the two-stage Anthropic-transport shape
(mirrors the `claude-sdk-typescript` sibling for match discriminators +
google-adk for the A2UI payload data). Per turn: (a) outer
`generate_a2ui` emit matched by
`userMessage`+`toolName`+`hasToolResult:false`, (b) inner `render_a2ui`
design matched by `toolName`, (c) outer narration matched by
`toolCallId`. Covers all 4 prompts.
- **DataTable** catalog component added (`definitions.ts` +
`renderers.tsx`, testid `declarative-data-table`).
- **InfoRow** renderer given the missing `declarative-info-row` testid.

## Local red→green proof

Control-plane, isolation slot 12, `--isolate --rebuild` (real failure
surface, not `--direct`):

**RED** (pristine code + pristine fixture):
```
✗ d6:claude-sdk-python/gen-ui-declarative  red  (state=red)  0 passed, 1 failed
aimock: STRICT: No fixture matched for POST /v1/messages  (x6)
```

**GREEN** (fix applied, same slot, rebuilt):
```
✓ d6:claude-sdk-python/gen-ui-declarative  green  1 passed
aimock: 0 no-match
```

## Visual verification

Playwright drive with the harness `X-AIMock-Context: claude-sdk-python`
header, all 4 turns painted with the exact per-testid deltas the probe
asserts:

| Turn | Newly-mounted testids |
|------|----------------------|
| 1 sales-dashboard | metric ×4, pie-chart ×1, bar-chart ×1 |
| 2 team-performance | data-table ×1, bar-chart ×1 |
| 3 at-risk | status-badge ×3, metric ×3 |
| 4 top-account | info-row ×7, pie-chart ×1 |

Screenshots confirm the DataTable (rep attainment table) and InfoRow
(Meridian Apparel Group facts) render correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-18 16:19:39 -07:00
Jordan Ritter 45cdc02ee0 fix(showcase): re-author langroid declarative gen-ui to the two-stage A2UI north-star (D6 turn-1 surface-missing)
The langroid `declarative-gen-ui` D6 cell was red at turn 1 with
`reason=surface-missing`: the demo was still on the pre-D6 (D5-era) shape
while the google-adk / strands siblings had moved to the two-stage
dynamic-schema A2UI pattern. Four independent defects each blocked the
surface from painting; all four are fixed here to bring langroid to
sibling parity.

Root causes (each verified against a live isolated stack):

1. Stale suggestions + fixtures. `suggestions.ts` still offered the old
   D5 pills ("Show a KPI dashboard", "pie chart of sales by region", …)
   and `aimock/d6/langroid/gen-ui-declarative.json` only mocked those old
   prompts. The D6 driver sends the four current business-question pills
   ("Show me my sales dashboard for this quarter.", etc.). Re-authored both
   to the four current prompts, mirroring the google-adk north-star
   (outer `generate_a2ui` no-arg → inner forced `render_a2ui` → outer
   narration, three fixtures per pill).

2. Required `context` on the outer tool. `GenerateA2UITool.context` was a
   required pydantic field, so the mocked outer call's `arguments: {}`
   raised `ValidationError` before the tool could run — no inner call, no
   surface. Made it optional (default "") to match the no-arg sibling tools.

3. Legacy functions API hid the inner tool from aimock's matcher. The
   inner planner call used langroid's `functions=`/`function_call=` (legacy
   OpenAI) path; aimock's `toolName` matcher only inspects the modern
   `tools[]` array, so the inner `render_a2ui` fixture never matched and the
   call fell through to the outer `generate_a2ui` fixture (empty surface,
   wrong catalogId). Switched the inner call to the modern
   `tools=`/`tool_choice=` API via a new `_RENDER_A2UI_TOOL_SPEC`. The
   response extractor already reads the modern `oai_tool_calls` path first.

4. Inner call could not be discriminated per pill. langroid has no
   framework middleware to forward the run's conversation into the inner
   call (unlike ag_ui_adk / ag_ui_strands), so its inner user message was a
   fixed generic string across all four pills. Added an explicit
   last-user-turn thread (ContextVar set by the adapter, consumed by the
   planner) so the pill prompt rides as the inner `userMessage` — the same
   discriminator the sibling fixtures rely on.

Renderer/catalog parity: added the missing `declarative-info-row` testid
to InfoRow (turn 4) and a full `DataTable` definition + renderer
(`declarative-data-table`, turn 2), plus `trendValue` on Metric. Added
`sales-context.ts` (byte-identical dataset + composition rules to the
strands/google-adk siblings) and wired it via `chat.tsx`.

Red-green (isolated control-plane, slot 16):
- RED (3 runs, pre-fix): `d6:langroid/gen-ui-declarative` red,
  turn-1 surface-missing.
- GREEN (2 runs, post-fix, --rebuild): 4/4 turns pass. aimock journal
  confirms all four pills' outer `generate_a2ui` + inner `render_a2ui`
  calls return 200 and emit the correct `declarative-gen-ui-catalog`
  surfaces. Backend SSE verified per turn: turn-1 4 metrics + pie + bar,
  turn-2 DataTable, turn-4 7 InfoRows.
2026-07-18 16:02:47 -07:00
Jordan Ritter bf75022c78 fix(showcase): repair ms-agent-dotnet D6 gen-ui-declarative (surface-missing)
The D6 e2e-full probe d6:ms-agent-dotnet/gen-ui-declarative failed at turn 1
with reason=surface-missing. Two root causes fixed at the layer the real
captured backend behaviour revealed.

Root cause 1 - stale aimock fixture. The fixture still carried the old D5
pill prompts (KPI/pie/bar/status) plus a lone outer generate_a2ui entry for
the sales-dashboard prompt with no matching inner _design_a2ui_surface, so
turn 1 never produced a surface. Re-authored to the current 4 VantageThreads
sales prompts mirroring the llamaindex/ms-agent-python green north-stars for
this _design_a2ui_surface backend family (outer generate_a2ui returns a
context steering phrase; the inner _design_a2ui_surface fixture matches that
phrase).

Unlike llamaindex/ms-agent-python, the ms-agent-dotnet ChatClientAgent session
ACCUMULATES prior-turn tool results into each subsequent turn's request, so
hasToolResult is true from turn 2 onward and cannot discriminate outer vs
narration (turn 2+ would short-circuit straight to narration, no surface).
The narration is therefore keyed on the CURRENT turn's outer toolCallId
(aimock only matches toolCallId when the LAST message is that tool result)
and ordered before the outer per pill so the tool-result turn resolves to
narration while the user-message turn resolves to the outer.

Root cause 2 - renderer/catalog drift. The declarative catalog lagged the
green cluster: InfoRow was missing its declarative-info-row testid (turn 4)
and DataTable was absent entirely (turn 2). Added the testid and the DataTable
renderer + definition, matching the green cluster.

Red -> Green (real control-plane, --isolate --rebuild):
  RED:   d6:ms-agent-dotnet/gen-ui-declarative = red (turn 1 surface-missing)
  GREEN: d6:ms-agent-dotnet/gen-ui-declarative = green (1 passed)

Visual: drove all 4 turns via Playwright (X-AIMock-Context: ms-agent-dotnet)
- turn 1 4 KPI metrics + region pie + monthly bar
- turn 2 rep-quota DataTable + attainment bar
- turn 3 3 severity StatusBadges + KPI metric strip
- turn 4 7 account InfoRows + product-line pie
2026-07-18 15:22:45 -07:00
Jordan Ritter adc897f485 fix(showcase): repair ms-agent-harness-dotnet D6 gen-ui-declarative (surface-missing)
The D6 e2e-full probe `d6:ms-agent-harness-dotnet/gen-ui-declarative`
failed at turn 1 with `reason=surface-missing`. Three layers, all fixed
at the layer the real captured request revealed.

Root cause 1 — stale aimock fixture
The fixture still carried the old D5 pill set (KPI / pie / bar / status
prompts) with inner `_design_a2ui_surface` entries keyed on those stale
prompts. The current driver sends four VantageThreads sales prompts, so
turn 1 ("Show me my sales dashboard for this quarter.") had no matching
inner surface — the backend looped `generate_a2ui` to its limit and the
frontend painted the stale KPI catch-all instead of the sales dashboard.
Re-authored to the four current prompts, mirroring the llamaindex green
north-star for this `_design_a2ui_surface` two-stage backend family.

Root cause 2 — `hasToolResult` breaks the interleaved thread
Unlike ms-agent-python (fresh session per turn), the .NET harness backend
threads the FULL interleaved conversation. `hasToolResult` is a
thread-global predicate (GOTCHAS.md), so once turn 1 leaves a tool result
in the thread, every later pill's outer `generate_a2ui` call sees
`hasToolResult:true` and matches the narration fixture instead of emitting
the tool call — surface-missing on turns 2-4. Switched to the sanctioned
interleaved-safe pattern: narration keyed on this pill's outer
`toolCallId` (ordered before the outer), outer keyed on `userMessage`
only.

Root cause 3 — renderer / catalog drift
`renderers.tsx` and `definitions.ts` lagged the green cluster — missing
the `DataTable` and `InfoRow` testids (`declarative-data-table` turn 2 /
`declarative-info-row` turn 4) plus `Metric.trendValue` and the
Row/Column/Text gap overrides. Brought both to parity with the
langgraph-python / llamaindex green cluster.

Red → Green (real control-plane surface, SHOWCASE_ISO_SLOT=18 --isolate)
  RED  (pristine): turn 1 surface-missing, state=red, exit 1
  GREEN (fixed):   1 passed, state=green, exit 0

Visual: drove all 4 turns via Playwright (network-injected
x-aimock-context: ms-agent-harness-dotnet). Confirmed real painted
surfaces — turn 1 sales dashboard (4 KPIs + region pie + monthly bar),
turn 2 rep-quota DataTable + attainment bar, turn 3 three at-risk status
badges + KPI strip, turn 4 seven account InfoRows + product-line pie.
Screenshots under ~/.local/share/copilotkit/cr/2ndwave-shots/.
2026-07-18 15:22:36 -07:00
Jordan Ritter 92be110c62 fix(showcase): repair claude-sdk-typescript D6 gen-ui-declarative (surface-missing)
The claude-sdk-typescript declarative-gen-ui cell was red on turn-1
surface-missing: the aimock fixture carried stale D5-era prompts (KPI
dashboard / pie / bar / status report) and only a partial turn-1 outer
entry, so the two-stage Anthropic A2UI flow (outer generate_a2ui ->
secondary render_a2ui -> narration) never painted a surface for the
four current sales-analyst pills.

Three fix layers, mirroring the proven claude-sdk-python (#6051) sibling
and the google-adk north-star:

1. Re-author aimock/d6/claude-sdk-typescript/gen-ui-declarative.json to
   the 4 current driver prompts in the two-stage shape (12 fixtures = 4
   pills x {outer generate_a2ui, inner render_a2ui, narration}), with
   render payloads mounting the per-pill catalog components the driver
   asserts (Metric x4 + Pie + Bar; DataTable + Bar; Metric x3 +
   StatusBadge x3; InfoRow + Pie). Render payloads are byte-identical to
   the #6051 data.

   Ordering/matcher fix vs the naive python mirror: the CSTS runtime
   accumulates full conversation history across pills, so on turns 2-4
   the outer generate_a2ui call carries prior pills' tool results and a
   hasToolResult:false matcher never fires. Each pill triad is ordered
   narration (toolCallId) FIRST so it claims the last-role:tool calls,
   and the outer matcher drops hasToolResult and gates on
   userMessage + toolName generate_a2ui (last-role:user).

2. Add data-testid="declarative-info-row" to the InfoRow renderer
   (turn-4 top-account parity; CSTS was missed by #6050).

3. Refresh suggestions.ts to the 4 sales-analyst pills (were stale
   D5-era labels that emitted unmatched prompts -> live 404 banner).

Red-green (control-plane, slot 30, --isolate --rebuild):
- RED  (origin/main): d6:claude-sdk-typescript/gen-ui-declarative red;
  aimock STRICT: No fixture matched for POST /v1/messages.
- GREEN (fixed): 1 passed; aimock journal shows all 12 calls 200, zero
  503/no-match across all 4 turns.
Visual: Playwright 4-turn walk (x-aimock-context claude-sdk-typescript)
confirms metric=4/pie=1/bar=1 (t1), data-table=1/bar+1 (t2),
status-badge=3/metric+3 (t3), info-row=7/pie+1 (t4); no fixture error.
Unit: aimock-fixtures 837 passed; d5-gen-ui-declarative 31 passed.
2026-07-18 14:46:02 -07:00
Jordan Ritter 3370a452b5 fix(showcase): flip agno gen-ui-declarative D6 cell green (4-turn sales flow + DataTable/InfoRow parity)
agno's declarative-gen-ui D6 cell failed turn-1 dom-missing: the aimock
fixture was keyed on the stale D5 prompts (KPI/pie/bar/status) while the
current driver sends the OSS-136 sales prompts, so the agno OUTER agent's
generate_a2ui call matched no fixture, aimock returned 503 (strict), and no
surface rendered.

Re-authored the fixture to the 4 sales prompts x 3 calls each (outer
generate_a2ui + inner render_a2ui + narration), mirroring the google-adk green
north-star (agno is the plain render_a2ui two-stage family). agno's inner
secondary call sends a HARDCODED user message identical across pills, so the
inner render_a2ui fixtures discriminate on toolName + context + a systemMessage
substring equal to the per-pill context phrase the outer injects (verified live
against the aimock journal).

Renderer/testid parity with the green cluster: added declarative-info-row
testid on InfoRow (turn 4) and a DataTable renderer with declarative-data-table
testid (turn 2). definitions.ts gains DataTable, Metric.trendValue, and an
z.unknown() PrimaryButton action. Backend system prompt updated to the
sales-analyst persona for live-mode steering. Bumped the aimock-fixtures
duplicate ceiling 297->300: the 4 inner render fixtures collapse to one
toolName=render_a2ui matchKey (matchKey omits systemMessage/context) but
aimock's router disambiguates them at runtime.

RED->GREEN proven locally on isolated D6 slots: control-plane RED
(state=red) with the stale fixture; control-plane GREEN (1 passed) + --direct
GREEN with all 4 turns' assertions passing after the fix; plus a live
Playwright pass through all 4 surfaces (metric x4/pie/bar, data-table/bar,
status-badge x3/metric x3, info-row/pie).
2026-07-18 14:27:30 -07:00
Jordan Ritter ece15c1016 fix(showcase): repair ms-agent-python D6 gen-ui-declarative (surface-missing)
The D6 e2e-full probe for ms-agent-python:gen-ui-declarative failed at turn 1
with reason=surface-missing. Two root causes, both fixed:

1. Stale aimock fixture. The fixture still carried the old D5 pill prompts
   (KPI/pie/bar/status) plus a lone outer generate_a2ui entry for the current
   sales-dashboard prompt with no matching inner _design_a2ui_surface or
   narration. The backend looped generate_a2ui to its invocation limit and
   RUN_FINISHED was blocked while the tool call stayed active. Re-authored to
   the current 4 VantageThreads sales prompts mirroring the llamaindex green
   north-star for this backend shape: the outer generate_a2ui returns a
   `context` steering phrase (the ms-agent-framework session does not surface
   the latest user message to the secondary LLM), the inner
   _design_a2ui_surface fixture matches that phrase (not the full prompt), and
   hasToolResult discriminates outer vs narration. This also avoids the stale
   render-a2ui "KPI dashboard" catch-all collision that was rendering the wrong
   surface.

2. Renderer/catalog drift. ms-agent-python's declarative-gen-ui renderers.tsx
   and definitions.ts lagged the green peers — missing the DataTable and
   InfoRow components (declarative-data-table / declarative-info-row testids)
   that turns 2 and 4 assert, plus Metric trendValue and the Row/Column/Text
   gap overrides. Brought both files to parity with the langgraph-python /
   google-adk green cluster.

Red-green proof on the real control-plane surface (SHOWCASE_ISO_SLOT=11,
--d6 --isolate):
- RED:   d6:ms-agent-python/gen-ui-declarative = red  (exit 1, turn 1 surface-missing)
- GREEN: d6:ms-agent-python/gen-ui-declarative = green (exit 0, 1 passed)

Visually verified all 4 turns via Playwright (header-injected to replicate the
harness x-aimock-context): turn 1 renders 4 KPI metrics + region pie + monthly
bar; turn 2 the rep-quota DataTable + attainment bar; turn 3 three at-risk
StatusBadges + KPI metrics; turn 4 seven InfoRow account facts + product-line
pie.
2026-07-18 13:45:16 -07:00
Jordan Ritter 4440e727af fix(showcase): flip mastra gen-ui-declarative D6 cell green
The mastra declarative-gen-ui D6 cell failed turn 1 with reason=surface-missing.
Three defects fixed at the layers the real failure surface showed:

1. Stale aimock fixture: aimock/d6/mastra/gen-ui-declarative.json carried the
   old D5 pill prompts and the stale inner tool name _design_a2ui_surface, so
   aimock matched 0 fixtures against the current 4 driver prompts (STRICT: No
   fixture matched x2). Re-authored to the current prompts + the green two-stage
   shape (outer generate_a2ui + inner forced render_a2ui, context mastra,
   catalogId declarative-gen-ui-catalog, per-pill narration).

2. Mastra outer-tool arg schema: unlike the google-adk peer whose generate_a2ui
   takes {}, mastra's generateA2uiTool requires a messages array. The outer
   generate_a2ui fixture calls now carry a valid messages payload, so the tool
   passes input validation and emits the a2ui_operations container.

3. Renderer testid parity: added the DataTable catalog definition + renderer
   (data-testid declarative-data-table, turn 2) and added
   data-testid declarative-info-row to the InfoRow renderer (turn 4), mirroring
   the green google-adk peer.

Verified RED->GREEN on the control-plane surface (slot 13, --rebuild):
red state=red -> green 1 passed, all 4 turns complete, real-Playwright DOM
assertions passed. Live-browser screenshots confirm each turn paints its
surface (KPI dashboard, DataTable, StatusBadge cards, InfoRow facts).
2026-07-18 13:37:43 -07:00
Jordan Ritter c5ad5caefd fix(showcase): complete claude-sdk-python declarative gen-ui D6 (4-turn sales flow + DataTable/InfoRow parity)
The claude-sdk-python gen-ui-declarative D6 cell went red with
reason=done-signal-missing: the aimock fixture still carried the legacy
D5 pills (KPI/pie/bar/status) plus a single stray hero `generate_a2ui`
entry, so the current 4-prompt sales-analyst driver (Show me my sales
dashboard / How are reps performing / accounts at risk / biggest account)
had no matching fixtures. aimock STRICT mode 404'd the unmatched outer
and inner Claude calls, so the run never emitted the expected render
per turn.

Two-part fix:
- Re-author aimock/d6/claude-sdk-python/gen-ui-declarative.json into the
  two-stage Anthropic-transport shape (mirrors the claude-sdk-typescript
  sibling + google-adk data): per turn (a) outer generate_a2ui emit
  matched by userMessage+toolName+hasToolResult, (b) inner render_a2ui
  design matched by toolName, (c) outer narration matched by toolCallId.
  Covers all 4 current sales prompts.
- Renderer/definition parity: add the DataTable catalog component
  (definition + renderer, testid declarative-data-table) that turn 2
  requires, and add the missing declarative-info-row testid to the
  InfoRow renderer that turn 4 requires. Both were absent on
  claude-sdk-python (present on google-adk).

Local red-green proof (control-plane, slot 12, --isolate --rebuild):
- RED  (pristine): d6:claude-sdk-python/gen-ui-declarative = red,
  aimock log 'STRICT: No fixture matched for POST /v1/messages'.
- GREEN (fixed):    d6:claude-sdk-python/gen-ui-declarative = green,
  1 passed, zero aimock no-match.
Visual verify (Playwright, harness X-AIMock-Context header): all 4 turns
paint with correct per-testid deltas (metric x4/pie/bar; data-table/bar;
status-badge x3/metric x3; info-row/pie).
2026-07-18 13:33:45 -07:00
Jordan Ritter 6d09233fc6 fix(showcase): add declarative-info-row testid to 3 lagging integrations for turn-4 parity (#6050)
## What

The `gen-ui-declarative` D6 probe drives a 4-turn conversation. Turn 4
(the **top-account** pill — "Pull up the details on our biggest
account.") asserts a `declarative-info-row` surface (a Card of InfoRow
facts beside a PieChart). Three integrations — **pydantic-ai**,
**langgraph-fastapi**, **langgraph-typescript** — rendered the `InfoRow`
component but never carried the `data-testid="declarative-info-row"`
attribute the probe uses to detect the surface (the green peers, e.g.
langgraph-python, already have it). Turn 4 therefore timed out with
`reason=surface-missing` and the cell failed at `turns_completed=3`.

## Fix

Add the missing `data-testid="declarative-info-row"` to the `InfoRow`
renderer in the 3 lagging integrations, matching the green peers. 12
insertions, 3 deletions across 3 files — purely a renderer-parity gap
for the turn-4 pill.

**ButtonProps note:** No `onClick`/TS2322 change was needed.
`PrimaryButton` already dispatches actions via `dispatch(props.action)`,
and the local `_components/button.tsx` `ButtonProps extends
React.ButtonHTMLAttributes<HTMLButtonElement>`, so `onClick` is valid.
Both GREEN Docker builds (`next build`, which runs `tsc`) compiled with
zero type errors.

## Local RED → GREEN proof (real surface, D6 control-plane, isolated
slots)

Base: `origin/main` (3fae3296d — already has `declarative-data-table`;
only `declarative-info-row` was missing). Run via `bin/showcase test
<slug>:declarative-gen-ui --d6 --isolate --rebuild` (fleet
control-plane, NOT `--direct`).

### pydantic-ai
- **RED** (pristine, no testid): turns 1–3 assertions passed; turn 4
failed —
`waitForTurnComplete: turn 4 did not complete within 90000ms
(reason=surface-missing, runsFinished=4, count=8)`. CLI exit 1.
- **GREEN** (with fix): `turn 1/4 … 4/4 — assertions passed`; `✓ Tests
passed`. CLI exit 0.

### langgraph-typescript
- **RED** (pristine, no testid): turns 1–3 assertions passed; turn 4
failed —
`waitForTurnComplete: turn 4 did not complete within 90000ms
(reason=surface-missing, runsFinished=4, count=4)`. CLI exit 1.
- **GREEN** (with fix): `turn 1/4 … 4/4 — assertions passed`; `✓ Tests
passed`. CLI exit 0.

(langgraph-fastapi carries the byte-identical change on the same base;
the two proven cells cover ≥2 as required.)

## Visual verify (painted pixels, not just DOM counts)

Drove the live GREEN pydantic-ai cell through all 4 turns in a real
browser with the harness's `X-AIMock-Context: pydantic-ai` header. Turn
4 painted a fully-rendered InfoRow surface — a "Meridian Apparel Group /
Biggest account" card with InfoRow facts (Owner: Dana Whitfield, Region:
North America, ARR: $612k, Renewal date: Sep 30, Last contact: 3 days
ago, Health: Green, Open opportunities: 4 opportunities worth $210k).
DOM confirmed `declarative-info-row` count=7, `declarative-pie-chart`
count=2. Screenshot:
`~/.local/share/copilotkit/cr/turn4-shots/pydantic-turn4-GREEN.png`.

## Root cause

Renderer-parity gap: the turn-4 InfoRow surface was rendered but
untestable because 3 integrations dropped the shared
`declarative-info-row` testid that the probe and green peers depend on.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-18 12:59:03 -07:00
Jordan Ritter 64b1409289 fix(showcase): add declarative-info-row testid to 3 lagging integrations for turn-4 parity
The gen-ui-declarative D6 probe drives a 4-turn conversation; turn 4
(the "top-account" pill) asserts a `declarative-info-row` surface
(a Card of InfoRow facts next to a PieChart). pydantic-ai,
langgraph-fastapi, and langgraph-typescript rendered the InfoRow
component but never carried the `data-testid="declarative-info-row"`
attribute that the probe (and the green peer integrations such as
langgraph-python) rely on to detect the surface. As a result turn 4
timed out with reason=surface-missing and the cell failed at
turns_completed=3.

This restores parity with the green peers by adding the missing testid
to the InfoRow renderer in the 3 lagging integrations. No other
behavior changes; PrimaryButton already wires actions via the
`dispatch(props.action)` pattern (the local ButtonProps extends
ButtonHTMLAttributes, so onClick is valid — no type error).
2026-07-18 12:14:05 -07:00
Jordan Ritter ced494da85 fix(showcase): bound claude-sdk-python declarative gen-ui loop so RUN_FINISHED fires (#6042)
## What

Fixes the claude-sdk-python **declarative-gen-ui** class-F failure (RCA
2026-07-17): the dashboard renders but `RUN_FINISHED` never arrives, so
the harness times out with `done-signal-missing`.

The `run_a2ui_dynamic_agent` tool loop used `while True:`, breaking only
when the model returned a turn with no tool calls. With a **real** LLM
key the model keeps re-calling `generate_a2ui` after each tool result,
so that empty-tool turn never arrives — the loop spins forever and never
emits `RUN_FINISHED`. (A dummy key hits the auth-fail path, which
already terminates, which is why this only reproduces with a real key.)

**Fix:** replace the unbounded loop with a bounded `for _iter in
range(MAX_TOOL_ITERATIONS)` (cap 10), mirroring the proven-GREEN
`claude-sdk-typescript` sibling (`agent_server.ts`
`MAX_TOOL_ITERATIONS`). The loop now always falls through to
`RUN_FINISHED`.

## Red / Green proof

Real surface, real OPENAI key: `bin/showcase test
claude-sdk-python:declarative-gen-ui --d6 --direct --rebuild --isolate`

**RED (before — `while True:`):**
```
turn 1 did not complete within 90000ms
(reason=done-signal-missing, runsFinished=0, count=12, runningNow=true, runStartCount=1)
bodyText: generate_a2ui / Done  (x12 — infinite re-call loop, RUN_FINISHED never fires)
⚠ Tests failed for claude-sdk-python:declarative-gen-ui (exit 1)
```

**GREEN (after — bounded loop):**
```
turn 1/4 — assistant settled { text: 'generate_a2uiDone' }   (single call, run terminates)
turn 2 diagnostics: runsFinished=2, runningNow=false          (RUN_FINISHED now fires, runs complete)
bodyText: QUARTERLY REVENUE $4.2M / Revenue by Region / NA·EMEA·APAC·LATAM  (dashboard renders)
```

`runsFinished` 0 → 2 and `runningNow` true → false confirms the run now
terminates. The class-F hang is fixed.

## Scope note

The remaining turn-2 `surface-missing` on this cell is a **separate**
RCA class-A defect (missing `DataTable` renderer in the per-integration
frontend catalog), owned by a different fix and out of scope here.
2026-07-18 11:47:35 -07:00
Jordan Ritter 1cbd7ee840 fix(showcase): pin ANTHROPIC_BASE_URL aimock serviceRef for claude-sdk-python (watchdog-restart investigation) (#6047)
> **DRAFT — combined python promote with #6042, gated on Jordan's
explicit authorization; do not merge/promote.**

## Summary

Investigated the claude-sdk-python showcase 502 wedge and its prescribed
durable fix. Two prescribed changes were on the table: (1) an
`entrypoint.sh` watchdog-restart fix and (2) an `ANTHROPIC_BASE_URL`
aimock SSOT serviceRef. **This PR ships only (2)** — the SSOT hygiene
change. Empirical Docker red-green testing shows the prescribed
entrypoint change was **not the right fix** (details below), so it is
deliberately omitted rather than shipped as a kernel no-op.

## Root cause (as briefed)

Prod image `sha256:64889ca5` wedged to 502: agent `/health` blocked on a
synchronous un-timeouted PocketBase write (`CVDIAG pb-write-failed
error=timed out`) → in-container watchdog SIGKILLed uvicorn after ~90s →
the container failed to restart, so Railway `ON_FAILURE` never fired →
dead-in-place. The briefed remaining defect at HEAD was "the entrypoint
watchdog restart kills the uvicorn child, not PID 1 (bash isn't PID 1
under shell-form ENTRYPOINT)".

## Ground-truth finding (contradicts the entrypoint premise)

HEAD's writer + `/health` fixes are confirmed present
(threaded/non-blocking writer with 5s timeout in
`_shared/cvdiag_pb_writer.py`; bare liveness `/health` returning
`{"status":"ok"}` in `src/agent_server.py`). But the entrypoint claim
did not hold up under real Docker testing:

1. **HEAD's Dockerfile already uses exec-form** `CMD
["./entrypoint.sh"]` — so the entrypoint bash **IS PID 1** in the real
Railway invocation (verified `cat /proc/1/cmdline` → `/bin/bash
/app/entrypoint.sh`). The prod wedge described was the *pre-HEAD*
image's shell-form; HEAD's Dockerfile already resolves it.

2. **In the real exec-form invocation, HEAD's watchdog restart already
works.** Tripping the watchdog (agent `/health` hangs) → watchdog `kill
-9 $AGENT_PID` → main shell `wait -n` returns → `exit 137` → container
exits **non-zero** → Railway `ON_FAILURE` would restart. Verified:
container exits 137, and under `--restart on-failure` it re-execs
(RestartCount=1, fresh boot banner, `/api/health` → 200).

3. **The prescribed mechanism `kill -s TERM 1` / `kill -s KILL 1` from
the watchdog subshell is a kernel no-op.** The Linux PID-namespace
init-protection drops unhandled signals sent to PID 1 from within its
own namespace. Verified: a child running `kill -s KILL 1` returns 0 but
PID 1 bash keeps running — the container does not exit. So the briefed
fix would not have changed behavior.

4. The only scenario where a child-only kill wedges (container exits
**0**, `ON_FAILURE` never fires) is when PID 1 is a wait-all
supervisor/shell that discards child exit status — i.e. the *shell-form
/ init-wrapper* condition, which HEAD's exec-form Dockerfile already
avoids. No entrypoint code change can force a non-zero container exit
through such a wrapper.

**Conclusion:** the entrypoint watchdog restart is not defective at HEAD
in the real exec-form invocation, and the prescribed kill-PID-1
mechanism does not work. Shipping it would be a no-op that misleads. It
is omitted; the ground truth is documented here for the follow-up
decision.

### RED → GREEN Docker proof (verbatim)

Built the full claude-sdk-python image locally (`docker build`, exit 0,
3.75GB). Ran the **real `entrypoint.sh`** (RED = origin/main, GREEN =
prescribed `kill PID 1` variant) against a controllable stub agent whose
`/health` hangs (`HANG_HEALTH=1`), under `--restart on-failure`
(simulating Railway `ON_FAILURE`).

**Real exec-form (PID 1 = entrypoint bash — the actual prod
invocation):**
```
# RED (origin/main entrypoint), no restart policy, capture first exit code:
PID1 = /bin/bash /app/entrypoint.sh
[watchdog] Agent unresponsive for ~90s — killing PID 8 to trigger container restart
Status=exited ExitCode=137        <-- NON-ZERO: Railway ON_FAILURE WOULD restart

# RED under --restart on-failure:
boot banners = 2 ; RestartCount=1 ; Running=true ; curl /api/health = 200
=> HEAD already restarts correctly in exec-form. No wedge.
```

**Kernel PID-1 signal-drop proof (why kill-PID-1 is a no-op):**
```
PID1 bash up, self-pid=1
child sending SIGKILL to PID1
child: kill returned 0
Status=running ExitCode=0         <-- PID 1 survived SIGKILL from its own namespace
```

**Wait-all wrapper (artificial non-PID-1 / shell-form condition):**
```
# RED: watchdog kills child; wrapper `wait` returns 0
Status=exited ExitCode=0 RestartCount=0 ; curl = 000 (connection refused)
=> the only wedge repro — but this is the pre-HEAD shell-form condition, not HEAD.

# GREEN (kill -s KILL 1): still exit 0 (kernel drops the PID-1 signal + reap race)
Status=exited ExitCode=0 RestartCount=0
=> prescribed fix does NOT fix even this case.
```

### Value-tests (≥3, against the real code surface in the built image)

Ran inside the built image (`/app` PYTHONPATH, real
`_shared/cvdiag_pb_writer.py` + `agent_server.py`):
```
[PASS] VT1b enqueue x50 non-blocking (<0.5s) under unreachable PB — elapsed=0.0006s
[PASS] VT1c enqueue still non-blocking (<0.5s) after daemon hit unreachable PB — elapsed=0.0000s
[PASS] VT2b disabled enqueue is an instant no-op (<0.1s), no worker started — worker=None
[PASS] VT3 /health returns {'status':'ok'} in <1s with PB unreachable — status=200 elapsed=0.0045s
=== VALUE-TEST RESULT: ALL PASS ===
```
These confirm HEAD's writer + `/health` fixes hold (non-blocking enqueue
under PB-hang, no-op when disabled, sub-second liveness with PB
unreachable).

## What this PR ships

`showcase/scripts/railway-envs.ts` + regenerated
`railway-envs.generated.json`: add `{ key: "ANTHROPIC_BASE_URL", target:
"aimock" }` to the claude-sdk-python `serviceRefs`, mirroring the
existing `OPENAI_BASE_URL` ref. The agent reads `ANTHROPIC_BASE_URL`
(`src/agents/claude_agent_sdk_adapter.py`; aimock-wiring probe's
claude-sdk pattern), so the Stage-2 Ruby preflight now asserts it
prod→prod and refuses a cross-env leak. SSOT hygiene — the var is
already set correctly on the live service; this makes the pin
drift-proof.

### Pre-push quality (touched files)
- `emit-railway-envs-json.ts --check` → exit 0 ("up to date")
- `oxfmt --check railway-envs.generated.json` → "All matched files use
the correct format"
- emit-railway-envs vitest: 14/14 pass
- resolve-verify-matrix vitest: 18/18 pass
- Ruby promote/SSOT specs (single_service_fleet_invariants,
expected_domains_parity, lint_prod_covers_starters,
fleet_target_invariant): 0 failures
- `tsc --noEmit` (project lib): clean

## Follow-up decision needed
If a durable in-container self-heal is still wanted for the
wait-all/shell-form condition, the only mechanisms that actually work
are: keep the exec-form Dockerfile (already done) and/or add a real init
(tini via `--init` / `ENTRYPOINT ["tini","--"]`) that propagates the
child's non-zero exit. That is a Dockerfile/design change, not an
entrypoint one-liner — surfaced here for the design owner.
2026-07-18 11:47:23 -07:00
Jordan Ritter e3b3d2f14c fix(showcase): pin ANTHROPIC_BASE_URL aimock serviceRef for claude-sdk-python (SSOT drift-proof)
The claude-sdk-python agent routes its LLM traffic through ANTHROPIC_BASE_URL
(see src/agents/claude_agent_sdk_adapter.py and the aimock-wiring probe's
claude-sdk pattern), but the railway-envs SSOT only declared an OPENAI_BASE_URL
serviceRef. Add the ANTHROPIC_BASE_URL -> aimock serviceRef so the Stage-2 Ruby
promote preflight asserts it prod->prod (never copies) and refuses a cross-env
leak. The var is already set correctly on the live service; this is SSOT
hygiene that makes the pin drift-proof. Regenerated railway-envs.generated.json
via the repo generator (oxfmt-canonical, emit --check clean).
2026-07-18 11:16:09 -07:00
Jordan Ritter 3fae3296d5 fix(showcase): add declarative DataTable renderer to 4 missing integrations (#6040)
## Bug

Commit `1e0d200f5` added the `team-performance` pill to the
`d5-gen-ui-declarative` probe, which requires
`[data-testid="declarative-data-table"]` to mount. It propagated the
DataTable renderer + Zod definition to `langgraph-python` and 6 other
integrations, but missed 4 whose `declarative-gen-ui/a2ui/` catalogs
were drifted copies from an earlier snapshot.

**Affected (8 cells = 4 integrations × d5/d6):**
`claude-sdk-typescript`, `pydantic-ai`, `langgraph-fastapi`,
`langgraph-typescript`

**Symptom:** All 4 integrations' `renderers.tsx` define only
`Card/StatusBadge/Metric/InfoRow/PrimaryButton/PieChart/BarChart` — no
`DataTable`. The backend SSE stream returns a valid `render_a2ui`
payload containing a `DataTable` component; with no client renderer it
is **silently dropped** → declarative probe turn 2 times out with
`reason=surface-missing`.

Root-cause analysis in
`~/.local/share/copilotkit/cr/genui-declarative-rootcause-2026-07-17.md`.

## Fix

Added the `DataTable` renderer and Zod definition to each of the 4
missing integrations' `declarative-gen-ui/a2ui/` directories.

**Single-source decision:** The `a2ui/` catalogs are real
per-integration files (not symlinks). Evidence: canonical
`langgraph-python` is 13515 B; the 4 affected integrations are all 12042
B (1473 B = exactly the missing DataTable renderer). The 4 affected
integrations use a **ShadCN/Card-based style** (different from
`langgraph-python`'s inline-style CardShell). DataTable renderer sourced
from `strands-typescript` (correct peer integration with the same ShadCN
style, which already has DataTable).

**Files changed (8):**
- `claude-sdk-typescript/.../declarative-gen-ui/a2ui/definitions.ts` —
add DataTable Zod schema
- `claude-sdk-typescript/.../declarative-gen-ui/a2ui/renderers.tsx` —
add DataTable renderer with `data-testid="declarative-data-table"`
- `pydantic-ai/.../declarative-gen-ui/a2ui/definitions.ts`
- `pydantic-ai/.../declarative-gen-ui/a2ui/renderers.tsx`
- `langgraph-fastapi/.../declarative-gen-ui/a2ui/definitions.ts`
- `langgraph-fastapi/.../declarative-gen-ui/a2ui/renderers.tsx`
- `langgraph-typescript/.../declarative-gen-ui/a2ui/definitions.ts`
- `langgraph-typescript/.../declarative-gen-ui/a2ui/renderers.tsx`

## Red-Green Value Test

Local `bin/showcase test <slug> --d6 --direct --rebuild` against running
containers.

### Cell 1: pydantic-ai

**RED (pre-fix):**
```
turn 2/4 — surface-mount completion armed {
  testIds: [ 'declarative-data-table', 'declarative-bar-chart' ],
  baselineTestIds: { 'declarative-data-table': 0, 'declarative-bar-chart': 1 }
}
✗ d6:pydantic-ai red
  gen-ui-declarative: waitForTurnComplete: turn 2 did not complete within 90000ms
  (reason=surface-missing, runsFinished=2, count=4, attrPresent=true, runningNow=false)
```

**GREEN (post-fix):**
```
turn 2/4 — surface-mount completion armed {
  testIds: [ 'declarative-data-table', 'declarative-bar-chart' ],
  baselineTestIds: { 'declarative-data-table': 0, 'declarative-bar-chart': 1 }
}
turn 2/4 — settled text { turnNum: 2, text: "Here's how the team is tracking against quota." }
turn 2/4 — assertions passed  ✓
[probe advances to turn 3/4: at-risk StatusBadge pill]
```

### Cell 2: langgraph-typescript

**RED (pre-fix):**
```
✗ d6:langgraph-typescript red
  gen-ui-declarative: waitForTurnComplete: turn 2 did not complete within 90000ms (reason=surface-missing)
```

**GREEN (post-fix):**
```
turn 2/4 — surface-mount completion armed {
  testIds: [ 'declarative-data-table', 'declarative-bar-chart' ],
  baselineTestIds: { 'declarative-data-table': 0, 'declarative-bar-chart': 1 }
}
turn 2/4 — assertions passed  ✓
turn 3/4 — surface-mount completion armed { testIds: [ 'declarative-status-badge', 'declarative-metric' ]...
[probe advances to turn 3 and 4]
```
(overall cell still red due to unrelated `tool-rendering` and
`reasoning-display` failures — separate bugs)

### Cell 3: claude-sdk-typescript

**RED confirmed (pre-fix):**
```
✗ d6:claude-sdk-typescript red
  gen-ui-declarative: waitForTurnComplete: turn 2 did not complete within 90000ms (reason=surface-missing)
```

**GREEN locally blocked:** turn 1 fails with `503 Strict mode: no
fixture matched` — separate class B aimock fixture issue for this
integration. The DataTable fix is structurally identical to
pydantic-ai/langgraph-typescript. Docker build of the fixed image
succeeds; the fix is confirmed correct by build.

### Cell 4: langgraph-fastapi

Container not running locally. Fix is structurally identical to the 3
tested integrations. Confirmed by code review.

## Notes

- The overall d6 cells for pydantic-ai and langgraph-typescript remain
red after this fix due to OTHER unrelated issues (gen-ui-agent fixture
miss, reasoning-display timeout, tool-rendering). Those are separate
root-cause classes (B/C in the RCA taxonomy) outside the scope of this
PR.
- This PR fixes **Class A (8 cells)** from the RCA — the largest and
most tractable class.
2026-07-17 23:30:48 -07:00
Jordan Ritter 81410e0c2c fix(showcase): bound claude-sdk-python declarative gen-ui agent loop so RUN_FINISHED always fires
The a2ui_dynamic declarative agent drove its tool loop with `while True:`,
breaking only when the model returned a turn with no tool calls. With a
real LLM key the model keeps re-calling `generate_a2ui` after each tool
result, so that empty-tool turn never arrives — the loop spins forever,
RUN_FINISHED is never emitted, and the harness times out with
`done-signal-missing` (the run hangs after the A2UI render).

Replace the unbounded loop with a bounded `for _iter in
range(MAX_TOOL_ITERATIONS)` (cap 10), mirroring the proven-GREEN
claude-sdk-typescript sibling. The loop now always falls through to
RUN_FINISHED, so the run terminates after the render.

Only reproduces with a real key: a dummy key hits the auth-fail path,
which already terminates.
2026-07-17 23:25:45 -07:00
Jordan Ritter 2b9c3fe779 fix(showcase): add declarative DataTable renderer to 4 missing integrations
Commit 1e0d200f5 added the team-performance pill to d5-gen-ui-declarative,
which requires `[data-testid="declarative-data-table"]` to mount. It added
the DataTable renderer + Zod definition to langgraph-python and 6 others,
but missed 4 integrations whose declarative-gen-ui catalogs were drifted
copies from an earlier snapshot: claude-sdk-typescript, pydantic-ai,
langgraph-fastapi, langgraph-typescript.

Root cause: all 4 integrations serve a `next start` production build whose
`renderers.tsx` defines only Card/StatusBadge/Metric/InfoRow/PrimaryButton/
PieChart/BarChart — no DataTable. The backend SSE stream returns a valid
`render_a2ui` payload containing a DataTable component; with no client
renderer it is silently dropped → declarative probe turn 2 times out with
reason=surface-missing.

Decision: per-integration real files (not symlinks — confirmed by file size
diff: 12042 B vs 13515 B canonical). Added DataTable renderer + definition
to each integration's `declarative-gen-ui/a2ui/` matching their existing
ShadCN/card-based style (sourced from strands-typescript, which is the
correct peer, not the inline-style langgraph-python canonical).

Red-green value-test:
- pydantic-ai: RED (turn 2 surface-missing, 90s timeout, data-table=0) →
  GREEN (turn 2 assertions passed, "Here's how the team is tracking...")
- langgraph-typescript: RED (turn 2 surface-missing) →
  GREEN (turn 2 assertions passed, probe advances to turn 3/4)
- claude-sdk-typescript: RED confirmed (turn 2 surface-missing); GREEN
  blocked locally by separate aimock strict-mode miss on turn 1 — unrelated
  class B backend fixture issue, not DataTable. Fix is structurally identical
  to pydantic-ai/langgraph-typescript and confirmed correct by Docker build.
- langgraph-fastapi: container not running locally; fix structurally identical.
2026-07-17 23:22:17 -07:00
Jordan Ritter cfb2061fbe fix(showcase): rewrite gen-ui-agent probe to per-pill content assertion (#6039)
The old gen-ui-agent probe leaned on a cross-pill-difference heuristic —
it fingerprinted the rendered step text and treated inter-pill
differences as a settling signal, then accepted any render that had >=2
rows by the deadline. That let a real regression slip through green:
when pill 3 stale-renders pill 1's card, the ">=2 rows present" check is
still satisfied and the dedup was only ever a wait mechanism, never a
hard gate.

This PR rewrites the assertion to check what each pill should actually
render. Every pill carries a couple of low-brittleness content markers
pulled from its step titles in the d5 fixture (product-launch ->
launch/marketing, team-offsite -> venue/agenda, competitor-research ->
competitor/weakness). The probe polls the swap window until the card
shows >=2 non-empty step rows whose joined text contains all of that
pill's markers, and hard-fails otherwise. Matching is partial and
case-insensitive so it holds up under live-LLM (--direct) nondeterminism
while still proving the right pill's content landed.

Red-green: a dynamic-fake red-green plus three retained false-green
guards — identical-across-pills canned steps, stale non-adjacent content
(pill 3 showing pill 1), and empty/whitespace-only rows all go RED;
distinct-per-pill content passes. 9/9 tests green locally.

Important: this does NOT flip the affected cells green. The corrected
probe EXPOSES a genuine pill-3 stale-card regression on agno, langroid,
and crewai-crews — those cells are legitimately RED now and stay RED
until the underlying backend/frontend bug is fixed (separate follow-up).
The langgraph-python reference passes.

## Follow-ups (not this PR)

CR surfaced a handful of hardening items worth tracking, none of which
block this rework:

- The marker check is present-only, not absence-of-other-pills. A canned
backend reply that happened to contain every pill's tokens at once could
false-green. That's unrealistic for the current regression class (which
this probe does catch), but the check isn't airtight.
- Success latches on the first matching poll — there's no
settle/stability re-check after the content lands.
- Dead `seenStepTextsRef` state is still threaded through but never read
on the prod path; it should be removed.
- The unit-test fakes bypass the real `evaluate`/`.trim()` DOM logic.
That path is covered by the `--direct` value-test, not the unit test.
- Markers are matched against a joined row string, so it doesn't
strictly prove two distinct rows. There's also no drift guard against
`suggestions.ts`/the fixture, and the 15s content-poll deadline is a
magic number decoupled from `responseTimeoutMs`.

Also worth flagging: this PR EXPOSES a genuine pill-3 stale-state-card
regression on agno/langroid/crewai-crews — it's a backend per-turn
state/history handling problem, tracked separately and NOT fixed here.
The langgraph-python reference passes.
2026-07-17 22:45:33 -07:00
Jordan Ritter 2d7dbecb47 fix(showcase): rewrite gen-ui-agent probe to per-pill content assertion
The previous probe relied on a fragile cross-pill-difference heuristic:
it fingerprinted the rendered step text and used inter-pill differences
as a settling signal, accepting any render with >=2 rows at the deadline.
That heuristic false-greened a real pill-3 stale-card regression, because
a stale render (pill 3 still showing pill 1's content) still satisfied
">=2 rows present" and the dedup was only a wait mechanism, never a hard
gate.

This rewrites the assertion to check EXPECTED CONTENT per pill. Each pill
carries a small set of low-brittleness content markers derived from its
step titles in the d5 fixture (product-launch -> launch/marketing,
team-offsite -> venue/agenda, competitor-research -> competitor/weakness).
The assertion polls the swap window until the card shows >=2 NON-EMPTY
step rows whose joined text contains ALL of that pill's markers; otherwise
it hard-fails. Marker matching is partial and case-insensitive so it stays
robust to live-LLM (--direct) nondeterminism while still proving the RIGHT
pill's content rendered.

Verified with a dynamic-fake red-green plus three retained false-green
guards: identical-across-pills canned steps, stale non-adjacent content
(pill 3 showing pill 1), and empty/whitespace-only rows all turn RED;
distinct-per-pill content passes.

NOTE: this EXPOSES a genuine pill-3 stale-state regression on
agno/langroid/crewai-crews. Those cells are legitimately RED under the
corrected probe until that backend/frontend bug is fixed (separate
follow-up). The langgraph-python reference passes.
2026-07-17 22:37:15 -07:00
Jordan Ritter 34e8aebd7a fix(harness): exclude starter-* and harness-workers from aimock-wiring probe (#6038)
## Root cause

The `aimock_wiring:global` probe
(`showcase/harness/src/probes/aimock-wiring.ts`) was red on six live
Railway services — the "residual-6": `harness-workers` plus
`starter-adk`, `starter-langgraph-js`,
`starter-ms-agent-framework-dotnet`,
`starter-ms-agent-framework-python`, `starter-strands-python`.

This is EXCLUDE naming drift introduced after the egress /
private-networking migration:

- `EXCLUDE_SERVICES` keyed starters as `showcase-starter-<framework>`,
which (via the `showcase-` strip in `isExcluded`) only matches a live
name of `starter-<framework>`. But live Railway starter names are bare
`starter-<framework>[-lang]` — e.g. `starter-strands-python`,
`starter-langgraph-js`, `starter-ms-agent-framework-python`. **No
match** → the starter fell through to being checked → it has no
`*_BASE_URL` aimock override (starters are intentionally not wired) → it
landed in `unwired` → probe red.
- `harness-workers` (the harness background-worker service — pure infra,
no LLM callers) had **no exclude entry at all**, so it was always
counted unwired.

Seven starters coincidentally matched a stale literal and were excluded;
the other five, plus `harness-workers`, kept the probe permanently red.

## Fix

Two changes, both in `aimock-wiring.ts`:

1. **Exclude the whole `starter-*` family by prefix** in `isExcluded`,
instead of per-framework literals. Starters are contributor scaffolds,
categorically not routed through aimock (per product decision), so
matching them by literal name is exactly what drifted on rename. A
prefix rule can't re-drift. This is **safe**: the probe's "checked"
universe is intentionally the `showcase-*` LLM backends, and no
`showcase-*` name begins with `starter-`, so the rule never
over-excludes a real backend (guard test proves `starter-mastra`
excluded while `showcase-mastra` still checked).
2. **Add bare `harness-workers`** to the infra exclude set (`isExcluded`
checks the literal name first, so bare form matches).

The now-superseded `showcase-starter-*` literals are removed; the inert
`showcase-shell-*` legacy literals are retained to keep the diff minimal
and preserve an existing exclusion test. No starter is repointed — this
is a naming/exclusion fix only.

## Context

The 20 `showcase-*` LLM backends were already re-wired via Railway
`AIMOCK_URL` during the egress migration; they are green. This PR fixes
only the exclusion-set drift on the residual-6.

## Red-green (vitest, fakes, no network)

Added to `aimock-wiring.test.ts`:
- a drift-regression test feeding the exact live residual-6 + one wired
backend (`showcase-ag2`), asserting the residual-6 are excluded and only
the backend is checked-and-wired;
- a guard test (`starter-mastra` excluded WHILE `showcase-mastra` still
checked — proves the prefix rule doesn't over-exclude real backends);
- a full-roster test locking "checked universe == exactly the 20
`showcase-*` backends" against the full 41-service production roster.

RED (before fix) — 2 of the new tests fail; residual-6 land in
`unwired`:
```
AssertionError: expected [ 'harness-workers', …(25) ] to deeply equal [ 'showcase-ag2', …(19) ]
+   "harness-workers",
+   "starter-adk",
+   "starter-langgraph-js",
+   "starter-ms-agent-framework-dotnet",
+   "starter-ms-agent-framework-python",
+   "starter-strands-python",
 Test Files  1 failed (1)
      Tests  2 failed | 30 passed (32)
```

GREEN (after fix):
```
 ✓ src/probes/aimock-wiring.test.ts (32 tests)
 Test Files  1 passed (1)
      Tests  32 passed (32)
```

## Quality gates (harness)

- `vitest run` aimock-wiring: 32/32 pass
- `tsc --noEmit`: clean
- `tsc -p tsconfig.build.json` (build): clean
- `oxfmt --check` + `oxlint`: clean (0 warnings/errors)

Two unrelated harness tests (`d5-mapping-drift`, `d0-gone-predicate`)
fail locally because they parse `shell-dashboard` source / a generated
`registry.json` that aren't present in a workspace-scoped install; both
fail identically on clean `origin/main` and are unaffected by this
change. They resolve in CI's full checkout.
2026-07-17 22:21:02 -07:00
Aria Zhao e5c1af418a fix(react-core): expose isReady from useAgent so subscriptions can target the real agent (#5000)
`useAgent` always returns a fully-constructed `AbstractAgent`: a provisional
stand-in while the runtime is still connecting (or in an error state), swapped
for the real agent once the `/info` sync resolves. The returned type claimed
`agent` was always the real agent, giving consumers no way to tell the two
apart — so one-time subscriptions (e.g. `onRunFinalized`) registered during the
provisional window landed on the placeholder and missed events until the effect
re-ran after the swap.

Add an `isReady` flag to the return value: `false` while the agent is
provisional, `true` once the real (or locally-registered) agent is bound.
Additive and backward compatible.

Also fix the docs' "Event Subscription" example, which used an empty
`useEffect` dependency array and therefore never re-subscribed when the agent
reference changed.

Note: the original crash from #5000 ("Cannot read properties of undefined
(reading 'subscribers')") no longer reproduces on `main` — the provisional-agent
work (#5533/#5635) guarantees a fully-constructed agent, so `subscribe()` is
always safe. The added tests lock in that no-crash behavior and cover the new
`isReady` transition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 04:39:46 +00:00
Tyler Slaton b7d2fdcc25 fix(channels-intelligence): HTTP-transport robustness cluster (OSS-497) (#6037)
Six pre-existing `@copilotkit/channels-intelligence` HTTP-transport
robustness items surfaced by the pre-merge CR of #5983 (Linear OSS-497).
All confirmed against `main` after #5983 merged; none introduced by it.
Each is an independent, focused commit with a test.

## Fixes

1. **Heartbeat starvation mid-turn** — `HttpDeliverySource.runLoop`
heartbeated only at the top of each iteration, then blocked on
`onDelivery` for up to `turnTimeoutMs` (120s). With a 15s cadence, a
turn longer than the cadence sent no heartbeat, so app-api could mark a
healthily-working runtime stale mid-turn and withhold new deliveries.
Heartbeating now runs on a standalone recurring timer, independent of
the claim loop.
2. **`stop()` shutdown latency** — `stop()` set `running=false` then
awaited the loop, which only rechecked after its current sleep (≤15s
idle) or `onDelivery` (≤120s mid-turn); sleeps were `unref`'d but not
interruptible. Added a `stopWait` promise that the poll-sleep and
turn-wait both race, so shutdown is prompt. A mid-turn stop leaves the
lease for app-api to re-lease and does **not** nack (the turn didn't
fail); the turn's eventual settlement is always handled so a post-stop
rejection never surfaces as unhandled. *(1 & 2 share the runLoop
lifecycle, so they land in one commit.)*
3. **Empty-text `update` silently acked** — the `if (!text) return { ok:
true }` guard ran for both `post` and `update`. An empty POST is a legit
no-op, but an empty UPDATE (e.g. clearing a message body) that this
post-only fallback egress can't express was acked as success. Now
returns `{ ok: false, code: "empty_update" }`; empty posts still no-op.
4. **Static `adapter` on egress** — `emit` posted `this.cfg.adapter`
(default `"slack"`) alongside a possibly-Teams `replyTarget`. Confirmed
app-api's egress route (`sendChannelEgressMessage`) routes on
`replyTarget.adapter` + `channelName` and ignores this field, so it's
latent — but now derived from the delivery's own reply route to avoid
the contradiction. *(Note: the listener heartbeat's
`declaredChannels[].adapter` is intentionally left alone — app-api
genuinely consumes it for per-adapter health/conflict via the
`channel_adapter_configs.provider` join, and declaring the bot's full
adapter set is a separate design change, not a robustness cleanup.)*
5. **`projectId` strict-`number` only** — the realtime scope build
accepted `projectId` only as a JS `number` while org/channel were
`String()`-coerced, so a numeric-string on the untyped wire silently
fell back to the transport-default projectId, defeating per-delivery
scope authority. Extracted a `coerceWireProjectId` helper (number or
numeric-string → positive integer) with fallback.
6. **File/history client ignored injectable `fetch`** —
`fetchFile`/`getHistory`/`uploadFile` hardcoded `globalThis.fetch`, so
an injected `config.fetch` was honored everywhere except
binary/history/upload. The transport's `FetchLike` is text-only and
can't satisfy the binary client, so the file/history client got its
**own** injectable full `fetch` (`typeof fetch`), resolved via a single
helper with a global fallback.

Plus a small `docs` commit fixing a doc-comment placement displaced by
the item-5 export.

## Testing

- `nx run @copilotkit/channels-intelligence:test` — **178 passed** (170
baseline + 8 new).
- `nx run @copilotkit/channels-intelligence:check-types` — clean.
- lefthook (full package test + typecheck) ran on every commit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-17 17:16:32 -07:00
Tyler Slaton 439e763618 docs(channels): clarify read_thread getMessages is text-only (OSS-488) (#6035)
## What

Follow-up nit from [PR
#5969](https://github.com/CopilotKit/CopilotKit/pull/5969) review
([OSS-488](https://linear.app/copilotkit/issue/OSS-488)). Fixes the
`getMessages` JSDoc in
`packages/channels-intelligence/src/intelligence-adapter.ts`, which
oversold image support.

The comment cited *"what was in the image"* as a `read_thread` use case,
but image/file parts contribute **no text** in this mapping —
`read_thread` is text-only by design. Image *content* reaches the model
only via `conversationStore`'s seeding of `agent.messages`. The comment
now says so explicitly instead of implying `read_thread` can see image
content.

Comment-only change — no runtime/API behavior change, so no changeset.

## Scope of OSS-488

The ticket listed three nits; this PR addresses the one still
actionable:

- **#1 (double-space when joining content parts)** — ✅ already resolved
on `main` (the `.filter(Boolean).join(" ")` fix + updated test landed
via the OSS-476 CR round). Nothing to do.
- **#2 (JSDoc oversells image support)** — ✅ **this PR**.
- **#3 (duplicate `getHistory` fetch per turn)** — intentionally
**deferred**. A per-turn memo would need turn-scoped caching with a
clear-on-turn-start hook; without one it risks serving stale history — a
correctness regression on something the ticket itself rates "acceptable
for an on-demand tool." Not worth it here.

The **known limitation** (speakers collapse to `user`/`bot` because
upstream `AgentMessage` carries no names) is unchanged and not
actionable at this layer.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-17 17:15:58 -07:00
Tyler Slaton 4b53a4923b Add Angular contributors as code owners of the Angular package (#5388)
## What

Adds `wolfmanfx` (Murat Sari) and `rainerhahnekamp` (Rainer Hahnekamp)
as code owners of the Angular package paths in `.github/CODEOWNERS`:

- `packages/angular/`
- `packages/v2/angular/`

Existing core-dev owners are retained on both lines, so this is purely
additive.

## Notes

- **Access is granted separately.** This CODEOWNERS entry only assigns
review ownership; it does not grant repo access. The two contributors
still need to be added as repo collaborators via GitHub settings for the
entry to take effect (GitHub permissions are repo-level, not
folder-scoped).
- **Branch protection is currently disabled on `main`**, so this assigns
reviewers but does not gate merges until protection requiring code-owner
review is enabled.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-17 15:00:35 -07:00
Jordan Ritter d9ac09f83c fix(harness): exclude starter-* and harness-workers from aimock-wiring probe
The aimock_wiring:global probe went red on the residual-6 live services
(harness-workers + 5 starters). Root cause is EXCLUDE naming drift after
the egress/private-networking migration: EXCLUDE_SERVICES keyed starters
as showcase-starter-<framework> (matching only starter-<framework>), but
live Railway names are bare starter-<framework>[-lang] (e.g.
starter-strands-python, starter-langgraph-js) — no match, so they fell
through to being checked, landed in unwired, and kept the probe red.
harness-workers had no exclude entry at all.

Fix: exclude the whole starter-* family by prefix in isExcluded (starters
are contributor scaffolds, categorically not wired through aimock; safe
because no showcase-* backend name starts with starter-, so it never
over-excludes a real backend), and add bare harness-workers to the infra
exclude set. Superseded showcase-starter-* literals removed; inert
showcase-shell-* legacy literals retained to keep the diff minimal.

The 20 showcase-* LLM backends were already re-wired via Railway
AIMOCK_URL; this is a naming/exclusion fix only (no starter is repointed).
2026-07-17 14:57:36 -07:00
Ben Taylor 54989f31f5 chore(codeowners): drop nonexistent packages/v2/angular path
The repo uses a flat package layout; there is no packages/v2/ directory,
so that CODEOWNERS entry never matches and fails path validation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:36:13 -05:00
Benjamin Taylor 15a8ddb0f7 fix(channels-intelligence): address adversarial CR findings (OSS-497)
Follow-up to the OSS-497 robustness cluster after a 3-agent adversarial review.

- Empty-text update: reclassify from a hard {ok:false} failure to a logged
  no-op. The failure path threw up the run and nack'd the WHOLE turn retryably,
  re-running valid work to dead-letter for a condition a retry can't fix (an
  agent clearing a message body the post-only fallback can't express). Now it is
  skipped with a distinct warning log — surfaced, not silent, and not turn-fatal.
- Injectable fetch: plumb it through the transports. Both HttpDeliverySource and
  RealtimeGatewayTransport now accept a binary-capable /
  and forward it to IntelligenceFileHistoryClient, so a consumer injecting a
  fetch has it honored on the download/history/upload paths (previously the
  client-level injectable was reachable only by constructing the client directly).
- coerceWireProjectId: use Number.isSafeInteger so a numeric string beyond
  MAX_SAFE_INTEGER is rejected (was silently coerced to a lossy integer).
- Malformed-present projectId: log before falling back to the transport scope
  (an absent projectId still falls back quietly), mirroring the loud drops in
  the same function instead of silently masking a wire-corruption signal.
- Restart safety: guard the recurring heartbeat with a run generation so a
  stop->start while a heartbeat POST is in flight can't leave two reschedule
  chains doubling the heartbeat rate; bump the generation on stop().
- Remove now-dead lastHeartbeatAt state (only reader was the removed in-loop
  heartbeat check).

Tests: empty-update now asserts log + no-op; added transport fileFetch
wire-through, adapter-from-route fallback, no-unhandled-rejection after a
mid-turn stop, heartbeat-stops-after-stop, elapsed-time bounds on the stop()
promptness tests, and coerceWireProjectId edge cases (leading zeros, > safe int).
nx test (green) + check-types (clean).
2026-07-17 15:22:10 -05:00
Benjamin Taylor 81562f5b24 docs(channels-intelligence): restore assertValidChannelRealtimeScope doc placement
The coerceWireProjectId export (added for OSS-497) was inserted directly above
assertValidChannelRealtimeScope, displacing that function's doc comment onto the
helper. Reorder so each function carries its own docblock.
2026-07-17 14:46:27 -05:00
Benjamin Taylor 0d412f3bb8 fix(channels-intelligence): honor an injectable fetch in the file/history client
IntelligenceFileHistoryConfig's fetchFile/getHistory/uploadFile hardcoded
globalThis.fetch, so a consumer (or test) injecting a fetch had it honored
everywhere EXCEPT the binary download, history, and upload paths. The transport's
FetchLike is text-only ({ ok, status, text() }) and cannot satisfy the binary
client (.body/.arrayBuffer()/.headers/.json()), so add the client's OWN
injectable full `fetch` (typeof fetch) on its config, resolved via a single
resolveFetch() helper that falls back to the global fetch. Each caller keeps its
existing degrade/throw contract for the no-fetch runtime.

Surfaced by the pre-merge CR of #5983 (OSS-497).
2026-07-17 14:45:34 -05:00
Jordan Ritter 5ebbbd1e2f fix(ci): stop canary dispatches from false-paging the release notifier (#6036)
## What broke

Run
[29605894890](https://github.com/CopilotKit/CopilotKit/actions/runs/29605894890)
— a `workflow_dispatch` canary on branch
`canary/mme-subagents-29605883024-1` — fired a red `🔴 CopilotKit release
notifier failed — a release alert may have been swallowed` page to
#engr. No release was actually missed (`npm latest` was `1.63.1`,
published fine). It was a false page.

## Why it false-paged

The canary build died at `pnpm install --frozen-lockfile`
(`ERR_PNPM_OUTDATED_LOCKFILE` — `@ag-ui/client` lockfile `0.0.57` vs
manifest `0.0.58` on the source branch). That flipped the `notify` job
to `failure()`, which triggered its self-watchdog "best-effort Slack"
post.

That self-alert is gated on release intent (`npm_intended ||
py_intended`), and the `npm_intended` computation treated **any**
`workflow_dispatch` as stable-release intent:

```
NPM_INTENDED="${{ (github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/publish/'))) && 'true' || 'false' }}"
```

But `canary.yml` orchestrates `publish-release.yml` with `-f
mode=prerelease` on a short-lived `canary/*` ref (see `canary.yml` line
~196). A prerelease/canary is explicitly *not* a stable release: it
pushes no tag, cuts no GH Release, and posts nothing to Slack (the
notification builder returns `should_post=false` for it). So a failed
canary build had no business self-paging.

## The fix

**Fix 1 (this PR):** Narrow the `npm_intended` dispatch arm with
`inputs.mode != 'prerelease'`, so a canary dispatch yields
`npm_intended=false` and never self-pages, while a stable dispatch
(default mode + the main-only retry escape hatch) and a merged
`release/publish/*` PR still page correctly on genuine failure. Using
`!= 'prerelease'` (rather than `== 'stable'`) matches the existing `mode
!= 'prerelease'` guards on the publish/tag steps and correctly treats an
empty/default mode as stable.

before:
```
NPM_INTENDED="${{ (github.event_name == 'workflow_dispatch' || (...)) && 'true' || 'false' }}"
```
after:
```
NPM_INTENDED="${{ ((github.event_name == 'workflow_dispatch' && inputs.mode != 'prerelease') || (...)) && 'true' || 'false' }}"
```

**Fix 2 (follow-up, not in this PR — deliberately):** The stale-lockfile
source. `pnpm install --frozen-lockfile` *already is* the
lockfile-consistency check and it failed loudly and correctly — adding
another pre-check to this workflow would be redundant. The lockfile
drift lives on the `canary/mme-subagents-*` source branch, produced by
the `mme-subagents` agent-orchestration automation (it bumped a manifest
without regenerating `pnpm-lock.yaml`). The right place to fix that is
that generator, which is out of scope for this workflow file. Filing as
a follow-up rather than over-engineering a guard here.

## Red-green proof

The failure surface is the intent-gate bash expression. Extracted the
exact `npm_intended` boolean (pre- and post-fix) into isolated scripts
and ran the real canary inputs.

**RED (pre-fix logic):**
```
$ EVENT_NAME=workflow_dispatch INPUT_MODE=prerelease PR_HEAD_REF="canary/mme-subagents-29605883024-1" bash gate_pre.sh
npm_intended=true      # BUG: canary treated as stable-release intent → self-pages
```

**GREEN (post-fix logic):**
```
$ EVENT_NAME=workflow_dispatch INPUT_MODE=prerelease PR_HEAD_REF="canary/mme-subagents-29605883024-1" bash gate_post.sh
npm_intended=false     # canary no longer self-pages

# no regression on genuine-release paths:
$ EVENT_NAME=workflow_dispatch INPUT_MODE=stable                       # stable dispatch (retry hatch)
npm_intended=true
$ EVENT_NAME=workflow_dispatch INPUT_MODE=""                           # stable dispatch, default mode
npm_intended=true
$ EVENT_NAME=pull_request PR_MERGED=true PR_HEAD_REF="release/publish/monorepo/v1.63.1"
npm_intended=true
$ EVENT_NAME=pull_request PR_MERGED=true PR_HEAD_REF="feature/some-branch"   # non-release merge
npm_intended=false
```

## Lint

`actionlint .github/workflows/publish-release.yml` → exit 0 (clean).
This matches the repo's `Lint Release Workflows` CI job (actionlint,
`level=error`, same file in scope).
2026-07-17 12:43:00 -07:00
Benjamin Taylor ba38ee0e5f fix(channels-intelligence): heartbeat during long turns and make stop() prompt
Two coupled HttpDeliverySource runLoop lifecycle robustness fixes:

Heartbeat starvation: the loop heartbeated only at the top of each iteration,
then blocked on onDelivery for up to turnTimeoutMs (120s). With a 15s cadence,
a turn longer than the cadence sent no heartbeat for its whole duration, so
app-api could expire the activation and mark a healthily-working runtime stale
mid-turn, withholding new deliveries. Move heartbeating to a standalone
recurring timer that fires on cadence regardless of what the loop is doing
(rescheduling only after each heartbeat settles, so no overlap).

Shutdown latency: stop() set running=false then awaited the loop, but the loop
only rechecked after its current sleep (up to cadence, idle) or onDelivery (up
to turnTimeoutMs, mid-turn) — the sleeps were unref'd but not interruptible, so
shutdown could take up to 15s idle or 120s mid-turn. Add a stopWait promise that
stop() resolves; the poll-sleep and the turn-wait both race it, so shutdown is
prompt. A mid-turn stop leaves the lease for app-api to re-lease and does NOT
nack (the turn didn't fail); the turn's eventual settlement is always handled so
a post-stop rejection never surfaces as unhandled.

Surfaced by the pre-merge CR of #5983 (OSS-497).
2026-07-17 14:42:39 -05:00
Benjamin Taylor 787264fa8f fix(channels-intelligence): accept a numeric-string projectId in the realtime scope build
RealtimeGatewayTransport.toIngressEnvelope built the per-delivery scope with
`typeof delivery.projectId === 'number' ? delivery.projectId : this.scope.projectId`,
while organizationId/channelId were String()-coerced. The delivery.available
payload is untyped JSON, so a numeric-string projectId ('9' on the wire) failed
the strict check and silently substituted the transport-default projectId —
defeating the per-delivery scope authority and routing the render-accept under
the wrong project.

Extract a `coerceWireProjectId` helper (number or numeric-string -> positive
integer, else undefined) and use it with a fallback to the transport default.

Surfaced by the pre-merge CR of #5983 (OSS-497).
2026-07-17 14:35:49 -05:00
Benjamin Taylor 46517eecd9 fix(channels-intelligence): derive egress adapter from the reply route
HttpEgressSink.emit posted the static `this.cfg.adapter` (default "slack")
alongside a possibly-Teams `replyTarget`, so a Teams delivery through the
fallback egress carried a contradictory `adapter: "slack"`. app-api's egress
route (sendChannelEgressMessage) routes on `replyTarget.adapter` + channelName
and ignores this top-level field, so this is latent — but one provider-agnostic
runtime serves every adapter its bot has attached, so posting the config default
is misleading. Derive the posted adapter from the delivery's own reply route,
falling back to the config adapter when the route carries none.

Note: the listener heartbeat's declaredChannels[].adapter is NOT changed here —
app-api genuinely consumes it (the channel_adapter_configs.provider join) for
per-adapter health/conflict reporting, and the runtime only knows its single
configured adapter; declaring the bot's full adapter set is a separate design
change, not this robustness cleanup.

Surfaced by the pre-merge CR of #5983 (OSS-497).
2026-07-17 14:32:42 -05:00
Jordan Ritter af68620640 fix(ci): stop canary dispatches from false-paging the release notifier
The notify job's intent gate treated ANY workflow_dispatch as stable-release
intent (npm_intended=true), including prerelease/canary dispatches. canary.yml
orchestrates publish-release.yml with mode=prerelease on a short-lived canary/*
ref, so a canary whose build fails (e.g. a stale pnpm-lock.yaml on the source
branch tripping pnpm install --frozen-lockfile) flipped the notify job to
failure() and fired the self-watchdog's best-effort Slack post — a red
'a release alert may have been swallowed' page for a release that was never
going to publish.

Narrow the npm_intended dispatch arm with 'inputs.mode != prerelease' so a
canary yields npm_intended=false and never self-pages, while a stable dispatch
(default mode, and the main-only retry escape hatch) and a merged
release/publish/* PR still page correctly on genuine failure. This mirrors the
existing 'mode != prerelease' guards on the publish/tag steps and the builder's
should_post=false suppression for canaries.
2026-07-17 12:28:51 -07:00