Now that strands-typescript ships as "AWS Strands (TypeScript)", rename the
existing Python integration's display name from "AWS Strands" to
"AWS Strands (Python)" so the two appear as a matched Python/TypeScript pair
in the docs framework selector and shell (mirrors LangGraph's
"(Python)"/"(TypeScript)" pairing). Slug, content, and behavior are
unchanged. The shared aws-strands docs language tabs already default per
framework (strands -> Python, strands-typescript -> TypeScript) via
TAB_DEFAULTS_BY_SLUG.
Add a new node/TypeScript-backed AWS Strands showcase integration at
showcase/integrations/strands-typescript.
Backend: a node/TS agent server (src/agent/) built on @strands-agents/sdk
`Agent`/`tool` wrapped in @ag-ui/aws-strands `StrandsAgent` and served via
@ag-ui/aws-strands/server (`createStrandsApp`/`addStrandsExpressEndpoint`),
modeled on the upstream ag-ui aws-strands TS example server and the
langgraph-typescript infra. A single shared agent at "/" serves most demos
(tools, shared state via toolBehaviors/stateContextBuilder, HITL,
sub-agents), with tool-free specialized agents mounted at /voice,
/byoc-hashbrown, /byoc-json-render. model-factory targets OpenAI chat
completions and honors OPENAI_API_KEY / OPENAI_BASE_URL so it works behind
the showcase aimock proxy. Node-based Dockerfile + entrypoint run the agent
server (:8000) alongside the Next.js frontend.
Frontend mirrors the strands (Python) sibling's demo set and the
langgraph-typescript conventions, with HttpAgent routes proxying to the TS
agent server.
Scope: base integration + standard demos only. A2UI / declarative-gen-ui /
a2ui-fixed-schema is intentionally excluded (no A2UI agents, routes, demos,
or deps) and layered on later.
Platform wiring (mirrors langgraph-typescript): docker-compose local/dev
services on host port 3119, local-ports.json, packages.json, slug-map.ts
(born-in-showcase), showcase_build.yml matrix + path filter + metadata,
shell-docs/dashboard registries, and a logo asset. The python strands
integration is untouched.
## Summary
Un-fences the `ms-agent-harness-dotnet` showcase column from per-cell
probe enumeration. The exclude was a placeholder added 2026-06-07
(commit f0edcd5d) before the column existed; the column shipped (PR
#5569) and its d6/d4 aimock fixtures landed today (e10df0b4), so the
fence is stale. Removed the slug from all 8 SSOT exclude sites
(catalog-enumerator.ts nameExcludes, 5 probe YAMLs, aimock-wiring.ts,
baseline-types.ts) + updated 3 stale comments/tests.
## Red-green proof (local, real control-plane surface)
- **RED** (origin/main): `enumerated services: [langgraph-python,
ms-agent-dotnet]` — slug present-in-nameExcludes:true → **0 d6 cells**.
- **GREEN (enumeration)**: after the edits, `enumerated services: [...,
ms-agent-harness-dotnet]` → slug enumerates (via the real
`railwayServicesSource.enumerate` + real `D6_DISCOVERY_FILTER`).
- **GREEN (value-test)**: live local control-plane D6 stack (aimock
fixtures) → **35 passed, 1 failed, 3 skipped, 3 incapable**.
- The single fail (`gen-ui-declarative` / sales-dashboard pie+bar charts
not rendering) is **at parity with the gold-standard sibling**:
PocketBase `d6:ms-agent-dotnet/gen-ui-declarative` is RED with the
byte-identical error (fail_count 92, since 2026-06-15) and is the
sibling's sole tolerated `1✗`. Shared sales-dashboard chart gap in the
ms-agent-dotnet family — not a regression from the AsHarnessAgent port.
Harness lands 35✓/1✗, matching the sibling.
## Test plan
- [ ] CI green
- [ ] After deploy, staging dashboard shows ms-agent-harness-dotnet
cells populating (BE✓, D6 filling) on the next probe tick
The ms-agent-harness-dotnet slug was excluded from per-cell D6/BE/smoke
probe enumeration by a placeholder fence added 2026-06-07, before the
real column existed. The column shipped in PR #5569 and its d6/d4 aimock
fixtures landed on main today (e10df0b4), so the fence is now stale.
Remove the slug from all 8 exclude SSOT sites so the column populates.
## Summary
Fixes two correctness bugs in the showcase staging→prod promote path
(`showcase/bin/railway`), discovered + live-validated while promoting
the full 19-service cluster.
**Bug #1 — promote pinned the wrong digest.** `resolved_prod_image`
re-resolved the mutable `:latest` tag against *current* GHCR instead of
pinning the digest staging is actually *running*
(`latestDeployment.meta.imageDigest`). When `:latest` drifted after
staging deployed, promote pushed an unvalidated (and once, regressed)
image to prod. Now pins staging's running digest. Adds a loud `⚠️
STAGING DRIFT` warning (promote stdout + `STAGING_DRIFT_MARKER:` →
`promote-fleet.sh` aggregation → both Slack payloads) when staging's
running digest ≠ current `:latest`, so the gap is visible without
blocking the promote.
**Bug #2 — the pin never activated.** `pin_and_verify` used
`serviceInstanceRedeploy`, which replays the *existing* deployment's old
image rather than the just-pinned `source.image`. Config showed the new
digest while prod kept serving the old one (this is why earlier promotes
"succeeded" while prod stayed broken). Switched to
`serviceInstanceDeployV2` + a new `verify_serving_digest!` gate that
polls the new deployment to SUCCESS and **fails loud** if the running
digest ≠ pinned.
Plus CR-round hardening: P2 in-flight race check now reads
`meta.imageDigest` (was dead on tag-form staging) and skips on
`--digest` override; `detect_staging_drift` fails loud (WARN) on
GHCR-resolve failure instead of swallowing; `--digest` override
suppresses spurious drift; `drift_line` LF-stripped at the
`GITHUB_OUTPUT` boundary; multi-service drift join fixed; fallback-log
drift preserved.
## Commits
1. `fix(showcase): pin prod to staging's running digest + loud
staging-drift warning`
2. `fix(showcase): activate prod pin via serviceInstanceDeployV2 +
verify running==pinned`
3. `fix(showcase): harden promote P2 race check + refresh ivar-lint
allowlist`
## Validation
- Live red-green on real Railway: claude-sdk-python prod flipped from
stale → pinned digest via the fixed CLI; all 19 cluster services
promoted green; 5 previously-degraded backends (ag2, llamaindex,
pydantic-ai, ms-agent-python, strands) recovered.
- 15/15 Ruby specs + 15/15 bats green; ruby -c, shellcheck (CI
invocation), actionlint clean.
- 3-round cr-loop (7 agents/round) converged to zero bucket-(a).
## Follow-ups (not in this PR)
- Defensive `.to_s.empty?` on `meta.imageDigest` extraction
(can't-happen on real Railway).
- Test-quality nits (capture_io scoping, weak bats glob, shared mock
fixture).
- Pre-existing verify-prod `succeeded_csv`/`GITHUB_OUTPUT` coupling
(graceful-degraded, audited STAY_IN_C).
- `deploy-to-railway.ts` births prod on `:latest` (the root provisioning
gap); CLI↔workflow notify equivalence (PR2).
## Test plan
- [ ] CI green on PR HEAD
- [ ] (post-merge) a real `gh workflow run showcase_promote.yml` shows
the drift line in the #team-showcase notification when staging is behind
:latest
## What
Brings `@copilotkit/bot-slack` up to the **current** Slack native
streaming API surface (`chat.startStream` / `appendStream` /
`stopStream`, GA Oct 2025; structured chunks + AI feedback elements) and
removes the type-erasure workarounds. Result of an audit
cross-referencing the live `@slack/web-api@7.16.0` /
`@slack/types@2.21.1` types and Vercel's `vercel/chat` Slack adapter.
## Changes
- **No more `as unknown as Parameters<…>` casts** — every
streaming/post/update call uses the SDK's typed args.
- **One streamed message per turn** — dropped the per-message
continuation splitting (Slack documents only a 12k-per-append limit, no
cumulative cap; matches `vercel/chat`), keeping ≤12k per-append
chunking.
- **Native `task_update` tool-progress chunks** (`task_display_mode:
"timeline"`) interleaved into the streamed reply, replacing the separate
`🔧` status messages — with automatic degradation back to
`🔧` rows where structured chunks aren't supported.
- **Opt-in AI feedback buttons** via `slack({ feedback })` — a typed
`context_actions` + `feedback_buttons` row attached at `stopStream`;
clicks are routed adapter-locally (bypassing the engine's interaction
dispatch). No handler ⇒ no buttons.
- **Recipient scoping** — `recipient_user_id` / `recipient_team_id` only
for channel targets.
- **Cadence** — native flush floor lowered to ~600ms (appendStream
Tier-4); legacy `chat.update` stays 800ms.
### Engine (`@copilotkit/bot`)
One small, backward-compatible addition: optional
`RunRenderer.finish?()`, called after `runAgentLoop` resolves, so a
turn-scoped renderer can finalize its single stream. No-op for existing
adapters.
## Verification
- `bot-slack`: type-check (both tsconfigs) clean, **208 tests pass**,
oxfmt + oxlint clean.
- `bot`: type-check clean, **33 tests pass** (incl. a new `finish()`
test).
- Reviewed for correctness (stream lifecycle, finish/interrupt
interaction, delta tracking, degradation) — no high-confidence bugs;
feedback-on-interrupt and missing-ref-logging were tightened.
### Not verifiable without a live workspace (flagged in-code)
- That a >12k reply truly streams into one message (the
no-cumulative-cap assumption).
- `startStream` with no initial content.
## Summary
Two follow-up fixes that complete the pinned-prod / floating-staging
contract for the showcase fleet (the contract enforced by the promote
CLI in #5566). Prod services must be digest-pinned (`@sha256`), staging
floats `:latest`.
**Fix 1 — `showcase/scripts/deploy-to-railway.ts`: provision prod
digest-pinned, not `:latest`.**
Prod services were being *born* on the mutable `:latest` tag, then later
pinned only at promote time. Now they are born pinned to a resolved
`@sha256` digest at create time, via a new TS GHCR resolver that mirrors
the Ruby promote CLI (`/token` exchange → manifest HEAD →
`Docker-Content-Digest`). Resolution failure is **fail-loud**
(`process.exit(1)`, never a `:latest` fallback). `goLive` asserts the
prod `source.image` is digest-pinned (`assertProdDigestPinned`,
refactored to be dependency-injectable and to throw a typed
`ProdPinError` instead of exiting inline).
**Fix 2 — `showcase/harness/.../image-drift.ts`: stop flagging pinned
prod red.**
Under the pinned-prod contract, prod is intentionally digest-pinned
behind `:latest`, so the image-drift probe was firing false-red on every
prod service. It now renders such prod services **green**
(`pinnedExpected`) while a genuinely missing digest stays **red**.
Staging behaviour is unchanged.
## Verification
- **Red-green proven locally** for both new test surfaces:
- `assertProdDigestPinned` guard: RED = 5 tests `assertProdDigestPinned
is not a function` (untestable inline-exit) → GREEN = 13 passed after DI
refactor; env-mismatch branch: RED = `promise resolved undefined instead
of rejecting` → GREEN after wiring the test's env control.
- Suites: `deploy-to-railway.digest-pin.test.ts` **14 passed**;
`image-drift.test.ts` **26 passed**. Typecheck (scripts + harness) 0
errors; oxfmt + oxlint clean; harness build green.
- **Empirical 6b against live prod** (settled two reviewer
masking-concerns as can't-happen under current config):
- prod harness `SHOWCASE_ENV` is *unset* (not `""`) and
`RAILWAY_ENVIRONMENT_NAME="production"`, so `isProductionEnv()`'s `??`
correctly resolves true → the prod-neutral fix **fires** in prod.
- prod image-drift is discovery-only; all 19 prod `showcase-*` services
are digest-pinned and tag-less, so `expectedTag` resolves to `latest`
for every prod service → a fixed tag cannot reach prod image-drift (no
false-green).
## Review
3-round cr-loop (7 agents/round) converged to zero actionable findings.
One fix-introduced test-scaffolding defect (dead `envId` helper option)
was caught in the confirmation round and fixed. Remaining reviewer notes
are pre-existing issues in untouched goLive/probe code or
theoretical-but-can't-happen-under-live-config items, tracked as
follow-ups (not in scope for this PR).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
- remove the retired top-level `docs/` Next app and the disabled
docs-sync workflow/script
- add `docs -> showcase/shell-docs` as a top-level symlink for `cd docs`
muscle memory without restoring the old docs tree
- move the docs model allowlist into `showcase/shell-docs/` and retarget
docs validation/doctest extraction to shell-docs content
- update docs/agent guidance and CI path filters so `docs/` is treated
as an alias, not an active separate docs surface
- tighten the pre-commit package check so non-package docs/tooling
changes do not fan out into the full package matrix
## Validation
- `pnpm exec tsx scripts/validate-doc-model-names.ts`
- `pnpm exec tsx scripts/doc-tests/extract.ts`
- `pnpm exec vitest run
scripts/__tests__/validate-doc-model-names.test.ts
scripts/doc-tests/__tests__/extract.test.ts
showcase/harness/src/cli/eval/scope.test.ts`
- `pnpm exec oxlint showcase/harness/src/cli/eval/scope.test.ts
scripts/doc-tests/extract.ts`
- `git diff --cached --check` before follow-up commit
- `test "$(readlink docs)" = "showcase/shell-docs"`
- `test -f docs/package.json`
- `pnpm exec oxfmt --check .claude/docs/documentation.md
.claude/docs/hooks.md AGENTS.md CLAUDE.md CONTRIBUTING.md
showcase/shell-docs/README.md`
- commit hooks passed
## Notes
- historical docs remain recoverable from
`archive/docs-save-do-not-prune` and `archive/docs-retired-2026-06-17`
- I intentionally left Vercel/project teardown out of this PR; this is
repo cleanup only
## What
Updates the v2 `useRenderToolCall` reference page and makes opting a
tool out of the default rendering a single, schema-free call.
## Why
The reference page had drifted out of sync with the hook implementation
(`packages/react-core/src/v2/hooks/use-render-tool-call.tsx`) — most
notably `toolCallId` in the render props, added after the doc was last
touched. While documenting how to opt out of rendering, the natural
example (`useRenderTool({ name: "...", render: () => <></> })`) only
type-checked for the wildcard `"*"`; a named tool required a
`parameters` schema, forcing a throwaway `z.any()`. This PR re-aligns
the doc and removes that rough edge.
## Changes
### Docs (`showcase/shell-docs`)
- `useRenderToolCall.mdx`:
- Document `toolCallId` in the render-prop shape (previously
undocumented).
- Describe agentId-scoped lookup priority: agent-specific → unscoped →
wildcard `"*"` → built-in `DefaultToolCallRenderer`.
- Note args are parsed with `partialJSONParse` (streaming), not strict
`JSON.parse`.
- Correct `toolCall` prop to `toolCall.function.name` /
`toolCall.function.arguments`.
- Rewrite the Status Resolution table to match real logic (`toolMessage`
presence + provider executing set).
- New **"Disable default tool rendering"** section, ordered least→most
specific: wildcard first (all tools), then a **"For specific tools"**
subsection. Both use a schema-free `useRenderTool` call; dropped the old
`useFrontendTool` handler/schema boilerplate.
- `useRenderTool.mdx`: document the render-only (no-schema) named
overload.
### react-core
- Make `parameters` optional on the named `useRenderTool` overload,
mirroring the existing wildcard path; `defineToolCallRenderer` defaults
the args schema to `z.any()` when none is given.
- This lets `useRenderTool({ name: "myTool", render: () => <></> }, [])`
type-check with no Zod schema. Typed `parameters` behavior is unchanged.
- Added a test for the named render-only registration.
## Verification
- `@copilotkit/react-core` tests pass (1280) including the new case;
`build` (tsc) passes.
- Opt-out snippets type-checked in-package (`tsc`): wildcard,
specific-name (no schema), and named-with-schema all compile.
- `oxlint` (shell-docs) passes — 0 errors.
- Previewed locally at `/reference/hooks/useRenderToolCall`.
Resolve event-renderer.ts onRunFinishedEvent: keep the native turn stream open
(finalized in finish()) AND retain the legacy per-message stream drain from main
(#5573) as a no-op-in-native safety net. app/index.ts (telegram adapter from
#5520 + showToolStatus:false) and create-bot.test.ts auto-merged.
Under the pinned-prod contract, prod is intentionally digest-pinned behind
:latest. image-drift now renders such prod services green (pinnedExpected)
instead of red; a genuinely missing digest stays red. Staging unchanged.
Prod services are now born pinned to a resolved @sha256 digest (mirroring
the Ruby promote CLI's GHCR resolver) instead of the mutable :latest tag,
fail-loud on resolve failure, and goLive asserts the prod source.image is
digest-pinned. Adds DI-testable assertProdDigestPinned + coverage.
Approach (A) reorder: decode -> html-strip -> headTailCap -> scrub(head)+scrub(tail).
The prior order scrubbed the FULL decoded body before the cap, so a body >32KB hit
scrubSecrets' bounded-prefix path: it truncated to a ...[unscanned:N] prefix BEFORE
headTailCap, which (1) lost the real tail, (2) never scanned the real tail for secrets,
and (3) zeroed elided_count. Reordering scrubs each retained <=16KB segment AFTER the
cap: input is bounded per-segment (linear regex x bounded length = ReDoS-impossible, no
scan-budget truncation), the captured head AND tail are the REAL ends of the body, and
elided_count comes from headTailCap on the FULL body. The unrelated 2KB metadata-hot-path
scan guard (SCRUB_MAX_SCAN_LEN) is untouched; the now-unneeded RAW_BYTE_SCAN_MAX export
is removed.
The emit-hardening redesign added a 2KB SCRUB_MAX_SCAN_LEN guard inside
scrubSecrets, correct for the DEFAULT-tier metadata hot path (legit values
<=512B). But raw-byte-capture.ts also calls scrubSecrets on the DECODED wire
body, which is legitimately large (<=16KB head + <=16KB tail = up to 32KB).
The 2KB guard truncated the body to ~2KB before headTailCap ran, so for an
>32KB body elided_count became 0 and the head+tail elision never triggered.
Approach: parameterize the scan cap (chosen over the reorder-and-scrub-segments
alternative because it is a minimal, local change that preserves the documented
decode->scrub->html-strip->headtail pipeline order and keeps the metadata
default behavior byte-identical for all existing callers).
- scrub.ts: scrubSecrets gains an optional maxScanLen param defaulting to the
exported SCRUB_MAX_SCAN_LEN (2KB). Metadata/validateMetadata callers are
unchanged; the ReDoS guard on the hot path is untouched.
- raw-byte-capture.ts: passes RAW_BYTE_SCAN_MAX (head+tail cap = 32KB) so the
full retained sample is scanned (no secret in the kept head/tail escapes) and
headTailCap sees the un-truncated body. The three scrub regexes are linear at
any bounded length, so the larger bound stays ReDoS-safe.
- scrub.test.ts: asserts the default param still applies the 2KB guard AND an
explicit larger cap does not truncate, proving both contexts.
The redesigned scrub regexes are linear, but the previous hard <50ms bound
flaked under JIT warm-up and shared-runner load (observed 77ms on a loaded
machine). Add a timeScrub helper that discards a warm-up call before timing
and assert against a 500ms ReDoS ceiling — wide enough to be jitter-immune,
still ~3x under the legacy ~1.4s catastrophic-backtracking blowup it guards
against.