---
title: 'Mullet 2, coding jobs you can leave running'
url: 'https://www.dealmobile.co.uk/writing/mullet-2-coding-jobs-you-can-leave-running'
type: Post
published: '2026-10-25'
summary: 'Mullet 2 plans a coding job, runs it unattended in a sandboxed clone, and passes each step only on evidence. Why I rebuilt it, and how to try it.'
---

# Mullet 2, coding jobs you can leave running

Published 2026-10-25

![A terminal session of Mullet 2 commands: job plan, show, approve, run, report and merge, with the approve and merge steps marked as the two points where a person decides.](https://www.dealmobile.co.uk/sites/default/files/styles/almanac_shot_md/public/content/images/posts/mullet-2-coding-jobs-you-can-leave-running/hero.png.webp?itok=LOCxPV67)

I have rebuilt [Mullet](https://www.dealmobile.co.uk/products/mullet.md). The old version sent coding tasks to the cheapest model that could do them. The new one runs a whole coding job while you are away, and only tells you a step is done when its own checks say so. I wrote about the old version in [Checking a delegate’s edits before trusting them](https://www.dealmobile.co.uk/writing/checking-a-delegates-edits.md) and [Picking the cheapest model that can do the job](https://www.dealmobile.co.uk/writing/picking-the-cheapest-model-that-can-do-the-job.md). This post is about why it needed replacing.

## What was wrong with the old one

I stopped using Mullet’s delegation once a bigger Claude subscription made working in Claude Code quicker. An audit of the old code then showed why it had become untrustworthy.

Every “done” and “passed” in the orchestration modes came from words in a model’s reply. The checks were substring matches for PASS and APPROVAL, so a reply saying “NO APPROVAL” passed. The agent runner treated a model that stopped calling tools as finished. The swarm tool returned `applied: true` without applying anything.

There was no isolation. A delegate could write anywhere under the home directory and run any shell command. opencode ran with `--auto`, and it was handed keys from the `.env` of whatever directory it started in.

The overnight loop could mark failed work as done, commit it and push to `origin master`. Autopilot had Mullet’s own test suite hardcoded as the thing to run.

It also only really worked from its own checkout. Paths came from `__file__`, settings came from the checkout’s `.env`, and hooks were registered by absolute path. The install script installed an unrelated PyPI package that happens to be called mullet.

Last, the test suite was about 17,900 tests run in series. That took about 30 minutes, so nobody ran it often.

## What Mullet 2 does instead

Mullet 2 is a job runner. You install it once, on Linux or macOS, and then use it in any repo.

```
mullet job plan "<goal>"
mullet job show <id>
mullet job approve <id>
mullet job run <id>
mullet job status <id>
mullet job report <id>
mullet job merge <id>

```

`plan` starts a read-only Claude session that proposes a task tree. `show` prints it. `approve` is the first of two points where a person decides: you approve the plan, not the code. `run` starts the job in the background and you can close the terminal. `status`, `tail`, `stop` and `resume` do what they say.

The planner breaks a goal down until each leaf fits one pass. The depth is at most 3 and the tree has at most 40 nodes. A parent step passes only when its children pass and its own integration checks pass on their combined change, with the full test suite. Workers never start other workers. Only the orchestrator creates nodes, so the budgets and checks live in one place.

Each node runs in a private clone of the base commit it was planned against, inside a sandbox. I cover that in [Sandboxing an AI coding worker](https://www.dealmobile.co.uk/writing/sandboxing-an-ai-coding-worker.md). When the worker stops, Mullet snapshots the tree, runs its gates over it, and commits exactly the tree it judged. The gates are in [Gates, not a model’s word](https://www.dealmobile.co.uk/writing/gates-not-a-models-word.md).

If a node fails, it is reset to where it started and retried with the gate output, up to 3 attempts. After that it is BLOCKED. It is never marked done because the model said so.

Status lives in a SQLite ledger of jobs, nodes, attempts, gate results and events. A node’s status can only be set by one function, `judge()`, from gate results. `mullet job report` builds the evidence report from that ledger. Anything the worker says about itself appears in the report, but it is labelled as a claim.

The second decision is `merge`. It refuses a branch name that already exists, fetches the job’s branch into your repo as a new branch, and stops. It never pushes. You read the report and the diff, and you merge it yourself.

Stopping is safe. The runner is detached with `setsid` and holds a lock. After a `kill -9`, `resume` puts the clone back at the last commit the ledger recorded and carries on.

## What it ran

I have two runs to report so far. A 2-node job on a small sample Python repo passed end to end in 32 seconds. A 5-node tree made by the planner, with the reviewer switched on, passed in about two minutes, and the ledger audit was clean.

Those are small. The first real ones were on Mullet’s own repo: over the night of 24 October it built its own night mode, the `mullet local` watchdog and the gated version of `mullet_opencode`, each a planned tree of five to seven steps, each merged after I read the diff. Along the way the reviewer stopped real bugs from the cheaper models, including a test class defined twice so that half its tests never ran. They were also slow, because of how cheap models climbed the ladder, which is the subject of [the delegation ladder](https://www.dealmobile.co.uk/writing/the-delegation-ladder.md).

## Where the worker comes from

The worker is your own `claude -p`, Claude Code in print mode, logged in as you. Mullet never reads, stores or passes Claude credentials. [Running your own Claude subscription unattended](https://www.dealmobile.co.uk/writing/running-your-own-claude-subscription-unattended.md) covers what that means in practice, including a trap with `ANTHROPIC_API_KEY`. One worker runs at a time across all jobs, because they share one subscription’s quota.

## Trying it

Install from GitHub with uv or pipx. Do not run `pip install mullet`: that is a different project.

```
git clone https://github.com/tomgould/mullet-public.git
cd mullet-public
scripts/install.sh
mullet doctor

```

`mullet doctor` checks Claude Code, bubblewrap, socat and whether a stray `ANTHROPIC_API_KEY` is set. `mullet paths` prints where everything lives. Settings go in `~/.config/mullet`. An older `~/.mullet` is still read, and `mullet migrate-state` moves it.

`mullet setup-claude` registers the MCP server and skills with `claude mcp add --scope user`. It records each change, so `mullet setup-claude --uninstall` undoes exactly what it did. Hooks are opt-in.

Then, in a repo you can afford to experiment on:

```
mullet job plan "add a --json flag to the export command"

```

Read the tree it proposes before you approve it.

## What it is not

It is not a way to spend less on its own. Cheaper workers came next: opencode with Kimi, OpenRouter, free or local models, under the same sandbox and gates, off unless a repo’s profile allows it. How that went, with the numbers, is in [the delegation ladder](https://www.dealmobile.co.uk/writing/the-delegation-ladder.md).

The old orchestration modes are gone, and the swarm no longer claims to apply anything. The Trawl knowledge search and the MCP server are still there, and so are the older delegation tools with their model routing and presets. `MULLET_DELEGATION=off` turns those off, which is how I run it.

The code is at <https://github.com/tomgould/mullet-public>.

- **Products:** [Mullet](https://www.dealmobile.co.uk/products/mullet.md)
- **Tags:** [Python](https://www.dealmobile.co.uk/writing/tag/python.md), [AI](https://www.dealmobile.co.uk/writing/tag/ai.md), [Testing](https://www.dealmobile.co.uk/writing/tag/testing.md)

---

Index of this site: [llms.txt](https://www.dealmobile.co.uk/llms.txt)
