# RULES — shared rules for the CometChat skills pack
# (Ships INSIDE the pack. Every skill references this file; rules are never inlined per family.
#  This is the shippable shared RULES.md — distinct from the factory's own references/RULES.md,
#  which governs how skills are AUTHORED. Keep them in sync when a rule changes.)

## Code safety
- APPEND, never REPLACE the user's code. Additive diffs only. Never delete a competing SDK's files/deps without an explicit, waited-for confirmation.
- No speculative scaffolding; reuse the developer's existing primitives; golden-path defaults over prop sprawl.

## Platform / package integrity
- One package per platform — never cross them:
  react → @cometchat/chat-uikit-react · angular → @cometchat/chat-uikit-angular ·
  react-native → @cometchat/chat-uikit-react-native · flutter → cometchat_chat_uikit ·
  android → Kotlin/Compose (V6) or Java/Views (V5) · ios → Swift.
- Web UI Kit code never goes into RN and vice-versa (CSS/`<a href>`/`document.*` vs native deps).
- NEVER mix SDK majors (V5/V6/V7) in one skill — different packages, primitives, theme systems.

## version_conflict — STOP gate
- When detection shows a version_conflict — a UI Kit major other than the one this family targets (React v7, Angular v5, RN v5, iOS v5, Android v6, Flutter v6; see `peers.yaml`), or two majors at once — STOP. Surface the detail, reconcile (upgrade kit / load matching-version skills / remove unwanted cohort), re-read. Not a warning. (Detection = `npx @cometchat/skills detect --json`, the pack's offline probe and the authority; else the skill's own repo read. Never the dashboard CLI.)

## Credentials & secrets
- Auth key is dev-only client-side; teach server-side token exchange for production. Never hardcode; never echo the auth key to stdout.
- **Never provision a NEW CometChat app or account on the user's behalf.** Reuse the existing configured app (`.cometchat/config.json` / env App ID); if unknown, `provision list` and ASK which EXISTING app to use (ask + wait). Create a new app ONLY on an explicit user request, confirming the name first — an integration run must not silently spawn apps.
- **The login UID must exist in the app — and never suggest a legacy/guessed sample UID.** `login(uid)` does not create users. Do NOT offer `superhero1`/`superhero1..5` (or any other remembered "classic sample" set) — they aren't seeded in modern apps and mislead users. When you ASK which UID to use, do not fabricate candidates from memory; the only tentative suggestion allowed is `cometchat-uid-1`, labelled "if this is a fresh app." Prefer the user's own UID / the Dashboard Users tab.
- Env conventions: Vite `VITE_`, Next `NEXT_PUBLIC_` (.env.local), Astro `PUBLIC_`, Expo `EXPO_PUBLIC_`, RN bare `COMETCHAT_`, Angular `environment.ts` object, Android `app/src/main/assets/cometchat-settings.json` for INIT credentials (build-time extras only: `local.properties`→`BuildConfig`), Flutter const/`--dart-define`, iOS `Secrets.swift`/xcconfig.

## Ordering invariants
- `init() → login() → render` — never render UI Kit components before init+login resolve.
- **Init via `initFromSettings` (telemetry attribution).** Use `CometChatUIKit.initFromSettings(...)` (UI Kit) — or `CometChat.initFromSettings(...)` for a headless SDK build. **The signature is per-platform:** web/JS pass a `CometChatSettings` object; **Android passes `(context, CallbackListener<String>)`** and the settings come from `app/src/main/assets/cometchat-settings.json` (there is no `CometChatSettings` type on Android). Either way, NOT the classic builder `UIKitSettingsBuilder`/`init()`. It persists `integrationSource="ai-agent"` (and routes the Calls SDK). **Web/JS only** — `settings` is a `CometChatSettings` object: `{ appId, region, credentials:{authKey}, chatSDK:{presenceSubscription:{type:"ALL_USERS"}} }`. **Android does NOT use this shape**: its `assets/cometchat-settings.json` uses `uiKit.subscribePresenceForAllUsers`.
  - **Calls SDK build (any framework) → `CometChatCalls.initFromSettings(settings)` by DEFAULT — the SAME ai-agent attribution** (`integrationSource="ai-agent"`), mirroring `CometChatUIKit.initFromSettings` / `CometChat.initFromSettings` above. NOT the publicly-documented `CometChatCalls.init(appId, region)` — that is the FALLBACK ONLY. `CometChatCalls.initFromSettings` is INTENTIONALLY undocumented (ai-agent-only / `@nodoc`, like `CometChatUIKit.initFromSettings`), so its settings shape is baked in the calls skill's `references/docs-map.md`. Applies to EVERY framework's calls skill going forward (React, Angular, and future React Native / Flutter / iOS / Android calls families). (AUDIT-175, sibling of AUDIT-084.)
