
Clanker Discipline
Review agent-generated state types and data models so boolean flags, optional fields, and mutable patterns do not compound into unmanageable state.
Overview
Clanker Discipline is an agent skill most often used in Ship (also Build) that catches state bloat, grab-bag models, and mutation ambiguity from AI-generated code.
Install
npx skills add https://github.com/gbasin/clanker-discipline --skill clanker-disciplineWhat is this skill?
- Derive computed values from event streams instead of storing redundant boolean flags
- Detect grab-bag models and mutation ambiguity common in AI-authored code
- Instructs full refactors when violations are found—not minimal-diff band-aids
- Covers cached flags anti-pattern with concrete before/after TypeScript examples
- Applies when writing or reviewing functions that manage application state
Adoption & trust: 750 installs on skills.sh; 22 GitHub stars; 3/3 security scanners passed (skills.sh audits).
What problem does it solve?
AI agents keep adding boolean flags and optional fields until your state model answers simple UI questions with four unrelated fields and fragile mutators.
Who is it for?
Solo builders reviewing thread/chat state, wizard flows, or domain models after heavy agent-assisted coding.
Skip if: Teams that only need generic linting with no focus on state modeling, or repos where specs are frozen and no state refactors are allowed.
When should I use this skill?
Reviewing or writing state types, boolean flags, optional-field models, or mutable data patterns from AI coding agents.
What do I get? / Deliverables
State types derive what they can from events, flags are removed, and functions manage state without ambiguous special cases—ready for a normal code review pass.
- Refactored state types with derived fields instead of redundant flags
- Review notes listing clanker violations and recommended structural fixes
Recommended Skills
Journey fit
Spans multiple journey phases - primary shelf plus alternate fits below.
Canonical shelf is Ship because the skill is written for review/refactor after code exists—catching clanker patterns before merge or during QA. Review subphase matches explicit triggers: reviewing state types, flags, optional-field models, and mutation ambiguity rather than greenfield architecture.
Where it fits
Reshape a chat UI ThreadState after an agent added four footer-related booleans that should be one derived predicate.
Block merge on a PR that introduced optional-field grab bags in the domain model.
Refactor legacy flags in production state when intermittent UI bugs trace to contradictory flag combinations.
How it compares
Use instead of ad-hoc “looks fine” review when agent-written TypeScript state types are the risk.
Common Questions / FAQ
Who is clanker-discipline for?
Indie and solo developers using AI coding agents who own frontend or full-stack state and want disciplined data models before technical debt compounds.
When should I use clanker-discipline?
During Ship review before merging agent PRs, during Build when shaping new ThreadState or domain types, and whenever you see boolean flags that could be derived from an event log.
Is clanker-discipline safe to install?
It is procedural review guidance only—no shell or network by default. Review the Security Audits panel on this Prism page before trusting any third-party skill package.
SKILL.md
READMESKILL.md - Clanker Discipline
# Clanker Discipline Apply these rules when writing or reviewing state types, data models, and functions that manage application state. Agents tend to add flags, optional fields, and special cases that compound into state nobody intended — catch that before it lands. When you find violations, refactor fully. The goal is clean, maintainable code, not minimal diffs. Rip out the flags, reshape the types, restructure the functions. A bigger diff now is better than layering workarounds that compound later. --- ## 1. Derive, don't store Every boolean you add doubles the theoretical state space. When a value can be derived from data you already have, do not store it. The best source to derive from is an event stream: a log of what happened. ### Before: cached flags An agent was asked to show a footer only when the assistant finishes naturally. It invented four flags: ```ts type ThreadState = { wasInterrupted: boolean; didAssistantFinish: boolean; didAssistantError: boolean; wasToolCallOnly: boolean; }; function shouldShowFooter(state: ThreadState): boolean { return state.didAssistantFinish && !state.wasInterrupted && !state.didAssistantError && !state.wasToolCallOnly; } ``` Four fields to answer one question, with four mutation sites elsewhere keeping them in sync. ### After: derive from evidence ```ts function shouldShowFooter(events: SessionEvent[]): boolean { const latest = getLatestAssistantMessage(events); if (!latest) return false; return latest.completed && !latest.error && latest.finish !== 'tool-calls'; } ``` The answer is now computed from events that already exist. ### When NOT to derive - The domain genuinely has a state machine with ordered transitions. A checkout step is not a cached conclusion; it IS the state. - A field contains temporal or external data that cannot be rederived (timestamps from async processes, API responses needed downstream). - The derivation would be more complex than the stored value. ### If you cannot derive, encapsulate If mutable state must exist, trap it in the smallest possible scope. A closure is better than a class field: ```ts // Bad: state visible to the whole class class Writer { private debounceTimeout: ReturnType<typeof setTimeout> | null = null; queueSend(text: string) { /* can touch debounceTimeout */ } flushNow() { /* can touch debounceTimeout */ } somethingElse() { /* can also touch debounceTimeout */ } } // Good: state trapped in a closure function createDebouncedAction(callback: () => void, delayMs = 300) { let timeout: ReturnType<typeof setTimeout> | null = null; return { trigger() { clearTimeout(timeout!); timeout = setTimeout(() => { timeout = null; callback(); }, delayMs); }, clear() { if (timeout) { clearTimeout(timeout); timeout = null; } }, }; } ``` Nothing outside the closure can touch the timer. ### The debugging payoff When state is derived from evidence, debugging becomes data-in, answer-out: ```ts test('footer is hidden for aborted runs', () => { const events = loadEvents('./fixtures/aborted-session.jsonl'); expect(shouldShowFooter(events)).toBe(false); }); ``` No mocking or timing reproduction. The bug is in the events or in the pure function. --- ## 2. Make wrong states impossible Every optional field is a question the rest of the codebase must answer every time it touches that data. ### Discriminated unions over optional bags ```ts // Bad: when status is 'idle', should gateway/transactionId exist? The type doesn't say. type PaymentState = { status: 'idle' | 'processing' | 'settled'; gateway?: 'stripe' | 'paypal'; transactionId?: string; initiatedAt?: string; settledAt?: string; }; // Good: each status carries exactly the fi