Skip to content
Back to skills

Stateloom Codegen

ASecurity

Turn a stateloom / smcraft .smdf.json state machine into runnable Python or TypeScript. Use when generating code with smcg or the generate_code MCP tool, validating a definition against V001-V014 before generating, running smcg --validate-only, wiring the stateloom runtime that generated code imports, running or testing a generated state machine, or diagnosing "Error parsing …: 'settings'" and lightweight fallback output.

  • 4 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 9, 2026
ai-agentstypescriptpythongobashreactnodeexpressawstesting

Works with

  • cli
  • mcp

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned October 9, 2026

npx -y skills add jgwill/miadi-orchestration-kit --skill stateloom-codegen --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Stateloom Codegen?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Stateloom Codegen
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/jgwill-stateloom-codegen/badge)](https://www.skillsdirectory.com/skills/jgwill-stateloom-codegen)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: stateloom-codegen
description: Turn a stateloom / smcraft .smdf.json state machine into runnable Python or TypeScript. Use when generating code with smcg or the generate_code MCP tool, validating a definition against V001-V014 before generating, running smcg --validate-only, wiring the stateloom runtime that generated code imports, running or testing a generated state machine, or diagnosing "Error parsing …: 'settings'" and lightweight fallback output.
---

# SMDF → validated → code

```
  machine.smdf.json
        │
        ▼
     Parser ──────────► EnrichedModel
        │                 state_map · event_map · timer_map
        │                 feeders_map · parent_map
        │                 all_states · leaf_states · composite_states
        ▼
   Validator  V001–V014
        │  (clean)
        ▼
   Generator ─────────► <machine>_fsm.py   (Python)
        │                <machine>_fsm.ts   (TypeScript)
        ▼
   your code imports the stateloom runtime and drives it
```

The runtime — `Context`, `ContextAsync`, `State`, `StateKind`, `TransitionHelper`,
`ObserverNull` — **ships as a dependency**. Generated code imports it; it is never inlined.
Install `miadi-stateloom-engine` (PyPI) or `@miadi/stateloom-engine` (npm) alongside the generated file.

---

## Step 1 — Validate before you generate

Generation on an invalid definition wastes a cycle. Validate first.

```bash
smcg /abs/path/machine.smdf.json --validate-only
```

Clean:

```
Validation passed.
```

Not clean — the rule ID tells you what to fix (full table in the `stateloom-design` skill's
`smdf-reference.md`):

```
Validation errors in /abs/path/machine.smdf.json:
  [V005] Transition references undefined event: OrderApprove (Pending)
  [V006] Transition references undefined state: Complete (Active)
  2 error(s) found
```

Exit code is non-zero when any rule fires, so it gates a build:

```bash
smcg "$DEF" --validate-only || { echo "definition is not ready"; exit 1; }
```

The MCP `validate_definition` tool is a **different, smaller** check with overlapping rule
IDs. Use it while designing; use `smcg --validate-only` before generating.

---

## Step 2 — Unwrap, if the file came from the tools

Every writer in the live loop — the MCP server, `smcx`, the web designer — persists the
**wrapped** form:

```json
{ "stateMachine": { "settings": …, "events": …, "state": … } }
```

`smcg` does not accept that wrapper. Handing it a wrapped file fails with:

```
Error parsing /abs/path/machine.smdf.json: 'settings'
```

This is a `smcg` (Python) constraint. The TypeScript engine's `parseFile` / `parseJson` unwrap
`{ "stateMachine": … }` themselves in current `@miadi/stateloom-engine` — but unwrapping first works on every
version of both engines, so make it the habit.

Unwrap first:

```bash
jq '.stateMachine // .StateMachine // .' /abs/path/machine.smdf.json > /tmp/machine.bare.smdf.json

# without jq
python3 -c "import json,sys; d=json.load(open(sys.argv[1])); json.dump(d.get('stateMachine', d.get('StateMachine', d)), open(sys.argv[2],'w'), indent=2)" \
  /abs/path/machine.smdf.json /tmp/machine.bare.smdf.json
```

Files written by hand (and the repository's `examples/*.smdf.json`) are already bare — check
with `head -3` before converting.

---

## Step 3 — Generate Python

```bash
pip install miadi-stateloom-engine                       # runtime + parser + generator + the smcg CLI

smcg /tmp/machine.bare.smdf.json -o ./generated/ -l python -v
```

```
Validation passed: 6 states, 3 events
Generated: ./generated/traffic_light_fsm.py
  States: 6
  Events: 3
  Feeders: 1
```

| Option | Meaning |
|---|---|
| `-o, --output <dir>` | Output directory (default: cwd). Created if missing. |
| `-l, --language <lang>` | Target. **`python` is the only accepted value.** |
| `-n, --name <name>` | Override `settings.name` for this run |
| `--validate-only` | Validate and stop |
| `-v, --verbose` | Print state / event / feeder counts |

**Output filename is snake_case:** `settings.name` `TrafficLight` → `traffic_light_fsm.py`.
Newer builds of the CLI also accept `smcg generate <input> …` as an explicit subcommand; the
bare positional form shown above works on both.

---

## Step 4 — Run the generated Python

The generated module gives you a context class named `<Name>Context`, one class per **leaf**
state, an `IntEnum` of every state, and a feeder per event source.

```python
from traffic_light_fsm import TrafficLightContext

ctx = TrafficLightContext()
ctx.enter_initial_state()          # required — nothing is entered until you call it
print(ctx.state_current.name)      # → Red

ctx.on_timer_elapsed()             # one method per event, snake_cased from the event id
print(ctx.state_current.name)      # → Green
```

Event methods on the context forward to whatever leaf is current:

```python
def on_timer_elapsed(self) -> None:
    if self.state_current:
        self.state_current.on_timer_elapsed(self)
```

Events carrying parameters take them positionally, typed as declared in the SMDF:
`ctx.on_pedestrian_call("north-crossing")`.

### Three generator behaviours to know before you rely on the output

All three verified by running the generator.

1. **An internal transition with no actions emits an empty handler — invalid Python.**
   A transition with no `nextState` and no `actions` produces
   `def on_x(self, context) -> None:` with nothing under it, and importing the module raises
   `IndentationError: expected an indented block after function definition`.
   Give every internal transition at least one action:

   ```json
   { "event": "ARTEFACT_TYPED", "actions": [ { "code": "on_artefact_typed_noop()" } ] }
   ```

   Diagnose it in one line before you import:

   ```bash
   python3 -m py_compile ./generated/*_fsm.py && echo "compiles"
   ```

2. **Handlers are emitted on leaf states only.** A composite is emitted as a plain
   `State(kind=StateKind.COMPOSITE)`, so a transition declared on a composite is **not**
   dispatched to its children at runtime, even though it is correct in the model and draws in
   every diagram. To make a shared exit executable, declare it on each leaf as well.

3. **Parallel regions are not generated.** `StateDef.parallel` is modelled and validated
   (V010, V011) but the generator emits no region contexts. Check the output before depending
   on it.

### Where your own logic goes

The generated file is regenerated wholesale — its header says *Do not edit manually*, and it
means it. Put behaviour in:

- `condition` expressions in the SMDF — emitted verbatim as guards
- `actions` on transitions, and `onEntry` / `onExit` blocks — each `{ "code": "…" }` is emitted
  as **`context.<code>`**, so write `"initialize()"`, never `"self.initialize()"`
- an **observer**: the runtime calls entry / exit hooks, so a custom observer sees every
  transition without touching generated code
- a subclass of the generated context, in your own file — this is where the methods your
  actions call actually live:

```python
from traffic_light_fsm import TrafficLightContext

class MyLight(TrafficLightContext):
    def open_ledger(self):        # called by { "code": "open_ledger()" }
        ...

ctx = MyLight()
ctx.enter_initial_state()
```

---

## Step 5 — TypeScript

The TypeScript generator exists in the npm `@miadi/stateloom-engine` package but is **not wired to any CLI**.
`smcg -l typescript` is rejected by argparse — `python` is the only choice.

Generate programmatically:

```bash
npm install @miadi/stateloom-engine
```

```js
// generate-ts.mjs
import { parseFile, enrich, validate } from "@miadi/stateloom-engine/parser";
import { TypeScriptCodeGenerator } from "@miadi/stateloom-engine/codegen";
import { writeFileSync } from "node:fs";

const def = parseFile("/tmp/machine.bare.smdf.json");
const model = enrich(def);

const errors = validate(model);
if (errors.length) {
  for (const e of errors) console.error(`[${e.ruleId}] ${e.message}`);
  process.exit(1);
}

writeFileSync("./generated/traffic_light_fsm.ts", new TypeScriptCodeGenerator(model).generate());
```

```bash
node generate-ts.mjs && head -20 ./generated/traffic_light_fsm.ts
```

**Fix the import line.** The generated TypeScript imports its runtime relatively:

```ts
} from "./runtime.js";
```

That is right only if the file sits next to the runtime. In your own project, rewrite it:

```bash
sed -i 's|} from "./runtime.js";|} from "@miadi/stateloom-engine/runtime";|' ./generated/traffic_light_fsm.ts
```

Then use it the same way as Python, camelCased:

```ts
import { TrafficLightContext } from "./generated/traffic_light_fsm.js";

const ctx = new TrafficLightContext();
ctx.enterInitialState();
ctx.onTimerElapsed();
```

The npm `@miadi/stateloom-engine` package also ships a `Machine` interpreter (`@miadi/stateloom-engine/machine`) that runs an
SMDF definition directly, with no code generation step — reach for it when you want to
*execute* a definition rather than compile it.

---

## Generating through MCP

```
generate_code { "language": "python" }
generate_code { "language": "typescript" }
```

The tool validates first with the MCP's own rules and refuses on errors:

```
Fix validation errors first:
[V003] State 'Pending' references unknown event 'OrderApprove'
```

When clean, it unwraps the definition into a temp file, calls `smcg`, and returns the code as
text.

### Know which generator produced your code — verified behaviour

`generate_code` silently falls back to a lightweight inline generator whenever the `smcg`
subprocess does not hand back the file it expected. Two situations trigger it, and both are
common:

- **`language: "typescript"`** — `smcg` rejects `-l typescript`, so TypeScript from this tool
  is *always* the fallback.
- **A `settings.name` that is not already snake_case** — `smcg` writes
  `interactive_production_fsm.py` while the tool looks for `InteractiveProduction_fsm.py`, does
  not find it, and falls back.

Tell them apart by the first line:

| First line | Generator | What you get |
|---|---|---|
| `"""Generated State Machine: <Name>"""` | Full `smcg` | `IntEnum`, entry/exit chains, guards, timers, feeder classes, `enter_initial_state` |
| `"""Generated by SMCraft"""` | Lightweight fallback | States and transitions only — no feeders, no entry/exit, no initial-state entry |

**For production code, run `smcg` on the file yourself** (Steps 2–3). Use `generate_code` to
sketch and to read structure inside a conversation.

If you want the MCP path to produce full output, name the machine in snake_case
(`settings.name: "order_workflow"`) so the expected and actual filenames coincide.

---

## End-to-end, one pass

```bash
DEF=/abs/path/machine.smdf.json
OUT=./generated

jq '.stateMachine // .StateMachine // .' "$DEF" > /tmp/bare.smdf.json
smcg /tmp/bare.smdf.json --validate-only || exit 1
smcg /tmp/bare.smdf.json -o "$OUT" -l python -v

# catches the empty-handler defect before it becomes a confusing traceback
python3 -m py_compile "$OUT"/*_fsm.py || { echo "generated code does not compile — see behaviour 1"; exit 1; }

python3 - <<'PY'
import glob, importlib.util, sys
path = glob.glob("./generated/*_fsm.py")[0]
spec = importlib.util.spec_from_file_location("fsm", path)
mod  = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
ctx  = next(getattr(mod, n)() for n in dir(mod) if n.endswith("Context"))
ctx.enter_initial_state()
print("initial state:", ctx.state_current.name)
PY
```

Passing means the definition parses, validates, generates, imports and enters its initial
state — the whole pipeline proven in one run.

---

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| `Error parsing …: 'settings'` | Wrapped `{ "stateMachine": … }` handed to `smcg` | Unwrap (Step 2) |
| `IndentationError: expected an indented block after function definition` | An internal transition with no `actions` produced an empty handler | Add an action to every `nextState`-less transition (behaviour 1) |
| `AttributeError: '<Name>Context' object has no attribute 'x'` | An action's `code` is emitted as `context.x` and nothing implements `x` | Implement it on a subclass of the generated context |
| `smcg: error: argument -l/--language: invalid choice: 'typescript'` | `smcg` only targets Python | Generate TypeScript programmatically (Step 5) |
| `ModuleNotFoundError: No module named 'stateloom'` | Runtime not installed where the generated file runs | `pip install miadi-stateloom-engine` |
| `Cannot find module './runtime.js'` | Generated TS still has the relative import | Rewrite to `@miadi/stateloom-engine/runtime` |
| Output is far smaller than expected | The lightweight fallback ran | Check the first line; run `smcg` directly |
| Machine does not react to an event | The transition is on a composite, not a leaf | Declare it on each leaf too |
| `[V0xx]` errors you do not recognize | Canonical rules, not the MCP subset | See `smdf-reference.md` in `stateloom-design` |
| Generated file name is not what you expected | `smcg` snake_cases `settings.name` | `TrafficLight` → `traffic_light_fsm.py`; override with `-n` |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.