brianwestphal/glassbox/.claude/skills/kerf-app/SKILL.md
kerf-app
Build UIs in the kerf reactive framework (https://github.com/brianwestphal/kerf). Use this skill whenever the user is writing or modifying code that imports `kerfjs`, asks to add a feature to a kerf app, or asks "how do I do X in kerf?". Use it proactively the moment you spot a kerf import in the file you're editing.
- Source repository stars
- 34
- Declared platforms
- 0
- Static risk flags
- 0
- Last source update
- 2026-08-26
- Source checked
- 2026-08-28
Decision brief
What it does: where it fits
Drop this file into your /.claude/skills/kerf-app/SKILL.md (or your project's .claude/skills/kerf-app/SKILL.md) so Claude Code activates it whenever you work on a kerf app.
Not for
- Tasks that require unconfirmed production actions or broad system permissions.
- Environments where the pinned source and install steps cannot be inspected.
Compatibility matrix
Platform support, with evidence labels
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
Inspect first. Install second.
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/brianwestphal/glassbox --skill ".claude/skills/kerf-app"Inspect the Agent Skill "kerf-app" from https://github.com/brianwestphal/glassbox/blob/0f2fd17891511441538204dcf832d51b0f4e2e6c/.claude/skills/kerf-app/SKILL.md at commit 0f2fd17891511441538204dcf832d51b0f4e2e6c. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
What the source asks the agent to do
- 01
Setup
Install: npm install kerfjs
Install: npm install kerfjstsconfig.json: "jsx": "react-jsx", "jsxImportSource": "kerfjs"Vite / esbuild need no extra config. - 02
Workflow guidance
When the user asks you to add a feature to a kerf app:
Check what state already exists. Is there a signal / store you should reuse? Don't create a new one for derived data — use computed.Decide where state lives. Module-scope signal for ephemeral UI state; defineStore for state shared across mounts or that needs reset() for tests.Decide who fires the action. A handler on a DOM event → delegate with a data-action attribute. A signal change → effect(). - 03
Public API — one import path
Review the “Public API — one import path” section in the pinned source before continuing.
Review and apply the “Public API — one import path” source section. - 04
Hard rules — every AI assistant gets these wrong at least once
1. JSX renders to HTML strings, not DOM nodes. Don't pass DOM nodes as JSX children — the runtime throws. Need a ref? Build the JSX, then querySelector after mount() / toElement(). 2. Diff keys: id first, then data-key. Lists MUST set data-key={item.id} per item — otherwise the…
JSX renders to HTML strings, not DOM nodes. Don't pass DOM nodes as JSX children — the runtime throws. Need a ref? Build the JSX, then querySelector after mount() / toElement().Diff keys: id first, then data-key. Lists MUST set data-key={item.id} per item — otherwise the diff matches by position and you lose focus, cursor, and identity on insert/delete.Three escape hatches for the morph: - 05
Decision-making axes
When deciding which primitive to reach for, work down the axes:
Originates inside the mount tree → delegate(rootEl, type, sel, handler). Originates outside (window-level keyboard, online/offline, beforeunload) → native window.addEventListener at module top-level.Gesture that needs to follow an element after press (drag, draw, resize) → at the start event, el.setPointerCapture(e.pointerId). Subsequent pointermove / pointerup redirect to the captured element and delegate(rootEl,…Well-known non-bubbler (focus, blur, scroll, load, error, mouseenter, mouseleave) → still delegate(); it auto-promotes to capture. Custom non-bubblers or capture-phase interception → delegateCapture() (also closest()-ma…
Permission review
Static risk signals and limitations
No configured static risk pattern was detected
This is not proof of safety. Runtime behavior, indirect dependencies, and hidden external systems are outside the static scan.
Evidence record
Why each signal appears
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 92/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 34 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
Provenance and original SKILL.md
- Repository
- brianwestphal/glassbox
- Skill path
- .claude/skills/kerf-app/SKILL.md
- Commit
- 0f2fd17891511441538204dcf832d51b0f4e2e6c
- License
- MIT
- Collected
- 2026-08-28
- Default branch
- main
View the original SKILL.md
Building apps with kerf
Drop this file into your
~/.claude/skills/kerf-app/SKILL.md(or your project's.claude/skills/kerf-app/SKILL.md) so Claude Code activates it whenever you work on a kerf app.
kerf is a ~12 KB reactive UI framework (~13 KB with arraySignal): signals + DOM morphing + JSX → HTML strings. No virtual DOM, no compiler, no scheduler. The whole public surface fits in 17 exports.
Setup
- Install:
npm install kerfjs tsconfig.json:"jsx": "react-jsx","jsxImportSource": "kerfjs"- Vite / esbuild need no extra config.
- Dev diagnostics are opt-in by import, and only an APP installs them. kerf does not infer dev mode. In the app entry add
if (import.meta.env.DEV) await import('kerfjs/dev');(Vite) orif (process.env.NODE_ENV !== 'production') await import('kerfjs/dev');(webpack/Node). That enables the read-only storeget()snapshot, the throwing dangerous-URL screen, and makes theKERF_DEV_WARN_*family available; omitting it is production shape and sheds ~4.7 KB min+gzip because the condition folds away and the chunk is never emitted. Put it FIRST if relying on the untracked-signal warning —signal()picks its constructor at creation time.- Switch individual warnings on with
enableWarnings(), which is the only switch that works in a browser (noprocessobject there, and a bundlerdefinecannot reach the read):const dev = await import('kerfjs/dev'); dev.enableWarnings({ staleBinding: true, narrowSet: true, invariants: 'throw' });. TheKERF_DEV_WARN_*env vars do the same for Node/SSR/CI; an explicit call wins either way. - A component package must NEVER import
kerfjs/dev. The hooks are process-global, so installing them is the consuming app's decision — a library that does it forces the diagnostics (and the chunk) on every consumer. Put the import in your demo page or test harness instead.
- Switch individual warnings on with
- Recommended companion:
npm install --save-dev eslint-plugin-kerfjsand addkerfjs.configs.recommendedto the project's eslint config. Enforces five of the hard rules below (no inline JSX event handlers, requiredata-keyineach(), capturedelegate()disposers, no nestedmount(), prefer module JSX augmentation) at edit time — useful as a self-correction signal when authoring kerf code.
Public API — one import path
import {
signal, computed, effect, batch, // reactivity
defineStore, resetAllStores, // stores
mount, morph, each, // render (reactive + one-shot) + keyed list
delegate, delegateCapture, // events
toElement, // direct JSX → DOM Element (or DocumentFragment for multi-root)
SafeHtml, isSafeHtml, raw, Fragment,
} from 'kerfjs';
// Optional, only when you need granular collection updates:
import { arraySignal } from 'kerfjs/array-signal';
// Development diagnostics — gate with YOUR build's dev flag, in YOUR code.
if (import.meta.env.DEV) await import('kerfjs/dev');
| Export | Use |
|---|---|
signal(initial) | atomic reactive state; .value get/set |
computed(fn) | derived value, read-only |
effect(fn) | side effect on signal change; returns disposer |
batch(fn) | coalesce multiple writes into one re-run |
defineStore({initial, actions}) | named multi-consumer state |
resetAllStores() | reset every store (test teardown) |
mount(el, render) | bind reactive render to a DOM element; returns disposer |
morph(liveRoot, template) | one-shot reconcile against a populated element (SSR hydration, page-refresh diffs). Template = Element, SafeHtml, or HTML string |
each(items, render, cacheKey?) | keyed list iteration; per-row memoization on identity (+ optional cacheKey — a passive comparator for external state). Distinct from data-key on the rendered element |
each(items, render, { cacheKey, key }) | same, options form. key gives the list a stable identity — required whenever a conditional list can render before this one, else kerf rebuilds this list and its rows lose focus/scroll/IME. A keyed list takes no positional slot, so keying the conditional list usually fixes its siblings too |
delegate(root, type, sel, h) | one listener at the root; closest(selector) walk from target |
delegateCapture(root, type, sel, h, opts?) | capture-phase escape hatch; closest() walk-up by default (same as delegate); pass { match: 'direct' } for strict target.matches() |
attr(name, value) | pre-computed AttrSpec<N,V> — .selector for delegate(), .attrs to spread into JSX (rename-safe) |
attr(name) | dynamic factory — attr<N,V=string>(name) returns (value: V) => { readonly [name]: V }; both generics off → N inferred, V defaults to string; specify both to constrain values |
toElement(jsx) | parse JSX into a DOM node (SVG-aware). Single-root → Element; multi-root (<><svg/> label</>, two icons side by side) → DocumentFragment that appendChild/replaceChildren/append inlines into the parent. |
raw(html) | inject pre-escaped HTML |
arraySignal(initial?) | granular keyed-list signal (subpath kerfjs/array-signal); each() reconciles in O(patches) |
html`…` | tagged template (subpath kerfjs/html) — JSX-identical runtime semantics with NO build step, for CDN/importmap projects. Real HTML attribute names (class, not className); holes only in text positions or as a COMPLETE attribute value (attr=${v} / attr="${v}") |
Hard rules — every AI assistant gets these wrong at least once
- JSX renders to HTML strings, not DOM nodes. Don't pass DOM nodes as JSX children — the runtime throws. Need a ref? Build the JSX, then
querySelectoraftermount()/toElement(). - Diff keys:
idfirst, thendata-key. Lists MUST setdata-key={item.id}per item — otherwise the diff matches by position and you lose focus, cursor, and identity on insert/delete. - Three escape hatches for the morph:
data-morph-skip— element AND subtree preserved verbatim. For library-owned hosts (Monaco, xterm, D3).data-morph-skip-children— attrs on the host morph, subtree preserved. For client-hydrated slots whose loading/state classes need to flow through.data-morph-preserve— element survives the trailing-removal pass even when the new template doesn't emit it. For imperatively-injected children (autoplay video, tooltip overlay, analytics pixel). Does NOT block a keyed-match move.
- Never
addEventListenerinside amount()-managed tree unless underdata-morph-skip. A morph re-render may discard the node. Usedelegate/delegateCaptureinstead. - Capture the
delegate()/delegateCapture()disposer whenever the registration's scope is shorter than the page. Both helpers return() => void; the listener closure pinsrootEl,handler, and everything the handler closes over (stores, signals, app state). Discarding the disposer on a transient root (modal, route view, mount swap, dynamic widget) leaks the listener AND the app graph it captures; re-mount cycles stack listeners linearly.mount()'s own disposer does NOT remove delegates for you. Safe to discard only when the registration is truly page-lifetime (root isdocument.bodyor equivalent, attached once at startup, never torn down). - One
mount()per root. Don't nestmount()calls. Compose with plain functions returning JSX. - Components are plain functions.
<MyComponent props />works — the JSX runtime callsMyComponent(props)and uses the returned JSX — but there's no hook system, no lifecycle, and no per-instance state. State lives in module-scope signals or stores, never in component closures. - Values bind, structure re-renders. For a value hole, pass the signal/computed ITSELF (
<span>{count}</span>,class={sig}) — kerf updates that one node directly, no render re-run. Read.valueonly when the JSX structure depends on the signal — and then the read must happen INSIDE the render function to be tracked:const x = count.value; mount(el, () => <span>{x}</span>)does NOT re-render. One caveat on bound holes: bind a STABLE signal/computed instance per hole (class={computed(() => …)}that switches internally), neverclass={cond ? sigA : sigB}— switching instances can go silently stale (detectable viaKERF_DEV_WARN_STALE_BINDING=1). The idiom's endpoint: a render that reads NO.valueruns exactly once — a fully bound mount never re-renders. To find.valueholes worth migrating,KERF_DEV_WARN_VALUE_ONLY_RERENDER=1flags re-renders whose only differences were text/attribute values. - Store actions take
(set, get), not(state).set(next)replaces state; mutatingget()does nothing. - Use
data-actionattributes, not inlineonClick. Inline handlers are NOT supported by the JSX → string runtime; delegate from the root. arraySignalis opt-in for long keyed lists where most updates are pointwise. For short lists / filter+sort pipelines, plainsignal+each(items.value, ...)is simpler and equally fast.- Custom-element types: declaration-merge into
kerfjs/jsx-runtime, NOT into a global JSX namespace. Pattern:declare module 'kerfjs/jsx-runtime' { namespace JSX { interface IntrinsicElements { 'my-tag': KerfCustomElement & { foo?: string } } } }. - Each
each()row must produce exactly one top-level element. Multi-root or empty rows throw a row-precise error. Wrap multiple roots in one parent. each()is for DYNAMIC lists. Use.map()for static structural arrays (constantCOLUMNS/TABS/ settings sections) whose row render reads signals.each()memoizes per-item HTML by object identity; constant items never change identity, so the cache hits forever, the row render is never re-invoked, and signal reads inside it silently stop tracking. Outer.map()for the static frame + innereach()for the dynamic sub-list is the idiomatic shape.
Decision-making axes
When deciding which primitive to reach for, work down the axes:
Events.
- Originates inside the mount tree →
delegate(rootEl, type, sel, handler). Originates outside (window-level keyboard, online/offline, beforeunload) → nativewindow.addEventListenerat module top-level. - Gesture that needs to follow an element after press (drag, draw, resize) → at the start event,
el.setPointerCapture(e.pointerId). Subsequentpointermove/pointerupredirect to the captured element anddelegate(rootEl, 'pointermove', '[data-card]', …)still picks them up. Don't reach forwindow.addEventListenerfor in-mount-tree gestures. - Well-known non-bubbler (
focus,blur,scroll,load,error,mouseenter,mouseleave) → stilldelegate(); it auto-promotes to capture. Custom non-bubblers or capture-phase interception →delegateCapture()(alsoclosest()-matched by default). Need strict element-match? Add{ match: 'direct' }on either helper.
Lists.
- Items change across renders (todos, chat messages, table rows) →
each(items, render). - Static structural enumeration whose row render reads signals →
STATIC.map(item => <jsx/>). Innereach(item.children, …)still gets keyed reconcile. - Long list with point-wise mutations →
arraySignal+each(arraySig, render)for O(patches) updates.
Side effects / imperative DOM.
- Library-owned subtree survives across renders →
data-morph-skipon host. - Host attributes morph but subtree preserved →
data-morph-skip-children. - Imperatively-injected element survives the trailing-removal pass →
data-morph-preserve. - Focused input / contenteditable caret survives re-renders → automatic; no opt-in.
Raw HTML.
- User-controlled HTML → sanitize first (DOMPurify) then
raw(sanitized). - Author-controlled trusted HTML →
raw(html)directly.
Dangerous URLs. javascript:/vbscript:/script-executing data: values on href/src/xlink:href/formaction/action/data are dropped — kerf throws in dev, warns + drops in prod. Sanitize user URLs upstream; wrap an intentional trusted one in raw(url) (bypasses the screen in both modes). The javascript: no-op placeholders (javascript:void(0), javascript:;, …) are allowed — they're the placeholder-link idiom, matched against the whole value so nothing can ride along.
Canonical patterns
// Pattern 1: signal + mount + delegate.
// THE core idiom — values bind, structure re-renders: pass the signal ITSELF
// into a value hole ({count}, class={sig}) so kerf updates that one node
// directly with no render re-run; read `.value` only when the JSX STRUCTURE
// depends on the signal (conditionals, list shape).
const count = signal(0);
const ACTIONS = { inc: attr('data-action', 'inc') } as const satisfies Record<string, AttrSpec<'data-action'>>;
mount(document.getElementById('app')!, () => (
<div>
<button {...ACTIONS.inc.attrs}>+</button>
<span>{count}</span>
</div>
));
delegate(rootEl, 'click', ACTIONS.inc.selector, () => { count.value += 1; });
// Pattern 2: keyed list with per-row memoization
mount(listEl, () => (
<ul>
{each(rows.value, (row) => <li data-key={row.id}>{row.label}</li>)}
</ul>
));
// Pattern 3: store with reset
const cart = defineStore({
initial: () => ({ items: [] as string[] }),
actions: (set, get) => ({
add: (id: string) => set({ items: [...get().items, id] }),
clear: () => set({ items: [] }),
}),
});
// access: cart.state.value.items, cart.actions.add('x'), cart.reset()
// Pattern 4: one-shot reconcile (no signals, no effect)
morph(liveCard, '<article class="card">…</article>');
// Pattern 5: fine-grained binding (opt-in) — pass the signal/computed ITSELF
// into a hole so a change updates ONLY that node (no render re-run, no
// reconcile). For a hot spot driven by an external signal (selection class,
// live status attr) — not everywhere. Use computed(), never a bare () => ….
const selectedId = signal<number | null>(null);
mount(listEl, () => (
<ul>
{each(rows.value, (row) => (
<li class={computed(() => (row.id === selectedId.value ? 'sel' : ''))}>{row.label}</li>
), (row) => row.id)}
</ul>
));
// selectedId.value = 3 → only the ~2 affected <li> class attrs update.
// Pattern 6: no build step (CDN / importmap) — the html tagged template
// instead of JSX. Same runtime semantics; real HTML attribute names; a hole
// must be a text position or a COMPLETE attribute value (partials throw).
import { html } from 'kerfjs/html';
mount(rootEl, () => html`
<div class="${cls}">Count: ${count}</div>
<ul>${each(rows.value, (row) => html`<li data-key="${row.id}">${row.label}</li>`)}</ul>
`);
Diagnosing common errors
| Error / symptom | Root cause | Fix |
|---|---|---|
JSX: DOM elements cannot be passed as children | passed a toElement() result inside JSX | Build the whole tree in JSX; refs via querySelector after rendering |
draggable={true} / spellCheck={false} / contentEditable={false} / writingsuggestions={false} / translate={false} / autocorrect={false} won't typecheck | these are HTML enumerated attributes, not boolean ones — they take keyword strings ("true"/"false"; "yes"/"no" for translate; "on"/"off" for autocorrect), and omitting one selects a third state, so the boolean form rendered the opposite of what was meant | Write the keyword: draggable="true", spellCheck="false", writingsuggestions="false", translate="no", autocorrect="off". Omit for the default state. Real boolean attrs (hidden, checked, disabled, autofocus, required, inert) are unaffected, as is popover (bare form = the spec's auto state); for a signal use signal('true') |
<select value={x}> / <textarea value={x}> won't typecheck | neither element has a value content attribute — the markup was inert | <option value="b" selected>; <textarea>{draft}</textarea> |
| Focus / cursor lost on every keystroke | list items lack data-key | Add data-key (or id) to each list item |
| Click handler stops firing after re-render | el.addEventListener was used | Replace with delegate(rootEl, 'click', ACTIONS.foo.selector, ...) (or a string literal for ad-hoc cases) |
| Render fn never re-runs | signal was read outside the render fn | Move signal.value read inside the render fn |
| SVG renders as broken / namespaceless markup | innerHTML used directly | Use mount or toElement (SVG-aware) |
| Library widget destroyed on every render | host reachable by the morph | Wrap host in data-morph-skip; mount the library imperatively after first render |
<my-tag> fails to typecheck | declaration merging targeted global JSX | Use declare module 'kerfjs/jsx-runtime' { namespace JSX { … } } instead |
each(): row render at index N produced K top-level elements | row returned multiple sibling elements or zero | Wrap them in one parent so the row renders exactly one element |
Drag/drop / state change has no visible effect; only elements outside each() update | Used each(STATIC_ARRAY, …) whose row render reads signals. Items never change identity → cache hits forever → row render never re-invoked → signal reads stop tracking | Replace outer with STATIC_ARRAY.map(...); keep inner each() for the dynamic sub-list. See Hard Rule 14 |
| Row-enter CSS animation no longer replays when only a row's content changed (kerf ≥ 0.15.0) | 0.15.0+ morphs a same-identity, same-position row in place instead of recreating its node, so a mount-keyed @keyframes never re-triggers on a content-only update (≤ 0.14.x recreated the node, so it fired). Intentional flip side: focus, scroll, IME, and in-progress transitions now survive | Key the animation on a state-class toggle, not element creation. To force a remount, churn the row's identity (new object ref / data-key) so the reconciler replaces the node |
| Want a hot spot to update without re-running the whole render | Fine-grained binding: pass the signal/computed ITSELF into the attr/text hole (class={computed(() => …)}), not .value. Use computed() not a bare () => … (memoization keeps a shared-signal flip to ~O(changed nodes)). Opt-in per hole. Limit: a bound hole depending on the row's OWN mutated data goes stale on a granular in-place update — use plain interpolation there | |
html ``: partial attribute values are not supported | In kerfjs/html templates a hole must be the COMPLETE attribute value | Build the full string first (class="${`a ${b}`}"), or bind class="${computed(() => a ${b.value})}" for a reactive one |
An each() list's rows lose focus / scroll / typing state when an unrelated conditional list above them appears or disappears (kerf warns about this in dev) | Lists without a key are identified by their position among the render's each() calls, so adding/removing one above shifts this list's identity and kerf rebuilds it | Give the lists stable keys: each(items, render, { key: 'results' }). Keying just the conditional list is usually enough |
Keyed each() list suddenly renders zero rows — only its <!--kf-list:N--> marker — with no errors, and it never recovers (kerfjs ≤ 2.0.1) | A conditionally-rendered sibling BEFORE the list (possibly higher in the tree, e.g. an error banner) was removed that render; older kerfjs rebuilt the shifted list container from the template, permanently detaching the list's internal binding | Upgrade kerfjs (fixed after 2.0.1 — the morph now moves the shifted container up in place, keeping node identity). On older versions, keep the structure before the list stable: wrap the conditional in an always-present container (<div class="banners">{cond ? <div/> : ''}</div>) |
A numbered / zebra-striped / "N of M" each() list shows the wrong number on rows that MOVED (reorder, or non-tail insert/remove), while unmoved rows look right | The render fn's index argument is NOT part of the memo key (only item identity + cacheKey + content version are), so a row that keeps identity but changes position keeps the HTML it rendered at its old index | Fold the index into the memo key so displaced rows re-render: each(items, (it, i) => …, { cacheKey: (_, i) => i }) (add key if used). Opt-in dev warn: KERF_DEV_WARN_STALE_INDEX=1 |
Workflow guidance
When the user asks you to add a feature to a kerf app:
- Check what state already exists. Is there a signal / store you should reuse? Don't create a new one for derived data — use
computed. - Decide where state lives. Module-scope signal for ephemeral UI state;
defineStorefor state shared across mounts or that needsreset()for tests. - Decide who fires the action. A handler on a DOM event →
delegatewith adata-actionattribute. A signal change →effect(). - Render output is JSX returning
SafeHtml. No JSX-as-DOM-node, no inline handlers. Lists getdata-key. - Test with
kerfjs/testing'sclearStoreRegistry()between unit tests if you useddefineStore.
When you spot user code that violates any of the hard rules above, fix it inline AND explain the rule briefly so the user learns the pattern.
Server / SSR
SafeHtml.toString() returns the HTML string. JSX works in Node with no DOM. mount, morph, delegate, toElement all require a DOM and run client-side.
Where to look next
- API reference: https://brianwestphal.github.io/kerf/api/
- Full AI guide: https://github.com/brianwestphal/kerf/blob/main/docs/ai/usage-guide.md
- llms.txt index: https://github.com/brianwestphal/kerf/blob/main/llms.txt
- Example apps: https://brianwestphal.github.io/kerf/examples/
Frequently asked questions
What to verify before installation and use
What does the kerf-app source document cover?
Drop this file into your /.claude/skills/kerf-app/SKILL.md (or your project's .claude/skills/kerf-app/SKILL.md) so Claude Code activates it whenever you work on a kerf app.
How do I install kerf-app?
The source record exposes this install command: npx skills add https://github.com/brianwestphal/glassbox --skill ".claude/skills/kerf-app". Inspect the command and pinned source before running it.
Alternatives
Compare before choosing
coreyhaines31/marketingskills
ab-testing
When the user wants to plan, design, or implement an A/B test or experiment, or build a growth experimentation program. Also use when the user mentions "A/B test," "split test," "experiment," "test this change," "variant copy," "multivariate test," "hypothesis," "should I test this," "which version is better," "test two versions," "statistical significance," "how long should I run this test," "growth experiments," "experiment velocity," "experiment backlog," "ICE score," "experimentation program
garrytan/gbrain
bulk-ingestion
End-to-end discipline for turning any large data source (audio libraries, email takeouts, document corpora, chat exports, API dumps) into brain pages at scale. The lifecycle spine: SCHEMA → ACCESS → TRIAL → EVALUATE → IMPROVE → CODIFY → TEST → SKILLIFY → BULK → MONITOR. State is tracked in a durable JSON manifest (see MANIFEST-PATTERN.md) so any crash, session boundary, or subagent fan-out resumes from ground truth instead of memory.
alirezarezvani/claude-skills
app-store-optimization
App Store Optimization (ASO) toolkit for researching keywords, analyzing competitor rankings, generating metadata suggestions, and improving app visibility on Apple App Store and Google Play Store. Use when the user asks about ASO, app store rankings, app metadata, app titles and descriptions, app store listings, app visibility, or mobile app marketing on iOS or Android. Supports keyword research and scoring, competitor keyword analysis, metadata optimization, A/B test planning, launch checklist
dotnet/skills
migrate-vstest-to-mtp
Migrates .NET test projects from VSTest to Microsoft.Testing.Platform (MTP). Use when user asks to "migrate to MTP", "switch from VSTest", "enable Microsoft.Testing.Platform", "use MTP runner", set OutputType=Exe only for test projects in Directory.Build.props, or mentions EnableMSTestRunner, EnableNUnitRunner, or UseMicrosoftTestingPlatformRunner. USE FOR: MTP behavioral differences vs VSTest (exit code 8, zero tests discovered, --ignore-exit-code, TESTINGPLATFORM_EXITCODE_IGNORE); centralizing