Repository navigation
Subroutine 0.9.0
0.9.0 is the release candidate for the first ready version of Subroutine. Everything
planned for it is in.
If you use the
subroutineplugin 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, thenclaude plugin update subroutine@subroutine, and start a new session.subroutine-remotetakes 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 addfailed, 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 withoutwebrefused, and its refusal threw away the rows the other returned -
so the search said nothing matched.subroutine list --project webhad worked for a long
time; the tolerance was keyed on the flag rather than on the project. The same was true of
parent:12when 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 timezonenamed 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/mepublishes it asreader_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,unsetorme. 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, wheredoc create,doc edit,
addandupdateread the pipe.--descriptiononproject create,project update,
workspace createandworkspace updatedid the same, and so did--becauseondone,
skip,plan,deferandupdate. 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 iscreated- 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 saydeletedbeside an item that
still exists. -
The capture shorthand's
!3can 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 captureoffered as its one worked example, was either refused as
event not foundor 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!, andexplain capturesays why.
- so
-
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 asdatabase is lockedafter 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/metano longer publishessortableandselectable. They were renamedorder
andfieldsin 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- anddone,todoorcancelled, or a document's
draft,current,supersededandarchived- 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 sayinginclude_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,PATCHand
DELETEon/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 viewlists what you
saved and what the workspace shared with you;subroutine view run <name>draws the work
it selects;save,editandforgetare 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--jsonand a pipe are unchanged.
An--orderyou ask for now beats the one the view saved. Writing nothing after--q,
--orderor--group-byclears that part, which is a different instruction from leaving
the flag off.subroutine explain viewsis 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 asservice_unavailable, reading " could not
be read: database is locked" under advice to checkdatabase_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.