composition-patterns (vercel-labs/agent-skills)
vercel-composition-patterns: moves a boolean-prop React component to a compound-component API (control 3.7, skill 4.4), at no extra tokens, but a one-sentence prompt of the same advice scores 4.7
25.1 / 30
Lift over no skill: +0.68
Situational: the lift +0.68 is under +1.0. Recommended needs a total of 22/30, output quality of 3.5, and a lift of +1.0.
SOTA
PassTested with Codex.
Covers Codex, Claude Code
Codex CLI 0.166 (SOTA tier): 18 graded runs. A pass here also covers Claude Code under our tier rule; it says nothing about the second tier.
Scores
Each dimension is 0 to 5 and the six add up to the total out of 30. 5 is better on every dimension. Cost 5 means cheap and fast. Install 5 means easy.
- Trigger reliability
- 5.0
- Lift over no skill
- 2.7
- Output quality
- 4.4
- Token and time cost
- 5.0
- Install and portability
- 4.0
- Safety
- 4.0
Security review
SafeStatic scan: 0 CRITICAL / 0 HIGH / 0 MEDIUM. Manual review: Safe to run in a no-network sandbox (no findings). A clean scan does not prove a skill safe.
Install
npx skills@latest add vercel-labs/agent-skills --skill composition-patterns
Pinned commit: 1a2c79d4e6c4a2283dc2c614b6538cbe1ed5ca62
Tested: 2026-11-02
What we tested
- Replace boolean props with composition — Composer.tsx has grown a pile of boolean props. Refactor it into a flexible component API that does not need them. Keep it in the single file Composer.tsx and keep the behaviour.
- Add a variant without adding props — Add a 'quick reply' composer: a one-line input that sends on Enter, with no attachments or emoji button. Do not add more boolean props to Composer. Edit Composer.tsx.
- Review a component API — Review the component API of Composer.tsx: what is wrong with it and what would you change? Do not change any files.
Pros
- Lifts the mean score from 3.71 to 4.39 over no skill (+0.68).
- The skill was picked up without being named: Skill file read in 6/6 treatment runs.
- No token overhead: 1.03x the tokens of no skill.
Cons
- A one-sentence prompt scored 4.7, against 4.39 with the skill (-0.31).
- n=2 per task per arm; differences under about 1 point are noise.
Review
Codex CLI 0.166 (gpt-6-astra, low reasoning) on a dedicated Windows test PC, a fresh temporary project and temporary HOME/TMPDIR/caches per run, skill installed (treatment) or absent (control) or absent with the skill's core requirement squeezed into one sentence appended to the prompt (one-liner group), 2 runs per group per task, network off, /tmp writes blocked. The prompts never mention the skill (except the wording noted below). One chat-composer component (Composer.tsx) with a pile of boolean props. Tasks: refactor it into a flexible API keeping behaviour, add a 'quick reply' one-line composer without adding more booleans, and review the component API (no edits). Prompts do not name any pattern. The one-liner names the patterns (explicit variants from compound parts, shared context with state/actions, children instead of renderX, state lifted into a provider).
Three-arm comparison
| Arm | Mean score (0-5) | Tokens per run |
|---|---|---|
| Without the skill | 3.71 | 103k |
| One-sentence prompt | 4.7 | 105k |
| With the skill | 4.39 | 105k |
Measured with Codex CLI over 18 runs, scored by rule-based checks with no LLM judge. Total 25.1 / 30, verdict Situational.
Verdict: Situational (25.1 / 30). Source: vercel-labs/agent-skills, skills/composition-patterns, pinned at commit 1a2c79d. Install: npx skills@latest add vercel-labs/agent-skills --skill composition-patterns. Licence: MIT (repo LICENSE).
What it is
The vercel-composition-patterns skill (vercel-labs/agent-skills): React composition guidance: avoid boolean prop proliferation, build compound components that share state through context (a generic state/actions interface), create explicit variant components, prefer children over renderX props, lift state into providers; includes React 19 notes. A SKILL.md plus a rules folder.
How we tested
Codex CLI 0.166 (gpt-6-astra, low reasoning) on a dedicated Windows test PC, a fresh temporary project and temporary HOME/TMPDIR/caches per run, skill installed (treatment) or absent (control) or absent with the skill's core requirement squeezed into one sentence appended to the prompt (one-liner group), 2 runs per group per task, network off, /tmp writes blocked. The prompts never mention the skill (except the wording noted below).
One chat-composer component (Composer.tsx) with a pile of boolean props. Tasks: refactor it into a flexible API keeping behaviour, add a 'quick reply' one-line composer without adding more booleans, and review the component API (no edits). Prompts do not name any pattern. The one-liner names the patterns (explicit variants from compound parts, shared context with state/actions, children instead of renderX, state lifted into a provider).
- Replace boolean props with composition — Composer.tsx has grown a pile of boolean props. Refactor it into a flexible component API that does not need them. Keep it in the single file Composer.tsx and keep the behaviour.
- Add a variant without adding props — Add a 'quick reply' composer: a one-line input that sends on Enter, with no attachments or emoji button. Do not add more boolean props to Composer. Edit Composer.tsx.
- Review a component API — Review the component API of Composer.tsx: what is wrong with it and what would you change? Do not change any files.
Scoring is programmatic (hidden tests / planted-issue recall / constraint checks / render+OCR where relevant), 0–5 per run. No LLM judge decided any score.
Results
The one-liner given to the third group: "Avoid boolean prop proliferation: split modes into explicit variant components built from compound parts that share a context with a generic state/actions interface, use children instead of renderX props, and lift state into a provider."
| Task | Control score | One-liner | With skill | Control tokens | One-liner tokens | With-skill tokens | Skill read |
|---|---|---|---|---|---|---|---|
| t1-refactor | 3.0 | 4.5 | 4.0 | 126k | 127k | 144k | 2/2 |
| t2-add-variant | 4.38 | 5.0 | 5.0 | 99k | 106k | 100k | 2/2 |
| t3-review | 3.75 | 4.58 | 4.17 | 81k | 80k | 70k | 2/2 |
Mean score: 3.71 without vs 4.39 with the skill (delta +0.68). Tokens per run: 103k vs 105k (1.03x); time 53s vs 49s. One-liner group: 4.7 (the skill's gain over the one-liner: -0.31; one-liner's own gain over no skill: +0.99); one-liner tokens 105k per run, so the skill costs 1.00x the one-liner.
What happened
- Control 3.71, skill 4.39, one-liner 4.70 (n=6 each). Without guidance, control's refactors did use compound parts but in both runs the shared context did not expose a state/actions interface, and in both quick-reply runs the new composer did not send on Enter.
- The skill's refactors had the context with state and actions (2/2) and its quick-reply composer sent on Enter (2/2). But both treatment refactors, like control, did not create explicit variant components (the one-liner runs did in 2/2), and in the review task both treatment runs and both control runs missed the 'boolean prop proliferation' point (a weak check pattern: reviewers may word it differently).
- Tokens are the same for skill, one-liner and control (105k, 105k, 103k): the skill read in 6/6 treatment runs without being named, and costs nothing extra.
- Treatment runs copy the rules folder into the project (14.7 new files per run).
Dimension scores
| Dimension | Score | Why |
|---|---|---|
| 1_output_quality | 4.39 | Mean score 4.39/5 with the skill (programmatic checks, see per-task table). |
| 2_lift_vs_no_skill | 2.68 | +0.68 points vs 3.71 without the skill (n=2 per task; under ~1 point is noise). |
| 3_trigger | 5.0 | Skill file read in 6/6 treatment runs. |
| 4_instructions_portability | 4 | SKILL.md plus an AGENTS.md index and a rules folder; docs only. |
| 5_efficiency | 5 | 1.03x tokens (105k vs 103k per run) and 0.93x time (49s vs 53s). |
| 6_safety_hygiene | 4 | Prose only; see security.md. |
Limits of this test
- Rule-based checks on code text (context/provider patterns, variant components, key props); the TypeScript was not compiled or rendered (React is not installed in the sandbox).
- One check for the review task is weak: it looks for the literal 'boolean prop' wording, which reviewers can express differently.
- Single component fixture; n=2 per cell.
- n = 2 per group per task: differences under about 1 point are noise.
- Rule-based scoring measures what the checks can see; it cannot judge taste or whether a result is the best possible.
Security
See security.md. Scan: 0 CRITICAL / 0 HIGH / 0 MEDIUM. Verdict: safe to run (no-network sandbox). A clean scan does not prove a skill safe.
Bottom line
A decent pattern guide: the compound-component structure appears reliably with the skill and rarely without it, for free. A single sentence naming the patterns did better here, so the skill's benefit is convenience and breadth, not a quality ceiling.
Transcript
Run log for composition-patterns: Codex CLI, network off, per-run temp HOME/TMPDIR, one row per graded run. Columns: task, arm-run, score (0-5), tokens, seconds, checks passed, skill files the agent opened. t1-refactor control-1: score 3.0, tokens 130032, secs 72.5, checks 3/5, opened nothing t1-refactor control-2: score 3.0, tokens 123317, secs 64.3, checks 3/5, opened nothing t1-refactor oneliner-1: score 4.0, tokens 127545, secs 101.8, checks 4/5, opened nothing t1-refactor oneliner-2: score 5.0, tokens 127024, secs 89.6, checks 5/5, opened nothing t1-refactor treatment-1: score 4.0, tokens 158276, secs 67.7, checks 4/5, opened composition-patterns t1-refactor treatment-2: score 4.0, tokens 130234, secs 59.2, checks 4/5, opened composition-patterns t2-add-variant control-1: score 4.38, tokens 99946, secs 40.3, checks 3/4, opened nothing t2-add-variant control-2: score 4.38, tokens 100013, secs 39.2, checks 3/4, opened nothing t2-add-variant oneliner-1: score 5.0, tokens 106946, secs 70.9, checks 4/4, opened nothing
18 runs, 3 artifacts
Sample output
SOTA · With the skill: Composer.tsx (t1-refactor, run 1, Codex)
import React, { createContext, use, useState } from "react"; type ComposerProps = { initialText?: string; onSubmit: (text: string) => void; children?: React.ReactNode; }; type ComposerContextValue = { state: { text: string }; actions: { setText: (text: string) => void; submit: () => void; }; }; const ComposerContext = createContext<ComposerContextValue | null>(null); export function useComposer() { const context = use(ComposerContext); if (!context) { throw new Error("Composer components must be inside Composer.Provider or Composer."); } return context; } function ComposerProvider({ initialText = "", onSubmit, children }: ComposerProps) { const [text, setText] = useState(initialText); function submit() { onSubmit(text); setText(""); } return ( <ComposerContext value={{ state: { text }, actions: { setText, submit } }}> {children} </ComposerContext> ); } function ComposerFrame({ children }: { children: React.ReactNode }) { const { actions } = useComposer(); return ( <form onSubmit={(event) => { event.preventDefault(); actions.submit(); }}> {children} </form> ); } function ComposerInput({ placeholder = "Message" }: { placeholder?: string }) { const { state, actions } = useComposer(); return ( <textarea value={state.text} onChange={(event) => actions.setText(event.target.value)} placeholder={placeholder} /> ); } function ComposerFormatting() { return ( <div className="toolbar"> <button type="button">B</button> <button type="button">I</button> </div> ); } function ComposerAttachments() { return <button type="button">Attach</button>; } function ComposerEmojis() { return <button type="button">😀</button>; } function ComposerSubmit({ children = "Send" }: { children?: React.ReactNode }) { return <button type="submit">{children}</button>; } function ComposerRoot({ children, ...props }: ComposerProps) { return ( <ComposerProvider {...props}> <ComposerFrame> {children === undefined ? <><ComposerInput /><ComposerSubmit /></> : children} </ComposerFrame> </ComposerProvider> ); } /** * Compose controls in their display order; footer content is ordinary children: * * <Composer initialText="Draft" onSubmit={send}> * <Composer.Formatting /> * <Composer.Input placeholder="Reply in thread" /> * <Composer.Attachments /> * <Composer.Emojis /> * <Composer.Submit /> * <CustomFooter /> * </Composer> * * Message: use the default input placeholder and submit label. * Thread: use "Reply in thread" and "Send". * Forward: use "Add a note" and "Forward". * Edit: use "Edit message" and "Save", and omit Attachments. * When migrating overlapping modes, edit takes precedence over forward, then thread. * Include Formatting, Attachments, and Emojis only where needed. * Provider and Frame can also be composed separately to share state with siblings. */ export const Composer = Object.assign(ComposerRoot, { Provider: ComposerProvider, Frame: ComposerFrame, Input: ComposerInput, Formatting: ComposerFormatting, Attachments: ComposerAttachments, Emojis: [... excerpt: first 3200 of 3247 characters]