rote-compile · diff
git:20261019.d1e5050 to git:20261020.29cb118
46 added, 8 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. Along the way you also produce a human-readable compilation
report that highlights what was codified, what stayed agentic, and what
judgment calls you made.
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, the report) 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`, then `compile-report.md` once, last. Then stop.
+
**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: a table mapping each step to its kind, with a one-sentence
- justification per row.
+ 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).
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.
- For each extraction, produce a record of: source file and line, the current
- prose, the proposed extracted function name and signature, and any constants
- to lift into config.
+ 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. The report's crystallization log summarizes it.
+ 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.
+ - Every `# rote: assumption:` gets its own Open questions entry in the
+ report: count them before you write it.
+
### 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`).
- `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:`.
- 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
Write `compile-report.md` **once, with a single Write, as the last file
you produce**. Do not create it earlier, and do not Edit it afterwards to
polish wording: every extra turn re-reads the whole conversation, and a
real compile spent a quarter of its turns on one-line report edits. The
- phase outputs above (the classification table, the extraction records)
- are working notes in your reasoning, not files; this report is where
- they land, in one pass. Cover:
+ classification and the extraction records already exist, in
+ `pipeline.yaml` and the modules. This report summarizes them for a human
+ reviewer, in one pass. Cover:
- **Summary metrics**: total nodes, breakdown by kind, estimated token cost
reduction per run, list of HITL gates and what they block on.
- **Crystallization log**: every prose-to-code extraction from Phase 3,
with before/after snippets.
- **Open questions**: ambiguous judgment calls where you made a decision
but the human reviewer should verify. Be explicit about what you chose
and why. List every `pure_function` you left as a stub, with the
fact the skill did not state, and every outcome you marked
`# rote: assumption:` in the code (implementation.md, "Every outcome
cites the skill").
- **Suggested next steps**: what to dogfood first, which evals to run,
which node to recompile after the first production run reveals new
information.
## 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, surface it as an open question in
Phase 7 — never as a silent omission.
- **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.