fsmp runs prompt-driven workflows backed by extended finite state machines.
The primary user is an AI coding agent. The agent instantiates a pre-authored state machine and drives it one transition at a time. Each call returns:
- the current step's instruction
- the transitions that are valid now
- the transitions that are blocked and why
This process helps agents follow complex workflows they might otherwise drift from.
- Drives an agent through a workflow defined as states and transitions, re-stating the current step's instruction on every call.
- Enforces sequencing: from a given state, only the legitimate next transitions are available. This targets omission failures — an agent skipping a required step (e.g. "prompt the reviewer to re-assess after the implementer responds").
- Supports cyclic workflows (loops, retry rounds) and counter gates — e.g. a transition that stays blocked until "2 clean reviews" have been recorded.
- Names blocked transitions and the reason they're blocked, so a rejected move redirects the agent rather than just erroring.
- Keeps a transition log per instance as an audit trail.
- It does not enforce content, only sequence.
fsmpcan't tell whether the agent reported a transition truthfully; it only guarantees the agent passes through the required states in order. - It's voluntary. As a CLI it can't force the agent to call it or to follow the returned instruction. It makes the correct path the available, legible one; it is not a sandbox. (Hard enforcement would need an MCP + hook layer — see Status.)
- It does not run the workflow's actual steps (spawn agents, open PRs, etc.). It tracks where you are and what's allowed next; the caller does the work.
- The definition is a fixed guardrail — the agent drives a machine but does not author or modify one at runtime.
- Definition — static, authored ahead of the run (by a person, or by an
agent working with one — see "Authoring your own workflows"), kept in
version control. The file
extension selects the parser (case-insensitively):
.yaml/.ymlfor YAML,.jsonfor JSON; any other extension (or none) is rejected. States +params(set once atnew, read-only) +context(mutable) + transitions with guards and effects. - Instance — a live run: a snapshot of the definition plus the current state,
context, and transition log. Stored as JSON under
~/.fsmp/state/<id>/, never in version control. The definition is snapshotted atnew, so editing the source file (or switching branches) doesn't mutate a running machine.FSMP_HOMEoverrides the~/.fsmphome directory (which holdsstate/alongside siblings like an installedbin/).
Guards are structured comparisons ({var, op, value|param|ctx}), all of which
must hold (implicit AND). Effects are set / incr / decr / conditional.
There is no expression language; guards and effects are plain data.
fsmp new --def <path> [--id <id>] [--set k=v ...] # instantiate; print the entry step
fsmp show --id <id> # current state + valid/blocked transitions
fsmp do <transition> --id <id> [--data k=v ...] # attempt a transition; print the new step
fsmp log --id <id> # transition history
fsmp lint --def <path> # check a definition for authoring problems
fsmp guide [topic] # print embedded authoring/driving docs
fsmp lint reports every authoring problem in a definition at once — unknown
initial state, transition to an unknown state, unreachable state, dead-end
(non-terminal with no exits), and terminal state that still declares transitions
— and exits non-zero if any are found.
fsmp guide serves reference docs compiled into the binary: fsmp guide definition is the definition format plus an authoring pattern/anti-pattern
catalog, fsmp guide driving is a short primer on driving any machine, and
fsmp guide with no topic lists them. (The docs live in docs/*.md — that
markdown is the single source of truth.)
Add --json for a machine-readable view (it does not apply to guide, which is
prose to stdout). A rejected fsmp do (unknown transition, missing required
data, or a failed guard) exits non-zero and prints the reason followed by the
current step.
.claude/skills/dev-cycle/ is a worked example that this repo also dogfoods: a
skill (SKILL.md) that delegates its process sequencing to fsmp, plus the
fsmp-definition.yaml it drives. The skill points its orchestrator agent at
the definition:
fsmp new --def .claude/skills/dev-cycle/fsmp-definition.yaml --id myproj-1234 --set bar=2
and then drives fsmp do <transition> as the cycle progresses. The agent can't
reach presenting until bar separate reviewers have each opened with a clean
initial verdict and reached SATISFIED — a count the machine tracks rather than
the agent — and even then only through a manual-verification state
(convergence ≠ done). See .claude/skills/dev-cycle/README.md for the division
of labour between the skill prose and the state machine.
To author a definition rather than drive one, start from fsmp guide definition (the format + patterns/anti-patterns). .claude/skills/author-fsmp-workflow/
is a skill that walks an agent and a user through it: design the state graph
together, then drive a pipeline-with-retry-gates machine
(fsmp-definition.yaml) that enforces the un-skippable tail — no YAML before
the graph is signed off, no dry-run before lint is clean, no sign-off before a
dry-run. It's a second worked example alongside dev-cycle.
v1. new / show / do / log work, and the dev-cycle definition drives a full
run (see .claude/skills/dev-cycle/fsmp-definition.yaml). Tested with
cargo test (unit tests inline; integration tests run the binary against that
definition).
Possible next steps: ls / defs inspection commands, and an --mcp-stdio mode
that exposes the same engine over MCP, where PreToolUse hooks could turn the
voluntary sequencing into hard gating. (fsmp lint, a definition linter for
unreachable / dead-end states, has since landed.)
With a Rust toolchain (rustup.rs):
cargo install fsmp
or straight from this repository:
cargo install --git https://github.com/jamesmacaulay/fsmp
Either installs the fsmp binary into ~/.cargo/bin (on your PATH if cargo
is set up normally).
fsmp is also a library — the same engine the CLI drives (cargo add fsmp);
API docs at docs.rs/fsmp.
The example skills (dev-cycle, author-fsmp-workflow) can be installed into a
project with the skills CLI —
npx skills add jamesmacaulay/fsmp — or copied from this repo directly (see
.claude/skills/dev-cycle/README.md).
make build # debug build (target/debug/fsmp)
make test # unit + integration tests
make check # fmt-check + clippy + test
make install # release build, installs to ~/.fsmp/bin/fsmp
make install mirrors the runtime layout — the binary lands in ~/.fsmp/bin/
next to ~/.fsmp/state/. Add it to your PATH:
export PATH="$HOME/.fsmp/bin:$PATH"
FSMP_HOME relocates both the binary and state (make install FSMP_HOME=...).
Run make help for the full target list; plain cargo build / cargo test also
work.
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.