Model UI behavior before you code it. Eliminate impossible states before they exist.
Compatible with Claude Code · Cursor · Windsurf · OpenCode · Any AI coding agent
Three booleans. Eight possible states. Five of them impossible.
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);| isLoading | isError | isSuccess | Result |
|---|---|---|---|
| ❌ | ❌ | ❌ | Idle — valid |
| ✅ | ❌ | ❌ | Loading — valid |
| ❌ | ✅ | ❌ | Error — valid |
| ❌ | ❌ | ✅ | Success — valid |
| ✅ | ✅ | ❌ | IMPOSSIBLE — loading and erroring at once |
| ✅ | ❌ | ✅ | IMPOSSIBLE — loading and succeeded at once |
| ❌ | ✅ | ✅ | IMPOSSIBLE — error and success at once |
| ✅ | ✅ | ✅ | IMPOSSIBLE — all three at once |
Every impossible state is a bug waiting to happen. A race condition, a missed reset, a late network response — and your UI shows a spinner over content, or an error over a success view.
A state machine replaces N booleans with one state variable. Instead of 2^N combinations (most invalid), you get exactly N states — all valid. If a state isn't in the alphabet, the component cannot enter it.
┌──────────┐ ┌──────────────┐ ┌──────────────────┐ ┌──────────┐
│ Describe │────>│ model verb │────>│ Agent asks: │────>│ Ready │
│ behavior │ │ (blueprint) │ │ "Implement now?" │ │ component│
│ in NL │ │ 3 sections │ │ → picks folder │ │ shipped │
└──────────┘ └──────────────┘ │ → generates │ └──────────┘
│ code + tests │
└──────────────────┘
model command: You describe the component behavior in natural language. The agent produces 3 sections: Behavior Specification, validated JSON contract, and ASCII diagram. Then it asks if you want to generate the component code now.
Implementation: If you accept, the agent scans your project directories, confirms the target folder with you, injects the micro-runtime (createLightMachine), generates the component code wired to the state machine, and writes unit tests covering every transition.
| Agent | Command |
|---|---|
| Any agent | npx skills add pauloriveross/state-machine-skill |
| Claude Code | npx skills add pauloriveross/state-machine-skill |
| Cursor | Copy SKILL.md → .cursor/rules/state-machine.mdc, references/ → .cursor/rules/state-machine/ |
| Windsurf | cp SKILL.md .windsurf/rules/state-machine.md + cp -r references/ .windsurf/rules/state-machine-references/ |
| OpenCode | cp -r * .agent/skills/state-machine/ (or just open the repo — auto-detected) |
| Manual | Copy SKILL.md + references/ into your agent's skill directory |
Input: A natural language description of component behavior.
Output: 3 sections (Behavior Specification, validated JSON contract, ASCII diagram), then an auto-prompt to implement.
How the agent executes it:
- Translates your description into a hierarchical JSON conforming to
schemas/fsm.schema.json - Saves to
.state-machine/temp-model.json - Runs
node scripts/validate-model.js .state-machine/temp-model.json - If validation fails, fixes the JSON and re-runs — no intermediate output, no asking for help
- On success, outputs the 3-section markdown block
- Asks: "Do you want me to generate the component code from this model?"
- If yes, scans your project directories, asks which folder, generates code + tests
Example:
> state-machine model a toggle that switches on and off
The agent returns:
## 1. Behavior Specification
A simple toggle switch with two states — On and Off. Clicking the toggle flips between the two states.
## 2. Structural Contract (`model.json`)
```json
{
"id": "toggle",
"initial": "Off",
"states": {
"Off": { "type": "atomic", "on": { "TOGGLE": { "target": "On" } } },
"On": { "type": "atomic", "on": { "TOGGLE": { "target": "Off" } } }
}
}
```
## 3. Transition Diagram (ASCII)
```
┌────────┐ TOGGLE ┌────────┐
│ Off │─────────>│ On │
└────────┘<─────────└────────┘
TOGGLE
```
✅ All 13 gates passed.Then the agent asks: "Do you want me to generate the component code from this model? I can place it in your project."
If you accept, it scans for project folders, confirms the target directory, and generates the implementation.
When you accept the post-model prompt, the agent:
- Scans your project for
package.json,src/,components/,app/directories - Asks you to confirm the target folder
- Copies
src/core/fsm.tsinto that folder asfsm.ts(if not already present) - Generates the UI component wired to
createLightMachine - Writes unit tests that simulate 100% of transitions
Every component carries a guarantee:
/* state-machine: Closed|Loading|Success|Error : TRIGGER|FETCH_SUCCESS|FETCH_ERROR|CLOSE|RETRY */This component cannot enter a state that was not explicitly modeled.
Input: A file path to an existing component using boolean flags (useState, ref, boolean fields).
Output: A structured JSON report with detected flags, combinatorial complexity, and impossible state analysis.
How the agent executes it:
- Scans the file for reactive boolean variables (
const [isXxx, ...],const [hasXxx, ...]) - Runs
node scripts/audit-processor.js file.tsx - Computes the 2^n combinatorial matrix
- Identifies which combinations have no visual representation or coherent logic
- Outputs a ranked punch list
Example:
> state-machine audit components/ui/OldModal.tsx
Output:
{
"detectedFlags": ["isLoading", "isError", "isSuccess"],
"complexity": "3 flags = 8 combinaciones posibles",
"impossibleStatesDetected": [
{
"combination": { "isLoading": true, "isError": true },
"severity": "CRITICAL",
"description": "Loading spinner and error message simultaneously"
}
]
}The linter auto-detects which format you're using.
{
"states": ["Closed*", "Loading", "Success", "Error"],
"transitions": [
{ "From": "Closed", "Event": "TRIGGER", "To": "Loading" },
{ "From": "Loading", "Event": "FETCH_SUCCESS", "To": "Success" },
{ "From": "Loading", "Event": "FETCH_ERROR", "To": "Error" }
]
}Supports compound states, guards, actions, lifecycle hooks, and cross-hierarchy targets.
{
"id": "async-modal",
"initial": "Closed",
"states": {
"Closed": {
"type": "atomic",
"on": {
"TRIGGER": { "target": "Loading" }
}
},
"Loading": {
"type": "atomic",
"onEnter": ["fetchData"],
"on": {
"FETCH_SUCCESS": { "target": "Success", "actions": ["storeData"] },
"FETCH_ERROR": { "target": "Error", "actions": ["logError"] }
}
},
"Success": {
"type": "atomic",
"on": {
"CLOSE": { "target": "Closed", "actions": ["clearData"] }
}
},
"Error": {
"type": "atomic",
"on": {
"RETRY": { "target": "Loading" },
"CLOSE": { "target": "Closed" }
}
}
}
}For compound (nested) states with cross-hierarchy targets:
{
"id": "auth-flow",
"initial": "Unauthenticated",
"states": {
"Unauthenticated": {
"type": "compound",
"initial": "Idle",
"states": {
"Idle": { "type": "atomic", "on": { "LOGIN": { "target": "Authenticating" } } },
"Authenticating": { "type": "atomic" },
"MfaRequired": { "type": "atomic" }
}
},
"Authenticated": {
"type": "compound",
"initial": "Active",
"states": {
"Active": {
"type": "atomic",
"on": {
"LOGOUT": { "target": "#auth-flow.Unauthenticated.Idle", "actions": ["clearSession"] }
}
}
}
}
}
}Each example demonstrates the model output in the unified 3-section format: behavior specification → structural JSON contract → ASCII diagram.
📋 Modal with async loading — 4 states, 5 events, 7 transitions
┌────────┐ TRIGGER ┌─────────┐ FETCH_SUCCESS ┌─────────┐
│ Closed │─────────>│ Loading │──────────────>│ Success │
└────────┘ └────┬────┘ └────┬────┘
│ │ CLOSE
CLOSE │ v
│ ┌─────────┐
└───────────────────>│ Closed │
└─────────┘
File: examples/modal.md
📋 Multi-step registration form — 7 states, 8 events, 11 transitions
┌────────┐ NEXT ┌────────┐ NEXT ┌────────┐ SUBMIT
│ Step1 │────────>│ Step2 │────────>│ Step3 │────────────┐
└───┬────┘ └───┬────┘ └────────┘ │
│ PREV │ PREV │
└───────────────────┘ │
┌────────────┐ │
│ Submitting │◄────────────────┘
└──────┬─────┘
┌───────────┼──────────┐
v v │
┌─────────┐ ┌─────────┐ │
│ Success │ │ Error │─────┘
└─────────┘ └────┬────┘
│ RETRY
v
┌────────────┐
│ Submitting │
└────────────┘
📋 Async toggle with optimistic update — 5 states, 5 events, 8 transitions
┌────────┐ ┌────────────┐ CONFIRM ┌────────┐
│ Off │─────>│ PendingOn │───────>│ On │
└───┬────┘ └──────┬─────┘ └───┬────┘
│ │ REJECT │
│ v │
│ ┌───────┐ │
└─────────────>│ Error │<──────────────┘
└───┬───┘
RETRY │ DISMISS
v
┌────────┐
│Pending │
└────────┘
File: examples/toggle-async.md
📋 Auth flow with MFA — HFSM, compound states, 13 events, 16 transitions
┌──────────────────────────────────────────┐
│ Unauthenticated │
│ ┌────────┐ LOGIN ┌──────────────┐ │
│ │ Idle │───────────>│ Authenticating│ │
│ └────────┘<────────────│ │ │
│ ^ LOGIN_ERR └──────┬───────┘ │
│ │ MFA_CANCEL LOGIN_MFA│ │
│ │ ┌──────v───────┐ │
│ └─────────────────│ MfaRequired │ │
│ MFA_SUBMIT(hasMfaCode) │ │
└──────────────────────────────────────────┘
│ LOGIN_SUCCESS
v
┌──────────────────────────────────────────┐
│ Authenticated │
│ ┌────────┐ REFRESH_TOKEN ┌────────────┐│
│ │ Active │───────────────>│ Refreshing ││
│ └───┬────┘<───────────────│ ││
│ │ REFRESH_SUCCESS └──────┬─────┘│
│ │ REFRESH_FAIL │
│ └──────────────────────────────┘ │
│ LOGOUT → Unauthenticated.Idle │
│ SESSION_EXPIRED → Unauthenticated.Idle │
└──────────────────────────────────────────┘
File: examples/auth-flow.md
| Script | Purpose |
|---|---|
node scripts/validate-model.js model.json |
Validate model against gates 01–13 + structural pre-check |
node scripts/validate-model.js model.json --light |
Fast-track: skip warnings, compact diagram |
node scripts/ascii-viz.js model.json |
Render ASCII transition diagram only |
node scripts/audit-processor.js file.tsx |
Analyze boolean flags in legacy code |
All scripts are zero-dependency. Exit code 0 = valid, 1 = invalid with gate report.
| File | Purpose |
|---|---|
SKILL.md |
Mandatory execution protocol — tells the AI agent exactly how to handle each verb |
src/core/fsm.ts |
Injectable micro-runtime (createLightMachine) — copied into your project during implementation |
schemas/fsm.schema.json |
Canonical JSON Schema for hierarchical FSM models |
scripts/validate-model.js |
Model validator — 13 gates + graph structural check, auto-detects flat/hierarchical format |
scripts/ascii-viz.js |
Terminal ASCII transition diagram renderer |
scripts/audit-processor.js |
Boolean flag analyzer — detects impossible state combinations in legacy code |
references/verb-dispatch.md |
Exact output format specification for model and audit verbs |
references/slop-gates.md |
38 validation gates (model 1–13, code 14–23, audit 24–38) |
references/state-theory.md |
FSM/HFSM fundamentals applied to UI components |
references/component-patterns.md |
12 canonical UI patterns with pre-built models and invariants |
references/impossible-states.md |
35 anti-patterns with elimination strategies |
references/xstate-compat.md |
Full mapping to XState v5 for every pattern |
references/framework-adapters.md |
useMachine adapter for React, Vue, Svelte, Vanilla JS |
examples/ |
4 worked examples in unified format (modal, toggle, form, auth) |
Every generated model maps directly to XState v5. Use the machine in any framework via @xstate/react, @xstate/vue, or @xstate/svelte.
import { setup } from 'xstate';
export const modalMachine = setup({
types: { /* ... */ },
actions: { fetchData: () => fetch('/api/data') },
}).createMachine({
id: 'modal',
initial: 'Closed',
states: {
Closed: { on: { TRIGGER: 'Loading' } },
Loading: {
on: {
FETCH_SUCCESS: 'Success',
FETCH_ERROR: 'Error',
CLOSE: 'Closed',
},
},
Success: { on: { CLOSE: 'Closed' } },
Error: { on: { CLOSE: 'Closed', RETRY: 'Loading' } },
},
});See references/xstate-compat.md.
| Framework | Mechanism |
|---|---|
| React | useMachine(config, impl) via useState + useRef |
| Vue | useMachine(config, impl) via ref + readonly |
| Svelte | useMachine(config, impl) via writable store |
| Vanilla JS | createMachine(config, impl) closure factory |
| XState v5 | setup() + createMachine() |
- Add a new component pattern to
references/component-patterns.md - Add corresponding anti-patterns to
references/impossible-states.md - Add validation gates to
references/slop-gates.md - Run
node scripts/validate-model.json the example models
MIT © 2026