cd /news/developer-tools/design-insufficiency-a-stack-agnosti… · home topics developer-tools article
[ARTICLE · art-104273] src=gist.github.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

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…

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.

read14 min views7 publishedAug 17, 2026

| // design-insufficiency pipeline: three tracks -> Skeptic -> Referee -> cross-link. | | // Track A (static smell / heuristic evaluation): one Scanner per Nielsen heuristic cluster (detectors.md). | | // Track B (role-play / cognitive walkthrough): one Walker per handicapped persona cluster (personas.md). | | // Track C (capability completeness): classify the archetype, diff expected capabilities vs code (archetypes.md). | | // All tracks feed the same Skeptic -> Referee verdict pipeline, then findings are cross-linked. | | // | | // Written for a multi-agent orchestration runtime that provides agent(prompt, opts), parallel(thunks) | | // and an injected args global (e.g. Claude Code's Workflow tool). If your harness has no such runtime, | | // treat this file as the spec: dispatch one read-only subagent per HEURISTICS/PERSONAS entry with the | | // same prompts, then one Skeptic + one Referee per finding, and apply the same scope-fence and | | // cross-link logic by hand. | | // | | // TEMPLATE — before running: | | // 1. Fill SCOPE_FILES + ROUTE_CONTRACT_MAP from SKILL.md Step 1 (routes, components, request/error contracts). | | // 2. Set STACK_CONTEXT to your stack (framework, design-system component set + prefix, token source, | | // where the typed API contracts live). | | // 3. Trim HEURISTICS / PERSONAS to what applies (drop e.g. bulk personas if it's not a list screen). | | // 4. Paste the relevant detectors.md rows into DETECTOR_GUIDANCE and the persona rows into PERSONA_GUIDANCE. | | | | export const meta = { | | name: 'design-insufficiency', | | description: 'Static usability-smell scan + handicapped-persona walkthrough + archetype completeness diff -> Skeptic -> Referee -> cross-link, over one feature scope', | | phases: [ | | { title: 'Scan', detail: 'static usability-smell scanner per Nielsen heuristic cluster' }, | | { title: 'Walk', detail: 'cognitive walkthrough per handicapped persona cluster' }, | | { title: 'Complete', detail: 'classify archetype + diff expected capabilities (research-backed) vs code' }, | | { title: 'Skeptic', detail: 'try to disprove each smell/wall/gap (find the path/affordance/capability missed)' }, | | { title: 'Referee', detail: 'binding REAL_GAP / HAS_PATH / MANUAL_REVIEW verdict' }, | | ], | | } | | | | // ---- ARGS NORMALISATION (do not skip) ---- | | // If the caller passes args as a JSON string (easy mistake), args?.files reads undefined and EVERY | | // field silently falls back to its default — an EMPTY SCOPE_FILES, which makes every scanner roam the | | // whole repo and audit the WRONG feature (this actually happened: a 163-agent run scanned an unrelated | | // module while the target was an import wizard). Coerce string->object so args.files/args.map are read. | | const A = typeof args === 'string' ? JSON.parse(args) : (args || {}) | | | | // ---- FILL THESE IN (or supply via args) ---- | | const SCOPE_FILES = A.files || [ | | // 'src/app/orders/order-form/order-form.component.ts', | | // 'src/api/generated/CreateOrder.ts', | | ] | | // FAIL LOUD, never scan nothing: an empty scope is always a caller error, not a valid "scan everything". | | // A silent empty-scope run wastes a full fan-out on the wrong code and looks like it worked. | | if (!Array.isArray(SCOPE_FILES) || SCOPE_FILES.length === 0) { | | throw 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.') | | } | | // The route/contract map from SKILL Step 1 — routes, nav depth, request-field TYPES, error outcomes. | | const ROUTE_CONTRACT_MAP = A.map || '<paste the route list + contract summary here>' | | if (ROUTE_CONTRACT_MAP.startsWith('<paste')) { | | throw 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.') | | } | | // Describe YOUR stack: framework, design-system component set + prefix, token source, contract location. | | const STACK_CONTEXT = A.stack || | | '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>.' | | | | // Track A — Nielsen heuristic clusters (detectors.md). Drop any that can't apply to the scope. | | const HEURISTICS = A.heuristics || [ | | 'H2-real-world (raw ID/JSON/enum-as-text/jargon, hardcoded or unlocalised terminology)', | | 'H3-user-control (view-no-edit, no-undo, destructive-no-confirm)', | | 'H5-error-prevention (text-for-number/date, rich-text-as-textarea, no-bounds)', | | 'H1+H9-status+errors (async-button-no-spinner, long-op-bare-spinner-not-progress, background-job-appears-idle, no-cancel/timeout, no /empty/success state, unhandled error branch)', | | 'H4-consistency (raw primitive where a DS component exists, off-token colour, unstyled screen)', | | 'H6+H10-recognition+wayfinding (placeholder-as-label, breadcrumb, orphan route)', | | 'H8-layout (too-many-columns, missing/inconsistent padding, cramped)', | | 'H8-scalability (per-item control overflow at high N, fixed-slot long text, unbounded list no scroll, seed-data-only layout)', | | ] | | // Track B — handicapped persona clusters (personas.md). | | const PERSONAS = A.personas || [ | | 'field-worker-mobile', 'new-hire-day1', 'back-office-bulk', | | 'finance-auditor', 'the-corrector', 'the-returner', 'wrong-order-user', 'the-handoff', | | ] | | // Track C — archetype(s) this feature is. Leave null to let the agent classify from the code. | | const ARCHETYPES = A.archetypes || null // e.g. ['outbound-email', 'rich-text-editor', 'audience-selector'] | | const ARCHETYPE_GUIDANCE = A.archetypeGuidance || '' // pasted archetypes.md checklist rows for the classified kinds | | const ALLOW_RESEARCH = A.allowResearch !== false // Track C may search the web for archetype checklists | | const DETECTOR_GUIDANCE = A.detectorGuidance || {} // heuristic cluster -> pasted detectors.md rows | | const PERSONA_GUIDANCE = A.personaGuidance || {} // persona -> pasted personas.md row + intents | | // ----------------------- | | | | // Set of scope files, for the hard scope-fence: findings anchored outside these are discarded. | | const SCOPE_SET = new Set(SCOPE_FILES) | | // The one instruction every track shares — subagents have Grep/Glob/Read and WILL roam the whole repo | | // unless told not to. This is why earlier runs audited an unrelated module. | | const SCOPE_FENCE = | | HARD SCOPE FENCE (non-negotiable): The ONLY files you may open, scan, grep, or report on are the scope | | files listed above. Do NOT Glob/Grep the wider repo, do NOT wander into neighbouring features, do NOT | | report a finding whose file is not one of the scope files. You may read a file named in the route/contract | | map for context, but every FINDING you output must anchor to a scope file. If you find nothing in scope, | | return an empty findings array — an empty result is correct, roaming out of scope is not. | | | | const FINDINGS_SCHEMA = { | | type: 'object', required: ['findings'], | | properties: { | | findings: { | | type: 'array', | | items: { | | type: 'object', | | required: ['track', 'file', 'line', 'heuristic', 'problem', 'expected', 'offered', 'severity'], | | properties: { | | track: { enum: ['smell', 'role-play', 'completeness'] }, | | persona: { type: 'string', description: 'role-play only: who hit the wall' }, | | intent: { type: 'string', description: 'role-play only: what they wanted to do' }, | | file: { type: 'string' }, | | line: { type: 'integer' }, | | heuristic: { type: 'string', description: 'Nielsen H# / wall_type / archetype' }, | | problem: { type: 'string', description: 'the smell or the wall, concretely' }, | | expected: { type: 'string', description: 'what a reasonable person expects' }, | | offered: { type: 'string', description: 'what the design actually offers (cite the real control/route)' }, | | suggested_fix: { type: 'string' }, | | confidence: { enum: ['A-deterministic', 'B-heuristic'] }, | | severity: { enum: ['critical', 'high', 'medium', 'low'] }, | | }, | | }, | | }, | | }, | | } | | const SKEPTIC_SCHEMA = { | | type: 'object', required: ['disproven', 'counter_evidence'], | | properties: { disproven: { type: 'boolean' }, counter_evidence: { type: 'string' } }, | | } | | const VERDICT_SCHEMA = { | | type: 'object', required: ['verdict', 'justification'], | | properties: { verdict: { enum: ['REAL_GAP', 'HAS_PATH', 'MANUAL_REVIEW'] }, justification: { type: 'string' } }, | | } | | | | const fileList = SCOPE_FILES.map(f => - ${f}).join('\n') | | | | function scannerPrompt(cluster) { | | const extra = DETECTOR_GUIDANCE[cluster] ? \nDetectors to run (from detectors.md):\n${DETECTOR_GUIDANCE[cluster]}\n : '' | | return `You are a static USABILITY-SMELL SCANNER. Scan ONLY for this heuristic cluster: ${cluster}. | | Scope files: | | ${fileList} | | Route/contract map (join controls against these TYPES — a control's data type decides the right control): | | ${ROUTE_CONTRACT_MAP} | | Stack: ${STACK_CONTEXT}.${extra}${SCOPE_FENCE} | | For EACH smell output: track="smell", file, line, heuristic (H#), problem, expected, offered (cite the | | real control/route), suggested_fix (the design-system component / picker / dropdown / edit route it | | should be), confidence (A-deterministic for type<->control mismatches & unhandled error branches & | | orphan update-operations; else B-heuristic), severity. | | RULES: | |

  • Cite a REAL file:line IN A SCOPE FILE. No anchor, or an anchor outside scope => discard. | |
  • Contract-join first: compare each control against the request field TYPE and the operation's error outcomes. | |
  • Only this cluster. Return the findings array (empty if none).| | } | | | | function walkerPrompt(persona) { | | const extra = PERSONA_GUIDANCE[persona] ?\nPersona + intents (from personas.md):\n${PERSONA_GUIDANCE[persona]}\n: '' | | returnYou are ROLE-PLAYING the "${persona}" user. You are NOT trying to complete the task cleverly — | | you are trying to find WHERE YOU WOULD GIVE UP, given your handicap. | | Scope files: | | ${fileList} | | REAL route/contract map (do NOT invent screens; only walk what's here): | | ${ROUTE_CONTRACT_MAP} | | Stack: ${STACK_CONTEXT}.${extra}${SCOPE_FENCE} | | Walk your intents step by step against the real routes/controls. Mark each step | | available|hidden|missing|requires-workaround. Stop at the first WALL. | | For EACH wall output: track="role-play", persona, intent, file, line (of the missing/broken affordance), | | heuristic (wall_type: missing-affordance|dead-end|hidden-path|forced-workaround|unhandled-reverse|context-assumed), | | problem, expected, offered, suggested_fix, confidence="B-heuristic", severity. | | RULES: | |
  • Real file:line IN A SCOPE FILE only. "Feels clunky" with no anchor, or an anchor outside scope => discard. | |
  • 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. | |
  • Report where you'd GIVE UP, not a completion. Return the findings array (empty if none).| | } | | | | function completenessPrompt() { | | const kinds = ARCHETYPES ?This feature's archetype(s): ${ARCHETYPES.join(', ')}.: | |First CLASSIFY this feature into its archetype(s) from the operations / entity / request shape / routes.| | const research = ALLOW_RESEARCH | | ?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.| | :Use only the baked archetype library guidance below (no web search).| | returnYou are a CAPABILITY-COMPLETENESS analyst. Find whole capabilities a feature of THIS KIND | | normally has but this one is MISSING (e.g. an outbound-email screen with no Subject; an import with no | | error report). ${kinds} | | Scope files: | | ${fileList} | | Route/contract map (operations, request fields, routes): | | ${ROUTE_CONTRACT_MAP} | | Stack: ${STACK_CONTEXT}. | | Archetype checklist guidance (baked library): | | ${ARCHETYPE_GUIDANCE || '(none supplied — derive from the archetype + research)'} | | ${research}${SCOPE_FENCE} | | (This track may consult the WEB and read a map-named contract for context, but every reported gap must | | still anchor to a SCOPE FILE — where the missing capability should live.) | | Method: classify -> build expected-capability checklist (banded table-stakes/expected/maturity) -> | | DIFF each expected capability against the code (present as a request field / operation / route / control, | | or absent?) -> report each ABSENT, plausibly-expected capability. | | For EACH gap output: track="completeness", file (where it SHOULD live — the request type/operation/route/component, | | even though absent), line (best anchor, or the screen/contract file), heuristic=the archetype, | | problem (the missing capability), expected (why users of this archetype assume it), offered (what exists | | instead / nothing), suggested_fix (+ source URL if researched), confidence="B-heuristic", | | severity (table-stakes=critical/high, expected=medium, maturity=low). | | RULES: | |
  • Only report capabilities genuinely EXPECTED for this archetype in THIS context. Do NOT inflate maturity | | features to critical. An internal password-reset mail does not need A/B testing. | |
  • Ground every gap: name where it should live. If it might already exist off-scope, say "verify: possibly via X". | | Return the findings array (empty if the feature is complete).| | } | | | | const skepticPrompt = (f) =>You are a SKEPTIC. Try to DISPROVE this design-insufficiency claim. | | Read defensively: find the picker sibling, the link from another screen, the edit route reachable | | elsewhere, the global error interceptor or error boundary, the parent padding class, the confirm dialog | | that already exists. | | Claim: ${JSON.stringify(f)} | | Scope: ${fileList} | | Map: ${ROUTE_CONTRACT_MAP} | | Set disproven=true only if you genuinely find the path/affordance; cite it (file:line + why) in counter_evidence.| | | | const refereePrompt = (f, s) =>You are the REFEREE. Independent binding verdict. Re-check the cited code + map yourself. | | Finding: ${JSON.stringify(f)} | | Skeptic: ${JSON.stringify(s)} | | Scope: ${fileList} | | Verdict REAL_GAP | HAS_PATH | MANUAL_REVIEW + one-line justification. | | MANUAL_REVIEW only when it needs a human eye on rendered pixels to judge.| | | | const verdictOf = (findingsPromise) => | | findingsPromise.then(res => parallel((res?.findings || []).map(f => () => | | agent(skepticPrompt(f), { label:skeptic:${f.file}:${f.line}, phase: 'Skeptic', schema: SKEPTIC_SCHEMA }) | | .then(sk => agent(refereePrompt(f, sk), { label: referee:${f.file}:${f.line}, phase: 'Referee', schema: VERDICT_SCHEMA }) | | .then(v => ({ ...f, skeptic: sk, verdict: v.verdict, justification: v.justification })))))) | | | | // All three tracks run in parallel, and each finding flows to Skeptic+Referee as soon as it lands. | | const smellRuns = HEURISTICS.map(h => () => | | verdictOf(agent(scannerPrompt(h), { label: scan:${h}, phase: 'Scan', schema: FINDINGS_SCHEMA }))) | | const walkRuns = PERSONAS.map(p => () => | | verdictOf(agent(walkerPrompt(p), { label: walk:${p}, phase: 'Walk', schema: FINDINGS_SCHEMA }))) | | const completenessRun = () => | | verdictOf(agent(completenessPrompt(), { label: 'complete:archetype', phase: 'Complete', schema: FINDINGS_SCHEMA })) | | | | const results = await parallel([...smellRuns, ...walkRuns, completenessRun]) | | const rawAll = results.flat().filter(Boolean) | | // Code-level scope-fence backstop: drop any finding whose anchor is NOT a scope file, so a subagent that | | // ignored the prompt fence and roamed cannot leak off-scope findings into the report. Surfaced as a count | | // (dropped_out_of_scope) rather than silently — a non-zero value means a scanner wandered. | | const all = rawAll.filter(f => SCOPE_SET.has(f.file)) | | const droppedOutOfScope = rawAll.length - all.length | | const surviving = all.filter(f => f.verdict === 'REAL_GAP' || f.verdict === 'MANUAL_REVIEW') | | | | // Cross-link: a smell and a wall at the same file (± a few lines) = two methods agreeing -> promote. | | const promote = { critical: 'critical', high: 'critical', medium: 'high', low: 'medium' } | | const crossLinked = [] | | for (const f of surviving) { | | const match = surviving.find(g => g !== f && g.track !== f.track && g.file === f.file && Math.abs((g.line || 0) - (f.line || 0)) <= 8) | | crossLinked.push(match ? { ...f, track: 'both', cross_linked_with: ${match.track}:${match.file}:${match.line}`, severity: promote[f.severity] || f.severity } : f) | | } | | | | const sevRank = { critical: 0, high: 1, medium: 2, low: 3 } | | const tierRank = { 'A-deterministic': 0, 'B-heuristic': 1 } | | const rank = (a, b) => (tierRank[a.confidence] ?? 9) - (tierRank[b.confidence] ?? 9) || (sevRank[a.severity] ?? 9) - (sevRank[b.severity] ?? 9) | | | | return { | | heuristics_scanned: HEURISTICS, | | personas_walked: PERSONAS, | | archetypes: ARCHETYPES || 'auto-classified by agent', | | scope_file_count: SCOPE_FILES.length, | | scope_files: SCOPE_FILES, | | dropped_out_of_scope: droppedOutOfScope, // >0 means a subagent roamed past the scope fence — investigate | | real_gaps: crossLinked.filter(f => f.verdict === 'REAL_GAP').sort(rank), | | manual_review: crossLinked.filter(f => f.verdict === 'MANUAL_REVIEW').sort(rank), | | false_positives_killed: all.filter(f => f.verdict === 'HAS_PATH').length, | | } |
── more in #developer-tools 4 stories · sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/design-insufficiency…] indexed:0 read:14min 2026-08-17 ·