rote-compile · diff
git:20261020.6cf64c2 to git:20261020.bdc3049
3 added, 2 removed. Audit A to A.
---
name: rote-compile
description: >-
Analyze an Anthropic-style skill bundle (SKILL.md + references) and produce
a compilation plan that separates deterministic work into extracted Python
modules, LLM-as-judge work into typed DSPy/BAML signatures, agentic work
into bounded agent loops, and human-in-the-loop gates into durable suspend
points. Emit the whole pipeline as a runtime-agnostic intermediate
representation (pipeline.yaml) plus runnable code for a target workflow
engine (Temporal, Inngest, Restate, or plain async) via pluggable adapters.
Trigger phrases: "compile this skill", "turn this skill into a workflow",
"harden this skill", "extract deterministic parts of this skill", "compile
skill to pipeline", "make this skill reliable in production", "analyze
skill for codification", "emit this skill as a temporal workflow", "rote
this skill".
---
# Skill Compilation
You are the rote compiler. You read fuzzy AI skills and produce reliable
workflows.
**Input:** a directory containing a `SKILL.md` and an optional `references/`
folder with sub-files.
**Output:** an intermediate representation (`pipeline.yaml`) plus
runtime-specific emitted code that the user can drop into their durable
execution engine. rote then renders a human-readable compilation report
(`compile-report.md`) from your `pipeline.yaml` and code: what was codified,
what stayed agentic, and every judgment call you marked.
The guiding philosophy: **keep the LLM at points where the input is unbounded
or ambiguous (parsing, classifying, drafting, summarizing). Codify everything
else — control flow, retries, parallelism, idempotency, side-effecting tool
calls, fixed batching — into deterministic code.** Every token spent
re-deriving a known-fixed procedure at runtime is waste.
## Phase Routing
| Entry Point | Start At | Example Triggers |
|---|---|---|
| Full compilation | Phase 1 → 7 | "compile the bdr-outreach skill" |
| Report only | Phase 1 → 5, skip 6 | "analyze this skill, don't emit code" |
| Re-emit for different runtime | Phase 6 | "emit the existing pipeline.yaml for inngest" |
| Update compilation after skill changed | Phase 1 → 7, diff | "recompile bdr-outreach, it changed" |
## Pipeline
**Think in files, not in your head.** Your reasoning is not a draft.
Anything you would write out in full (the classification table, a
module, a test) belongs in a file, written once. Reason
about one decision at a time, just before the Write it informs. Do not
plan the whole compile, or draft its files, in one long block of
thinking first: a real compile spent 64,000 tokens and 14 minutes that
way before its first Write, and the block was discarded when it hit the
per-response output limit. So the order is:
1. One turn of Reads: every rubric reference and every file of the
source skill, together.
2. Write `pipeline.yaml` next, before any module: it is the Phase 2
classification and the data flow in one artifact. Every module then
takes exactly its node's `inputs:` keys (and `constants:`) as
parameters, which is a check rote runs.
3. Write each module together with its test, two or three modules per
turn.
4. `eval.yaml`, last. Then stop, and end your turn with one short
sentence, not a summary: rote renders `compile-report.md` and prints
the verdict, so a summary is output nobody reads.
**Progress markers.** At the START of each Phase N, rewrite
`progress.ndjson` in the work directory so it holds one JSON line per
phase entered so far, in order:
```
{"phase": 1, "name": "Intake"}
{"phase": 2, "name": "Node Classification"}
```
Rewrite the whole file each time (append the new phase's line to the
ones already there). Send that Write in the same turn as the phase's
first real tool call, never in a turn of its own: each turn re-reads the
whole conversation, so a marker-only turn costs as much as real work.
This file is machine-read for live progress display; it is not a
deliverable and never affects the IR.
### Phase 1: Intake
Read the target directory exhaustively:
1. `SKILL.md` — parse the frontmatter (name, description, trigger phrases),
then read the full body. Note every named phase, gate, and rule.
2. `references/` — read every sub-file. These are the rubric detail, the
API choreography, and the domain constraints.
3. Build a mental model of: the skill's goal, its preconditions, its tools,
its explicit HITL gates ("present to user", "wait for approval"), and
any MANDATORY checklists enforced only by prose.
Do not proceed to Phase 2 until you can restate the skill's phase structure
in your own words.
### Phase 2: Node Classification
Read `references/node-kinds.md` before starting this phase.
Walk through the skill from start to finish. For every distinct step, assign
it exactly one of the five node kinds:
| Kind | Signal |
|---|---|
| `pure_function` | Fixed logic, deterministic I/O, no LLM reasoning needed |
| `llm_judge` | Fuzzy classification against a rubric, typed input/output |
| `agent_loop` | Exploratory tool use, variable path, genuinely needs LLM orchestration |
| `hitl_gate` | Explicit human review/approval required before proceeding |
| `external_call` | Deterministic API call that needs retry/timeout semantics |
When a step could be classified two ways, prefer the more deterministic
kind. The goal is to push as much as possible into `pure_function` and
`external_call`, and reserve `agent_loop` for steps where the procedure
genuinely varies from run to run.
Output: `pipeline.yaml`, written now (Phase 5 says what goes in it, and
`references/ir-schema.md` gives its shape). It is the table mapping each
step to its kind. The one-sentence justification per row goes in each
node's `description`. Phase 5 is then a check of this file, with an Edit
if a module changed a decision.
### Phase 3: Codification Scan
Read `references/crystallization-heuristics.md` and
`references/implementation.md` before starting this phase. The second
file governs how far to take each extracted module. **Every
`pure_function` gets a real implementation plus tests derived from the
skill's own rules and examples**: the skill is the spec, so a parser,
validator, or renderer it describes is never left as a stub (the one
exception, a fact the skill does not state, is marked in the stub
itself). Vendor calls in `external_call` modules stay contract-documented
stubs unless ground truth is available (a `probe-context/` directory of
- observed MCP traffic in your work dir, and/or web research tools to
- verify current vendor API shapes).
+ observed MCP traffic in your work dir, and/or a `--backend api` compile,
+ which gives you web search and fetch tools to read the vendor's current
+ docs and asks for a working call, citing each page as `# docs: <url>`).
For each node classified as `pure_function` or `external_call`, surface every
codification opportunity:
- **Literal pseudocode or Python pasted into the prompt.** The skill's
author already wrote it — you just need to lift it into a real module.
- **Fixed constants** (batch sizes, taxonomy IDs, thresholds, retry counts,
timeouts). If the prompt says "batch of 10", that 10 is a constant, not
a judgment call.
- **Deterministic loops** with clear termination conditions.
- **String templates** for reports, output formats, or structured
responses.
- **MANDATORY checklists** enforced by English prose. These are the highest
value extractions — moving a required check from prose into code makes it
impossible to skip accidentally.
Each extraction's record (source file and line, the prose, the function
and its signature, the constants lifted into config) lives in the module
itself, in its docstring and `# skill: "..."` citations. Do not write it
out anywhere first. rote's report lists every module's citations.
Five provenance mistakes account for nearly every repair round real
compiles were sent back for. Get them right as you write each module:
- A status or label string that a renderer or summary writes (`"OK"`,
`"pending review"`, `"auto-approved"`, a flag like `"BELOW 75%"`) is an
outcome literal too, and needs its marker.
- Cite a table row whole, pipes included, never one cell or a
paraphrase of it.
- Never shorten a citation below 4 words of prose per fragment. Quote
the sentence, not the label.
- Close a multi-line `# skill: "` quote with a `"` at the end of its last
comment line.
- Anything a reviewer must check that the skill does not state is a
`# rote: assumption:` marker in the code. rote lists every marker under
the report's Open questions, so the marker's text is the whole entry:
say what you chose and what the skill leaves open.
### Phase 4: LLM-Judge Extraction
Read `references/llm-judge-extraction.md` before starting this phase.
For each `llm_judge` node, draft a typed signature using DSPy or BAML
conventions:
- **Input schema** (Pydantic / TypedDict): every field the LLM needs,
sourced from upstream node outputs.
- **Output schema**: enum-bounded decisions plus structured rationale.
Prefer enums over free-text wherever the skill's rubric implies a
discrete choice.
- **Seed examples**: harvest 3–5 examples directly from the skill's own
rubric. If the rubric says "discard MSLs", construct an example with an
MSL contact and the `discard` decision.
- **Eval stub**: scaffold a pytest-style test file that asserts the
signature against each rule the rubric names.
Output: a `signatures/<node_name>.py` file per `llm_judge` node, plus a
matching `evals/<node_name>.jsonl` seed.
### Phase 5: IR Assembly
Read `references/ir-schema.md` before starting this phase.
Assemble the full DAG as `pipeline.yaml`. Include:
- `input.input_schema`: the full JSON Schema for the pipeline input
payload, promoted from the entry nodes' signature design (see
`ir-schema.md`). When your instructions carry an `INPUT CONTRACT`,
it is decided: use its field and row names exactly, and require no
other input. A file under the skill's `references/` is never an input
("The input contract comes from data" in ir-schema.md).
- `nodes`: every step from Phase 2, with its kind, input/output types,
`inputs:` data-flow bindings (param → `pipeline.input[.field]` /
`<node_id>.output[.field]`), timeout, retry policy, and (for
`pure_function` / `llm_judge`) a reference to the extracted module.
Give every node a `source.section` naming the exact SKILL.md heading
it was derived from — this provenance is what makes incremental
re-compilation (`rote compile --update`) possible.
- `edges`: data flow between nodes, including fan-out for batch
processing.
- `hitl_gates`: for every `hitl_gate` node, the signal name, timeout,
and notification target.
- `config`: top-level schedule, on_failure handler, observability
settings.
The `pipeline.yaml` is the runtime-agnostic source of truth. Everything
Phase 6 emits is derived from it.
Alongside it, write the eval sidecar. Read
`references/eval-estimates.md`, then produce `eval.yaml` next to
`pipeline.yaml`: per-step agent-turn estimates for running the source
skill as raw instructions. You just classified every step in Phase 2 —
this captures, at near-zero extra cost, the cost model that
`rote eval`'s before/after scorecard is built on.
### Phase 6: Adapter Invocation
**You do not emit runtime code yourself.** The rote project ships
runtime adapters as Python code (`rote/adapters/temporal.py`,
`rote/adapters/inngest.py`, etc.) that consume the `pipeline.yaml` you
produced in Phase 5 and emit runnable code. Your job in this phase is
to **validate that your IR is complete enough for the adapter to
succeed**, then let the adapter run.
Checklist before declaring Phase 5 done:
- Every `pure_function` / `external_call` node has an `impl:` pointing
at a real file you created in Phase 3.
- Every step that calls an MCP tool in the source skill carries the
matching `mcp:` binding (see ir-schema.md — and cross-check against
`probe-context/observed-tools.json` when it exists: observed traffic
with no binding is a compilation bug).
- Every `pure_function` is implemented (or is a stub whose
`NotImplementedError` message says what the skill leaves unstated),
parses on Python 3.11, and is called from a `tests/test_<module>.py`
that is deterministic and offline. Every outcome the code decides
cites the skill text that decides it (`# skill: "..."`) or is marked
`# rote: assumption:`. rote runs the tests after you stop and sends
any failure back to you (implementation.md, "What rote checks after
you stop").
- Every `llm_judge` node has a `signature:` pointing at a real file
you created in Phase 4.
- Every `llm_judge` node's `inputs:` keys match the top-level
properties of its `signature_spec.input_schema`: each `required`
property is bound, and nothing is bound that the schema does not
declare. The validator rejects a mismatch (see
ir-schema.md rule 9). On a `fan_out` judge the element is ONE
property holding the whole element, never its fields flattened.
- Every `agent_loop` node has `tools:` listed.
- Every `hitl_gate` node has a `signal:`, and its `output` is a mapping
whose keys are the skill's own names for what the person provides.
- With a sample input, every pure function runs on it unchanged: rote
executes them on `grounding/sample-input.json` after you stop.
- Every node mentioned in `entry_nodes`, `exit_nodes`, `edges.from`,
`edges.to`, and `loop_body` exists in `nodes:`.
- `pipeline.yaml` would pass `rote.ir.load_pipeline`. You have no
shell to run it: rote validates it after you stop and sends the
errors back to you if it fails.
The `rote emit <pipeline.yaml> --runtime temporal --out <dir>` CLI
command (or its programmatic equivalent) takes it from there. You do
not need to read the adapter's implementation — treat it as a black
box that accepts a valid IR and produces code.
**Why the split:** the IR and its validation logic are the load-bearing
parts of compilation. Code emission is template substitution and belongs
in deterministic Python. Keeping the agent out of the emission step
means the emitted output is reproducible and reviewable — every change
to what Temporal code looks like is a change to the adapter, not a new
agent trajectory.
### Phase 7: Compilation Report
**rote writes `compile-report.md`. Do not write it.** Every fact a
reviewer needs is already in your deliverable, and rote renders the
report from it after you stop:
- **Summary and node table**: from `pipeline.yaml`. Each node's
`description` is its row, so make it the one specific sentence that
justifies the node's kind.
- **Crystallization log**: every `# skill: "..."` citation in
`extracted/`, per module and line, under the module docstring's first
line.
- **Open questions**: every `# rote: assumption:` marker, and every stub
with its `NotImplementedError` message. A judgment call the reviewer
should check belongs in one of those two places, in words that stand
alone: what you chose, and what the skill leaves open. rote reads
markers in `extracted/`, in `signatures/`, and in `pipeline.yaml`. A
structural guess (a kind, a gate, a `mandatory:` or `fan_out:` you
inferred) is a YAML comment on its node, and a judge prompt's guess
goes in the prompt or its signature module:
```yaml
- id: controller_review
# rote: assumption: one controller approves; the skill names no quorum
kind: hitl_gate
```
A report written by the agent cost real compiles 17 to 66 seconds of
output, was rewritten in repair rounds, and sent compiles back to repair
when it listed fewer questions than the code had markers.
## Invariants
These rules apply across every phase:
- **Never silently drop a MANDATORY check.** If the skill says a check is
required and you can't codify it, stub it with a `NotImplementedError`
that says why, or mark it `# rote: assumption:`. rote lists both under
the report's Open questions. Never omit it silently.
- **Prefer deterministic over agentic.** When in doubt, codify. Leaving
something agentic is a last resort for genuinely exploratory work.
- **The IR is the source of truth, not the emitted code.** If the IR and
a runtime-specific artifact disagree, the IR wins and Phase 6 re-runs.
- **Two adapters minimum before declaring the IR generic.** Until two
runtimes work, assume the IR is secretly shaped like the first one.
- **Dogfood yourself.** Once a stable version exists, point rote-compile
at its own SKILL.md. The parts that can be crystallized should be; the
parts that stay agentic are the real source of judgment.
## Related Concepts
- **"Workflow vs. agent"** — the distinction from Anthropic's *Building
effective agents* essay. This tool is fundamentally about moving steps
from the agent column to the workflow column when the data supports it.
- **"Blueprint First, Model Second"** — the EBF paper's framing. The
blueprint is the `pipeline.yaml`; the LLM is a specialized tool at
specific nodes.
- **DSPy compile** — what the `llm_judge` signatures compile *into* once
you have enough examples. rote produces the signature; dspy.compile
optimizes the prompt and few-shots against your eval set.