state-machine-skill
v1.3.0
Published
AI agent skill: model UI components as finite state machines before coding. Claude Code · Cursor · Windsurf · OpenCode
Maintainers
Readme
🔄 state-machine
Model UI behavior before you code it. Eliminate impossible states before they exist.
Compatible with Claude Code · Cursor · Windsurf · OpenCode · Any AI coding agent
Why state machines for UI?
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.
How it works: single-command workflow
┌──────────┐ ┌──────────────┐ ┌──────────────────┐ ┌──────────┐
│ 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.
Install
| 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 |
Two verbs. One guarantee.
model — From natural language to validated blueprint (with optional implementation)
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 offThe 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.
Implementation (auto-prompted after model)
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.
audit — Detect impossible states in legacy code
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.tsxOutput:
{
"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"
}
]
}Model format: two ways to write your JSON
The linter auto-detects which format you're using.
Flat format (simple)
{
"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" }
]
}Hierarchical format (schema — per schemas/fsm.schema.json)
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"] }
}
}
}
}
}
}4 worked examples
Each example demonstrates the model output in the unified 3-section format: behavior specification → structural JSON contract → ASCII diagram.
┌────────┐ TRIGGER ┌─────────┐ FETCH_SUCCESS ┌─────────┐
│ Closed │─────────>│ Loading │──────────────>│ Success │
└────────┘ └────┬────┘ └────┬────┘
│ │ CLOSE
CLOSE │ v
│ ┌─────────┐
└───────────────────>│ Closed │
└─────────┘File: examples/modal.md
┌────────┐ NEXT ┌────────┐ NEXT ┌────────┐ SUBMIT
│ Step1 │────────>│ Step2 │────────>│ Step3 │────────────┐
└───┬────┘ └───┬────┘ └────────┘ │
│ PREV │ PREV │
└───────────────────┘ │
┌────────────┐ │
│ Submitting │◄────────────────┘
└──────┬─────┘
┌───────────┼──────────┐
v v │
┌─────────┐ ┌─────────┐ │
│ Success │ │ Error │─────┘
└─────────┘ └────┬────┘
│ RETRY
v
┌────────────┐
│ Submitting │
└────────────┘File: examples/multistep-form.md
┌────────┐ ┌────────────┐ CONFIRM ┌────────┐
│ Off │─────>│ PendingOn │───────>│ On │
└───┬────┘ └──────┬─────┘ └───┬────┘
│ │ REJECT │
│ v │
│ ┌───────┐ │
└─────────────>│ Error │<──────────────┘
└───┬───┘
RETRY │ DISMISS
v
┌────────┐
│Pending │
└────────┘File: examples/toggle-async.md
┌──────────────────────────────────────────┐
│ 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
Scripts
| 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.
Project structure
| 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) |
XState v5 compatible
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 support
| 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() |
Contributing
- 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
License
MIT © 2026
