Skip to content

Subroutine 0.9.0

Choose a tag to compare

@github-actions github-actions released this 22 Sep 13:05
· 445 commits to main since this release

0.9.0 is the release candidate for the first ready version of Subroutine. Everything
planned for it is in.

If you use the subroutine plugin in Claude Code, it stays on 0.8.x until you refresh
it.
It runs the version its manifest names and never moves to a new minor on its own. Run
claude plugin marketplace update subroutine, then claude plugin update subroutine@subroutine, and start a new session. subroutine-remote takes its tools from
your server, so it follows whatever version the server runs.

This release changes the database schema, to cfeea5d4dd35.

Install it, then run subroutine db upgrade. That reports both versions, takes a
verified backup, migrates and checks the result — in that order. Stop the service
first if you are running one; expect it to be down for the length of the migration.

Fixed

  • An appointment with a deadline beside it is filed rather than refused. Workshop from
    Monday 9am to 5pm by friday
    stopped the whole capture - subroutine add failed, and the
    API, the agent tools and the browser's capture box answered with an internal error - from
    the release that learned to read an appointment's two ends. The deadline is counted from
    the appointment's day now, as it is from any other start.

  • A range of times is read the way it is written, or not at all. 0.8.29 began reading two
    times as an appointment and read three kinds of line wrongly without saying so. Dinner on
    friday at 7:30-9:30pm
    was stored as 07:30 to 21:30; a range beside a defer made an
    appointment today; and a range in the middle of a sentence was read as one because a date
    came later in the line. A meridiem written once now counts at both ends where it fits. A
    range whose end reads earlier than its start, with nothing on the line to say which clock
    it is - 12:30-1:30 - stays in the title and is reported, as every range was before. And a
    time counts as written beside a date only when nothing stands between them.

  • Two ISO spellings are read as the words are, and the calendar's edges no longer break
    capture.
    from 2026-10-01 by friday is due on the Friday after the first rather than the
    one before it. from 2026-10-02T09:00 to 17:00 is an appointment, where it was a hidden
    defer with to 17:00 left in its title. A span in the first or last years the calendar
    holds is read, where it was an internal error.

  • A search narrowed to a project finds it in whichever workspace holds it. On an
    instance with two workspaces, subroutine search "project:web" put the question to both,
    the one without web refused, and its refusal threw away the rows the other returned -
    so the search said nothing matched. subroutine list --project web had worked for a long
    time; the tolerance was keyed on the flag rather than on the project. The same was true of
    parent:12 when item 12 is a task and the documents were asked too. A project that is in
    no workspace at all is still refused by name.

  • A name of . or .. is refused rather than sent over HTTP. From a terminal on a
    served instance, subroutine user remove .. removed the whole workspace and said a
    member had left, because an address reads .. as the level above; project unshare web ..
    reached the project the same way. Both are refused before anything is sent now, and a
    username or a tag cannot be . or .., so nothing is ever called one. The workspace went
    to the trash, not away, so anything removed this way can be restored.

  • Whose timezone an agent's days follow is named correctly. whoami, the agent tools and
    subroutine user timezone named an agent's account parent whenever it had one, whether or
    not that account had set a zone - so an agent whose days came from further up, or from the
    workspace or the instance, was told the wrong account. They name the account that set it
    now, or say that nobody has. /v1/me publishes it as reader_timezone_set_by.

  • A refused credential in the browser says why. Issuing or revoking one on the People page
    and being refused drew an empty banner, because the notice was handed the error rather than
    its words.

  • A username cannot be set, unset or me. A search reads each of them as something
    other than a person - assigned to anybody, to nobody, and to whoever is asking - so an
    account called one could not be found by it, and a board drew a person called unset under
    Nobody.

  • - reads what is piped everywhere a command takes words. subroutine comment 42 -
    stored a single hyphen and reported the comment as noted, where doc create, doc edit,
    add and update read the pipe. --description on project create, project update,
    workspace create and workspace update did the same, and so did --because on done,
    skip, plan, defer and update. All of them read the pipe now, and a - with nothing
    piped is refused rather than stored.

  • A link no longer reads as the item being created. A link is a row of its own, so the
    event's action is created - and the journal, the change feed at a terminal and the journal
    in the agent tools each printed that bare action. A linked item appeared twice, at the same
    second, by the same person
    , with nothing saying which line was the link, and the obvious
    inference is that something was filed twice. All three now say a link was made and name the
    item at the far end and the kind of link, which is what the browser has said since it was
    built. Undoing a link says so as well, where it used to say deleted beside an item that
    still exists.

  • The capture shorthand's !3 can be typed the way the help shows it. In an interactive
    bash or zsh a ! starts a reference to an earlier command, and double quotes do not stop it

    • so subroutine add "Call the dentist before Sunday !3 ~15m #health", which is what
      subroutine explain capture offered as its one worked example, was either refused as
      event not found or had an earlier command's text pasted into the title with nothing said.
      The task was created either way, so nothing looked wrong. Every example this program prints
      is single-quoted where it carries a !, and explain capture says why.
  • An agent's tool call no longer waits five seconds on itself. Reaching an instance over
    the stdio MCP server runs the instance in process, and each tool call opened a second
    database session while the request still held its own - two connections, and on SQLite one
    waits the other out. It surfaced as database is locked after exactly five seconds, from a
    single process with nothing else running, and it needed only one call to one server. The
    request now holds nothing while the tool works.

  • A credential used only for reading is recorded as used. Resolving a credential writes
    when it was last used, and the command line and the stdio MCP server wrote that into the
    transaction doing the work and then threw it away - so a token in daily read-only use read
    as never used, which is exactly the evidence somebody acts on when deciding whether to
    revoke one. The served API has committed it separately since 0.6; now both do. It also
    held a write lock for the whole of every read
    in order to record a timestamp, which on
    SQLite makes every command a writer whatever it is called.

