本文へ移動
cccskills
無料GitHub で公開

reskin

Author a NEW skin for the reskinnable-demo app. A skin is a self-contained domain plugin under src/skins/<id>/ that implements the frozen `Skin` contract (src/shell/skin-contract.ts) to swap the app's entire experience — brand, theme, layout, pages, tools, data, and agent — as a live sales demo. Use when the user says "add a skin", "create a skin", "new skin", "reskin the app", "make a <domain> skin", or wants the app re-themed as a new product. Do NOT use for editing the shell itself (src/shell/**), the shared token vocabulary (src/app/globals.css), or the shipped skins unless explicitly asked.

インストール方法を見る

含まれるファイル(4)

  • SKILL.md66.4 KB
  • demo-beats.md65.2 KB
  • failure-modes.md45.5 KB
  • templates.md89.8 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Authoring a reskinnable-demo skin

This app hosts one skin-agnostic shell (src/shell/) that renders one skin per URL segment /[skin]/.... A skin is a domain plugin living entirely under src/skins/<id>/. Its ONLY inbound dependency is the frozen Skin contract in src/shell/skin-contract.ts — that is what lets skins be authored in isolation without touching shared code.

To add a skin you (1) map the demo beats it must hit, (2) implement the Skin contract in src/skins/<id>/, (3) put its server-only agent in src/skins/<id>/agent.ts, and (4) register both — the client skin in src/shell/registry.ts and the agent in src/shell/agent-registry.ts, keyed by the identical id.

Before writing anything, re-open src/shell/skin-contract.ts (the source of truth) and read the shipped skins as worked references (ls src/skins/ is the registered set — do not memorise a count). They are good at different things; demo-beats.md § "Which skin to copy for what" is the routing table. The short version: every registered skin but bookstore is demo-complete, so nearly any of them is a fair end-to-end reference, and what you pick between is which one is cleanest for the problem in front of you — banking the original reference, people and commerce the beat-first pair whose beat maps are written out in their suggestions.ts, logistics the layout reference, airline runtime identity without RuntimeProviders (and an entitlement-shaped rather than authority-shaped beat-6 gate), keel the fullest parameterized routing, bookstore the only useData implementor and the worked example of a beat map with two rows marked SKIPPED rather than deleted, exec the BI/executive-analytics domain and the worked example of the block:-prefixed inline a2ui convention (an alternative to CanvasSurface — see "A block:-prefixed a2ui surface…" below). Those files win on any conflict with this skill.

