Skip to content

v0.9.0

Choose a tag to compare

@fabrodz fabrodz released this 22 Aug 20:50
· 14 commits to main since this release

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 in state.json, a prompt skeleton
    in .milestoner/prompts/ - under the state lock, and prints the id and the prompt path. Until
    now the count chosen at init was final short of hand-editing state.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 clears runComplete - 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
    to POST /api/milestone/add ({ title } optional) behind --write, the key, the Host
    allowlist and the Origin check 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 to POST /api/init, which calls the same init() 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/init validates its body before init() 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), milestones takes the CLI's 1-99 bounds, and
    force must be an explicit true. It is behind --write, the key, the Host allowlist and the
    Origin check 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.json can be read and edited from the panel. The per-run view carries the whole
    document in a text box, fed by GET /api/config and saved by POST /api/config, so every key -
    the infra thresholds, 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
    checks loadConfig runs 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.
    projectRoot is dropped rather than written, as init has always left it out.
  • A model per milestone: models in .milestoner/config.json 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 resolved at each session launch, not once
    at startup, so an edit mid-run applies from the next session. --model overrides the whole map;
    a fallback agent keeps its own model, because model names are not interchangeable across
    agents. milestoner lint warns (orphan-model) about a models key 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 models map 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/start accepts
    milestone, once, maxAttempts and model beside noLint and 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
    overrides environment.attendSeconds for that one run of the adapter.
  • Start options are validated in the panel's process before anything is spawned: an unknown or
    empty milestone, a maxAttempts that is not a positive integer, an empty or non-string model
    and a non-boolean once are 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 (init included), and the hub summarises the ones the registry has
    never heard of from their own state.json, reported unknown rather than gone because 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 by POST /api/prompt; a Protocol card holds
    .milestoner/protocol.md through GET and POST /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-residue findings. A prompt is reachable only by a name some milestone's prompt field
    carries: path separators, a missing .md or a name no milestone owns are refused, read and write
    alike. Writes are atomic and behind --write, the key and the Origin check 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.ts handlers, 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 file milestoner report writes,
    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 --open sets 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.md opens with a
    title and a backtick-quoted format line, written by init before anything has ever happened, and
    the panel's INTERVENTIONS card and status --json's recentInterventions both tailed the file
    raw and served them. Reading the log is now one shared function that drops its own header, used by
    the panel, status and 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 pulse names one, a "watch the live
    transcript" link - and its counter reads "attempt 1 of 3 running"; with the runner gone or on
    another milestone it says exactly that instead of claiming a session, and the graded history keeps
    its table below either way. "Not started yet." is reserved for a pending milestone with no history
    and no live session. The report has the matching fix: an in-progress milestone with nothing graded
    yet reads "in progress since , no session graded yet" rather than "0 sessions" and "no
    evidence recorded" - worded from state alone, because a static report cannot vouch for a live
    session.

  • The timeline says what its bars are worth. A bar's length answered nothing before: there was no
    axis, no scale and no label. Each track now carries its sessions' durations on the right, the
    timeline carries a five-tick time axis under it, and a scale line names the extent it spans
    ("Scale: 2026-08-22 19:36 to 2026-08-22 20:07, 31m end to end"). A session whose record has no
    usable end is drawn as an open-ended bar reading "no end recorded" rather than being given a
    length; the wall-clock tile reads "-" rather than a figure when no session has run at all.

  • The report's headline no longer says "in progress" for a run that never launched. A scaffolded run
    with zero sessions reads "not started yet" and its timeline says there is nothing to place on the
    clock. The four states - not started, in progress, blocked at <id>, run complete - come from
    summarize, the same verdicts status and the panel read, rather than from a second reading of
    the milestone list.

  • The hub is reachable on a machine with exactly one project. It opens that run on arrival, as
    before, but "all runs" now says so in the URL and stays on the hub instead of bouncing straight
    back into the only run - which left the hub, and now the new-run form on it, unreachable.

  • GET /api/transcript with no name answers 404 rather than 500. An empty name resolved to the
    logs directory itself, and reading a directory as a file threw EISDIR, which the panel returned
    as a filesystem error message.