dirtywork
Drive dirtywork — delegate a coding task to a local model that works in an isolated git worktree, then review the result, resume it with feedback, and record a verdict. Use when asked to run dirtywork
SKILL.mddirtywork/contract/SKILL.md
GitHubDriving dirtywork (v{{VERSION}})
If you are the dirtywork worker reading this inside the sandbox: this file is for the orchestrator that launched you. Ignore it and follow your task.
You are the orchestrator. dirtywork runs a worker — a local model — in an agentic tool-use loop inside an isolated git worktree and hands you the result. You pick the task, write the brief, review what comes back, and decide. The worker never merges anything; you do.
Reference: dirtywork contract prints every flag, the stdout JSON schema,
exit codes and transcript events for the installed version. Run it before your
first run in a session. Do not guess flags.
Before the first run
dirtywork --version— installed and on PATH (pipx install dirtyworkif not).- The endpoint you will pass is serving and the model you will pass as
--modelis loaded (LM Studiocurl -s http://localhost:1234/v1/models; Ollamaollama ps) or available (another OpenAI-compatible server:curl -s <base-url>/models). If the request does not say which provider, find out; if more than one is running, ask — never pick one silently, and never assume LM Studio.--provider anthropicneedsANTHROPIC_API_KEY; an OpenAI-compatible server with a key needsOPENAI_API_KEY(sent as a Bearer token when set). docker info— docker mode (the default, and the contained one) needs a running Docker; the worker image is pulled on the first run if it is absent (exit 2 with "Build or pull the worker image" means that failed — see the contract's--imageentry).--sandbox noneruns the worker on your host: read the contract's Security entry first.
The loop
1. Write the brief
One run = one task. The worker is a small model with a small context window: it executes, it does not design. Put in the brief:
- the exact files to touch, by path — and any it must not touch;
- the exact names, strings and signatures, quoted;
- the test(s) that must pass and the command that runs them (the worker runs them itself);
- decisions already made ("use X, not Y") so it does not re-decide them.
Keep it under ~40 lines. If you cannot name the files, you are not ready to
delegate — explore first, then brief. The target repo's CLAUDE.md /
AGENTS.md (at the base commit) is injected into the worker's prompt
automatically: put worker-facing conventions there, not in every brief.
2. Run
dirtywork run --repo <path> "<brief>" \
--model <model> \
[--provider openai|anthropic|ollama] [--base-url <url>] \
--verify "<test command>" --verify-rounds 2 \
--max-turns 60 --timeout 1800
--provider/--base-url: omit both for LM Studio onlocalhost:1234(the default). Use--provider ollamafor Ollama; for another OpenAI-compatible server, pass--base-url <url>.resumeinherits--provider, but it does not restore a custom--base-url— repeat that custom URL on every resume.--verifyruns your gate in the sandbox after the worker finishes and feeds failures back for up to--verify-roundsfurther attempts.- Progress is on stderr: transcript path, worktree path,
error:lines.tail -fthe transcript to watch a run. - Past preflight, stdout is exactly one JSON object. Parse it; do not
grep it. On exit 2 stdout is empty and the reason is the
error:line on stderr. Fields you will use:status,worktree,branch,base_commit,transcript,run_dir(its last path component is the slug),resumed_from,turns,files_changed,final_message,stuck_on,last_tool_result,last_assistant_text. - Exit codes:
0=completed.1= any other status (max_turns,timeout,stalled,stuck,verify_failed,context_exhausted,model_error,budget_exceeded,unchanged, …) — the worktree and branch are kept for review and salvage.2= preflight or environment error; nothing was created. - In the default docker mode the container has no network and no host
directories mounted (only the run volume and a read-only view of the repo's
git objects), so the worker cannot install dependencies; it has what the image
ships — git, bash, python3, node/npm, .NET, ripgrep, jq, curl, shellcheck.
--allow-networkgives the container bridge networking so installs work and the offline guarantee is gone — use it deliberately, and for anything permanent build a derived image instead (see the contract). Run any richer gate yourself, on the host, against the exported worktree.
3. Review — always, before anything merges
- Start timing before reading the diff:
dirtywork runs review-start <slug>. Recording the verdict stops the timer and saves its wall-clock duration. If an older installed version does not supportreview-start, record the actual start and end times with your clock tool and pass their elapsed seconds as--review-secondson the verdict; never estimate or use worker-end-to-verdict delay as review time. git -C <worktree> statusthengit -C <worktree> diff: read the diff first, the transcript second. Comparefiles_changedwith the brief — a touched file you did not name is a finding.- Run the repo's own gate on the host against the worktree.
- Judge the work, not the status:
completedwith a wrong change is a reject;max_turnswith a correct change is salvageable.
4. Resume with feedback, or re-brief
If the work is close: dirtywork resume <slug> --feedback "<exactly what to change>" — same worktree, same branch; the worker is told to inspect its
earlier work and apply your feedback. A resume is a new run with its own
slug and run_dir; its JSON's resumed_from names the run it continued while
branch and worktree still carry the original slug. From here on use the
newest slug for runs verdict and runs clean — cleaning an earlier slug in
the chain leaves the worktree and branch in place. Two resumes without
convergence means the brief was wrong: reject, rewrite the brief, run again
from a clean base.
5. Verdict and cleanup
dirtywork runs verdict <slug> accept|reject --note "<why>"records your decision and stops a started review timer in the run'srun.json.--review-seconds <seconds>overrides the timer with an explicit duration.- Accept: commit or PR the
dirtywork/<slug>branch as you would any contributor's work. - Reject: record it, then discard the work but not the record —
dirtywork runs clean <slug> --force --keep-transcript.--forcebecause a rejected worktree has uncommitted changes andcleanrefuses to delete those otherwise;--keep-transcriptkeepsrun.jsonand the transcript and removes the container, volume, worktree and branch. Plainruns clean <slug>deletes the receipts too. dirtywork runs listanddirtywork runs show <slug>inspect earlier runs.
Rules of thumb
- Never merge unreviewed worker output. The worker ran
bashin a sandbox; a shell is a shell — see the contract's Security entry. - Parallelism is processes: run independent briefs concurrently (LM Studio serves several requests per loaded model). Never two tasks in one brief.
- On
stuck, readstuck_on(the repeated failing command); onstalledormax_turns, readlast_tool_resultandlast_assistant_text. Either way the fix is usually in the brief, not the model. - Keep receipts:
run.json, the transcript and the verdict are the record of what the worker did and what you decided.runs clean --keep-transcriptpreserves them; plaincleandoes not.