Do not model a new skin on the ABSENCE of a field. Most optional fields are set by most skins, and every omission in the tree has a stated reason next to it, so "airline omits it" is not permission to omit it. Derive what a skin sets rather than trusting prose: grep -nE '^\s+(Providers|CanvasSurface|sandboxFunctions|toolLabels|chatHeaderActions|onSuggestionSelect|RuntimeProviders|useRuntimeProperties|useData)[,:]' src/skins/*/skin.tsx.


⚠️ FIRST: a skin is a live sales demo, not a theme

A skin exists to prove CopilotKit and Intelligence top to bottom, in front of a Fortune 500 buyer. Wiring the contract correctly is table stakes; a skin that compiles, looks sharp and proves nothing is a failed skin. The banking demo's ~10 steps are tuned and land with customers — so copy its beats, not its steps. Your domain can be 1000% different.

BeatThe audience must concludeMinimum mechanism
1 Give it a face"Generative UI — right out of the gate."A useComponent visual answers pill #1
2 Rich thread"Reload the browser and the chart is still there. Nobody else stores AG-UI streams."Durable visuals via useComponent; replay-safe tools
3a Drive the app"It changed the app — and the secret never reached the assistant."A mutation whose sensitive payload stays in the UI
3b Sees my screen"Shared state is real." (ask on two different pages)A route readable + per-page on-screen readables
3c Levers"That was a maneuver, not a link."HITL confirm → navigate → sort + filter, visibly highlighted
3d Multimodal"It takes real documents, and the output belongs to my app."Attachment path + artifact written to the store, surviving thread deletion
4 Memory"It remembers how I like things, and says so."Seeded topical memory + recall-first prompt + a slot naming the "why"
5 Stored skill"One sentence and it already knows our procedure."Seeded operational memory + 3 visible writes + distractors
6 Teach a skill"It learned by watching me once, then did it alone."Symptom-only gate + unlock path + recording context + save/recall

Write the beat map before you write code — the table template and the full per-beat spec are in demo-beats.md, which also covers the presentation requirements (a pill per beat so the presenter never types, a visible affordance on every mutation, pretty markdown prose, a Reset control, the chat-placement framing) and the quality bar.

If the user named the beats — fewer, more, or different — theirs win. Record what they asked for in the beat map and build that. Absent instructions, build all nine.

Then read failure-modes.md, before you write tools or pages. It is the cross-cutting half of this skill, and its through-line is the one thing to carry into every file: a skin's characteristic bug is not a crash — it is a confident falsehood. A crash is visible on stage and gets fixed; a convincing lie reads as success and proves nothing. An empty chart drawn with confidence, a lever chip naming a choice the agent never made, a receipt for a write that did not land, a readable reporting an all-clear it never checked — all of those compile, lint, pass tests, and land as a successful demo. That file states the principles and points at the shipped commerce code for each; the per-file scaffolds stay in templates.md.

⚠️ Beats 2, 4, 5 and 6 are runtime-conditional: they need all three Intelligence env vars — INTELLIGENCE_API_URL, INTELLIGENCE_GATEWAY_WS_URL and CPK_INTELLIGENCE_API_KEY, which does NOT match an INTELLIGENCE_* glob and is the one people miss — and beats 4/5 additionally need a seeded-memory file (src/skins/<id>/intelligence/seed-memories.ts). Without those they degrade silently — the agent simply doesn't know you. See demo-beats.md.


⚠️ CRITICAL: the client / server boundary

The AGENT is server-only and is NOT part of the client Skin contract. @copilotkit/runtime must never be bundled client-side.

  • Each skin puts its agent in a server-safe src/skins/<id>/agent.ts with NO "use client" and NO JSX — just: export const <id>Agent = () => new BuiltInAgent({ ... });
  • The client skin.tsx NEVER imports agent.ts. The only link between them is the shared id (id === agentId).
  • The client skin registers in src/shell/registry.ts (SkinRegistry); the agent registers separately in src/shell/agent-registry.ts (agentRegistry, as { createAgent, identifyUser? }). Two registries, one id.

Theming is a per-skin theme.css, never the shared globals. The shell owns the token vocabulary in src/app/globals.css (@theme inline + the semantic utilities bg-surface, text-ink, border-hairline, shadow-soft, bg-brand, …). Do not edit globals.css. Instead create src/skins/<id>/theme.css containing a single .theme-<id> { … } block that re-values the shared CSS variables, and import it as a side-effect from the skin's layout.tsx (import "./theme.css";). Your skin.themeClass must equal "theme-<id>" so the shell applies your block. Never invent new token names — only re-value existing ones.

Dark mode is an explicit opt-in — --nw-dark-capable: 1. src/hooks/use-theme.ts forces any skin WITHOUT that flag to light and ignores the stored dark preference, so a skin that writes a .dark .theme-<id> block but forgets the flag stays stuck in light. To support dark you must do BOTH: set --nw-dark-capable: 1 on the .theme-<id> root AND ship a .dark .theme-<id> block (which re-values only surfaces / ink / semantic tokens and lets the brand ramp and --radius inherit). Omit both to stay light-only — a legitimate choice (airline does exactly that). If you kept the theme toggle in your layout, note it is a dead control until this flag and the dark block both exist.

OGUI renders full-region on the shared canvas. A generateSandboxedUi call becomes an open-generative-ui activity that the shell renders full-region on the canvas via the workspace OpenGenerativeUIActivityRenderer. A skin does not supply an OGUI renderer — it only contributes sandboxFunctions? + designSkill, which the shell wires onto the provider. (An a2ui report surface is different: a skin renders its own via the optional CanvasSurface.)

A block:-prefixed a2ui surface renders INLINE in the chat transcript instead of the handoff pill — everything else still gets the pill + canvas path unchanged. The shell's chat activity renderer (A2UISurfaceActivity in src/app/[skin]/layout.tsx, wired module-level into the A2UI_RENDERERS array so the reference stays stable across renders — CopilotKitProvider requires that) reads an a2ui-surface activity's content.a2ui_operations, walks it for the first createSurface/updateComponents/updateDataModel surface id, and checks whether that id starts with block: (blockSurfaceIdFrom, src/shell/chat/inline-block-surface.tsx). A match renders InlineBlockSurface right where the activity message appears.

The canvas asks the same question a different way, and the difference matters if you mint ids. classifyA2uiSurface's claimOf (src/shell/canvas/canvas-context.tsx:61-78) scans EVERY op rather than stopping at the first one carrying a surfaceId, because a surfaceId can arrive on any of createSurface/updateComponents/updateDataModel and a snapshot's leading op is routinely something else. It returns "canvas" the moment it sees a non-block: id, "inline-block" if it saw only block: ids, and "unclassifiable" otherwise — biased against the canvas on purpose, since claiming the canvas for content no CanvasSurface can read blanks the page. So a mixed op list (one block: surface plus one report surface) reads as a canvas claim there while the chat's first-id walk may still render it inline. Keep one surface per activity and the two agree, which is what exec does.

That card mounts its OWN <A2UIProvider> per rendered activity, and must: there is no ambient a2ui store on this path. CopilotKitProvider's a2ui.catalog prop mounts no provider — the only thing that would is the built-in a2ui-surface renderer's ReactSurfaceHost, and the shell's renderActivityMessages array SHADOWS that built-in (user-supplied renderers resolve first), so useA2UIActions() would throw and take the page down. The isolation is also deliberate: one provider per activity means a block's surface state can never collide with, or be clobbered by, the canvas's. The catalog comes from useSkin().catalog — the same object the layout hands CopilotKitProvider — reached through the contract rather than by importing from src/skins/. Anything else — including a CanvasSurface report like banking's render_report or logistics' renderBrief — still falls back to ReportHandoffPill; this convention adds a second inline path, it does not change the existing one. Because the shell must not import from src/skins/, the block: spelling is duplicated by hand in TWO places, both spelled BLOCK_SURFACE_PREFIX: src/skins/exec/blocks/build-block-ops.ts:22 (the write side, the worked example a skin copies to mint its own ids) and src/shell/canvas/canvas-context.tsx (the shell's single reader-side decision, decideA2uiSurface; the chat's inline-block-surface.tsx delegates to it rather than keeping a copy). Both ARE checked: the drift guard in src/skins/exec/blocks/build-block-ops.test.ts runs freshly minted ops through the shell's classifier, so either side drifting fails there. Grep for BLOCK_SURFACE_PREFIX before you pick your own spelling.

A sandboxFunction's parameters schema is DOCUMENTATION, not a gate — and its returns are undocumented unless the description says so. Two traps, both of which produce a generated panel that renders and is wrong:

  • The provider serializes parameters into agent context and the renderer then hands your bare handler to the iframe (api[fn.name] = fn.handler). Nothing validates the arguments, so a loose parameter (category: z.string()) filters on a value nothing matches and returns [] — a convincingly blank view, with the model never told it guessed wrong. Enumerate every parameter to its real domain (z.enum(YOUR_CONST_TUPLE), so the vocabulary reaches the model too) and parse the args in the handler, throwing a message that names the accepted values. Commerce's define() wrapper in src/skins/commerce/sandbox-functions.ts is the worked example. One exception, and it is load-bearing: a beat-6 gate's unlock vocabulary must NOT be enumerated — putting those codes in front of the model is exactly the defect, because then it never has to learn them. Take a free z.string() there and say so in the .describe(). See failure-modes.md § 10.
  • The model never sees a sample result — only name, description and the JSON-schema-ified parameters. So a figure whose unit is not in its FIELD NAME must have it in the description: an unlabelled ratio (0.418) renders as "0.42%" or "41.8%" with equal confidence. Commerce ships ratios as …Ratio + a …Label string built with the app's own formatter, which also makes the generated panel read identically to the app card beside it.

EVERY a2ui surface must be fed by a SERVER tool, never a client one — the canvas CanvasSurface path and the inline block: path alike. Emit the { [A2UI_OPERATIONS_KEY]: buildOps(spec) } payload from a server-side defineTool on the BuiltInAgent in agent.ts — not from a client useFrontendTool. The a2ui middleware only converts that payload into an a2ui-surface activity when it observes it in an in-stream TOOL_CALL_RESULT event, which a client frontend-tool result never produces. Do it client-side and NO a2ui-surface activity is ever minted, so the canvas stays permanently blank AND the inline block card never appears — the rule is about how the activity is born, not about where it renders. Banking (render_report) and logistics (renderBrief) do it server-side for the canvas; exec's render_metric_block (src/skins/exec/agent.ts) does it server-side for the inline block path. The agent.ts template shows the shape.


The Skin contract, field by field

Quoted from src/shell/skin-contract.ts (the frozen interface). Diff your object against that file; it wins.

Required:

FieldTypePurpose
idstringStable id — MUST equal the route segment AND the agent id.
identityobject (below)Brand identity the shell renders.
themeClassstringCSS class scoping this skin's tokens — set to "theme-<id>".
LayoutComponentType<{ children: ReactNode }>The app-shell chrome (nav/header) wrapping page content.
navNavRoute[]Nav entries the layout renders. Display-only — NOT the segment validator (see below).
resolvePage(segments: string[]) => ComponentType | nullMaps URL segments (after /[skin]) to a page, or null → 404. The sole segment validator.
ToolsComponentTypeRegisters frontend tools / HITL / gen-UI + agent-context readables. Renders null.
catalogA2uiCatalogThe skin's a2ui catalog from createCatalog().
suggestionsSuggestion[]Static suggestion pills ({ title, message }), shown available:"always".
designSkillstringOGUI design brief — injected as agent context to style generated UIs.

identity object:

FieldTypeNotes
brandstringShown in the selector + chat header.
taglinestringSelector tooltip; default chat greeting when greeting omitted.
logoComponentType<{ className?: string }>Logo mark (inline SVG/glyph).
favicon?stringEmoji browser-tab icon (e.g. "✈️"). The shell's FaviconSync renders it into a <link rel="icon"> per skin; omit to keep the static favicon.ico.
assistantName?stringChat header title. Defaults to brand.
greeting?stringChat welcome message. Defaults to tagline.

Optional:

FieldTypePurpose
Providers?ComponentType<{ children: ReactNode }>Skin-specific provider stack mounted below CopilotKitProvider (escape hatch). Omit → shell substitutes a pass-through.
CanvasSurface?ComponentTypeRenders the skin's own a2ui report surface full-region on the shared canvas. Omit if no a2ui report canvas — or if every a2ui surface you emit uses the block:-prefixed inline convention instead (see "A block:-prefixed a2ui surface…" above); exec omits CanvasSurface for exactly that reason.
sandboxFunctions?SandboxFunction[]Functions exposed inside OGUI sandboxed iframes for this skin.
toolLabels?Record<string, string>Human labels for this skin's OWN tool-activity chips, keyed by tool name. Unlisted tools fall back to a prettified raw name.
chatHeaderActions?ChatHeaderAction[]Buttons this skin contributes to the shared chat header (drawn before the shell's own controls).
onSuggestionSelect?(suggestion: Suggestion, index: number) => booleanIntercept a suggestion click. Return true if fully handled (shell does nothing further); return false/omit for the default "send the message" path. true is a PROMISE that something happened — the handler it launches must either do the thing or tell the presenter why it could not (see beat 3d in demo-beats.md); true plus silence is the bug this contract keeps producing.
RuntimeProviders?ComponentType<{ children: ReactNode }>Provider stack mounted above CopilotKitProvider (unlike Providers, below). The sanctioned place to establish context your useRuntimeProperties must read — it has to sit above the provider so the provider owns properties from its first commit. See "Contributing end-user identity" below.
useRuntimeProperties?() => Record<string, unknown> | undefinedContributes this skin's runtime properties; the shell threads the result into CopilotKitProvider's properties prop. How a skin scopes its Intelligence runs / durable memory per end-user. Return a stable/memoized object. Omit if the skin contributes no runtime identity.
useData?() => unknownSeed-backed data hook; the shell runs it in SkinProvider, components read via useSkinData<T>(). The in-memory escape hatch — the minority path, and it splits exactly along the substrate line. Derive who takes it: grep -l 'useData:' src/skins/*/skin.tsx names the implementors (bookstore, via data/use-data.ts); every other registered skin omits it and reads its REST ledger through its own context/hook, so there useSkinData<T>() returns undefined. Read the implementor first, templates.md § data/use-data.ts second.

