{"slug": "design-insufficiency-a-stack-agnostic-claude-code-skill-find-where-a-feature-is", "title": "design-insufficiency — a stack-agnostic Claude Code skill: find where a feature is badly designed for the human using it (Nielsen smell scan + handicapped-persona walkthrough + archetype completeness…", "summary": "A developer released design-insufficiency, a stack-agnostic Claude Code skill that audits features for usability flaws by combining a Nielsen heuristic scan, handicapped-persona walkthroughs, and an archetype completeness diff, with Skeptic and Referee agents verifying findings. The skill is designed for multi-agent orchestration runtimes and includes safeguards to prevent empty-scope runs that could audit the wrong code.", "body_md": "|\n// design-insufficiency pipeline: three tracks -> Skeptic -> Referee -> cross-link. |\n|\n// Track A (static smell / heuristic evaluation): one Scanner per Nielsen heuristic cluster (detectors.md). |\n|\n// Track B (role-play / cognitive walkthrough): one Walker per handicapped persona cluster (personas.md). |\n|\n// Track C (capability completeness): classify the archetype, diff expected capabilities vs code (archetypes.md). |\n|\n// All tracks feed the same Skeptic -> Referee verdict pipeline, then findings are cross-linked. |\n|\n// |\n|\n// Written for a multi-agent orchestration runtime that provides `agent(prompt, opts)`, `parallel(thunks)` |\n|\n// and an injected `args` global (e.g. Claude Code's Workflow tool). If your harness has no such runtime, |\n|\n// treat this file as the spec: dispatch one read-only subagent per HEURISTICS/PERSONAS entry with the |\n|\n// same prompts, then one Skeptic + one Referee per finding, and apply the same scope-fence and |\n|\n// cross-link logic by hand. |\n|\n// |\n|\n// TEMPLATE — before running: |\n|\n// 1. Fill SCOPE_FILES + ROUTE_CONTRACT_MAP from SKILL.md Step 1 (routes, components, request/error contracts). |\n|\n// 2. Set STACK_CONTEXT to your stack (framework, design-system component set + prefix, token source, |\n|\n// where the typed API contracts live). |\n|\n// 3. Trim HEURISTICS / PERSONAS to what applies (drop e.g. bulk personas if it's not a list screen). |\n|\n// 4. Paste the relevant detectors.md rows into DETECTOR_GUIDANCE and the persona rows into PERSONA_GUIDANCE. |\n|\n|\n|\nexport const meta = { |\n|\nname: 'design-insufficiency', |\n|\ndescription: 'Static usability-smell scan + handicapped-persona walkthrough + archetype completeness diff -> Skeptic -> Referee -> cross-link, over one feature scope', |\n|\nphases: [ |\n|\n{ title: 'Scan', detail: 'static usability-smell scanner per Nielsen heuristic cluster' }, |\n|\n{ title: 'Walk', detail: 'cognitive walkthrough per handicapped persona cluster' }, |\n|\n{ title: 'Complete', detail: 'classify archetype + diff expected capabilities (research-backed) vs code' }, |\n|\n{ title: 'Skeptic', detail: 'try to disprove each smell/wall/gap (find the path/affordance/capability missed)' }, |\n|\n{ title: 'Referee', detail: 'binding REAL_GAP / HAS_PATH / MANUAL_REVIEW verdict' }, |\n|\n], |\n|\n} |\n|\n|\n|\n// ---- ARGS NORMALISATION (do not skip) ---- |\n|\n// If the caller passes `args` as a JSON *string* (easy mistake), `args?.files` reads undefined and EVERY |\n|\n// field silently falls back to its default — an EMPTY SCOPE_FILES, which makes every scanner roam the |\n|\n// whole repo and audit the WRONG feature (this actually happened: a 163-agent run scanned an unrelated |\n|\n// module while the target was an import wizard). Coerce string->object so args.files/args.map are read. |\n|\nconst A = typeof args === 'string' ? JSON.parse(args) : (args || {}) |\n|\n|\n|\n// ---- FILL THESE IN (or supply via args) ---- |\n|\nconst SCOPE_FILES = A.files || [ |\n|\n// 'src/app/orders/order-form/order-form.component.ts', |\n|\n// 'src/api/generated/CreateOrder.ts', |\n|\n] |\n|\n// FAIL LOUD, never scan nothing: an empty scope is always a caller error, not a valid \"scan everything\". |\n|\n// A silent empty-scope run wastes a full fan-out on the wrong code and looks like it worked. |\n|\nif (!Array.isArray(SCOPE_FILES) || SCOPE_FILES.length === 0) { |\n|\nthrow new Error('design-insufficiency: SCOPE_FILES is empty. Pass `args` as a JSON OBJECT (not a stringified JSON) with a non-empty `files` array from SKILL Step 1. An empty scope makes every scanner roam the whole repo and audit the wrong feature.') |\n|\n} |\n|\n// The route/contract map from SKILL Step 1 — routes, nav depth, request-field TYPES, error outcomes. |\n|\nconst ROUTE_CONTRACT_MAP = A.map || '<paste the route list + contract summary here>' |\n|\nif (ROUTE_CONTRACT_MAP.startsWith('<paste')) { |\n|\nthrow new Error('design-insufficiency: ROUTE_CONTRACT_MAP is the unfilled placeholder. Supply args.map (the route + request/error contract summary from SKILL Step 1) — without it the tracks are ungrounded and hallucinate screens.') |\n|\n} |\n|\n// Describe YOUR stack: framework, design-system component set + prefix, token source, contract location. |\n|\nconst STACK_CONTEXT = A.stack || |\n|\n'A component-based web UI calling a typed API. Design-system components live in <DS_COMPONENTS>; colour/spacing tokens in <DS_TOKENS>; typed request/error contracts in <CONTRACTS>.' |\n|\n|\n|\n// Track A — Nielsen heuristic clusters (detectors.md). Drop any that can't apply to the scope. |\n|\nconst HEURISTICS = A.heuristics || [ |\n|\n'H2-real-world (raw ID/JSON/enum-as-text/jargon, hardcoded or unlocalised terminology)', |\n|\n'H3-user-control (view-no-edit, no-undo, destructive-no-confirm)', |\n|\n'H5-error-prevention (text-for-number/date, rich-text-as-textarea, no-bounds)', |\n|\n'H1+H9-status+errors (async-button-no-spinner, long-op-bare-spinner-not-progress, background-job-appears-idle, no-cancel/timeout, no loading/empty/success state, unhandled error branch)', |\n|\n'H4-consistency (raw primitive where a DS component exists, off-token colour, unstyled screen)', |\n|\n'H6+H10-recognition+wayfinding (placeholder-as-label, breadcrumb, orphan route)', |\n|\n'H8-layout (too-many-columns, missing/inconsistent padding, cramped)', |\n|\n'H8-scalability (per-item control overflow at high N, fixed-slot long text, unbounded list no scroll, seed-data-only layout)', |\n|\n] |\n|\n// Track B — handicapped persona clusters (personas.md). |\n|\nconst PERSONAS = A.personas || [ |\n|\n'field-worker-mobile', 'new-hire-day1', 'back-office-bulk', |\n|\n'finance-auditor', 'the-corrector', 'the-returner', 'wrong-order-user', 'the-handoff', |\n|\n] |\n|\n// Track C — archetype(s) this feature is. Leave null to let the agent classify from the code. |\n|\nconst ARCHETYPES = A.archetypes || null // e.g. ['outbound-email', 'rich-text-editor', 'audience-selector'] |\n|\nconst ARCHETYPE_GUIDANCE = A.archetypeGuidance || '' // pasted archetypes.md checklist rows for the classified kinds |\n|\nconst ALLOW_RESEARCH = A.allowResearch !== false // Track C may search the web for archetype checklists |\n|\nconst DETECTOR_GUIDANCE = A.detectorGuidance || {} // heuristic cluster -> pasted detectors.md rows |\n|\nconst PERSONA_GUIDANCE = A.personaGuidance || {} // persona -> pasted personas.md row + intents |\n|\n// ----------------------- |\n|\n|\n|\n// Set of scope files, for the hard scope-fence: findings anchored outside these are discarded. |\n|\nconst SCOPE_SET = new Set(SCOPE_FILES) |\n|\n// The one instruction every track shares — subagents have Grep/Glob/Read and WILL roam the whole repo |\n|\n// unless told not to. This is why earlier runs audited an unrelated module. |\n|\nconst SCOPE_FENCE = ` |\n|\nHARD SCOPE FENCE (non-negotiable): The ONLY files you may open, scan, grep, or report on are the scope |\n|\nfiles listed above. Do NOT Glob/Grep the wider repo, do NOT wander into neighbouring features, do NOT |\n|\nreport a finding whose file is not one of the scope files. You may read a file named in the route/contract |\n|\nmap for context, but every FINDING you output must anchor to a scope file. If you find nothing in scope, |\n|\nreturn an empty findings array — an empty result is correct, roaming out of scope is not.` |\n|\n|\n|\nconst FINDINGS_SCHEMA = { |\n|\ntype: 'object', required: ['findings'], |\n|\nproperties: { |\n|\nfindings: { |\n|\ntype: 'array', |\n|\nitems: { |\n|\ntype: 'object', |\n|\nrequired: ['track', 'file', 'line', 'heuristic', 'problem', 'expected', 'offered', 'severity'], |\n|\nproperties: { |\n|\ntrack: { enum: ['smell', 'role-play', 'completeness'] }, |\n|\npersona: { type: 'string', description: 'role-play only: who hit the wall' }, |\n|\nintent: { type: 'string', description: 'role-play only: what they wanted to do' }, |\n|\nfile: { type: 'string' }, |\n|\nline: { type: 'integer' }, |\n|\nheuristic: { type: 'string', description: 'Nielsen H# / wall_type / archetype' }, |\n|\nproblem: { type: 'string', description: 'the smell or the wall, concretely' }, |\n|\nexpected: { type: 'string', description: 'what a reasonable person expects' }, |\n|\noffered: { type: 'string', description: 'what the design actually offers (cite the real control/route)' }, |\n|\nsuggested_fix: { type: 'string' }, |\n|\nconfidence: { enum: ['A-deterministic', 'B-heuristic'] }, |\n|\nseverity: { enum: ['critical', 'high', 'medium', 'low'] }, |\n|\n}, |\n|\n}, |\n|\n}, |\n|\n}, |\n|\n} |\n|\nconst SKEPTIC_SCHEMA = { |\n|\ntype: 'object', required: ['disproven', 'counter_evidence'], |\n|\nproperties: { disproven: { type: 'boolean' }, counter_evidence: { type: 'string' } }, |\n|\n} |\n|\nconst VERDICT_SCHEMA = { |\n|\ntype: 'object', required: ['verdict', 'justification'], |\n|\nproperties: { verdict: { enum: ['REAL_GAP', 'HAS_PATH', 'MANUAL_REVIEW'] }, justification: { type: 'string' } }, |\n|\n} |\n|\n|\n|\nconst fileList = SCOPE_FILES.map(f => `- ${f}`).join('\\n') |\n|\n|\n|\nfunction scannerPrompt(cluster) { |\n|\nconst extra = DETECTOR_GUIDANCE[cluster] ? `\\nDetectors to run (from detectors.md):\\n${DETECTOR_GUIDANCE[cluster]}\\n` : '' |\n|\nreturn `You are a static USABILITY-SMELL SCANNER. Scan ONLY for this heuristic cluster: ${cluster}. |\n|\nScope files: |\n|\n${fileList} |\n|\nRoute/contract map (join controls against these TYPES — a control's data type decides the right control): |\n|\n${ROUTE_CONTRACT_MAP} |\n|\nStack: ${STACK_CONTEXT}.${extra}${SCOPE_FENCE} |\n|\nFor EACH smell output: track=\"smell\", file, line, heuristic (H#), problem, expected, offered (cite the |\n|\nreal control/route), suggested_fix (the design-system component / picker / dropdown / edit route it |\n|\nshould be), confidence (A-deterministic for type<->control mismatches & unhandled error branches & |\n|\norphan update-operations; else B-heuristic), severity. |\n|\nRULES: |\n|\n- Cite a REAL file:line IN A SCOPE FILE. No anchor, or an anchor outside scope => discard. |\n|\n- Contract-join first: compare each control against the request field TYPE and the operation's error outcomes. |\n|\n- Only this cluster. Return the findings array (empty if none).` |\n|\n} |\n|\n|\n|\nfunction walkerPrompt(persona) { |\n|\nconst extra = PERSONA_GUIDANCE[persona] ? `\\nPersona + intents (from personas.md):\\n${PERSONA_GUIDANCE[persona]}\\n` : '' |\n|\nreturn `You are ROLE-PLAYING the \"${persona}\" user. You are NOT trying to complete the task cleverly — |\n|\nyou are trying to find WHERE YOU WOULD GIVE UP, given your handicap. |\n|\nScope files: |\n|\n${fileList} |\n|\nREAL route/contract map (do NOT invent screens; only walk what's here): |\n|\n${ROUTE_CONTRACT_MAP} |\n|\nStack: ${STACK_CONTEXT}.${extra}${SCOPE_FENCE} |\n|\nWalk your intents step by step against the real routes/controls. Mark each step |\n|\navailable|hidden|missing|requires-workaround. Stop at the first WALL. |\n|\nFor EACH wall output: track=\"role-play\", persona, intent, file, line (of the missing/broken affordance), |\n|\nheuristic (wall_type: missing-affordance|dead-end|hidden-path|forced-workaround|unhandled-reverse|context-assumed), |\n|\nproblem, expected, offered, suggested_fix, confidence=\"B-heuristic\", severity. |\n|\nRULES: |\n|\n- Real file:line IN A SCOPE FILE only. \"Feels clunky\" with no anchor, or an anchor outside scope => discard. |\n|\n- If a path MIGHT exist off-map, say so in offered (\"verify: possible path via X\") and let the Skeptic check — do not assume missing. |\n|\n- Report where you'd GIVE UP, not a completion. Return the findings array (empty if none).` |\n|\n} |\n|\n|\n|\nfunction completenessPrompt() { |\n|\nconst kinds = ARCHETYPES ? `This feature's archetype(s): ${ARCHETYPES.join(', ')}.` : |\n|\n`First CLASSIFY this feature into its archetype(s) from the operations / entity / request shape / routes.` |\n|\nconst research = ALLOW_RESEARCH |\n|\n? `You MAY search the web for \"<archetype> feature checklist / best practices\" and the 2-3 leading products' feature lists — especially for unusual/domain-specific archetypes. Tag each researched capability with its source URL in suggested_fix.` |\n|\n: `Use only the baked archetype library guidance below (no web search).` |\n|\nreturn `You are a CAPABILITY-COMPLETENESS analyst. Find whole capabilities a feature of THIS KIND |\n|\nnormally has but this one is MISSING (e.g. an outbound-email screen with no Subject; an import with no |\n|\nerror report). ${kinds} |\n|\nScope files: |\n|\n${fileList} |\n|\nRoute/contract map (operations, request fields, routes): |\n|\n${ROUTE_CONTRACT_MAP} |\n|\nStack: ${STACK_CONTEXT}. |\n|\nArchetype checklist guidance (baked library): |\n|\n${ARCHETYPE_GUIDANCE || '(none supplied — derive from the archetype + research)'} |\n|\n${research}${SCOPE_FENCE} |\n|\n(This track may consult the WEB and read a map-named contract for context, but every reported gap must |\n|\nstill anchor to a SCOPE FILE — where the missing capability should live.) |\n|\nMethod: classify -> build expected-capability checklist (banded table-stakes/expected/maturity) -> |\n|\nDIFF each expected capability against the code (present as a request field / operation / route / control, |\n|\nor absent?) -> report each ABSENT, plausibly-expected capability. |\n|\nFor EACH gap output: track=\"completeness\", file (where it SHOULD live — the request type/operation/route/component, |\n|\neven though absent), line (best anchor, or the screen/contract file), heuristic=the archetype, |\n|\nproblem (the missing capability), expected (why users of this archetype assume it), offered (what exists |\n|\ninstead / nothing), suggested_fix (+ source URL if researched), confidence=\"B-heuristic\", |\n|\nseverity (table-stakes=critical/high, expected=medium, maturity=low). |\n|\nRULES: |\n|\n- Only report capabilities genuinely EXPECTED for this archetype in THIS context. Do NOT inflate maturity |\n|\nfeatures to critical. An internal password-reset mail does not need A/B testing. |\n|\n- Ground every gap: name where it should live. If it might already exist off-scope, say \"verify: possibly via X\". |\n|\nReturn the findings array (empty if the feature is complete).` |\n|\n} |\n|\n|\n|\nconst skepticPrompt = (f) => `You are a SKEPTIC. Try to DISPROVE this design-insufficiency claim. |\n|\nRead defensively: find the picker sibling, the link from another screen, the edit route reachable |\n|\nelsewhere, the global error interceptor or error boundary, the parent padding class, the confirm dialog |\n|\nthat already exists. |\n|\nClaim: ${JSON.stringify(f)} |\n|\nScope: ${fileList} |\n|\nMap: ${ROUTE_CONTRACT_MAP} |\n|\nSet disproven=true only if you genuinely find the path/affordance; cite it (file:line + why) in counter_evidence.` |\n|\n|\n|\nconst refereePrompt = (f, s) => `You are the REFEREE. Independent binding verdict. Re-check the cited code + map yourself. |\n|\nFinding: ${JSON.stringify(f)} |\n|\nSkeptic: ${JSON.stringify(s)} |\n|\nScope: ${fileList} |\n|\nVerdict REAL_GAP | HAS_PATH | MANUAL_REVIEW + one-line justification. |\n|\nMANUAL_REVIEW only when it needs a human eye on rendered pixels to judge.` |\n|\n|\n|\nconst verdictOf = (findingsPromise) => |\n|\nfindingsPromise.then(res => parallel((res?.findings || []).map(f => () => |\n|\nagent(skepticPrompt(f), { label: `skeptic:${f.file}:${f.line}`, phase: 'Skeptic', schema: SKEPTIC_SCHEMA }) |\n|\n.then(sk => agent(refereePrompt(f, sk), { label: `referee:${f.file}:${f.line}`, phase: 'Referee', schema: VERDICT_SCHEMA }) |\n|\n.then(v => ({ ...f, skeptic: sk, verdict: v.verdict, justification: v.justification })))))) |\n|\n|\n|\n// All three tracks run in parallel, and each finding flows to Skeptic+Referee as soon as it lands. |\n|\nconst smellRuns = HEURISTICS.map(h => () => |\n|\nverdictOf(agent(scannerPrompt(h), { label: `scan:${h}`, phase: 'Scan', schema: FINDINGS_SCHEMA }))) |\n|\nconst walkRuns = PERSONAS.map(p => () => |\n|\nverdictOf(agent(walkerPrompt(p), { label: `walk:${p}`, phase: 'Walk', schema: FINDINGS_SCHEMA }))) |\n|\nconst completenessRun = () => |\n|\nverdictOf(agent(completenessPrompt(), { label: 'complete:archetype', phase: 'Complete', schema: FINDINGS_SCHEMA })) |\n|\n|\n|\nconst results = await parallel([...smellRuns, ...walkRuns, completenessRun]) |\n|\nconst rawAll = results.flat().filter(Boolean) |\n|\n// Code-level scope-fence backstop: drop any finding whose anchor is NOT a scope file, so a subagent that |\n|\n// ignored the prompt fence and roamed cannot leak off-scope findings into the report. Surfaced as a count |\n|\n// (dropped_out_of_scope) rather than silently — a non-zero value means a scanner wandered. |\n|\nconst all = rawAll.filter(f => SCOPE_SET.has(f.file)) |\n|\nconst droppedOutOfScope = rawAll.length - all.length |\n|\nconst surviving = all.filter(f => f.verdict === 'REAL_GAP' || f.verdict === 'MANUAL_REVIEW') |\n|\n|\n|\n// Cross-link: a smell and a wall at the same file (± a few lines) = two methods agreeing -> promote. |\n|\nconst promote = { critical: 'critical', high: 'critical', medium: 'high', low: 'medium' } |\n|\nconst crossLinked = [] |\n|\nfor (const f of surviving) { |\n|\nconst match = surviving.find(g => g !== f && g.track !== f.track && g.file === f.file && Math.abs((g.line || 0) - (f.line || 0)) <= 8) |\n|\ncrossLinked.push(match ? { ...f, track: 'both', cross_linked_with: `${match.track}:${match.file}:${match.line}`, severity: promote[f.severity] || f.severity } : f) |\n|\n} |\n|\n|\n|\nconst sevRank = { critical: 0, high: 1, medium: 2, low: 3 } |\n|\nconst tierRank = { 'A-deterministic': 0, 'B-heuristic': 1 } |\n|\nconst rank = (a, b) => (tierRank[a.confidence] ?? 9) - (tierRank[b.confidence] ?? 9) || (sevRank[a.severity] ?? 9) - (sevRank[b.severity] ?? 9) |\n|\n|\n|\nreturn { |\n|\nheuristics_scanned: HEURISTICS, |\n|\npersonas_walked: PERSONAS, |\n|\narchetypes: ARCHETYPES || 'auto-classified by agent', |\n|\nscope_file_count: SCOPE_FILES.length, |\n|\nscope_files: SCOPE_FILES, |\n|\ndropped_out_of_scope: droppedOutOfScope, // >0 means a subagent roamed past the scope fence — investigate |\n|\nreal_gaps: crossLinked.filter(f => f.verdict === 'REAL_GAP').sort(rank), |\n|\nmanual_review: crossLinked.filter(f => f.verdict === 'MANUAL_REVIEW').sort(rank), |\n|\nfalse_positives_killed: all.filter(f => f.verdict === 'HAS_PATH').length, |\n|\n} |", "url": "https://wpnews.pro/news/design-insufficiency-a-stack-agnostic-claude-code-skill-find-where-a-feature-is", "canonical_source": "https://gist.github.com/paulmorrishill/4bc1a845c289f164f4397d5d616ef885", "published_at": "2026-08-17 08:10:38+00:00", "updated_at": "2026-08-20 10:44:35.819729+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-products"], "entities": ["Claude Code", "Nielsen"], "alternates": {"html": "https://wpnews.pro/news/design-insufficiency-a-stack-agnostic-claude-code-skill-find-where-a-feature-is", "markdown": "https://wpnews.pro/news/design-insufficiency-a-stack-agnostic-claude-code-skill-find-where-a-feature-is.md", "text": "https://wpnews.pro/news/design-insufficiency-a-stack-agnostic-claude-code-skill-find-where-a-feature-is.txt", "jsonld": "https://wpnews.pro/news/design-insufficiency-a-stack-agnostic-claude-code-skill-find-where-a-feature-is.jsonld"}}