Removed

  • /v1/meta no longer publishes sortable and selectable. They were renamed order
    and fields in 0.8.1, both spellings were published since, and 0.8.1's notes said the old
    pair would go in 0.9.0. A client from 0.8.1 on reads the new names already. A client from
    this release reads only the new names, so it reads a 0.8.0 instance as offering nothing to
    order or select by: upgrade that instance first.

Added

  • A search line can narrow by status category, so the board a team asks for first can be
    saved.
    status_category:in_progress - and done, todo or cancelled, or a document's
    draft, current, superseded and archived - compiles like any other term now: in the
    search box, on the command line, and inside a saved view. A saved view's narrowing is a
    search line, so until now what the team has in progress was the one view that could not be
    kept. The words are the product's rather than your workspace's, so a status you renamed still
    answers to the category it belongs to; a word that is no category at all is refused by
    listing the four that are; and asking for a finished category reaches finished work without
    also saying include_completed, exactly as the older spellings do.

  • A view can be saved under a name, and shared with the people you work with. A saved
    view is a query and an arrangement: one search line for what is narrowed, written the
    way you would type it into a search box, and a few named fields for how it is drawn - as an
    agenda, a list or a board, in this order, grouped on this axis. GET, POST, PATCH and
    DELETE on /v1/views, addressed by the name you gave it rather than by a number, because
    a view exists to be typed, sent and clicked. A view is yours by default and shared on
    purpose
    , so nobody's half-finished experiment appears in everybody's list; only the person
    who saved one can change it, and sharing publishes a view rather than handing it over. Names
    are one per workspace so that a link you send somebody draws what you were looking at.

  • A view can be saved and reapplied from the browser, under the place it belongs to. What
    is showing becomes a search line and an arrangement: the chips you set - a tag, a person,
    an importance, who somebody answers to - are written as terms, and the two a search line
    cannot carry are named before you save rather than dropped quietly. Coming back to one
    expands it into the address rather than replacing it with a name, so what you send a
    colleague is still the thing you were looking at, you can see what you are looking at, and
    you can take one part away.

  • A saved view can be run and managed from a terminal. subroutine view lists what you
    saved and what the workspace shared with you; subroutine view run <name> draws the work
    it selects; save, edit and forget are the rest of it. A terminal honours what a view
    narrows to and draws a list
    , so a view saved as a board, or grouped by a field, says which
    part it is not drawing on the line above the results rather than quietly coming back as
    something else - and that line goes to standard error, so --json and a pipe are unchanged.
    An --order you ask for now beats the one the view saved. Writing nothing after --q,
    --order or --group-by clears that part, which is a different instruction from leaving
    the flag off. subroutine explain views is the page.

  • A busy database says so, and says to try again. New error code database_busy. SQLite
    allows one writer at a time, so a second connection writing at that moment is ordinary and
    clears by itself - but it arrived as service_unavailable, reading " could not
    be read: database is locked"
    under advice to check database_url. That named a cause
    nobody had established, about what was usually a write, and gave an agent nothing it could
    act on. It now reads "The database was busy: another connection was writing to it", with the
    remedy, and on both transports: a served instance reported the same condition as an
    internal error blaming itself. It claims no configured bound - SQLite does not always
    consult its busy timeout before reporting this - and the command line says how long its
    attempt actually took, because whether a refusal came back at once or after a full timeout
    is the difference between two unrelated causes. Where one command makes two changes and the
    second meets a busy database, it says which went through.

Security

  • An instance served over HTTPS reads only __Host-subroutine_session. 0.8.29 read the old
    name beside it so that a browser signed in before the change stayed signed in - and so did a
    cookie a page on a neighbouring subdomain had written under the old name, which then reached
    a browser that had never signed in there, or had just signed out, and was handed back under
    the protected name. Nothing can tell the two apart, so the old name is not read. A browser
    that signed in before 0.8.29 and has not opened the instance since will be asked to sign in
    once.
    An instance on loopback or plain HTTP is unchanged.