Convert an LLM agent skill into a deterministic, assertion-checked skill
An LLM-executed skill is a SKILL.md full of prose: the model reads your instructions and decides the tool calls fresh on every run. That is exactly what you want for open-ended judgment — and exactly what you do not want for the deterministic core, where the same three API calls should fire the same way every time. This guide compiles a deterministic, assertion-checked twin of that skill: same identity, but the steps are frozen tool calls that replay for 0 tokens and fail loudly on drift.
The rule that keeps it honest: Reelier never writes steps from your instruction text — a step guessed from prose and replayed deterministically would be a lie with a receipt. You record one real run, and reelier compile --from-skill carries the skill's name and description onto a twin whose steps come only from that trace. Keep the LLM skill for the judgment; run the deterministic twin for the plumbing.
Before you start
- Reelier installed —
npm i -g reelier(or dropnpx -y reelier@latestin front of every command below). - The LLM-executed skill you want a twin of: an Agent-Skills
SKILL.mdwithname:anddescription:in its frontmatter. Reelier carries those two fields verbatim and refuses to guess a missing one. - One real run of that skill's task to record — a data pull, a status sweep, a release check. Reelier replays tool calls (
mcp__server__tool,http.get/http.post), notBash/Read/Edit/Write/Grep, so pick a task whose value is in its API calls. - Nothing else. No account, no API key: compiling a trace never calls a model.
Record one real run of the skill's task
Reelier compiles steps from a recorded trace, never from prose — so first you capture one honest run. Put the recorder in front of the same MCP server(s) your agent uses, then have it execute the skill's task once the way it normally would. Every tool call lands in a .jsonl trace under .reelier/traces/, results and all. (The recorder is the universal capture path; on Cursor it is the only capture path — a wrap proxy, not a history scan. Where each agent stores transcripts lives on the platform page, not here.)
If you already ran the task through a wrapped server, the trace is on disk — reelier scan lists the replayable ones. The example below uses the trace the zero-setup reelier init demo records (two real http.get calls to the npm registry), so the whole conversion is reproducible without a private MCP server.
reelier mcp --wrap "<your MCP server command>"Compile the trace, carrying the skill's identity
Point compile at the recorded trace and pass your instruction skill with --from-skill. Reelier reads only the frontmatter of that SKILL.md — the name and description — and stamps them onto the compiled twin. The headline line says exactly what happened: name + description carried; steps from the trace only. Not one step was derived from your prose.
The stats tell the story. binds: 1 — Reelier saw the second call's URL in the first call's response and recovered it into a {{homepage}} variable instead of hard-coding it. asserts: 3 is the minimal-assertion rule, not one-per-step: step 1 gets status == 200 plus json.homepage is set (a later step consumes that field), while step 2 gets only status == 200 — nothing brittle pinned. effects: read=2 confirms both steps are read-only, safe to replay on a schedule.
Open questions: (none) means Reelier had nothing it needed a human to resolve — the recorded steps carried narration and no ambiguous literals. What skill compilation does under the hood is defined in the glossary; this is the command that does it.
reelier compile .reelier/traces/reelier-init-demo-1.jsonl \
--from-skill npm-release-check/SKILL.md -o npm-release-check.skill.mdWrote npm-release-check.skill.md compiled from SKILL.md + reelier-init-demo-1.jsonl (name + description carried; steps from the trace only) steps: 2 asserts: 3 binds: 1 effects: read=2 idempotent-write=0 destructive=0 Open questions: (none)
Read the deterministic twin it wrote
The output is plain markdown, checked into your repo next to the original. The frontmatter is your skill's identity, carried verbatim: name: npm-release-check and description: Check whether a package has a fresh published release..., plus a recorded_with: reelier v0.14.0 stamp. The first body line states the contract in the file: every step comes from the recorded trace, none from the instruction text.
The steps are the frozen calls. Step 1 is action: http.get {"url":"https://registry.npmjs.org/reelier/latest"} with assert: json.homepage is set, assert: status == 200, and bind: homepage = json.homepage; step 2 is action: http.get {"url":"{{homepage}}"} with assert: status == 200. A step that had recorded no clean result would carry no assert and replay unchecked, never a false passed — so what is and isn't checked is always visible in the file. This is where you edit: promote a literal to an {{input}}, tighten a title, or leave it as-is.
Replay the twin — 0 tokens, read-only
Run it. At the default Level 0 the LLM is never constructed, so the replay costs 0 tokens and re-issues the two recorded calls against the live registry, checking each assertion. That deterministic core is the entire replay — nothing is re-reasoned. Both steps passed, and because they are read-only, this is safe to run repeatedly.
Writes stay gated: a recorded write step does not re-fire on replay unless you pass --allow-writes (idempotent writes only), and a destructive step still needs --yes. A read-only conversion like this sidesteps all of that — it re-reads, it never re-writes.
reelier run npm-release-check.skill.md✓ Step 1 — Fetch reelier's own versioned npm registry metadata [passed] 141ms ✓ Step 2 — Fetch the package homepage, using the URL bound from the registry response [passed] 231ms PASSED: 2/2 steps ok, 0 failed, 372ms total
Gate drift — diff two runs
Run the twin a second time, then reelier diff <name> compares the last two runs step by step. It reports SAME and exits 0 when every outcome and escalation level match; DRIFTED and exits 1 when an outcome flips, a step is added or removed, or a step needed a different level to pass. That exit code is the point — wire it into CI and a drift blocks the merge. The fuller snapshot loop has its own guide; this is the conversion-specific gate.
Fresh response bodies each run are not drift: the assertions check a shape (status == 200, json.homepage is set), so honest data churn updates the receipt without failing the diff — the verdict below stayed SAME across two live calls.
reelier diff npm-release-check= Step 1 "Fetch reelier's own versioned npm registry metadata": passed, unchanged. = Step 2 "Fetch the package homepage, using the URL bound from the registry response": passed, unchanged. ✓ SAME — 2 step(s), every outcome and escalation level identical to the baseline.
The receipt
$ reelier compile .reelier/traces/reelier-init-demo-1.jsonl --from-skill npm-release-check/SKILL.md -o npm-release-check.skill.md Wrote npm-release-check.skill.md compiled from SKILL.md + reelier-init-demo-1.jsonl (name + description carried; steps from the trace only) steps: 2 asserts: 3 binds: 1 effects: read=2 idempotent-write=0 destructive=0 Open questions: (none) $ reelier run npm-release-check.skill.md ✓ Step 1 — Fetch reelier's own versioned npm registry metadata [passed] 141ms ✓ Step 2 — Fetch the package homepage, using the URL bound from the registry response [passed] 231ms PASSED: 2/2 steps ok, 0 failed, 372ms total $ reelier diff npm-release-check = Step 1 "Fetch reelier's own versioned npm registry metadata": passed, unchanged. = Step 2 "Fetch the package homepage, using the URL bound from the registry response": passed, unchanged. ✓ SAME — 2 step(s), every outcome and escalation level identical to the baseline.
The whole conversion, verbatim: reelier compile --from-skill carried the npm-release-check instruction skill's name and description onto a deterministic twin whose two steps came only from a recorded trace, which then replayed and diffed clean. Produced on 2026-07-22 with reelier v0.14.0 in a scratch directory; the trace is the reproducible reelier init demo recording (two real http.get calls to the npm registry), and the source was a real Agent-Skills SKILL.md with name/description frontmatter. No model was constructed at any point; reelier diff exited 0 (a drift would exit 1).
FAQ
Does this replace my LLM-executed skill?
No — it complements it. The instruction SKILL.md stays for the open-ended, judgment-heavy work where the model should decide fresh each run; the deterministic twin freezes the tool-call core so it replays for 0 tokens and gates drift. The name and description carry across so the two are recognizably the same skill — the reelier-vs-agent-skills page walks the tradeoff in full.
Why can't Reelier just read my SKILL.md and generate the steps?
Because generating steps from instruction text is guessing, and a wrong guess replayed deterministically is a lie with a receipt. --from-skill reads only the frontmatter identity; every step comes from a trace of a run that actually happened. Point it at an instruction skill with no recorded trace and it tells you to record one — it never fabricates.
The twin carried my description but its steps look different from my prose. Bug?
Working as intended. The prose is carried as identity, not as steps — the steps are the exact tool calls your recorded run made. Assertions stay minimal: only a step with a clean recorded result gets one, and it asserts a shape, not a pinned value. A step that recorded no result replays unchecked, never falsely passed.