Run massive plans unattended for hours, gated on evidence.
Supervised autonomous-run engine for coding agents. A milestone state machine that launches one fresh headless agent session per milestone, grades what each session claims against written evidence, and refuses to spend a retry on a usage limit.
A milestone only counts when something on disk proves it: a passing test, a diff, a commit. The marker moves only when the session's claim and the evidence it left behind agree.
milestoner does not remove your work; it moves it. The hours of execution go to the agent, but the design, the decomposition and the acceptance criteria are yours, written before the run starts. It is a verification engine with an executor inside, not the other way around.
The milestoner binary is the engine: it runs a run, grades each session and owns the state machine.
npm install -g milestonerOr from source, which is what you want if you intend to change the engine:
git clone https://github.com/fabrodz/milestoner.git
cd milestoner && npm install && npm run build && npm linkRequires Node 20+ and an agent CLI on PATH (Claude Code by default).
The supervisor and planner skills for Claude Code ship inside the package; milestoner skill install writes them into .claude/skills/ (-g for ~/.claude/skills/). There is no separate
plugin or marketplace install: the skills shell out to the milestoner binary, so distributing
them apart from it would install something with nothing to call.
Runs on Windows, macOS and Linux; CI exercises all three. Everything platform-specific is inside
the engine: killing a session and everything it spawned (taskkill /T or the session's own POSIX
process group), opening the report (start, open or xdg-open), and launching through an npm
.cmd shim on Windows with the quoting cmd.exe needs.
The one thing you supply per platform is the environment adapter, because unsticking a host is
inherently host-shaped; examples for both families ship in
examples/adapters/.
milestoner exists to run a coding agent for hours while you are not watching, so the default
agent.args include --dangerously-skip-permissions. A headless session cannot answer a permission
prompt; without that flag it hangs until the timeout instead of working.
The consequence is real and worth stating plainly: for as long as the run lasts, the agent can read, write and delete anything your user account can, and run any command, unattended. The engine does not sandbox it and cannot review what it does.
Before leaving a run overnight:
- Run it in a project directory you would be willing to restore from git.
- Commit or push first. The protocol template tags every green milestone, which is what makes
git reset --hard <tag>a real rollback. - Prefer a VM, container or dedicated user account when the project is not yours.
- Consider removing
--dangerously-skip-permissionsand supplying a narrower allowlist through your agent's own settings. The engine passesagent.argsthrough verbatim, so this is a config change.
First time here? The guide's quickstart walks the whole first run - scaffold, plan, run, supervise, report - assuming nothing. The short version:
milestoner init --run my-run --milestones 5
# write .milestoner/protocol.md and the milestone prompts, then:
milestoner run
milestoner statusThe protocol and the milestone prompts are yours to author; that friction is deliberate (the evidence gate only means something when a person wrote the criteria). If you do not know where to start, the planner skill walks a Claude session through it with you: it interviews you, proposes a milestone breakdown for your approval, and only then writes the prompts, the protocol TODOs and the liveness config. Nothing is generated without your sign-off. That lowers the bar from authoring to reviewing, not to zero: you still need to tell a verifiable acceptance criterion from a vague one, because a bad plan approved makes the gate theatre.
milestoner skill install plannerThen, in a Claude Code session at the project root: "Use the milestoner-planner skill to plan this run."
| Command | What it does |
|---|---|
milestoner init [--run <name>] [--milestones <n>] [--force] |
Scaffold .milestoner/: config, state machine, protocol template, prompt skeletons. |
milestoner add [--title <text>] |
Append one pending milestone with its prompt skeleton, mid-run included; a live runner picks it up when its turn comes. |
milestoner lint [--json] |
Check the run's form before a session spends time on it: prompts, protocol, config. Errors exit 1; warnings alone stay 0. |
milestoner run [--milestone <id>] [--max-attempts <n>] [--model <name>] [--once] [--no-lint] [--no-panel] [--open | --no-open] [--serve] |
Drain the run: one fresh agent session per milestone until complete or blocked. Lints first and refuses to start on error-level findings on pending milestones; --no-lint overrides. Brings the machine panel up by default; --serve attaches a per-run panel instead. |
milestoner status [--json] |
Milestones, attempts, evidence counts, and the pulse. |
milestoner runs [--json] |
Every run registered on this machine, from anywhere: project, milestone, progress, liveness. |
milestoner unblock <id> [--keep-attempts] |
Clear a block after fixing it; sets the milestone back to pending. |
milestoner steer ["<text>"] [--append] [--clear] |
Course-correct a run in flight; applies to the next session launched. |
milestoner report [--out <path>] [--open] |
Write a single self-contained HTML report of the run. |
milestoner serve [--all] [--port <n>] [--write] |
Local web panel. Loopback only, key in the URL. --all serves every run on the machine, from any directory. |
milestoner skill install [<name>] [-g|--global] [--force] [--print] |
Install the bundled skills (supervisor, planner) into .claude/skills/; name one to install just it. |
milestoner transcript [--name <file>] [--lines <n>] [--raw] |
What the running session is doing, as prose rather than as its wire format. Read-only. |
milestoner kill [--reason <text>] [--rule <n>] |
Supervisor intervention: kill the hung agent session. Never the runner. |
milestoner attend [--seconds <n>] [--rule <n>] |
Supervisor intervention: run the configured environment adapter. |
Exit codes: 0 ok, 1 error, 2 blocked - and for runs, 2 also when a listed run's runner is
gone, which is the same "this needs you" signal.
runs is the only command that does not need a project. status answers for the directory you are
in; runs answers for the machine, reading a registry at ~/.milestoner/runs.json that every runner
adds itself to on start and removes itself from on exit. A runner that was killed never gets to
remove its entry, so it stays, reported gone, for a day. That is the point: a run that died
overnight is the one worth being told about.
- Fresh session per milestone. Clean context every time. State lives in files, never in the conversation.
- The engine owns
state.json. The session writes one small drop box,.milestoner/result.json, with its status, its evidence lines and, when blocked, its diagnosis. The engine grades that, merges it, and archives the raw claim under.milestoner/results/. - Evidence is a gate.
donewith no evidence line per acceptance criterion is downgraded to incomplete and retried. The verdict never comes from the exit code. blockedneeds a diagnosis: exact symptom, everything tried, the single clearest user action. Blocked is a handoff with an address, not a failure.- Infrastructure is not failure. A session that dies in seconds with a tiny transcript, hits a usage limit, or crashes at any point leaving a next-to-empty transcript, does not consume an attempt. An announced reset time is parsed and waited out.
- Liveness comes from side signals. Watched source dirs, test-result files and tool logs, never
the transcript: it sits still through a long generation and grows while an agent narrates a retry
loop. An mtime moves only when something was done. What the transcript is for is the other
question - what the session is doing - which
milestoner transcriptanswers in prose, for a person or for a supervisor deciding whether to step in.
The engine keeps a run correct. The supervisor keeps it alive: a Claude session that wakes every ten minutes, decides whether the run is advancing, and intervenes inside a bounded playbook.
milestoner skill install supervisorThen, in a Claude Code session at the project root:
/loop 10m Use the milestoner-supervisor skill to perform one supervision cycle.
Each cycle it reads the whole run through milestoner status --json, reads what the live session is
doing through milestoner transcript, and applies the first matching rule: healthy, environment
stalled, agent session hung, waiting out a usage limit, runner dead, blocked for real, or something
it cannot explain. The transcript is why an intervention can name what it saw instead of quoting a
timer; it is never a liveness signal. Its entire write surface is milestoner kill,
milestoner attend, relaunching milestoner run, and appending to .milestoner/supervisor-log.md.
It never edits project code, never touches state.json, and never runs the project's own tools
while a session owns them. Clearing a block stays a human decision.
milestoner kill targets the agent session, not the runner: the runner sees the session end, grades
it incomplete, consumes an attempt and relaunches with fresh context. The kill is recorded so it
cannot be mistaken for an infrastructure death and silently refunded.
Environment adapter. Some environments get stuck in ways no agent can fix from inside its own
session: a GUI editor loses focus, a native modal blocks the main thread, a connected device drops
off the bus. Playbook rule 3 runs environment.attendCommand against exactly that, and nothing
else; the adapter is one command line you write, and a headless project leaves it null so the rule
cannot fire. Two examples ship in examples/adapters/.
The full playbook, rule by rule, is in docs/GUIDE.md.
You do not have to kill a run to correct it. milestoner steer writes .milestoner/STEERING.md, and
every session launched from that point on gets the text inlined into its kickoff as an override on
the milestone prompt:
milestoner steer "prefer the simpler fix over the general one"
milestoner steer --append "do not touch the public API"
milestoner steer # show what is in force
milestoner steer --clear # back to the milestone prompts aloneIt persists until you clear it, and every attempt records the steering that was in force, so it is
always visible which sessions saw it. It overrides the prompt; it does not license dropping an
acceptance criterion. A steer that makes a milestone impossible comes back as blocked.
The supervisor cannot steer. If it thinks a run needs correcting, it proposes the wording and you decide.
milestoner run # the machine panel comes up with the first run and spans every run
milestoner serve --all # the same machine panel, by hand, from any directory
milestoner run --serve # a panel pinned to this run only, closed when the run ends
milestoner serve --write # the per-project panel on its own, against whatever is or is not runningEvery form prints a URL carrying a one-time key. The panel shows the same run status does,
refreshed over server-sent events, and lets you act on it: scaffold a new run in a directory, write
the milestone prompts and the protocol, edit the run's config, add a milestone to the run, give a
milestone its own model, set or clear steering, start a runner with the same options run takes on
the command line, stop it, kill a hung session, unblock a milestone, run the environment adapter. It is
deliberately the same surface as the CLI, calling the same functions, which is what keeps one
audit trail rather than two. Every card carries one line saying what it is for and every
consequential control a tooltip saying what it costs, because the panel is read at 3am by someone
who is not going to open the guide first.
The guide walks a whole run through it,
from an empty directory to the report - the milestone prompts and the protocol included, each in a
save-and-reload editor with the lint card naming what is still skeleton; what stays outside the
browser is bringing the panel up. The machine panel runs as a detached daemon the first run starts,
lists every run on the machine with a switcher between them, and exits on its own ten minutes
after the last run ends
(D-033).
It lists more than the live ones: the CLI records every project it works in to
~/.milestoner/projects.json, so a project whose run is finished, or never started, is there to be
opened and started after a reboot
(D-037).
The hub also scaffolds: a directory path, a run name and a milestone count call the same init()
the CLI does, with the same refusals, and the new project joins the listing on the next refresh
(D-038).
Read what this is before you run it. Everything the panel can do, it does with your account's
permissions on the machine it runs on: starting a run launches an agent with
--dangerously-skip-permissions, and attend runs your attendCommand through a shell. A
write-enabled panel is a remote code execution endpoint by construction - and the machine panel a
run starts is write-enabled, because kill, steer and unblock at 3am are why it exists. It binds
127.0.0.1 only and that is not configurable, every request needs the key from the URL, and
non-loopback Host headers and cross-origin writes are refused.
Treat the URL like a password: it is one. Never paste it anywhere that syncs.
To reach it from another device, forward the port over SSH rather than exposing it. That recipe, the full security model and the differences between the panel forms are in docs/GUIDE.md, with the reasoning in D-027 and D-033.
milestoner report --openOne self-contained HTML file: stat tiles, a wall-clock timeline of every session that ran, a card per milestone with its evidence and diagnosis, the attempt table, and the interventions. No scripts, no external assets, so it opens offline and survives being sent to someone.
The timeline is the part status cannot give you: the gaps are as informative as the bars. A
usage-limit wait looks different from a slow session, and the infrastructure retries that were
never charged against the attempt budget are visible after the fact. Every bar carries its duration
and the timeline carries its axis and its extent, so a length is readable rather than suggestive.
The panel serves the same report at the foot of its page, with a link back to it. That copy is the only one that knows a panel exists; the file version links nowhere, which is what lets it travel.
.milestoner/config.json, written by init and edited by you:
{
"run": "my-run",
"maxAttempts": 3,
"agent": { "command": "claude",
"args": ["-p", "{{kickoff}}", "--output-format", "stream-json", "--verbose",
"--dangerously-skip-permissions"] },
"liveness": ["src", "tests/results/latest.txt"],
"environment": { "attendCommand": null, "attendSeconds": 120 }
}Set liveness. It is the list of paths whose mtime proves work is happening. Without it,
status can tell you a process exists but not that it is doing anything, which is the difference
between a run that is thinking and a run that is wedged.
models maps a milestone id to the model its session runs on ({"M03": "opus"}), so a plan can
spend a cheap model on the mechanical milestones and a stronger one on the hard ones. It is read at
every session launch, so an edit mid-run applies from the next one.
Edit it in your editor or in the panel, which validates a save with the same loader the runner uses and refuses anything that would not start.
Every key, including the infra block that decides what counts as an infrastructure failure rather
than a milestone failure, is documented in
docs/GUIDE.md; what every file under .milestoner/ is
for is in the guide's layout section.
The agent is a command in .milestoner/config.json, not a dependency. {{kickoff}} is substituted
with the milestone prompt:
"agent": { "name": "claude", "command": "claude",
"args": ["-p", "{{kickoff}}", "--output-format", "stream-json", "--verbose",
"--dangerously-skip-permissions"] }Anything works that accepts a prompt as an argument, can read and write files in the project, and
exits when finished; Claude Code and Codex are both exercised in this repository's own runs.
fallbackAgents covers the 2am usage limit: when a session fails for a reason the infra rules
recognise, the failing agent is benched until its announced reset and the next authenticated one
takes over, without sleeping and without consuming the attempt. Full recipes - Claude Code, Codex,
a local model through Ollama, the fallback pool - are in
docs/GUIDE.md.
- v0.1 engine:
init,run,status,unblock. Done. - v0.2 active supervisor as an installable Claude Code skill; intervention log; environment adapter as a config string. Done.
- v0.3 single-file HTML run report; steering file support. Done.
- v0.4 plugin packaging: manifest, the supervisor skill and four slash commands as plugin components, and an in-repo single-plugin marketplace. Done, and retired in v0.7: a plugin that cannot work without the npm install is not a second channel, it is a second copy of the first one (D-034). A second agent behind the config string is done too: see Running a different agent.
- v0.5 the debt v0.4 exposed, plus one addition: a green test suite on Windows,
killending the whole session on macOS and Linux rather than one process, a machine-level registry behindmilestoner runs, and the panel coming up with the run behind--serve. Done. - v0.6 three bugs the v0.5 run found by running: a state lock that a contender could break in
the instant after it was taken, a crashed session charged an attempt it did not deserve, and
inithanding a new run the previous run's protocol. First version published to npm. Done. - v0.7 the planner skill, the machine panel (one panel spanning every run, brought up by the
first one), one distribution channel (the plugin retired, D-034), and
skill install -g. Done. - v0.8
milestoner lint: the run's form checked before a session spends real time on it, the same rules gating the start of a run and shown in the panel. The line between form and judgement is D-035. Done. - v0.9 the panel-only workflow: a run created, a milestone added mid-run, the config and the protocol edited, all from the browser, against the same engine primitives the CLI calls. Every attempt also records the model that ran it, not just the agent. Done.
- v0.10 the live transcript. A headless session streams its output instead of flushing at exit,
which took teaching the infrastructure rules to weigh what the agent produced rather than what
the file holds (D-040). One renderer serves both readers: the panel follows the running session,
and
milestoner transcripthands the supervisor the same thing in prose, so an intervention can name what it saw instead of quoting a timer. Done.
Validated end to end, by building itself. v0.4 was a four-milestone milestoner run and v0.5 to v0.6 was a seven-milestone one, every milestone a fresh Claude Code session graded against the evidence it wrote. Nine of the eleven closed on the first attempt.
The more useful result is what the second run found by running. Three of its seven milestones were
not planned: they were bugs in the engine, hit by the engine while it executed. A session crashed
after fifteen minutes of finished work leaving a fifteen-byte transcript, and the classifier charged
it a real attempt because it only read a tiny transcript as a crash inside infra.deathSeconds
(D-029). A state lock could be broken by a contender in the instant after it was taken, losing an
update (D-028). init handed a new run the previous run's protocol, so five sessions read rules
naming a finished run and nothing said so (D-030). Each became a milestone of the run that exposed
it.
The runs' own state and evidence are not in the tree, because a run's record belongs to the machine that ran it. They are in this repository's git history up to v0.6.0 if you want to read what those sessions actually wrote.
What neither run exercised: a run long enough to hit a real usage limit or agent fallback mid-flight, a non-Claude agent across a whole run rather than a single milestone, and the supervisor loop against a live multi-milestone run rather than the one blocked run it has been tried on.
docs/GUIDE.md is the user guide: the mental model, a full walkthrough, command and config reference, grading and infra rules, use cases, recipes and troubleshooting. docs/NEXT.md is what is left to do and in what order.
This grew out of two real overnight runs on a private Unity 6 project, driven by a PowerShell
orchestrator. That script is not in the tree: every rule it carried now lives in src/ with tests,
and keeping the ancestor beside the engine only invited the question of which one to run.
docs/DECISIONS.md records every product decision, what was rejected and why, including the ones taken before the first line of TypeScript.
npm install
npm test # rule-level tests: infra classification, grading, state migration, quoting,
# config merging, exit codes, the report, and the supervisor playbook's shape
npm run typecheck
npm run buildCI runs all three on Node 20, 22 and 24 on Linux, and on the ends of that range on macOS and
Windows. The working tree is pinned to LF by .gitattributes, because several tests compare bytes
read from disk and a CRLF checkout fails them.
MIT. See LICENSE.