- Web UI Kit: theme/CSS variables imported ONCE at app root.
- **Follow-the-OS theming is a CORE-surface obligation, not a customization add-on, on any platform whose kit does NOT follow the OS by itself.** When the plan's theming answer is "Auto — follow OS" (the fresh-app default), the family's **core** golden path MUST emit whatever host wiring makes the kit track the device light/dark — the "Auto" default is a broken promise otherwise. It differs per platform (see onboarding `references/platforms.md` for the authoritative per-platform table): **web/React** syncs `theme` ↔ `prefers-color-scheme` (AUDIT-004); **React Native** supplies BOTH `light` and `dark` to the provider / `mode:"auto"` (AUDIT-211); **Android Compose** wraps the kit UI in `CometChatTheme(colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()){}` (AUDIT-217) — with no wrapper it renders light defaults and ignores the OS. **Truly-automatic platforms (iOS; Android Views via `CometChatTheme.DayNight`) need NO host code — do not add any.** The rule: the CORE skill owns the bare follow-OS wiring; `customization` owns brand tokens/typography on top. A core skill that recommends "Auto" but omits the wiring is under-delivering its own default.

## Layout / sizing — the reflow-free surface (give the UI real dimensions or it collapses/reflows)
- UI Kit components are `height:100%`/flex-fill: they take the size of their parent, and they ship their OWN loading/empty state (`loadingView`/`emptyView`). A surface that looks broken is a HOST **container-sizing** defect, not a kit bug — one root cause (a **content-driven** box the kit then fills), two failure modes: **static collapse** (a ~0px sliver / top-left cram) and **load-transition reflow** (the surface is SMALL while the conversation list loads, then GROWS to full size — the box expands WITH content instead of being pinned; looks broken and can misplace the UI).
- **PIN the box independent of content so it's full-size from frame 1 and the loading→loaded swap causes no shift.** Four invariants (the pack's `references/layout.md` is the single source of truth for the web recipe):
  - **Prepare the ancestor chain** — `html, body, #root { height:100%; margin:0 }` so a full-height container isn't sized against `0`. A bare app has none → collapse.
  - **Pin a content-INDEPENDENT height** — full-app → `100dvh`; embedded → a fixed height (e.g. `600px`) or a sized grid/flex cell. **Never `min-height`/`auto`** on the surface box (it grows with content = the reflow).
  - **Size the panes** — each column/pane `height:100%; min-width:0; min-height:0; overflow:hidden` so content **scrolls internally** and never spills; sizing the root alone leaves the kit filling nothing. Give the app container `overflow:hidden`.
  - **Let the kit's own loading state fill the pinned box** — render `loadingView`/`emptyView` INSIDE the pinned surface so it's full-size before content loads; do NOT gate the whole surface behind your own placeholder→chat swap (that IS the reflow).
  - (Also: no `transform`/`filter`/`backdrop-filter` on an ancestor — it clips the kit's `position:fixed` overlays.)
- **Full-page chat: NEUTRALIZE the scaffold's boilerplate CSS first.** A fresh Vite/CRA/Next app ships template CSS that centers + width-caps + pads the root — `#root { max-width:1280px; margin:0 auto; padding:2rem; text-align:center }` + `body { display:flex; place-items:center }`. Full-height chat dropped into that renders as a centered, ~1280px, padded box with side gutters, and message bubbles/overlays clip off the edge ("exceeding the viewport"). Reset it before the chat CSS: `html, body, #root { height:100%; margin:0; padding:0 }` + `#root { max-width:none; text-align:left; display:block }` + remove `body{place-items:center}`. Find the platform's equivalent scaffold cruft (Next `globals.css`, CRA `App.css`) and clear it.

## Localization & CSS isolation
- **Never render a raw localization key as a label.** The kit localizes via `CometChatProvider` (it auto-wires `LocaleProvider`); a snake_case token in the UI (e.g. `group_info`, `add_members`) is a **missing/wrong key** — `getLocalizedString` returns the key on a miss. Use the kit component or `useLocale().getLocalizedString(key)`, never `<x>group_info</x>`. (v6→v7 changed some keys; a stale key falls through to the raw string.)
- **Don't let the host app's global CSS leak into the `.cometchat` subtree.** The kit's layout is correct by default (conversations header is fixed `flex-shrink:0`, the list is `flex:1`, item titles are left-aligned). A global `text-align`/flex-centering/`flex`/reset (or Tailwind base) on the kit's ancestors mis-aligns (centered list names) or mis-sizes (expanding header) kit internals. Scope global styles away from `.cometchat`; never override the internal BEM classes — customize via `--cometchat-*` vars + view slots.
- **To differentiate a NESTED kit surface, override THAT surface's OWN background variable — a wrapper background is not enough.** A kit sub-surface that paints an **opaque** background from its own token (e.g. `.cometchat-message-list` paints `--cometchat-message-list-bg`, default `--cometchat-background-color-03`) COVERS any background set on a wrapping `div` — the wrapper bg never shows. Canonical case: the **thread panel** — its header/composer paint their own opaque `--cometchat-background-color-01`, but the thread message list paints `-03` (same as the main list), so the thread list looks undifferentiated even after you give the thread wrapper a background. **Fix:** scope the list's OWN token on the thread wrapper — `.cc-thread-panel { --cometchat-message-list-bg: var(--cometchat-background-color-01); }` (cascades to the list, matches the thread header/composer, differs from the main list's `-03`; verified vs 7.1.0). Generalizes to any opaque sub-surface — override its background variable via a scoped ancestor, never the internal BEM class. Depth: your platform's `customization` skill. (AUDIT-020/024.)
- **A per-theme override must select the element the theme attribute is ON — not a descendant.** `CometChatProvider` renders ONE wrapper — `<div data-theme="light|dark" class="cometchat">` — so `data-theme` and `.cometchat` are the SAME element. A per-theme brand override is `.cometchat[data-theme="dark"] { … }` (same element); `[data-theme="dark"] .cometchat { … }` targets a `.cometchat` NESTED inside a `[data-theme]` ancestor, which does not exist → it matches nothing and silently does nothing. To retint a kit INTERNAL per theme use `[data-theme="dark"] .cometchat-<element-class> { … }` (the wrapper as ancestor, a kit class as the descendant — the kit's own convention). Verified vs 7.1.0. (AUDIT-025.)

## Clarification contract
- Where a skill must ask, it ASKS and WAITS — never infers, never defaults, even in auto/approval mode. On agents without a question primitive, render a numbered list and wait; map by value/label, accept free-text; if headless, STOP and emit the question.
- **CURATE large choice sets — don't dump a long list.** When the options are many (e.g. many CometChat apps to pick from), show the **top 3 most-relevant as selectable options + a "Show all N" option**, with the free-text/"Other" answer as the manual/paste fallback — expand to the full list only if the user picks "Show all." A wall of 20+ rows as the prompt is a UX regression.

## Truthful output
- Every `CometChat*` symbol must exist in the target family+major catalog. Every claimed feature must exist in `features.json`. Read real signatures from live docs; compile-verify before "done". Treat live docs as source of truth for what's new.
- **A baked "closed list" (model FIELD sets, enum values, method inventories) and a NEGATIVE-capability claim ("no X on this platform / that symbol is invented") are the two easiest things to get WRONG, and both are asserted as FACT — so both must be RE-PROBED against the SHIPPED source of the exact target major every authoring pass, never carried forward from an earlier probe.** A closed list that isn't (undercounts a real field) makes a real API look phantom — worse than no list; the correct fix for a docs PHANTOM field is the shipped list, not a hand-counted subset. A negative claim ("Flutter has no local screen share") must be re-checked against the BARREL/exports before it is stated — a symbol the barrel reaches that an emit can call COMPILES, so "invented"/"impossible" is falsifiable. Docs can LAG the shipped SDK (a method ships before its page); when source and docs disagree on existence, source wins for "does it compile," and note the docs lag. (AUDIT-231; mirrors AUDIT-230's signature-drift class — same lesson at field-set + capability-absence granularity.)
- **Canonical doc channel = a 3-TIER ladder: (1) the CometChat docs MCP, auto-connected → (2) guide the user to add it → (3) plain fetch.** Try them in order, and never HARD-STOP.
  - **Tier 1 — MCP, auto-connected.** The pack ships the first-party docs MCP in its plugin manifest (plugin-root `.mcp.json` → server `cometchat-docs` = `https://mcp.cometchat.com/mcp`), so for a plugin install it connects automatically (plugin-scoped servers need no trust prompt). PREFER its tools — `search_cometchat_docs` · `fetch_cometchat_doc_page` · `get_cometchat_implementation_bundle` (the bundle tool is **NON-AUTHORITATIVE**: curated static recipes that can lag live docs, so prefer `fetch_cometchat_doc_page` and reconcile any bundle against it).
  - **Tier 2 — if the MCP is NOT connected, tell the user and help them add it** (one line, never masked): `claude mcp add --transport http cometchat-docs https://mcp.cometchat.com/mcp` — or reinstall/enable the plugin (`/plugin`, then `/mcp` to reconnect) — then retry. Verify with `claude mcp list`.
  - **Tier 3 — if it still won't connect, fall back to a plain fetch** of `DOCS_BASE`/`SDK_DOCS_BASE` + path + `.md`. This tier keeps every skill working with NO MCP at all, so never require the MCP and never hardcode a docs host.
  - The `<path>` catalog in each family's `references/docs-map.md` stays the source of truth for WHICH page; the tier is only HOW to read it. The MCP tracks whatever docs environment it points at (production by default; a preview when repointed), so tier 1 also subsumes the `DOCS_BASE` env-swap.
- **If asked to "fix" something, check the live doc first and say where the fix belongs.** When a defect is really a **feature-correctness/behavior gap in the CometChat docs** (the doc is wrong or missing it), tell the user plainly it's a **docs** issue to report to CometChat — don't silently hand-roll a permanent local workaround for what the docs should carry. If the live doc is already correct, fix the integration to match it.
- **Compile hygiene — import the `CometChat` SDK namespace only when used as a VALUE.** It ships as an ambient global, so `CometChat.User`/`.Group`/`.BaseMessage` resolve as types with NO import. Import it only for value use (`instanceof CometChat.User`, `CometChat.CometChatHelper.*`, `new CometChat.*RequestBuilder`). A type-only import trips strict TS (`noUnusedLocals`+`verbatimModuleSyntax`, the Vite React-TS defaults) → `TS6133`; `import type` does not fix it. (AUDIT-007.)

## Completeness & determinism
- Emit a contracted feature/component's FULL `contracts.json` min_capabilities every time (same minimum on every prompt) — no random subset, no unrequested scope. Least-code = use the drop-in that already provides them; the minimum set itself is fixed.

- **A family that cannot be resolved is REPORTED, never guessed.** No "nearest family" fallback, no
  default-to-react: installing one framework's skills into another's project hands the agent APIs
  that do not exist there, and it looks like success. Refuse, name what was detected and what is
  shippable, write nothing, exit non-zero (AUDIT-105).

## Fetch discipline — docs are the source of truth (never `.d.ts`)
> Paths below are **per-family**: use YOUR family's core skill — `cometchat-react-v7-core` or `cometchat-angular-v5-core` (and the same shape for any family added later). Every family ships its own `references/docs-map.md` with its own `DOCS_BASE`.
- The hot path (install + `init → login → render` + the drop-in props) is BAKED in your family's **`-core`** skill — do NOT fetch it.
- For anything else (exhaustive props, view-slots, long-tail components, theming tokens, feature steps) fetch the docs **`.md` twin**: append `.md` to the page URL — clean Markdown + the "AI Integration Quick Reference" JSON. Route with your family's **`-core/references/docs-map.md`**.
- Fallback order: `.md` twin → if it 404s, the HTML docs page (same URL, no `.md`) → **NEVER** `node_modules/**/*.d.ts`, and never training memory.
- Versions, **migrations**, dashboard paths and best-practices change fast: always fetch, never answer from memory. **A migration is a fetch-heavy task, not a baked-only one** — the skill's baked breaking-change map is a *closed list* (what was removed/renamed); the *shapes* (a target component's inputs/outputs, an SDK method's signature) still come from docs. Each family's `docs-map.md` carries a migration/upgrade row; the baked map naming a symbol is not a substitute for fetching what replaces it.
- **If the bundle is the only place left to look, that is a SKILL defect — say so.** Reading `node_modules` is not a permitted last resort; it is a signal that the skill gave you no route. State plainly which lookup was unroutable (so it can be fixed) rather than silently reverse-engineering the shipped bundle — the bundle shows a symbol's shape but never whether it is the *sanctioned* API, and an internal-only `declare class` is indistinguishable from an exported one.
- **Manifest / native-platform symbols are NOT docs symbols — verify them against the AAR/framework artifact per COHORT, and prefer auto-merge.** Manifest `<activity>`/`<provider>`/`<service>` fully-qualified class names, package namespaces, and permission strings are not in the docs `.md` twins. When a mobile skill bakes one, it MUST match the fully-qualified name in the **installed artifact's manifest** for that cohort (Android: the AAR `AndroidManifest.xml`), and it MUST NOT carry a name from a different cohort or an older major. Most kit-owned Activities are **already declared in the AAR manifest and merge in automatically** — the default guidance is "you do NOT declare these; they auto-merge," and any manual `<activity>` example is a cohort-split, artifact-verified fallback, never a copy of a prior major's namespace. (A v6 Android kit renamed `com.cometchat.chatuikit.*` → `com.cometchat.uikit.{compose,kotlin}.*`; baking the old `com.cometchat.chatuikit.calling.CometChatCallActivity` is exactly this class of defect — AUDIT-218.)

## Default-on affordances — wire or hide (never dead-end)
- A drop-in can render a live-looking control **by default** that is **inert/partial until stitched** (callback + companion panel/sibling component, sometimes a Dashboard toggle). The UI Kit gives the trigger; the host must supply the destination.
- When fetching a component's props, classify every default-visible affordance: works standalone, or needs wiring? If it needs wiring, the emitted code MUST either wire the destination or hide the trigger. Never leave a default-on affordance dead-ending. `enablement:"default"` ≠ "no wiring."
- **Wire the FULL round-trip, not just the entry.** When you open/mount a companion component in response to a trigger, wire its COMPLETE interaction — the way IN *and* the way OUT (its own back/close/cancel callback), plus any result/confirm path. A component you toggle open owns state you must also toggle closed: its built-in back/close button fires a callback that does nothing unless YOU handle it. Before shipping, check every rendered control of the companion (back, close, cancel, result-click, submit) has a wired handler — an opened panel whose back button dead-ends is the SAME defect as an unwired trigger. Concretely, `CometChatSearch` (opened from `onSearchBarClicked`) renders a back button by default (`hideBackButton` default `false`) → you MUST wire **`onBack`** to close it (same state that opened it), and **`onConversationClicked`/`onMessageClicked`** to close it + select the result. Opening it without `onBack` is a broken product (AUDIT-017).
- Known v7 cases (`features.json` `needs_stitching`): `CometChatConversations` `showSearchBar` (default `true`, client-side name filter only) → `onSearchBarClicked` → render `CometChatSearch` **in the conversations list column** (over the list; the message pane stays — NOT a full-screen takeover; full-screen only on mobile's single pane) **+ wire its `onBack` (close) and `onConversationClicked`/`onMessageClicked` (close + select)**, or `showSearchBar={false}`; `CometChatMessageList` thread-reply indicator → `onThreadRepliesClick` + thread panel (**+ the panel's `CometChatThreadHeader` close button**) or `hideReplyInThreadOption`. (Wire the destination, its EXIT, AND put it in the right place — a full-screen search opened from a sidebar bar is a placement defect, AUDIT-014; a search with no `onBack` is a round-trip defect, AUDIT-017.)
- **Selection has THREE obligations, and every panel closes (AUDIT-079).** When you compose a selector (conversations/users/groups tabs) or a list ↔ a message pane, selecting an item has THREE jobs — doing only the first is a dead-end: (1) **render** the target pane; (2) **REFLECT** the selection in the list so the open row highlights — pass the built-in active-item prop: `CometChatConversations activeConversation` · `CometChatUsers activeUser` · `CometChatGroups activeGroup` (real v7 props; without them there's no active state and the user can't tell what's selected); (3) make EVERY side panel you open **closeable** — the round-trip above applies to the **user/group details** panel too (`CometChatGroupMembers` renders a header back button whose `onBack` is the host's to wire; a host-composed user-details panel adds its own close), not just search/thread. If a selection opens a panel, a close must lead back out of it.
- **Two searches, two SCOPES — never both global (AUDIT-041).** `CometChatConversations onSearchBarClicked` → a **GLOBAL** `CometChatSearch` (all conversations + messages, **NO `uid`/`guid`**) over the list column; `CometChatMessageHeader onSearchOptionClicked` (`showSearchOption` default `true`) → a **SCOPED** `CometChatSearch` passing the OPEN chat's **`uid` (1:1) / `guid` (group)** so it searches ONLY that conversation, in the side panel. Emitting the header search WITHOUT `uid`/`guid` (in-chat search returns whole-app results) is a defect. Docs already cover it (`components/search` → "Scoped Search", `uid`/`guid`); the rule is spelled out here so the scope isn't dropped from dense prose.
- **A capability-gated surface must be GATED ON REAL AVAILABILITY — a comment is not a gate, and a fallbackView is not always a fix (AUDIT-162).** Some drop-ins render the kit's raw "OOPS! Looks like something went wrong" screen when a backend capability/plan is unavailable — e.g. `CometChatCallLogs` when call-logs aren't in the plan. **Verified live: `CometChatCallLogs` renders that from its OWN INTERNAL error state WITHOUT throwing** — so an outer `CometChatErrorBoundary`/`fallbackView` never fires and CANNOT override it (its `onError` may also not fire). So the emitted code MUST **actually gate the trigger** on an availability flag (omit/disable the tab/route — a REAL condition in code, NOT a `// omit when …` TODO comment; a comment-only gate ships the dead-end). **A `fallbackView` only helps if the component THROWS — verify per component; do NOT assume it does.** For a component that renders its own internal error state (proven: `CometChatCallLogs`), the ONLY reliable protection is the availability gate. **The gate must key on real AVAILABILITY, not a proxy:** a feature being ENABLED (e.g. calling via `uiKit:{callsSDK:{}}`, which makes the header Voice/Video buttons appear) is necessary to OFFER the surface but is **not sufficient** for it to render — call-logs availability is a SEPARATE plan gate (proven live). Confirm availability with a one-time SDK probe you own (`CallLogRequestBuilder → fetchNext()`) or the known plan; default the surface OFF. Emitting an always-mounted `CometChatCallLogs` (or any such surface) with no real availability gate is a dead-affordance defect.
  - **This rule is FAMILY-WIDE, and the SAME "OOPS!" screen has a SECOND root cause: SDK-readiness, not just plan (AUDIT-163, Angular).** On Angular, `<cometchat-call-logs>` mounted unguarded renders the identical "OOPS! / Retry" screen from a *client-side crash* — `[CometChatCallLogs] Error: TypeError: Cannot read properties of null (reading 'CallLogRequestBuilder')` — because its `ngOnInit` synchronously does `new CometChatUIKitCalls.CallLogRequestBuilder()` while `CometChatUIKitCalls` is still `null` (the Calls SDK is lazy-loaded via `loadCallsSDK().then(...)` and populates late; **Retry does NOT recover** and **no network request is made** — so this is NOT the plan gate). The fix here is a **readiness gate**, distinct from the availability probe — but **`callingReady` ALONE is NOT sufficient (proven live):** `CometChatUIKit.callingReady` defaults to a pre-resolved `Promise.resolve()` and even after `initCalling()` runs the lazily-loaded `CometChatUIKitCalls` namespace can still be `null`. Gate on the NAMESPACE actually being populated: `callingReady.then(() => callsReady.set(!!CometChatUIKitCalls))`, then `*ngIf="callsReady()"`. So a call-logs/`callsSDK`-backed surface needs BOTH gates conceptually — **readiness** (SDK loaded) AND **availability** (in plan) — before it may mount; default it OFF and reveal it only when its gate(s) pass. Every family that ships a lazily-loaded calls surface (React, Angular, RN, …) inherits this — do not treat it as a React-only or plan-only concern. (STOPGAP in the skills until the kit component awaits `getCometChatCalls()`/`callingReady` itself — filed upstream; the call-logs docs page omits the prerequisite — DOCS-BACKLOG.)

## Reuse built-in props/callbacks/slots — UI Kit first, SDK fallback, hand-roll last
- **The order for EVERY feature: UI Kit component → SDK method → (only if neither) hand-roll.** First check if the UI Kit has a component/prop for the feature and use it. If the UI Kit has NO component for it (AI agents, campaigns, advanced/AI moderation, bots, webhooks, transient messages, low-level presence, …), drop to the **JavaScript Chat SDK** and call its method directly — the SDK library is already installed under the UI Kit. Hand-roll ONLY when neither the UI Kit nor the SDK exposes it. Never re-implement a feature that already exists in either.
- Before adding custom UI (buttons, handlers, panels) for a requested capability, map the affordance to an existing prop on the component (callbacks, `show*/hide*`, view slots). Don't scatter your own buttons when a built-in trigger exists.
- **Render custom controls in the component's OWN view slot — NEVER as a sibling on top.** A search icon / action button / banner / custom header goes INSIDE the component via its slot, not above or beside it: Conversations header → `headerView` (search bar → `searchView`); MessageHeader actions → `trailingView` / `auxiliaryButtonView`; MessageList banner → `headerView` / `footerView`; Composer extra button → `auxiliaryButtonView` / `headerView`. Rendering search "on top of" the conversation list instead of in its `headerView` (or over the message header instead of its `trailingView`) is a defect. Baked slot map: your platform's `core` skill -> `references/component-props.md`.
- **A FULL-REPLACE slot replaces everything it covers — re-render what you keep (AUDIT-083).** A list's `headerView` replaces the ENTIRE default header, including the "Chats"/"Groups"/"Users" **title**. So to add a "New chat"/"Create group" button, render the title AND your button together inside `headerView` — a bare button there silently drops the title. Use `useLocale().getLocalizedString(...)` for the title.
- **A form the kit ships NO component for must cover its FULL domain (AUDIT-081).** Host-built forms (e.g. create-group — v7 has no `CometChatCreateGroup` component) include EVERY variant from the SDK: a create-group form offers Public · Private · **Password**-protected (with a password field), not just public/private (`CometChat.GROUP_TYPE.PASSWORD` + `new CometChat.Group(guid, name, type, password)`).
- Discovery order: (1) the baked hot-path props in your family's **`-core`** skill; (2) the component's **`.md` twin** — its prop table + "AI Integration Quick Reference" JSON (append `.md` to the docs URL; route via your family's **`-core/references/docs-map.md`**); (3) if the UI Kit has NO component for the feature → the **SDK method** from the SDK docs (`docs-map.md` → SDK docs: the `llms.txt` index + the page's AI-Integration-Quick-Reference accordion); (4) only hand-roll when NEITHER the UI Kit nor the SDK has it. **Never read `node_modules` `.d.ts`; never guess from memory.**
- Use these instead of custom buttons: `CometChatMessageHeader` `onItemClick` → open user/group profile on header click; `CometChatMessageHeader` search button (`showSearchOption` default `true`) + `onSearchOptionClicked` → open message search; `CometChatConversations` `onSearchBarClicked` → open conversation search; `CometChatMessageList` `onThreadRepliesClick` → open thread panel.
- **Scope the conversation list to the request (reuse the built-in request-builder prop — don't hand-roll a filter).** A **1:1 / DM-only** ask must not show the app's seeded groups: pass `CometChatConversations conversationsRequestBuilder={new CometChat.ConversationsRequestBuilder().setConversationType("user")}` (groups-only → `"group"`) — the builder INSTANCE, not `.build()` (the kit builds it; passing the built object breaks the list). Don't add a Groups tab to a 1:1 app. A generic unscoped "add chat" keeps both types. (AUDIT-038)

## Responsive / mobile-first layout — collapse to one screen at a time (never dead-end mobile)
- Any layout that composes more than one pane (list + message view, list + detail/thread, a full-app placement) MUST collapse to ONE screen at a time on small viewports. A fixed side-by-side layout that squashes/overflows on a phone is a defect — the same class as a dead-ending affordance (§ wire-or-hide): it dead-ends the mobile user.
- There is NO built-in responsive shell (no `WithMessages`/`CometChatUI` in v7); the host composes it. Mobile = a navigation stack: conversation list → tap a conversation → the message view REPLACES the list full-screen with a back control → tap the header → the details/thread view REPLACES the message view. The same selection state renders side-by-side on wide viewports.
- Reuse the built-in back affordance, don't hand-roll it: `CometChatMessageHeader` ships `hideBackButton` (default `false`) + `onBack()`. On mobile wire `onBack` to clear the selection (pop to the list); on wide set `hideBackButton` (the list is always present).
- Web recipe: drive the visible pane from selection state + `window.matchMedia("(max-width: 768px)")` (SSR-guarded, with a `change` listener). Panes need explicit height; never transition them with `transform` on a chat wrapper (clips the kit's `position:fixed` overlays — animate `right`/`left`/width). Native (RN/Flutter): stack navigation, one screen per route. Depth + recipe live in your platform's `placement` skill.

## The CLI is a dashboard/API client only (loaded on demand)
- **Env-writing and codegen are the SKILL's job** — done by reading your repo and emitting code, never a CLI command. **Detection** is `npx @cometchat/skills detect --json` (the skills pack's offline probe: framework, UI Kit + `version_conflict`, `existing_cometchat` = a CometChat UI Kit/SDK dependency is present), topped up by the skill's own repo read. The dashboard CLI (`@cometchat/skills-cli`, loaded on demand) does DASHBOARD/API operations only: `auth` (dashboard login) + `provision` (fetch an app's App ID/Region/Auth Key) + `config` + **`features`** (enable/disable dashboard **extensions + AI** for an app — `features list`/`enable <id>`/`disable <id>`). It writes no code and knows no framework (see your platform's `core` skill -> `references/setup-credentials.md`). For a feature: the CLI `features enable <id>` flips the DASHBOARD toggle; the SKILL wires the component/prop in code. Installing/refreshing the skill files is the separate `@cometchat/skills add`.

## Verification scope — BUILD the feature; do NOT test it unless asked
- **Your job is to BUILD the requested integration, not to test it.** After building, a `tsc`/build to confirm it compiles is sufficient. Do **NOT** scaffold or run end-to-end / browser / Playwright / headless / unit / integration tests, add a test framework or test files, or set up CI — **unless the user EXPLICITLY asks for tests.** "Add chat to my app" means add chat, not write a test suite or launch a browser to prove it.
- **A skill's "Verify it works" section is an ADVISORY, human-run sanity check** (start the app, see the conversation list render, send a message) — it is NOT an instruction for you to automate. Offer the checklist to the user if useful; never spin up a headless browser / E2E run to satisfy it yourself.
- Automated E2E/smoke testing of these skills is an INTERNAL CometChat-team activity (a separate skill-reviewer), never part of a customer integration. If you think testing is warranted, ASK first — don't do it unprompted.