Supporting types (also in the contract):

export interface NavRoute {
  segment: string; // URL segment after the skin, e.g. "" (index), "cards".
  label: string;
  icon?: ComponentType<{ className?: string }>;
}
export interface Suggestion {
  title: string;
  message: string;
}
export interface ChatHeaderAction {
  icon: ComponentType<{ className?: string }>;
  label: string;
  onClick: () => void;
}
export type A2uiCatalog = ReturnType<typeof createCatalog>;

The agent is deliberately absent from this interface — see the boundary section above. It lives in agent.ts and registers separately.

nav does not decide what resolves. It is display-only — the list the layout draws as navigation. resolvePage is the single source of truth for which segments are valid (the contract says so, skin-contract.ts around lines 86-92), and it may accept segments nav omits (banking's resolvePage accepts a cards index alias its nav never lists). So every segment a user can reach — nav entries, aliases, deep links — must be handled in resolvePage; anything it returns null for is a 404, regardless of what nav contains.


The layout contract (viewport height + nav insets)

One thing every shipped Layout gets right and the naive version gets wrong — src/skins/logistics/layout.tsx is the reference, and the template mirrors it:

  • The root is h-full overflow-hidden, NOT h-screen or min-h-screen. Your chrome fills the shell's app CARD, not the viewport — the frame insets that card by its own padding, so a viewport-height root overflows it by exactly that much. It still has to be BOUNDED, though: if the container can grow past the card the whole document scrolls, the pinned nav scrolls away with it, and <main>'s own overflow-y-auto goes inert because its parent is unbounded. h-full overflow-hidden on the root, plus h-full on the <aside>, so only <main> scrolls.

Do not publish --nw-nav-inset-left / --nw-nav-inset-right. Nothing reads them: the switcher is a dropdown in a card at the top of the assistant column, so it occupies a slot and never overlaps your nav.

The URL contract (never hardcode the skin prefix)

Every in-skin link and router.push must go through useSkinHref (src/shell/skin-path.ts), and every "which nav entry is active" derivation through its companion useSkinSegments. Both are in the layout template.

Skins live under /[skin] on the normal demo (one segment per registered skin), but a LOCK_SKIN deploy is served at / with the segment gone from the URL space entirely — src/proxy.ts rewrites the prefix-free space onto the route tree. So:

  • skinHref("cards") → /banking/cards unlocked, /cards locked.
  • A hardcoded `/${skin.id}/cards` still RESOLVES under a lock, which is why this is easy to miss: it just puts /banking back in the address bar on the first nav click, and the single-tenant illusion is gone.
  • A hand-rolled pathname.split("/").slice(2) is worse — it silently eats the first real segment when there is no prefix to skip, so under a lock every page reports itself as the index and the wrong nav entry highlights.

Deep links append their own hash: `${skinHref(`knowledge/${docId}`)}#${sectionId}`. A skin with many parameterized links should wrap the hook once for itself — see src/skins/keel/href.ts, which exists so keel's id appears in exactly one place.

The one legitimate exception is a link to a DIFFERENT skin (the shell's skin switcher), which must keep the prefix and only ever renders unlocked.

pnpm lint enforces this via no-restricted-syntax selectors in eslint.config.mjs (scoped to src/skins/**, tests exempt). They fail and NAME YOUR FILE if an in-skin path literal (i) opens with a skin id segment ("/banking/cards", `/keel/runs/${id}`), (ii) concatenates a path onto an interpolated base (`${base}/charges` — the // shape) when that template is a navigation target, or (iii) opens with a leading-slash interpolation (`/${skin.id}/…`). The rule reads the AST, so a skin prefix inside a comment or prose string is fine and a $ in a variable name cannot fool it.

Selector (ii) is deliberately narrowed to navigation contexts: the `${x}/${y}` shape is AST-identical to an ordinary date `${month}/${day}` or ratio `${used}/${total} used`, so flagging it everywhere false-positives on any skin component that formats a date or fraction. It therefore fires only when the template is passed to router.push/router.replace, to location.assign, assigned to location.href, or set as a JSX href={...}. Trade-off, stated honestly: a URL built into a variable first and then navigated (const u = ${base}/x; router.push(u)) is NOT caught by (ii) — the literal-prefix guards (i)/(iii) still catch the common hardcoding shapes regardless of use site. Because (ii) is now nav-scoped, REST/data-layer files (actions.ts, intelligence/**) that build absolute SERVER urls (`${BASE}/shipments`) never trip it anyway; they stay explicitly scoped out as belt-and-suspenders.

The meta-utility strip

The presenter/dev utilities — Reset, theme toggle, Help — are skin-authored chrome, not shell-provided. A new skin gets none of them for free; you add them in the layout (the template puts them in an mt-auto group at the bottom of the sidebar). Three controls:

  • Reset (RotateCcw) — render it only when usePresenterReset() (from @/shell/presenter-reset-context) is true; on click, window.confirm then POST /api/<id>/v1/dev/reset then window.location.assign(skinHref()) for a pristine slate. Branch on what the route says about the STORE, not on res.ok — the route wipes the store first and can still answer non-2xx, so an ok-only branch leaves the page (and the readables describing it) asserting rows that are gone, and throws away the body's memoryError sentence, which is the only warning that beat 6 may start out already taught. See the scaffold in templates.md and runPresenterReset in src/skins/commerce/layout.tsx. This one deliberately IS a full document load rather than a router.push — dropping every module reload-fresh is the point (new store, new thread, cleared canvas) — but the URL it navigates to is still built by useSkinHref, exactly as in the layout template. Do not hand-roll it as `/${skin.id}`: that is shape (iii) from the URL contract above, so it fails pnpm lint, and on a locked deploy it re-introduces the tenant segment the reset is supposed to leave behind (skinHref() returns / there). Keep the button and the endpoint in agreement: your skin's own dev/reset route should allow the reset when presenterResetEnabled() || process.env.NODE_ENV !== "production" (mirror src/app/api/logistics/v1/dev/reset/route.ts), or a production booth shows a button that 403s.
  • ThemeToggle — import { ThemeToggle } from "@/components/ui/theme-toggle". It is a SHARED component under src/components/ui, so importing it is fine and is NOT a cross-skin import. Remember it is a dead control unless your skin also ships a dark palette (--nw-dark-capable: 1 + a .dark .theme-<id> block — see the theming rules above).
  • Help (HelpCircle) — calls a useAskCopilot() that opens the panel and sends a message as the user. Port it into your own src/skins/<id>/components/use-ask-copilot.ts (copy logistics'); do NOT import from src/skins/banking/** — a skin's only inbound dependency is the contract.

Registering tools: deps, render signatures, replay safety, readables

Six rules that the tools.tsx template bakes in; miss any and the failure is silent.

  • Every useComponent / useFrontendTool / useHumanInTheLoop / useRenderTool registration closes with a deps array. Each takes an optional deps array as a second argument (useFrontendTool(tool, deps?: ReadonlyArray<unknown>), useHumanInTheLoop(tool, deps?), useComponent(spec, deps?), useRenderTool(config, deps?) — the installed types confirm it). Do not skip useRenderTool because it is the rarer hook: it is exactly the one a skin reaches for when a render needs status/result (banking's, and exec's file_variance_narrative), which is live-data rendering, which is where a stale closure hurts most. The declarations live in the hashed bundle type file — ls node_modules/@copilotkit/react-core/dist | grep d.cts finds it (copilotkit-B1K0Tgnz.d.cts today; the hash changes on every SDK bump, so derive it rather than copying this one). Omit it and the closure captures whatever the data was at REGISTRATION time — for a REST-backed skin, the EMPTY array from before the first fetch — forever. This is the nastiest bug in the app because it compiles, lints, and passes every test: the agent narrates confidently ("the trade-offs are on screen") while the component renders its "not found" branch over stale data. Banking documents the same trap in a code comment (search "closure captures empty arrays" in src/skins/banking/tools.tsx); logistics passes deps on every registration. But a non-empty deps array is not automatically safe: useFrontendTool keys its registration effect on JSON.stringify(extraDeps) (use-frontend-tool.tsx:45), so only deps that actually serialize — strings, numbers, plain objects — vary that key. A Map, a Set or a function stringifies to a constant regardless of its contents (JSON.stringify([new Map(), () => {}]) is the fixed string "[{},null]"), so a deps array built from one is as INERT as an empty one: the tool registers once and its closure is stuck on whatever those values were at that first commit. Data reached through a Map, a Set or a stable callback belongs in a ref, read as ref.current inside the handler/render, not in the deps array. src/skins/bookstore/tools.tsx's openBook is the worked ref-pattern example for a non-write tool (its []-deps comment spells out why [router, data.books, skinHref] would never re-register); banking's cardsRef comment (src/skins/banking/tools.tsx:130-136, above setCardPin) is the original write-case version, and warns about the opposite trap too — a serializable [cards] dep there would tear the tool down and rebuild it mid-write.

  • A parameterized useComponent render receives the schema output DIRECTLY — render: ({ myParam }) => …, NOT wrapped in { args }. Per the installed types, InferRenderProps<T> = T extends StandardSchemaV1 ? InferSchemaOutput<T> : any and render: ComponentType<NoInfer<InferRenderProps<TSchema>>>. By contrast useHumanInTheLoop and useFrontendTool renders DO receive { args, status, respond }. Airline has no parameterized useComponent, so don't learn the render shape from it — see the template and logistics' showShipment.

  • A gen-UI render's parameters schema is NOT enforced either, and a render-only tool has no way to report a bad argument back. Same trap as a sandboxFunction's schema (above), one degree worse. A useComponent render is handed partialJSONParse(toolCall.function.arguments) verbatim (use-render-tool-call.tsx in @copilotkit/react-core); the schema is only serialized into the tool definition the model reads. And because a render-only tool has no handler, core posts an EMPTY tool result (executeSpecificTool in run-handler.ts), so there is no string to correct the model with — the sandbox's "throw a message naming the accepted values" escape hatch does not exist here. So do BOTH: enumerate the parameter to its real domain (z.enum(YOUR_CONST_TUPLE), which is what puts the vocabulary in front of the model), AND resolve it explicitly in the render, drawing a plain "there is no such X, the real ones are …" card instead of the visual. Commerce's showMarginLadder + src/skins/commerce/category-argument.ts is the worked example: with a free z.string() category, a model saying "Shoes" for "Footwear" draws the signature five-rail ladder with ZERO dots on it, and an empty view rendered confidently is the worst outcome available because it looks like an answer. Note the third state that module carries: arguments STREAM, so a value that is still a PREFIX of a real member is "not arrived yet", not a refusal — refuse it and you flash a red card on every call the demo makes. (Same beat-6 carve-out as above: a GATE's unlock codes are the one closed set you must leave un-enumerated — failure-modes.md § 10.)

  • EVERY argument is undefined mid-render, including the ones your schema declares REQUIRED. The point above is about a value that arrived and was wrong; this one is about a value that has not arrived at all. A render runs from the first frame of its tool call, and partialJSONParse returns {} for those frames, so .optional() is not what makes a field absent and a required field is not what makes it present. Two different bugs come out of that and one guard fixes only one of them:

    • it THROWS: orderIds.map(…) / list.length / id.replace(…) on an argument that is still undefined is a TypeError inside React render. Guard the shape — banking's showTable is the reference (columns ?? [], rows ?? [], src/skins/banking/tools.tsx:793-794) — and remember the CONTENTS too: a half-streamed [" parses to [""].
    • it LIES: formatting an absent value into a confident label asserts a choice nobody made — a Sort chip reading "Sort · oldest first" over an unset lever (src/skins/commerce/order-queue-levers.ts), a red "nothing matches ''" before the needle arrives, beat 4's rose "why" band drawn as an empty coloured bar while the note streams. The fix is never a default — it is to render only what is known.

    And do not over-guard into silence: a card that returns nothing while arguments stream is worse television than a placeholder, because beat 1 leads with generative UI and the room is watching it appear. Commerce's ArrivingCard + arrivedText in src/skins/commerce/tools.tsx are the worked example — one muted card that names only what has arrived, and the confident branch (a miss, a receipt, a label) reserved for arguments that actually landed.

  • Renders must be REPLAY-SAFE: key them off the tool result, NOT off status. Reopening a thread (or reloading the browser in Intelligence mode) replays recorded tool calls, so you get the stored result and no live status transition. A render keyed on status looks perfect during the demo and then renders blank or wrong the moment anyone revisits the thread — which is exactly when beat 2 ("reload and the chart is still there") is being shown. Re-derive display state from the replayed result, and never depend on client state that only existed during the live call. Every shipped skin is written this way; banking's is the canonical example: setCardPin re-derives its card from the replayed result plus a module map holding only brand/last4 — never the PIN (tools.tsx:70-89, 418-451) — and showCharges keys off result not status (tools.tsx:553-572). This is lint-enforced, per skin. The statusKeyedTerminalRender selector in eslint.config.mjs fails any status === ToolCallStatus.Complete — but only inside its files glob, which lists the skins verified clean rather than all of them (grep -n statusKeyedTerminalRender eslint.config.mjs and read the block's files, because that list grows). Add your skin's .tsx to that glob, restating every selector the block already resolves to (see "flat-config rules are REPLACED, not merged" in the verification list below), and add a row for your files to the resolved-selector table in src/shell/skins-config.test.ts. status === ToolCallStatus.Executing on an INTERACTIVE branch is correct and deliberately not matched — an executing HITL card only ever exists live.

  • Register a ROUTE readable and per-page on-screen readables, not just global ones. useAgentContext({ description: "The current page…", value: <segment> }) in your layout tells the agent which page is open; readables registered inside each page component tell it what is visibly on screen (active filters, the rows actually rendered, the figures shown). Without both, "what's on my screen?" (beat 3b) returns the same answer on every page and the beat dies. Every shipped skin does this, so copy whichever is closest to your domain — derive the set rather than trusting a list: grep -rln useAgentContext src/skins/*/layout.tsx for the route readable, grep -rln useAgentContext src/skins/*/pages/ for the page ones. Banking: route readable at layout.tsx:141-143, page-scoped readables in dashboard.tsx:148, cards.tsx:376, team.tsx:54, and the richest in charges.tsx:139. People does the same across all four of its pages, and is the tighter read if you want one worked example — the route readable maps the index segment to a real page NAME (layout.tsx's ROUTE_READABLE_NAME) rather than reporting "". Pair them either way with a prompt clause telling the agent its context IS its view of the screen and that it must never claim it cannot see (agent.ts:61-71).


Contributing end-user identity (only if your skin has its own auth / memory)

You can skip this whole section only if your skin has no per-user scoping at all — then omit RuntimeProviders, useRuntimeProperties and identifyUser, and the runtime falls back to a generic identity. No shipped skin does that (ls src/skins/*/intelligence/user-id.ts returns every registered skin), and a skin claiming beats 4, 5 or 6 cannot: durable memory needs a stable bucket. Read this if your skin has its own end-user identity. It is a three-part client→server mechanism — banking implements all three:

The three parts are separable, and airline and exec are the proof. Both supply useRuntimeProperties and a server identifyUser and NO RuntimeProviders: each has ONE persona and no switcher (airline's account holder, exec's chief of staff), so the hook reads no context and returns a frozen module constant instead (src/skins/airline/runtime-properties.ts, src/skins/exec/runtime-properties.ts — the latter's header docblock writes the reasoning out). Part 1 exists to let a hook read CONTEXT above the provider; if yours does not need to, do not mount an empty provider for symmetry. grep -LE '^\s+RuntimeProviders[,:]' src/skins/*/skin.tsx derives the set, so this stays checkable as skins are added.

  1. RuntimeProviders (client, in providers.tsx) — a provider stack the shell mounts above CopilotKitProvider. Put whatever context supplies your identity here (banking hoists its AuthContextProvider). It MUST sit above the provider because properties is a prop of CopilotKitProvider, so its source has to exist before the provider's first commit — otherwise a child would have to race an imperative setProperties. (Your other providers that consume the CopilotKit context still go in Providers, below it.)
  2. useRuntimeProperties (client, in providers.tsx) — a hook the shell calls inside RuntimeProviders and threads straight into CopilotKitProvider's properties prop. Read your identity context and return a stable/memoized object (banking returns { userRole, userId }, memoized on the member). Do not set a2uiCatalogAvailable — the shell adds that itself when a catalog is present.
  3. identifyUser (server, in a .ts module) — registered in agent-registry.ts as { createAgent, identifyUser }. It receives the client-forwarded properties and returns { id, name } for thread + durable-memory scoping. Because it is reached through the server-only registry, it MUST be server-safe: no "use client", no JSX, no .tsx imports. Keep it in a plain .ts file (mirror src/skins/banking/intelligence/user-id.ts):
// src/skins/<id>/intelligence/user-id.ts  — server-safe: no "use client", no JSX
import type { IdentifyRunUser } from "@/shell/agent-registry";

export const <id>IdentifyUser: IdentifyRunUser = (properties) => {
  const userId = properties?.userId ?? "<id>-demo-user";
  return { id: userId, name: properties?.userRole ?? "<Brand> User" };
};

The shared API route reads the target agentId from the URL and delegates to that skin's identifyUser; agentId-less inspector routes (/memories/*, /info) delegate to the default skin's resolver. You never edit the route.


Files to create under src/skins/<id>/

Mirror the shipped skins' layout:

src/skins/<id>/
├── skin.tsx          # assembles + default-exports the Skin object ("use client")
├── identity.ts       # brand, tagline, logo, optional assistantName/greeting/favicon
├── theme.css         # .theme-<id> { … } re-valuing shared tokens
├── layout.tsx        # Layout chrome; side-effect `import "./theme.css"`
├── pages/            # one component per nav segment
├── tools.tsx         # <XTools/> — frontend tools/HITL/gen-UI + readables; renders null
├── catalog/          # createCatalog(...) → the a2ui catalog (index.tsx)
├── suggestions.ts    # Suggestion[] — ONE PILL PER BEAT, in demo order
├── design-skill.ts   # the OGUI design-brief string
├── data/             # seed data, types, derivations (+ an OPTIONAL useXData hook → useData)
│   └── ledger-context.tsx  # the ONE `GET /ledger` every page/tool/surface shares
├── intelligence/     # user-id.ts (identifyUser) + seed-memories.ts + forget-memories.ts
└── agent.ts          # SERVER-ONLY: export const <id>Agent = () => new BuiltInAgent(...)

data/ledger-context.tsx is where the majority put it (commerce, exec, people); airline and keel keep theirs at the skin root instead (find src/skins -name 'ledger-context.tsx' settles it). Either works — the contract does not see the path. And "every surface" is literal: pages, tools, the OGUI sandbox functions, and whichever a2ui surface the skin ships — a canvas CanvasSurface for most, the inline block: cards for exec, which has no canvas at all.

Slots the CONTRACT calls optional, and what the tree actually does with them: providers.tsx (→ Providers and/or RuntimeProviders + useRuntimeProperties), intelligence/user-id.ts (→ server identifyUser), canvas-surface.tsx (→ CanvasSurface), sandboxFunctions, chatHeaderActions, onSuggestionSelect, toolLabels, useData.

A demo-complete skin sets nearly all of them, so do not read "optional" as "skip it". Every omission in the tree has a stated reason beside it — airline omits sandboxFunctions and RuntimeProviders; exec omits RuntimeProviders (one persona, no switcher) and CanvasSurface (its a2ui blocks render inline on the block: path, not on the canvas), and states both in skin.tsx; bookstore, the one skin that skips beats by direction, omits the five that serve the beats it skips. Derive it, since this paragraph rots:

grep -nE '^\s+(Providers|CanvasSurface|sandboxFunctions|toolLabels|chatHeaderActions|onSuggestionSelect|RuntimeProviders|useRuntimeProperties|useData)[,:]' src/skins/*/skin.tsx

useData / data/ is where the SUBSTRATES split, and it is a minority path: grep -l 'useData:' src/skins/*/skin.tsx returns bookstore alone, whose useBookstoreData holds a seed catalog plus a cart mirrored to localStorage so the basket survives beat 2's hard reload. In every other skin useSkinData<T>() returns undefined and data/ holds the seed, the types and the pure derivations feeding a REST store plus a ledger-context.tsx. Read the implementor first; templates.md § data/use-data.ts is the scaffold.

toolLabels is optional in name only: it is what makes tool-activity chips read as human phrases ("Pulling up your flight") instead of raw tool names (showFlight). Every skin ships one. RuntimeProviders/useRuntimeProperties/identifyUser are for a skin with its own end-user identity (see the identity section above), and they are SEPARABLE — every skin ships useRuntimeProperties and identifyUser; airline and exec ship no RuntimeProviders, because in each the hook reads no context (grep -LE '^\s+RuntimeProviders[,:]' src/skins/*/skin.tsx).

intelligence/seed-memories.ts is not optional if you are building beats 4 and 5 — "it already knows me" is a seeded file, not emergent behaviour, so every skin claiming those beats ships one, each alongside a sibling forget-memories.ts its dev/reset route calls first (ls src/skins/*/intelligence/seed-memories.ts names them, and it returns the whole roster — including the skin with no teach loop, because seeding arms beats 4 and 5 independently of beat 6). A skin claiming those beats without one is claiming behaviour it does not have. It seeds the topical preference (beat 4) and the operational procedure (beat 5), and deliberately does NOT seed beat 6's procedure — that is the one the agent has to learn on stage. See demo-beats.md § "Seeding memories".

Templates for each file are in templates.md — copy them and fill in your domain. They are written against this app's real contract.


Authoring order (slot by slot)

Step 0 — the beat map, before any code. Fill in the nine-row table from demo-beats.md: for each beat, this skin's step, its pill, and what implements it. This is what stops you from building a technically perfect skin that proves nothing — the documented failure mode is an author who wires the contract beautifully and silently drops beats 2, 3b, 5 and 6. Decide the demo, then build it.

Then build in dependency order so each slot compiles before the next depends on it:

  1. identity (identity.ts) — brand, tagline, logo.
  2. theme (theme.css) — .theme-<id> token values.
  3. data (data/ + ledger-context.tsx) — the seed, its types, the pure derivations, and a REST store behind src/app/api/<id>/v1/* (one GET /ledger snapshot read plus the write paths). Every shipped skin is REST-backed; the useXData() + useData shape still works but nothing uses it. Seed two of anything beat 6 gates, so the replay lands on a fresh one. ⚠️ If any of your data is TIME-DEPENDENT, settle it server-side on every read — never tick it on a client interval. A client ticker is a second clock: it paints progress the server never heard of, and the next re-read after any write silently rewinds it. src/app/api/keel/v1/settle-runs.ts (called by both GET /ledger and GET /runs/[runId]) is the shape, with the client interval reduced to a re-read.
  4. layout (layout.tsx) — chrome; side-effect-import ./theme.css here; the route readable (beat 3b) and the meta-utility strip live here.
  5. pages (pages/) — one component per nav segment, each registering its own on-screen readable (beat 3b).
  6. tools (tools.tsx) — frontend tools / HITL / gen-UI + useAgentContext readables. Replay-safe renders (beat 2), visible affordances on every mutation.
  7. catalog (catalog/) — a2ui catalog via createCatalog.
  8. agent (agent.ts) — server-only BuiltInAgent factory. This is where the beats are enforced: screen-awareness, recall-first, procedure separation, "never write a markdown table", pretty bold prose.
  9. intelligence (intelligence/, for beats 4–6) — user-id.ts + seed-memories.ts + forget-memories.ts. Scope the seeded procedure user, NOT project — see demo-beats.md § "Seeding memories".
  10. suggestions (suggestions.ts) — one pill per beat, in demo order — plus design-skill (design-skill.ts).
  11. register — skin.tsx assembles the object; then wire both registries.

After wiring things up run pnpm typecheck and pnpm build, not one or the other. There is no typecheck script, and pnpm build is NOT a full type-check: next build only visits what the app's module graph reaches, so it never type-checks a test file, and Vitest transpiles without type-checking at all. tsc --noEmit is the only thing in the tree that sees **/*.test.ts(x). pnpm lint catches the rest.


Registration (six appends across four shared files)

Steps 1–5 are REQUIRED; step 6 changes the / redirect and is optional. They land in four files, because 4, 5 and 6 are all in src/shell/skins-config.ts: registry.ts, agent-registry.ts, eslint.config.mjs, skins-config.ts. The first two are keyed by the identical id; the third teaches the lint guard that your id exists; the rest are the hand-copied config that server components read.

1. Client skin — src/shell/registry.ts:

import type { Skin } from "./skin-contract";
import banking from "@/skins/banking/skin";
import airline from "@/skins/airline/skin";
import support from "@/skins/support/skin"; // ← add

export { defaultSkinId } from "./skins-config";

export const SkinRegistry: Record<string, Skin> = {
  [banking.id]: banking,
  [airline.id]: airline,
  [support.id]: support, // ← add
};

2. Server agent — src/shell/agent-registry.ts. Each entry is { createAgent, identifyUser? }. identifyUser is optional in the TYPE and required in practice for any skin with memory beats: every registered skin supplies one (ls src/skins/*/intelligence/user-id.ts), so the bare-factory form below is the shape for a skin that has not got there yet, not a target:

import { bankingAgent } from "@/skins/banking/agent";
import { bankingIdentifyUser } from "@/skins/banking/intelligence/user-id";
import { supportAgent } from "@/skins/support/agent"; // ← add
import { supportIdentifyUser } from "@/skins/support/intelligence/user-id"; // ← add

export const agentRegistry: Record<string, AgentRegistration> = {
  banking: { createAgent: bankingAgent, identifyUser: bankingIdentifyUser },
  support: { createAgent: supportAgent, identifyUser: supportIdentifyUser }, // ← add (same id)
  // support: { createAgent: supportAgent },   // ← only while you have no memory beats
};

(AgentRegistration and the IdentifyRunUser type are declared in agent-registry.ts itself — import the type from there for your resolver.)

⚠️ This is the one append with NO automated guard. No test imports agent-registry.ts (grep -rln agentRegistry src --include='*.test.*' is empty), and its Record<string, AgentRegistration> type accepts a missing key, so forgetting it builds, lints and renders — the only symptom is Verification step 5: sending a chat message errors with an unknown agent. Appends 3–5 below are all compared against registry.ts by skins-config.test.ts; this one is on you.

3. Lint guard id list — append your id to LINTED_SKIN_IDS in eslint.config.mjs. This is REQUIRED, not optional: that array is what the URL-contract selectors interpolate into their regexes, so until your id is in it pnpm lint is BLIND to your skin and a hardcoded "/support/tickets" href passes clean while breaking the address bar under a lock. It is a hand-copy of skinIds because an ESLint flat config is loaded by Node and cannot import a .ts module.

export const LINTED_SKIN_IDS = [
  "banking",
  "airline",
  "logistics",
  "keel",
  "people",
  "commerce",
  "bookstore",
  "exec",
  "support", // ← add
];

Forgetting this fails pnpm test:unit — src/shell/skins-config.test.ts lints a synthetic prefixed link for every registered skin through the real selectors, so an unguarded id is RED rather than silent.

4. LOCK_SKIN id list — append your id to skinIds in src/shell/skins-config.ts, in registry order. That module stays import-free so server components can read it, which forces the list to be a hand-copy of the registry's keys — and it is the set the LOCK_SKIN validator accepts, so until your id is in it LOCK_SKIN=<id> throws at boot and / cannot serve your skin:

export const skinIds = [
  "banking",
  "airline",
  "logistics",
  "keel",
  "people",
  "commerce",
  "bookstore",
  "exec",
  "support", // ← add
] as const;

5. Locked-deploy metadata — add an entry to skinIdentities, in the same file, copying your identity.brand and identity.tagline VERBATIM. The root layout's generateMetadata (src/app/layout.tsx) is a server component and reads this map — not your skin module — to give a locked deploy the brand as its <title> and the tagline as its <meta name="description">:

export const skinIdentities: Record<
  (typeof skinIds)[number],
  { brand: string; tagline: string }
> = {
  // …existing skins…
  support: { brand: "Support Desk", tagline: "Every ticket, answered." }, // ← add
};

Neither of those two can be forgotten quietly. A missing skinIdentities entry is a pnpm build type error, because the Record key type is derived from skinIds; a WRONG brand or tagline, or an id missing from skinIds, is caught by skins-config.test.ts, which compares both against the registry.

6. (Optional) default skin — set defaultSkinId in src/shell/skins-config.ts if the / redirect should land on your new skin:

export const defaultSkinId = "support";

Do NOT touch anything else in the shell.


Verification

  1. Four gates, cheapest first, all green: pnpm lint · pnpm typecheck · pnpm test:unit · pnpm build.

    ⚠️ pnpm build is not the type-check gate. There is no typecheck script, so it is easy to conclude next build covers it — it does not. next build type-checks only what the app's module graph reaches, so it never opens a single test file, and Vitest transpiles without type-checking at all. tsconfig.json DOES include **/*.tsx, so the tests are in the project and nothing else looks at them. pnpm typecheck is the ONLY command in this tree that type-checks a test, and several of the guards this skill tells you to write (exhaustiveness gates over a union, typed fixtures) are type-only — they are decoration until you run it.

  2. pnpm dev (needs OPENAI_API_KEY; copy .env from .env.example). Enough for YOUR skin — its agent runs in-process. It is NOT enough for banking, whose agent is a separate Python service (agent/, :8124), and / redirects to banking: so if your first "does this work at all" check is a message sent on the default skin, you get silence and misread it as your own wiring. Send it on /<your-id>, or start everything with ./run-demo.sh.

  3. The skin appears in the selector dropdown at the top of the assistant column — open it from the trigger showing the active skin's brand.

  4. Navigating to /<id> renders your Layout with the correct theme (your .theme-<id> token values visibly applied — accent color, canvas, etc.).

  5. Sending a chat message gets a reply from your agent (confirms id === agentId and that the agent registered correctly).

  6. Your suggestion pills appear, and if you registered frontend tools / HITL / gen-UI, the agent can drive them.

    Automating a pill click? Select by ROLE, not by text. getByRole("button", { name: "…" }), never getByText("…"). The thread rail (.nw-chat-rail, shell-owned, so this bites every skin identically) accumulates saved thread titles, and a thread gets titled after the message its pill sent — so on the second run getByText("Decision brief") matches the rail entry, your driver clicks a thread instead of the pill, and the beat appears not to fire. It reads as a broken app rather than a wrong selector.

  7. pnpm lint — green. This includes the URL-contract guard: the no-restricted-syntax skin-prefix selectors in eslint.config.mjs, which fail and NAME YOUR FILE if any link in your skin hardcodes its route prefix or hand-concatenates onto a builder result (a leading //). It is the cheap check for the contract above; step 8 is the real one. This only works if your id is in LINTED_SKIN_IDS (registration step 3) — otherwise lint is green because it is not looking. pnpm test:unit must also be green; skins-config.test.ts is what catches an id missing from that list, and skin-roster-docs.test.ts what catches prose left behind — a skin count or a "valid ids" list in CLAUDE.md, README.md, .env.example or this skill that predates your skin. While authoring, filter to one file with pnpm test:unit <path>. Passing the path after a bare -- is silently swallowed and runs the entire suite, which destroys the red-green signal you need while writing a new skin's tests.

  8. Run your skin locked: stop the dev server, then LOCK_SKIN=<id> pnpm dev, and open / (not /<id>). Your skin must render at the root, every nav href in the DOM must be prefix-free, and clicking through must keep the address bar prefix-free. /<id> itself should 404. If the prefix survives anywhere, a link in your skin is bypassing useSkinHref — see "The URL contract" above. pnpm test:e2e --project=locked covers this shape for banking; extend e2e/locked-skin.spec.ts if your skin is the one being shipped locked.

If the skin 404s: check resolvePage returns a component for [] (the index segment). If the theme doesn't apply: confirm themeClass === "theme-<id>" and that layout.tsx side-effect-imports ./theme.css. If chat errors with an unknown agent: confirm the agent is in agent-registry.ts under the same id. If the whole app 404s under a lock: LOCK_SKIN must be one of the registered ids — an unrecognised value throws at boot naming the typo.

Then walk the demo (this is the part that actually gates "done")

A green build proves the wiring. Only walking the beats proves the skin. Click every pill in order, typing nothing, and check each beat's failure mode — all of these compile and lint clean while failing live:

  1. Beat 1 — the first pill renders a visual, not a paragraph.
  2. Beat 2 — reload the browser, reopen the thread: the visuals are still there and still correct. (Needs Intelligence env vars. A render keyed on status fails only here.)
  3. Beat 3a — the mutation lands, and the sensitive value appears nowhere in the transcript. Earlier gen-UI is still in the thread.
  4. Beat 3b — ask on two different pages; the answers differ and cite real on-screen figures. Identical answers mean no route readable.
  5. Beat 3c — a confirm card lists the levers before navigating, and after navigation the applied controls are visibly highlighted.
  6. Beat 3d — the artifact appears in the app, then delete the thread: it is still there.
  7. Beat 4 — the answer names the preference it recalled. If it just answers normally, either the seed file or the recall-first prompt clause is missing.
  8. Beat 5 — one vague sentence fires all the procedure's steps in order, no confirmation, each visibly. If it offers to record something, beats 5 and 6 are bleeding into each other in the prompt.
  9. Beat 6 — it declines, records, saves; then on a different gated record it runs the procedure alone. If it clears the gate BEFORE being taught, you published the unlock vocabulary to it somewhere — readable, schema z.enum, tool description, prompt, or refusal body (failure-modes.md § 10). Also prove the gate over pure REST with no agent involved: copy docs/teach-mode/verify-logistics-gate.sh (or banking's verify-teachable-gate.sh) for your routes, and add BOTH your tools.tsx and your agent.ts to the withheldGateVocabulary rule's files glob in eslint.config.mjs — restating EVERY selector those files already resolve to, because flat-config rules are replaced and not merged (listing only your new one silently deletes the rest, and pnpm lint stays green). Do not verify this by COUNTING selectors; a count rots. Add a row for each file to the resolved-selector table in src/shell/skins-config.test.ts, which asserts the resolved selector LIST by name through ESLint#calculateConfigForFile; npx eslint --print-config <file> is the by-hand version. The rule sees identifiers only; the prose channels are yours to grep.
  10. Reset — restores the data, wipes learned memory, re-seeds beats 4/5, and leaves beat 6 unlearned so the demo can run again.

Any beat you deliberately skipped should say so in the beat map. A beat that is merely absent is a bug.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Use when a developer wants to build their first CopilotKit Channels agent and get it answering in Slack or Microsoft Teams: "set up a channel", "connect my agent to Slack", "get my agent into Teams", or starting from nothing and wanting a working channel end to end. Covers the whole path: inspecting or scaffolding the project, building the AG-UI agent, creating and reconciling the managed Channel with the public CopilotKit CLI, running the long-running host, and proving a real provider mention gets a reply. The workflow is not in this file. Run `npx --yes copilotkit@latest onboard start` and follow the graph it prints.

日本語の概要は準備中です。原文の説明を表示しています。

CopilotKit/CopilotKit3.8万2026年10月11日 更新

DEPRECATED: This skill has been replaced by `mcp-app-builder`. Check if `mcp-app-builder` is available in the skills folder. If not, install it: `npx skills install mcp-use/mcp-use --skill mcp-app-builder` Use `mcp-app-builder` instead of this skill. Build ChatGPT apps with interactive widgets using mcp-use and OpenAI Apps SDK. Use when creating ChatGPT apps, building MCP servers with widgets, defining React widgets, working with Apps SDK, or when user mentions ChatGPT widgets, mcp-use widgets, or Apps SDK development.

日本語の概要は準備中です。原文の説明を表示しています。

CopilotKit/CopilotKit3.8万2026年10月11日 更新

Use for any CopilotKit question — adding it to an app, chat UI, frontend or server tools, generative UI, shared state, human-in-the-loop, agent frameworks (LangGraph, CrewAI, Mastra, ADK, PydanticAI, and others), the runtime, Intelligence, threads, voice, or diagnosing something that is not working. Do not answer from memory: this skill exists to point you at the current documentation and source, both of which you can read.

日本語の概要は準備中です。原文の説明を表示しています。

CopilotKit/CopilotKit3.8万2026年10月11日 更新

Use for the CODE half of a managed Intelligence Channel with Slack or Microsoft Teams: customising the Channel a CLI-scaffolded project already ships, or — for a project the CLI did not generate — writing the Channel declaration, the long-running host, and the awaited activation call. Teams provider setup is in scope, because the CLI or dashboard wizard performs it. Creating a Slack app for the first time is not: if no Slack app exists yet, use setup-slack-channel for the provider half and return here for the code.

日本語の概要は準備中です。原文の説明を表示しています。

CopilotKit/CopilotKit3.8万2026年10月11日 更新

Use for the CopilotKit CLI — `npx copilotkit@latest`. Covers proving a project's wiring with `verify` before debugging anything by hand, scaffolding with `create`, signing in and selecting a hosted Intelligence project, agent-assisted onboarding of an existing app, generating type-safe agent ids, and importing thread history. Reach for `verify` first whenever a CopilotKit app is not working.

日本語の概要は準備中です。原文の説明を表示しています。

CopilotKit/CopilotKit3.8万2026年10月11日 更新

Keeps CopilotKit docs pointing at shipped Inspector panes so readers open the overlay. Use when adding, changing, renaming, or removing an Inspector pane, tab, or overlay action in @copilotkit/web-inspector, or when editing docs that mention Inspector. Don't use for Inspector UI polish that does not add a pane, for CLI or agent-prompt copy, or for unshipped Inspector ideas.

日本語の概要は準備中です。原文の説明を表示しています。

CopilotKit/CopilotKit3.8万2026年10月11日 更新

CopilotKit のスキルをすべて見る

このスキルの問題を報告する