Releases: fabrodz/milestoner
Release list
v0.10.0
The transcript of a running session is readable while it runs - in the panel, and by the supervisor that has to decide whether to step in.
Changed
-
One transcript renderer, in
src/transcript.ts, behind the panel's/api/transcriptandmilestoner transcript. The page no longer decodes anything; a test asserts it never grows a parser back. A line without a top-leveltypeis not an event, so a plain-text transcript and NDJSON of another shape (codex exec --jsonnests its type undermsg) are passed through exactly as written, which is what keeps every agent watchable. -
The default
agent.argsrun Claude Code with--output-format stream-json --verbose. A headlessclaude -pwith the default text format writes nothing until it exits, so the panel's "watch the live transcript" opened a file that was empty for the whole session. It now fills turn by turn while the session works: an event appears when a turn completes or a tool returns, so a long generation is a still frame rather than a stall. -
Infrastructure classification weighs what the agent produced, not what the transcript file holds. A stream-json session opens with a four-kilobyte
system/inittool inventory and closes with aresultenvelope; counted as evidence those alone clearedtinyTranscriptBytesandcrashTranscriptBytes, which would have charged an attempt for every infrastructure death.readTranscriptEvidencediscounts them, so both thresholds keep their existing calibration and a plain-text transcript from any other agent weighs exactly what it always did. See D-040. -
The init inventory is no longer searched for
infraFailurePatternsorusageLimitPatterns. It lists every installed skill and slash command, so a session could be classified from somebody else's prose. Theresultenvelope still is, but only its words -result,subtypeandapi_error_status- and not the kilobyte of costs, token counts and hex uuids around them, any of which can carry the429pattern.
Added
-
A usage limit the agent reports outright (a
rate_limit_eventwith statusrejected, and its epochresetsAt) is checked before the text patterns and outside thedeathSecondsbound they sit behind, so a quota that ran out mid-session refunds the attempt and waits for the announced reset. Onlyrejectedcounts:allowed_warningmeans the limit is near, not reached, and a session runs straight through it. The last event decides. -
milestoner transcript [--name <file>] [--lines <n>] [--raw]prints what the running session is doing, as prose rather than as its wire format: the agent's own text,>for a tool call and<for its result, cut to 240 characters. Its first line names the session and says whether it is still going, so a transcript quoted in an intervention cannot silently be a finished one;--rawomits it. Read-only, and outside the supervisor's intervention set on purpose - knowing what a session is doing has to be free to be done every cycle. -
The panel follows an open transcript while the session runs, refreshing every 2.5s, and shows the same rendering the command prints. The scroll sticks to the bottom only while the reader is already there, and following stops when the pulse says no session is writing to that file - which also ends the pointless follow of a transcript opened from the attempt history.
-
The bundled supervisor skill reads the transcript every cycle and quotes what it saw when it intervenes. It also no longer claims a transcript proves nothing: liveness and diagnosis are now two separate questions, with the transcript answering only the second. Re-run
milestoner skill install --forceto pick this up - the installer will not overwrite a skill you already have. -
milestoner lintwarns (agent-not-streaming) when aclaudeagent is configured withoutstream-json, which is what an existing.milestoner/config.jsonwritten before this release will be.
Upgrading
- Re-run
milestoner skill install --forceto replace an already-installed supervisor skill; the installer will not overwrite one you already have. - Existing
.milestoner/config.jsonfiles have no streaming flags.milestoner lintwarns (agent-not-streaming), but it is a warning: the run still starts, the panel just stays empty until the args are updated.
Compatibility
Only Claude Code's stream-json is decoded. A line without a top-level type is not an event, so plain-text transcripts and NDJSON of another shape pass through byte for byte and no agent is worse off than before. The per-agent table is in the guide, under Running a different agent.
v0.9.2
Added
- Every attempt now records the model that ran it, not just the agent. Resolved at launch alongside the agent (
runner.ts), it lands on theAttemptRecord, onpulse.jsonwhile a session is live, and inrun-log.md's launch line - surfaced in the panel's milestone cards and attempt tables, the standalone report, andmilestoner status. The field is a plain config string, the same one already threaded throughagent.modelArgs, so it needed no per-agent logic: it records correctly whatever the agent, Claude, Codex, or a fallback under any name.
Changed
The panel got a pass on its rough edges and its visual identity:
- The "watch the live transcript" button did nothing when clicked, and the per-attempt "transcript" link and both unblock buttons had the same bug: each built its
onclickattribute by splicingJSON.stringify(...)straight into a double-quoted HTML attribute, andJSON.stringify's own quotes closed the attribute early. The value is now HTML-escaped before it goes in. - The report link fell back to unstyled browser-default blue with no CSS of its own. It is now a real button, styled like every other action on the page, and opens in a new tab so the live panel is not lost.
- The transcript viewer scrolled sideways on long lines. It now wraps and scrolls vertically only.
- The config editor had no guidance on what it accepts. A collapsed reference now documents every field plus copy-paste agent blocks for Claude, Codex, and Ollama-via-Codex.
- IDs, timestamps, durations, and agent/model names now render in a tabular monospace face throughout (stat tiles, milestone cards, attempt tables, lint findings, activity log, interventions), while narrative text (the verdict, a diagnosis, evidence) stays sans-serif - a console feel that fits a coding-agent supervision tool better than a generic SaaS dashboard. Added
:focus-visibleand:activefeedback, previously absent on every control. - Zero em-dashes left in visible panel text.
v0.9.1
Fixed
- The panel's transcript buttons - "watch the live transcript" and the per-attempt "transcript" link - and both unblock buttons did nothing when clicked. Each built its
onclickattribute by splicingJSON.stringify(...)straight into a double-quoted HTML attribute;JSON.stringify's own quotes closed the attribute early, leavingonclickholding invalid JavaScript. The value is now HTML-escaped before it goes in, matching every other attribute the panel builds this way.
v0.9.0
A run can be created, written, configured, started, corrected and read from the browser. The panel
was already the run's control surface once a run existed; what it grew here is everything before
that and everything the CLI could do that it could not. The walkthrough is in
the guide; what stays outside the browser
is bringing the panel up.
Added
- A milestone can be added to an existing run, mid-run included.
milestoner add [--title <text>]
appends one pending milestone - the next id after the highest instate.json, a prompt skeleton
in.milestoner/prompts/- under the state lock, and prints the id and the prompt path. Until
now the count chosen atinitwas final short of hand-editingstate.json, a file the engine
owns. - The append is safe while a runner is alive: the runner picks its next milestone from a fresh
state load on every loop pass, so an added milestone is reached when its turn comes, with no
restart. Appending to a completed run clearsrunComplete- a run that gains a milestone is a
run again. A prompt file already sitting at the skeleton's name is kept, so the prompt can be
written before the slot is added. - The panel grew the same control: an Add a milestone card under the milestone list, posting
toPOST /api/milestone/add({ title }optional) behind--write, the key, theHost
allowlist and theOrigincheck like every mutation. The reply names the new id, and the card
appears on the next refresh; both front ends call the one engine primitive. - A run can be created from the panel. The hub grows a New run card - directory, optional run
name, milestone count - posting toPOST /api/init, which calls the sameinit()the CLI does,
so the scaffold and its refusals are the command's. The new project is recorded in
~/.milestoner/projects.json, so it joins the hub listing on the next refresh with no CLI command
run anywhere. POST /api/initvalidates its body beforeinit()sees it: the path must be absolute and an
existing directory (a relative one would resolve against the panel daemon's working directory, and
a missing one is refused rather than created),milestonestakes the CLI's 1-99 bounds, and
forcemust be an explicittrue. It is behind--write, the key, theHostallowlist and the
Origincheck like every other mutation, and it is a machine-panel route: a panel serving one
project answers 404. Only an existing-config refusal reveals the force checkbox; a protocol naming
another run (D-030) is refused even with force and says which run it names, because force cannot
answer it. The reasoning for accepting a filesystem path over HTTP is D-038..milestoner/config.jsoncan be read and edited from the panel. The per-run view carries the whole
document in a text box, fed byGET /api/configand saved byPOST /api/config, so every key -
theinfrathresholds,fallbackAgents,liveness,environment, the agent command - is
reachable without leaving the browser. A runner that is already going read its config at startup,
so an edit applies to the next one; the card says so rather than blocking the edit.- A save is validated by the loader itself: the submitted text is parsed and put through the same
checksloadConfigruns on every runner start, and only a document that passes is written, through
the same atomic write every other engine write uses. A refusal carries the loader's own sentence
(missing required field "agent", or the JSON parser's position) and leaves the file byte for byte
as it was, so nothing that would stop the next runner from starting can be saved from the panel.
projectRootis dropped rather than written, asinithas always left it out. - A model per milestone:
modelsin.milestoner/config.jsonmaps 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 resolved at each session launch, not once
at startup, so an edit mid-run applies from the next session.--modeloverrides the whole map;
a fallback agent keeps its ownmodel, because model names are not interchangeable across
agents.milestoner lintwarns (orphan-model) about amodelskey naming no milestone in
state.json, which is otherwise a model silently never used. - A model field on every milestone card, holding that milestone's entry in the
modelsmap and empty
when it has none. Saving reads the config, changes that one key and sends the whole document back
through the same validated endpoint; clearing the field removes the entry and the milestone goes
back to the agent's own model. - The panel starts a run with the same options the CLI takes.
POST /api/run/startaccepts
milestone,once,maxAttemptsandmodelbesidenoLintand translates each to its flag on
the spawned runner; the start control grows a collapsed options row with a milestone picker built
from the run's own ids, a "one session, then stop" box, an attempts field and a model field, all
optional, posting only what was filled in. "Unstick the environment" gains a seconds input that
overridesenvironment.attendSecondsfor that one run of the adapter. - Start options are validated in the panel's process before anything is spawned: an unknown or
emptymilestone, amaxAttemptsthat is not a positive integer, an empty or non-stringmodel
and a non-booleanonceare refused with a message naming the field and no runner started. The
runner is spawned detached with its output discarded, so a flag it would reject would otherwise
fail where nobody can see it. - The machine panel lists every project on the machine, not only the ones whose runner is alive or
started while the panel was up. Every command that works inside a project records its directory in
~/.milestoner/projects.json(initincluded), and the hub summarises the ones the registry has
never heard of from their ownstate.json, reportedunknownrather thangonebecause nothing
died there. They resolve for every control, so a run can be started, steered or unblocked from the
browser after a reboot. Writing the file is best-effort, a corrupt one is treated as empty, and an
entry whose directory is gone is skipped and left in place. - The milestone prompts and the protocol can be written from the panel. Each milestone card carries
an editor for its prompt file, collapsed behind an "edit the prompt" link, fed by
GET /api/prompt?name=<file>and saved byPOST /api/prompt; a Protocol card holds
.milestoner/protocol.mdthroughGETandPOST /api/protocol. Nothing structural is checked
before writing - both files are hand-written prose by design, so the lint card is the feedback
rather than a write gate, and it refreshes on a save so filling in a skeleton visibly clears its
template-residuefindings. A prompt is reachable only by a name some milestone'spromptfield
carries: path separators, a missing.mdor a name no milestone owns are refused, read and write
alike. Writes are atomic and behind--write, the key and theOrigincheck like every other
mutation; an edit applies to the next session launched, as steering does, and the editors say so. - Direct tests for the
api.tshandlers, which the panel's whole write surface goes through and
which until now were only exercised indirectly over HTTP. - The report served by the panel carries a link back to the view it came from, and one line saying
what it is: the run's archival snapshot, the same self-contained filemilestoner reportwrites,
where the panel is the live view. The link carries exactly what the report's own URL carried - the
run on a machine panel, the key only when the caller used one - so a page reached through the
cookie--opensets never gains a key in its history. The file on disk passes no such link and so
carries none: one renderer, and only the served consumer knows a panel exists. - Every card on the panel says what it is for, in one visible line under its heading: the verdict
banner spells out what alive, slow, hung and gone mean, the lint card that error-level findings on
pending milestones refuse a start, steering that a correction is read by the next session launched
and not by the one running, the interventions card that its lines are outside actions on the run,
the engine log which file it is reading, and the report link what a self-contained report is good
for. Every consequential control carries a tooltip naming what it does and what it costs - kill
spends an attempt and brings the runner straight back, "start anyway" skips the gate and leaves
the findings unfixed, stop costs nothing. The subtitles are visible text rather than tooltips
because a tooltip does not exist on a touch screen.
Fixed
-
An empty supervisor log no longer renders as two interventions.
supervisor-log.mdopens with a
title and a backtick-quoted format line, written byinitbefore anything has ever happened, and
the panel's INTERVENTIONS card andstatus --json'srecentInterventionsboth tailed the file
raw and served them. Reading the log is now one shared function that drops its own header, used by
the panel,statusand both reports, and a run nobody has intervened in says so. -
The milestone card a session is working on shows that session. The card body rendered only graded
attempt history, so while attempt 1 ran the page contradicted itself: the verdict banner said
"Working on M01, attempt 1" while the card below read "IN PROGRESS", "no attempts used" and "Not
started yet." Now, when the pulse names the milestone and the runner is alive, the card carries a
live block - when the session started, the agent when the pu...
v0.8.0
A run's form is now checked before its hours are spent. milestoner lint verifies every milestone
prompt against the conventions the evidence gate relies on - criteria that name their evidence, an
exit section with the run's tag, no scaffold residue - milestoner run refuses to start a run that
fails it, and the web panel reads, gates and overrides with exact parity to the terminal. The line
between mechanical form and human judgement is D-035: the linter checks form, the planner keeps
judgement. The feature was built by milestoner supervising itself, the third such run (v08-lint,
four milestones, all closed on the first attempt).
Added
milestoner lint: checks the run's form - every milestone prompt, the protocol header and the
config - before a session spends real time on it. Errors (missing prompt file, scaffold residue,
missing objective or criteria, a criterion without an evidence note, an exit section without the
run's tag) exit1; warnings (orphaned prompt, protocol naming another run, empty liveness)
alone exit0.--jsonprints{ run, errors, warnings, findings }for scripts. The rules are
the purelintRuncore insrc/lint.ts; the line between form and judgement is D-035.milestoner runlints at startup: error-level findings on milestones that are still pending
refuse the start with the findings printed the waymilestoner lintprints them, exit1,
before any session, state change or panel.--no-lintskips the gate; findings on done or
blocked milestones never stop a resume, and warnings never block. Every start, gated, clean or
bypassed, writes onelintsummary line torun-log.md.- The web panel keeps lint parity with the terminal:
GET /api/lintreturns exactly what
milestoner lint --jsonprints, behind the same auth as every API route; the panel shows a lint
card with the counts and the per-milestone findings before anything is started. Starting a run
from the panel lints first, in the panel's process (the runner is spawned detached with its
output discarded, so its own gate would refuse invisibly): error-level findings on pending
milestones refuse with the counts and the first findings in the message, and a deliberate
"start anyway" control passes--no-lintthrough to the spawned runner so the run log records
the bypass.
Removed
publish.yml, one release after it arrived. Its first exercise ended at npm auth (the trusted
publisher was never configured) and 0.7.0 went out by hand; a publish is one command with
prepublishOnlyalready gating it, so the automation was overhead, not safety.
Fixed
bindrops its./prefix. npm 11 warns that the./dist/cli.jsform is invalid and rewrites
the manifest while publishing; the plain relative path publishes clean, and npm 10 accepts both.
v0.7.0
The run gets a planner and the machine gets one panel. The planner skill authors the run's plan
with you in conversation without ever inventing an acceptance criterion; the machine panel comes
up with the first milestoner run and spans every run on the machine. The Claude Code plugin is
retired: it could not do anything without the npm-installed binary its commands and skills shell
out to, so milestoner skill install is now the one way the skills reach a machine.
Added
- The planner skill,
milestoner-planner: plans a run together with the user - it interviews,
proposes the milestone breakdown for approval, and only then writes the prompts, the protocol
TODOs and the liveness config. It never writes before approval and never invents an acceptance
criterion, so the boundary against generated specifications (D-031) stands; the argument is
D-032. Shipped like the supervisor skill: one source insrc/templates/, written by
milestoner skill install. milestoner initnow points at the planner skill for the authoring steps it cannot do itself.milestoner skill install -gas the short form of--global.- The machine panel (D-033):
milestoner serve --allserves every run the registry knows about,
from any directory - a hub across runs plus the familiar per-run view with a switcher. The first
milestoner runon the machine brings it up as a detached daemon that stays while any run is
alive (plus a ten-minute linger) and then exits and cleans up after itself; every run prints its
URL, discovery is~/.milestoner/panel.json, andmilestoner runsnames the URL when a panel
is live.--no-panelopts a run out. milestoner run --openopens the machine panel in the browser without writing the key into
browser history: the CLI mints a single-use token and the panel exchanges it at/authfor an
HttpOnly cookie, so the URL history keeps is already dead. By default the browser opens only on
the run that started the daemon;--no-openstops even that. D-027's refusal of--openfor
the attachedrun --servepanel stands.
Changed
milestoner skill installtakes an optional skill name (supervisor,planner, or the full
names). With no name it installs every bundled skill, where it previously installed only the
supervisor;--printnow requires a name.milestoner runnow brings the machine panel up by default (read-write: kill, steer and unblock
at 3am are why it exists; the guards are the same loopback bind, Host allowlist, key and Origin
check as always).run --servekeeps its per-run attached panel unchanged and skips the machine
panel.
Removed
- The Claude Code plugin and its in-repo marketplace (D-034). The plugin could not do anything
without the npm-installed binary its commands and skills shell out to, so it was a second copy
of the npm channel, not a second channel.milestoner skill installis now the one way to get
the skills, and with them gone gocommands/, the generatedskills/mirror, the manifest
sync/check scripts, and the manifests CI job.
v0.6.1
Documentation only; the engine is byte-identical to 0.6.0. The package's front page on npm is the
README from its own tarball, so a README this size and this wrong about its own licence was worth a
patch release rather than waiting for the next feature.
Fixed
- The licence badge claimed Apache 2.0. The project is MIT, as
LICENSE,package.jsonand the
published package all say, and it always has been. The CI badge pointed at an unrelated
repository's workflow. - README images used repository-relative paths. The published tarball carries four files and
docs/assets/is not among them, so those images rendered on GitHub and broke on the package
page. They are absolute now.
Removed
BRIEF.md, the genesis document. Its architecture section was the only place recording that the
engine is generic, the protocol is templated and milestone prompts are always hand-written, and
three documents cited it; that claim is now D-031, where a decision belongs. Its "product
decisions to make first" section had been resolved into D-001 to D-009 since 2026-08-18, and its
suggested roadmap stopped at v0.4 and had been overtaken.docs/PLAN-v05.md, a plan that has been executed. What it planned is in the[0.5.0]and
[0.6.0]entries in the changelog and in D-025 to D-030.docs/PLAN-flow-authoring.md. Its substance, including themilestoner lintexperiment that
would settle the question with data instead of taste, is folded into item 5 ofdocs/NEXT.md.docs/runs/, the archived record of thev04-pluginrun, and.milestoner/is now gitignored
rather than partly tracked. A run's state and evidence belong to the machine that ran it. Both are
in this repository's git history up to v0.6.0 for anyone who wants to read what those sessions
wrote.
Documentation
README.mdis a quarter shorter. Five sections duplicateddocs/GUIDE.mdand one of them, the
agent recipes, had drifted into being a strict subset of it. Each now carries what a reader needs
before installing and links to the guide for the rest. The panel's security model stays in the
README, condensed rather than moved, because a write-enabled panel is a remote code execution
endpoint and that is not a detail to look up elsewhere.- The roadmap's validation paragraph said v0.5 was a four-milestone run. It was seven, three of
which were bugs the run found in the engine while executing.
npm install -g milestonerv0.6.0
The first version published to npm, and the first three milestones the engine set for itself. M01 to
M04 of the v05-debt run were debt planned in advance; M05, M06 and M07 were bugs that run exposed
by executing, in the engine that was executing it. Two of the three cost the run real attempts before
anyone knew they existed.
A minor rather than a patch, despite reading as a list of fixes: init now refuses where it
previously proceeded, the tag scheme the templates write has changed, and there is a new
infra.crashTranscriptBytes option.
Added
infra.crashTranscriptBytes(default 100 B), the threshold below which a transcript is read as a
crash regardless of how long the session ran. See the refund entry under Fixed.
Changed
- The protocol and milestone templates tag green milestones
<run>-<milestoneId>instead of
<run>/<milestoneId>. A tag with a slash collides with the branch name a session naturally picks
for the same milestone, andgit push origin <name>then fails withsrc refspec matches more than one. Existing tags are untouched. Recorded as D-030.
Fixed
milestoner initover an existing.milestoner/no longer silently keeps a protocol that names a
different run. The protocol is hand-edited, soinitstill never rewrites or deletes it; what it
does now is stop with exit 1 before scaffolding anything when.milestoner/protocol.mdnames
another run in its header, and say what to bring in line. Previously every session of the new run
was handed the finished run's rules, tag instruction included, and nothing said so - it happened
to this repository's own v0.5 run. Recorded as D-030.- A session that crashed mid-run no longer costs the milestone an attempt. The infra classifier
read a tiny transcript as a crash only insideinfra.deathSeconds(90 s), so an agent that
worked for fifteen minutes and then died leaving fifteen bytes was gradedincompleteand
charged - it happened to the v0.5 run itself, on a milestone whose work was already done. A
transcript belowinfra.crashTranscriptBytes(new, default 100 B) with noresult.jsonis now
refunded as acrashat any duration; a session that left a real transcript and no result is
still charged, and the refund shares the existing ceiling ofinfra.maxRetriesconsecutive
infrastructure failures. Recorded as D-029. - Two milestoner processes writing state at the same moment could silently lose one of the writes:
anunblockissued while the runner was grading could vanish as if never typed, and a run could
be missing frommilestoner runseven though its runner was alive (seen once in CI, where one of
six simultaneous runners was absent from the listing). The state lock that should have prevented
this could be broken by a contender in the first instant after it was taken; the lock now names
its holder from the moment it exists, a lock that cannot be read yet is waited out instead of
discarded, and the lock is no longer stolen after a 5-second wait. The worst case for a contender
facing a crashed or wedged holder is a bounded wait (3 or 30 seconds), never a lost update.
Recorded as D-028.
npm install -g milestoner