---
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: `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.

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
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.
