Automates agentic work on a repo: given a prompt and a target repo directory,
anneal will use the local pi CLI to plan the work, split it into small tasks, then
implement → verify (with bounded retries) each task, and finally verify the whole.
anneal exists to make local models do real agentic coding work — models like Qwen3 30B/35B or 27B-class LLMs running locally. These models struggle with large, complex prompts (they lose the thread, skip steps, or drift), but they do well with small, focused prompts where each call has one clear job.
So anneal's whole design is built around that constraint: it breaks a goal down into many tiny tasks and gives each pi call only its own minimal, self-contained context (exactly the task at hand — not the whole goal, not the other tasks). The repo on disk is the shared state between them. A local model that can't plan and execute an entire feature in one shot can still implement and verify one small, well-scoped step at a time — and anneal chains enough of those together to finish the whole job.
Each stage spawns the local pi CLI (--print --mode json) to do real
work on a real repo. Configure models per stage and a workspace mode (current branch / new
branch / new worktree).
anneal is pi — it spawns the real pi CLI — so the obvious alternative is to package all
of this as a pi extension (a tool or "superpowers"-style skill) rather than a standalone
orchestrator. That route is a non-starter, for two reasons that are really one: anneal
needs to own the process, and it needs to show you the process.
Extension limitations: anneal is the controller, not a guest. An extension runs inside pi's runtime — it shares pi's process, event loop, model-call plumbing, tool sandbox, and lifecycle. It is a guest in a host it cannot supervise, and anneal's core features are exactly the ones a guest cannot have:
- Process-tree control. Every stage is a separate pi child process (
cmd -> node -> ...on Windows). Cancel, restart-per-task, and timeout each kill that tree — on Windows viataskkill /Tso nothing is orphaned. An extension is already in pi's own process; it cannot kill its host, and it has no sub-processes of its own to supervise. - Deterministic git ownership. anneal snapshots the working tree before each task,
stages only the files that task touched, commits each passing step itself with
--no-verify, and installs a pre-commit guard hook that rejects any commit a model tries to make directly — because pi runs with a bash tool that cangit commit, and a verifier could chase a phantom "commit message says X" criterion by amending anneal's commits. That guard, and the whole git lifecycle, live in the controller, not in the agent. - Workspace isolation. current-branch / new-branch / isolated-worktree modes, so a run never corrupts your checkout. The controller creates and owns the worktree; the model never sees more than its working directory.
- Lean, deterministic pi. anneal invokes pi with
--no-extensions --no-skills --no-prompt-templates --no-context-filesfor a fast, predictable worker. If anneal itself were an extension, it would have to be loaded as one — directly contradicting the lean design it depends on, and inheriting pi's session state, accumulated context, and prompt machinery it deliberately strips out.
Visibility: anneal shows you the whole pipeline. An extension is opaque — it runs inside pi and surfaces whatever pi chooses to expose; debugging means digging into pi's own machinery. anneal is built to be fully inspectable, and that only works from outside:
- Live streaming. Every stage's raw transcript streams to the UI as
stage_outputevents — you watch each pi call's output in real time, per task, per attempt. - Heartbeat & supervision. The UI shows elapsed time and time-since-last-output so you can see a run stuck; Cancel and per-task Restart kill the pi process tree on demand.
- Cost accounting. Each call's dollar cost is accumulated from pi's
usage.cost.total(message_end) events — extension APIs don't hand you model pricing. - Editable prompts. Every stage template lives in a file-backed config store you can edit; anneal owns prompt composition — each call gets exactly its own minimal context, and exploration findings are captured and threaded forward — rather than inheriting pi's prompt machinery.
- Deterministic contracts. anneal parses pi's JSONL output itself: the task-list JSON,
the
ANNEAL_VERDICT: PASS|FAILsentinel, and commit messages. The model↔controller contract is anneal's to define.
So pi is treated as a headless worker: spawn it lean per stage, feed it one tiny self-contained prompt, read its output, and do everything else — supervision, git, commits, workspace, UI — in the controller, where it can be seen and controlled. That is the difference between using pi and wrapping it.
- Plan — pi inspects the repo and returns a self-contained task list (JSON).
- Per task — pi implements the task (edits files), then verifies it (runs
checks, ends with
ANNEAL_VERDICT: PASS|FAIL); a failing verify retries up to max attempts times. - Final verify — pi checks the whole result against the original goal.
Each pi call gets only its own minimal context; the repo on disk is the shared state.
Some tasks in the plan are exploration: they investigate the repo read-only (locating code, spiking an approach), and their written findings — not file changes — are the deliverable, captured and passed to the later implementation tasks as context. Implementation tasks carry their work forward as committed code instead.
Instead of planning from a goal, you can start from an existing implementation plan
(plan_from_doc, which splits it into tasks rather than re-deriving one), or paste a raw
pi plan back in to resume a run whose plan failed to parse, or skip planning entirely with
a hand-written task list.
- Plan review (checkbox, on by default): after planning, the run pauses so you can edit each step's title/description/acceptance criteria, add or remove steps, then Approve & run. Acceptance criteria are fed to the verify stage.
- Retries: a failing verify retries up to max attempts (default 100). Each retry gets the previous verify output so it knows what to fix. If a step never passes, the run stops and reports — the failing step's changes are left uncommitted.
- Commits: anneal commits deterministically — it snapshots the working tree before each
task, stages only the files that task touched (never pre-existing junk), and commits each
passing step with
--no-verifyusing a pi-written message (the verify model). For the run's duration it also installs a pre-commit guard hook that rejects any commit a model tries to make directly, so a verifier can't amend anneal's commits chasing a phantom "commit message says X" criterion; the hook is removed when the run ends. - Runtime: each step and the whole job show elapsed time.
pi is invoked lean — --no-extensions --no-skills --no-prompt-templates --no-context-files
— so anneal runs a fast, deterministic pi that just does the task, without loading your
globally-installed extensions/MCP servers (which can, e.g., index the whole repo and make a
run look stuck).
While a run is in flight the UI shows a heartbeat (elapsed time / time since last output) and a Cancel button that kills the run's entire pi process tree immediately. Each task's card also has a Restart button while that task is actively implementing/verifying: it kills just that task's pi process and starts a fresh attempt in place. Restart does not revert any file edits already made, and it consumes one of the task's max attempts — so restarting on the last attempt fails the task, same as a natural retry exhausting the cap.
Every stage's prompt template is editable. Templates live in a file-backed config store
(~/.anneal/prompts.json, or ANNEAL_PROMPTS_FILE), seeded from built-in defaults; edit
and Reset them on the Settings → Prompts page (/settings/prompts). Templates are
plain str.format strings referencing that stage's variables ({prompt}, {criteria},
{verify_feedback}, {diff}, ...); save-time validation rejects stray braces or unknown
placeholders. implement_fix is the retry variant of implement (used when a verify fails),
and plan_from_doc the variant of plan for an existing plan document.
ANNEAL_PI_BIN— pi executable (defaultpi).ANNEAL_PI_TIMEOUT— per-call timeout in seconds (default1800).ANNEAL_HEARTBEAT_SECONDS— seconds between heartbeat events (default5).ANNEAL_MAX_ATTEMPTS— default max attempts per step (default100; also set per run).ANNEAL_PROMPTS_FILE— prompt-template config file (default~/.anneal/prompts.json).ANNEAL_FINDINGS_DIR— where exploration findings are persisted for post-run review (default~/.anneal/findings).ANNEAL_DEFAULT_REPO_DIR— default repo directory prefilled in the UI (default empty).
Models are loaded from pi's ~/.pi/agent/models.json (providers → models).
- current — edits your checked-out branch in place.
- new_branch — creates a new branch and works there.
- worktree — creates an isolated git worktree on a new branch; your checkout is untouched.
For new_branch and worktree you may type your own branch name (used verbatim, e.g.
feature/foo); leave it blank to get the default anneal/<run-id>. If the branch already
exists, the run fails with a clear error so nothing is overwritten.
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
uvicorn backend.main:app --port 8000Open http://localhost:8000, pick models per stage, and click Start run.
pytest -vbackend/— FastAPI app, orchestrator state machine, pi runner (real).frontend/— no-build-step UI (HTML/JS/CSS).docs/superpowers/— design spec and this plan.