Autonomous coding loop based on the [Ralph Wiggum technique](https://ghuntley.com/ralph/) by Geoff Huntley. Each iteration: reload context from disk, pick one task, execute it, test, commit on green. The loop is the outer process; the LLM decides what to work on.
```mlld
>> ralph.mld - autonomous coding agent loop
import { @claudePoll } from @mlld/claude
var @tools = ["Read", "Write", "Edit", "Glob", "Grep", "Bash(git:*)", "Bash(npm:*)"]
>> Cheap model picks the most important task from current plan
exe llm @pickTask(plan, specs) = [
let @prompt = `Given this plan and these specs, identify the SINGLE most
important next task. Search before assuming something isn't implemented.
Return JSON: { "task": "...", "type": "implement|fix|test", "files": [...] }
@plan
@specs
IMPORTANT: Write your JSON response to @mx.outPath using the Write tool.`
@claudePoll(@prompt, {
model: "haiku",
tools: ["Read", "Glob", "Grep", "Write"],
poll: @mx.outPath
})
=> <@mx.outPath>? | @parse.llm
]
>> Worker executes the task with full agent capabilities
exe llm @doTask(task, specs) = [
let @prompt = `# Task
@task.task
# Specs
@specs
Implement this task. Search the codebase before assuming anything is
not implemented. After implementing, run tests for just this change.
IMPORTANT: Write your result JSON to @mx.outPath using the Write tool.`
@claudePoll(@prompt, {
model: "sonnet",
tools: @tools,
poll: @mx.outPath
})
=> <@mx.outPath>?
]
>> Validate with tests
exe @test() = [
let @out = sh { npm test 2>&1 }
=> { pass: @out.exitCode == 0, output: @out }
]
>> The loop
loop(endless) [
>> Fresh context every iteration — the context IS the history
let @plan =
when @plan.trim() == "" => done "complete"
let @specs =
>> One task per loop — trust the LLM to pick what matters
let @task = @pickTask(@plan, @specs)
let @result = @doTask(@task, @specs)
>> Test backpressure — only commit what passes
let @check = @test()
when @check.pass => run sh { git commit -am "@task.task" && git push }
continue
]
```
**Core principles:**
- **One task per loop** — Each iteration picks a single task and executes it. Narrowing scope keeps context usage low and outcomes predictable.
- **Fresh context from disk** — Plan and specs reload every iteration. No chat history carried forward. The filesystem is the state.
- **Test backpressure** — Tests gate commits. Failing iterations aren't fatal; the next iteration sees the current state and adapts.
- **LLM picks the work** — The cheap classifier decides priority. The orchestrator doesn't encode task selection logic.
**Crash recovery** — The `llm` label on `@pickTask` and `@doTask` enables automatic caching. If the loop crashes mid-iteration, re-running the script replays completed LLM calls from cache.
```bash
mlld run ralph # auto-resumes via cache
mlld run ralph --resume @doTask # re-run all worker calls
mlld run ralph --new # fresh run, clear cache
```
**Hook telemetry:**
```mlld
hook @progress after op:loop = [
log `iteration @mx.loop.iteration`
]
```
**With pacing** — Add a delay between iterations to avoid hammering APIs:
```mlld
loop(endless, 5s) [
...
]
```
**With a cap** — Limit total iterations:
```mlld
loop(50) [
...
]
```
Expose mlld functions as MCP tools with fixed context parameters and security guards. The agent sees a narrow interface; guards enforce what data can flow through.
**Define the function, guard, and tool collection:**
```mlld
exe @searchIssues(org: string, repo: string, query: string) = cmd {
gh issue list -R @org/@repo --search "@query" --json number,title
} with { description: "Search GitHub issues" }
guard @noSecrets before op:exe = when [
@input.any.mx.labels.includes("secret") => deny "Secret data cannot flow to tools"
* => allow
]
var tools @agentTools = {
searchIssues: {
mlld: @searchIssues,
bind: { org: "mlld-lang", repo: "mlld" },
expose: ["query"],
description: "Search mlld issues by keyword"
}
}
export { @searchIssues, @agentTools }
```
The agent sees one parameter (`query`). The bound `org` and `repo` are invisible and fixed. The guard blocks any call carrying `secret`-labeled data.
**Serve it:**
```bash
mlld mcp tools.mld --tools-collection @agentTools
```
The `--tools-collection` flag tells the MCP server to use the reshaped tool definitions instead of raw exports.
**Give it to an agent:**
Point any MCP client at the command. For Claude Code:
```json
{
"mcpServers": {
"my-tools": {
"command": "npx",
"args": ["mlld", "mcp", "tools.mld", "--tools-collection", "@agentTools"]
}
}
}
```
**Add operation labels for policy:**
```mlld
var tools @agentTools = {
searchIssues: {
mlld: @searchIssues,
bind: { org: "mlld-lang", repo: "mlld" },
expose: ["query"],
labels: ["read-only"],
description: "Search mlld issues"
},
createIssue: {
mlld: @createIssue,
bind: { org: "mlld-lang", repo: "mlld" },
expose: ["title", "body"],
labels: ["destructive"],
description: "Create an mlld issue"
}
}
```
Guards can then check `@mx.op.labels.includes("destructive")` to block or require approval for write operations. See `mcp-guards` for after-guard patterns that validate tool outputs.
The `prose {}` syntax executes LLM-interpreted DSL skills. By default it uses **OpenProse**, but any custom interpreter can be configured.
## What is Prose Execution?
Prose execution invokes skills that an LLM interprets at runtime. Unlike `run js {}` which executes deterministically, `prose {}` sends content to an LLM with specific skills enabled. This enables complex multi-agent workflows defined in a domain-specific language.
## Setup
1. Install the OpenProse plugin in Claude Code:
```
/plugin marketplace add git@github.com:openprose/prose.git
/plugin install open-prose@prose
```
2. Restart Claude Code and boot OpenProse:
```
/prose-boot
```
3. Skills will prompt for approval on first use.
## Basic Usage
```mlld
import { @opus } from @mlld/prose
exe @research(topic) = prose:@opus {
session "Research @topic"
agent researcher { model: sonnet, skills: [web-search] }
researcher: find current information about @topic
output findings
}
run @research("quantum computing trends")
```
## Key Concepts
**session** - Names the workflow for context
**agent** - Defines an agent with model and skills
**loop until** - Iterates with semantic exit conditions:
```mlld
exe @refine(draft) = prose:@opus {
session "Refine document"
loop until **the draft meets publication standards** {
critique @draft
revise based on critique
}
}
```
**parallel** - Run tasks concurrently:
```mlld
exe @gather(topics) = prose:@opus {
session "Research multiple topics"
parallel for each topic in @topics {
research topic
}
combine results
}
```
## Template Files
For complex workflows, use external files:
```mlld
exe @workflow(ctx) = prose:@opus "./workflow.prose"
exe @workflow(ctx) = prose:@opus "./workflow.prose.att" >> ATT interpolation
```
## Custom Interpreters
Use any LLM-interpreted DSL by configuring different skills:
```mlld
import { @claude } from @mlld/claude
>> Create a custom model executor
exe @myModel(prompt) = @claude(@prompt, { model: "opus" })
>> Configure with custom skills
var @myDSL = {
model: @myModel,
skills: ["my-custom:boot", "my-custom:run"]
}
exe @process(data) = prose:@myDSL {
>> Your custom DSL syntax here
analyze @data
output result
}
```
The skill determines how the LLM interprets the prose content. OpenProse is one implementation - you can create your own DSL skills or use other prose interpreters.
## OpenProse Requirements
For OpenProse specifically:
- **Claude Code** with Opus (only model that reliably interprets OpenProse syntax)
- **OpenProse skills** approved: `open-prose:prose-boot`, `open-prose:prose-compile`, `open-prose:prose-run`
See `mlld howto exe-prose` for syntax details. OpenProse docs: https://prose.md