Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

Back to skills

Subroutine

ASecurity

Track work in Subroutine — file tasks, find what can actually be started, record what happened, and write down conclusions the next session will need. **Read this before the first subroutine_* call of a session, including a read-only one.** It carries conventions the tool descriptions do not — how to open so you are not answering from a stale snapshot, how to ask for work that can actually be started, what a title has to say, and which end of a blocks link is the blocker. Also when the user a...

6 stars
0 votes
0 copies
0 views
Added 9/22/2026
ai-agentsrustgoshellawsgitapi

Works with

claude codecursorterminalcliapimcp

Security Analysis

A100/100

Scanned 9/22/2026

Install to Claude Code

$npx -y skills add simonholliday/subroutine --skill subroutine --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Subroutine?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Subroutine
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/simonholliday-subroutine/badge)](https://www.skillsdirectory.com/skills/simonholliday-subroutine)

More formats (shields.io, HTML) on the badges page.

Download Zip
Files
SKILL.md
---
name: subroutine
description: Track work in Subroutine — file tasks, find what can actually be started, record what happened, and write down conclusions the next session will need. **Read this before the first subroutine_* call of a session, including a read-only one.** It carries conventions the tool descriptions do not — how to open so you are not answering from a stale snapshot, how to ask for work that can actually be started, what a title has to say, and which end of a blocks link is the blocker. Also when the user asks what to work on, reports a problem, asks you to log or file something, finishes a piece of work, wonders where something was left, asks whether Subroutine is connected or what is in it, or says this project uses Subroutine — and for adopting Subroutine in a project that does not use it yet.
---

# Working with Subroutine

Subroutine is a task and project tracker that a person and an agent use as equals. What you
write in it is attributed to you, addressable by a number, and still there after your context
is gone — which is the whole reason to spend calls on it.

**If the `subroutine_*` tools are not available, stop and say so — and establish which plugin
is installed before diagnosing anything.** There are two, they fail for different reasons, and
every remedy for one is a wasted evening on the other. Four causes, and one command separates
them:

```
claude mcp list
```

- **`subroutine`** starts Subroutine as a program on the machine your client runs on, so it
  needs the program installed there.
- **`subroutine-remote`** connects to a server by address and installs nothing, so it needs an
  address and a token.

**Rule out the browser first, because it is the only cause nobody can fix.** A client running on
claude.ai has no machine to start a program on, and it does not read either of these plugins at
all — reaching an instance from there is a *connector*, set up in Claude's own settings against
a publicly reachable address. So on the web the tools are absent by construction, however
anything is configured, and every remedy below is wasted effort. Say that plainly rather than
beginning a diagnosis; Claude Code and the desktop apps are where these work.

**If `claude mcp list` says the server is connected and this session still has no tools, the
session started before the plugin was configured.** Reload the window, or start a new session.
MCP servers are attached when a session begins and are not rebuilt when configuration changes
underneath them, so a session opened before somebody filled in the address and token keeps the
tool list it was born with — **which is the ordinary state in the minutes right after anybody
sets this up**, and therefore the likeliest moment to meet it.

Check this before anything below. It is the cheapest remedy and the only one where every other
signal says the setup is fine: `claude mcp list` runs in a *new* process, so it reports the
configuration as it is now, correctly, while describing a session that no longer matches it. A
`✔ Connected` beside absent tools reads as "connected but broken" and is not — it means "connected
now, and this session predates now".

**With `subroutine-remote`, read the line `claude mcp list` prints and pass it on.** The client
names the actual fault — a token the server rejected, an address with no MCP endpoint on it, a
server that did not answer — and quotes the instance's own words back. That sentence is written
for the person who has to fix it. Do not translate it into a guess, and do not offer any of the
installation advice below: there is nothing to install.

**With `subroutine`, the failure is silent and needs that command to see at all.** Installing a
plugin and starting its server are separate moments and only the first one reports, so "not
installed" and "installed where the editor cannot see it" look identical from in here — no
tools, and no error. `✘ Failed to connect` beside `plugin:subroutine:subroutine` means the
program was not found on the `PATH` the editor passes down, which is nearly always a virtualenv.
Say so, and offer the two ways out: install it as a tool so it is on the `PATH` for good, or
point the plugin at the copy that already exists.

```
uv tool install subroutine     # or: pipx install subroutine
subroutine init
```

`/plugin configure subroutine` takes the absolute path instead — `<venv>/bin/subroutine` — for
somebody who would rather not install it twice. If no server is listed at all, the plugin itself
is not installed or is disabled.

**If the tools are there but every call fails, read what the failure says — it names the
remedy.** This is the ordinary case on a fresh local install and it is not the one above: the
MCP server starts perfectly well against an instance nobody has created yet, so the tools appear
and then refuse. `no Subroutine instance has been set up here yet.` means the user needs to run
`subroutine init` once; a schema message means `subroutine db upgrade`. Pass the failure on
verbatim rather than diagnosing it — the sentence is written for them.

**On a server somebody else runs, the same rule applies and the remedies are not yours to
offer.** A refusal naming a permission means the token you were given does not carry it, and an
ambiguous-workspace refusal names the workspaces it could have meant — which the person who
gave you the address can put right by adding `?workspace=` to it. Report the sentence and who
needs to act on it; do not attempt to reconfigure someone else's instance.

**Whichever of these it is, do not work around it by writing to a file instead.** A tracker
nobody can reach is worse than an honest failure, and a to-do list in a scratch file is a
tracker nobody can reach.

`GET /v1/docs/agent` on any served instance is the reference for the HTTP API. This page is
about *when* to reach for it and what good practice looks like — it does not repeat the API.

## Adopting Subroutine in a project that does not use it yet

If the user has just installed Subroutine, or says "we use Subroutine now", set the project up
before doing anything else. **Ask only what cannot be undone; state the rest and proceed.** A
setup interview is how a tool loses the person who just installed it.

1. **Look before creating.** `subroutine_project` with no arguments. If something already
   covers this repository, use it. A duplicate project is invisible until somebody files into
   the one nobody reads.

2. **Propose a key and say what it is.** Lower case, letters and digits, and **a hyphen between
   words** — `claude-test` rather than `claudetest`. It starts with a letter and is at most 32
   characters. Derive it from the title, or from the repository directory name where that reads
   better. State it and proceed — a key can be renamed later, and a checkout marked with step 6
   follows the rename, so this is not a decision to stop over.

   The hyphen is worth spending a character on: a key is a path segment and will be part of a
   URL, so it is read far more often than it is typed.

   ```
   subroutine_project(key="website-redesign", title="Website redesign")
   ```

   What a rename *does* cost is addresses somebody has already written down — a `+web` in a
   note, a URL, the key in another checkout's marker. The old key then stops working, loudly.
   So it is worth a sentence if they seem to be choosing a name they will regret, and it is
   not worth a question. Renaming is the person's to do, not yours: it is a command that
   counts what will break and asks first.

3. **Do not ask which workspace unless there is more than one.** A fresh install has exactly
   one. If there are several, ask — and say why: **this is the one thing here that cannot be
   undone.** Items are numbered per workspace and a project cannot be moved between them, so
   the wrong workspace means starting again rather than renaming.

4. **Propose a parent, do not ask for one.** Placement in the project tree can be changed
   later, so state where it is going and put it there. Ask only when more than one existing
   project is a plausible parent.

5. **Ask about privacy only when it can matter** — that is, when the instance has more than one
   account. Use `--private` if they want it; the creator keeps access either way, and it can be
   changed later.

6. **Record which project this checkout is, so later sessions do not have to guess.** This is
   the step that makes everything after it reliable — without it, a session starting in this
   directory has no way to tell which of the instance's projects the work belongs to.

   ```
   subroutine use --here --project web
   ```

   **`use` is not listed by `subroutine --help`, and it does exist.** Neither are `claim`,
   `release` or `connections`. They are held back from the first thing a newcomer reads, not
   removed — `subroutine explain connecting` names them. Do not conclude from an absent line in
   `--help` that a command in this page is gone; run it, or ask `explain`.

   **This one needs the command line**, so a session connected to a server by address cannot do
   it. Name the project on each call instead — `+web` in a captured line — and say that a
   marker is worth adding by somebody who has `subroutine` installed in this checkout.

   That writes a small `.subroutine` file at the repository root. Say that you have written it
   and that it is safe to commit — it names a project, not a credential. From then on, work
   added anywhere under this directory goes to that project unless a line says `+other`.

   **Only where the directory is the project's own.** A marker covers everything beneath it, so
   one written in a home directory would file every capture made anywhere under it into that
   project, and nothing would say so. If you are not standing in a checkout of this project, do
   not write one: name the project on each call instead.

7. **Do not import an existing to-do list unless asked.** Filing thirty items out of a
   `TODO.md` is a large write that is tedious to undo and that nobody requested.

8. **Write the pointer into the project's agent file** — `CLAUDE.md`, `AGENTS.md`, whichever it
   already uses. One line naming the project key is enough. Without it the next session does
   not know adoption happened, and adopts again.

## Working day to day

**Open by asking what changed.** Your context is a snapshot and it does not decay — nothing
will tell you that something you read is now stale, so you will answer from it confidently and
be wrong:

```
subroutine_changes()
```

Keep the `seq` it prints last and pass it back as `since` next time. It is inclusive, so you
will see that one again; ignore what you already have. `mine=true` narrows it to what your own
credential did, which is how you pick up your own unfinished work rather than everybody's.

**When somebody asks what happened, use the journal instead.** These are two readings of one
store and they answer different questions:

```
subroutine_journal(filter={"created_at.gte": "yesterday"})
```

`subroutine_changes` says what **moved**. It is cheap, it resumes from a number, and it is what
to poll — but it reports only that a comment was created, never what it said, and it renders a
status change as two identifiers. Measured on a real day here: 130 of 450 events were comments
carrying no text at all.

`subroutine_journal` says what **happened**. Same events, with how each comment opens joined on
(at most 140 characters of it, and `said_truncated` true when there was more), the
actors named — an agent shows as `@name (agent, @person)`, exactly as it does on a row — and a
change rendered as *status: "Open" to "Done"*. Ask it for a period rather than a cursor.

**So: `changes` to pick up where you left off, `journal` to tell somebody what a day contained.**
Reaching for `changes` when you were asked to summarise a week means reporting that fourteen
comments exist without one word of what any of them said.

**Know whose credential you are writing with, before you write anything.** Ask once at the
start of a session:

```
subroutine_whoami()
```

It names the account, the credential by its title, what that credential is limited to, the
versions of everything in play and, for an agent, its *account parent* — whom a question goes to
when nobody assigned you the work. One machine commonly holds more than one credential — the
person's own, and one per agent — so the answer is not obvious and is not something to assume.
Four answers are worth acting on:

- **A person's name where you expected an agent's** means your work is being recorded as
  theirs. Say so rather than carrying on: attribution is the reason a person hands over work
  they would otherwise supervise, and it is silent when it is wrong.
- **`No workspace here can be read with this credential`** means the credential reaches
  nothing. Every other command will report that as an empty instance, which reads as "there is
  no work" rather than "you cannot see it".
- **More than one workspace** means a number can name a different item in each, so a call that
  does not say which is refused, naming them. Pass `workspace` on every call from the first one,
  or ask the person to set a default, which every tool then takes: the `workspace` setting on
  the `subroutine` plugin, or `?workspace=` on the address of the `subroutine-remote` one, which
  has no such setting.
- **A line after the versions** names something worth acting on, and they are different
  problems: *the program and the instance disagree* means one of them has a field the other does
  not; *the plugin is older than the program* — or *older than the instance*, where you reached
  this by address and have no program of your own — means this skill and the plugin's settings
  describe an earlier version of these tools; *a development build's version cannot be ranked*
  means the comparison was not made at all, so read the numbers yourself rather than waiting to
  be told. **A line saying a newer version *is out*** means that installation is behind what has
  been released, and it ends with what to type: pass it on as it stands, since upgrading is the
  person's. *…is the newest release* means nothing compared is behind, and *the last check for
  new releases failed* means nobody knows — not that nothing is newer.

**That is why the versions are printed at all.** The plugin, the program and the instance
upgrade separately, so you may be holding a tool description written for a program that has not
been updated, or talking to an instance that has.

**And *"what you are running is not visible from here"* now means you sent nothing, rather than
that it could not be known.** These tools answer on the instance, so what is on your own machine
reaches it only because your plugin announces it on every request. A plugin that names itself is
reported and compared; one that does not is met with that sentence, and the remedy is the same as
it ever was — run `subroutine whoami` in a terminal on your own machine. You cannot tell a capability that does not
exist from one that is merely too old from where you sit, and guessing wrongly costs the person
an hour. Report the line as it stands and let them fix it; refreshing any of them is theirs to
do, not yours.

**Three numbers that are not identical is normal, and there is deliberately no line about it.**
The plugin's version moves whenever its own contents change, so it runs ahead of the program
between releases by design. Only the things above are said out loud.

**Silence means the comparisons were made and nothing was wrong.** It used to mean less than
that: a version this cannot put in order — a build from source, `0.8.2.dev45+g1234567` — was not
compared at all and printed nothing, exactly as agreement does, so no line meant *either* nothing
is wrong *or* the question could not be answered. That case says so now. So a line about ranking
is the one that asks you to read the numbers yourself, and no line at all is an all-clear.

Go back to the whole answer whenever a tool does something you did not expect: an argument
refused, a field missing, a capability you have read about here that does not seem to be there.

**And if you can also run shell commands, ask twice.** The tools and the shell resolve
credentials independently — the tools use whatever the plugin was configured with, the shell
uses what the command line finds, and nothing reconciles them. So run this as well:

```
subroutine whoami
```

**Two different answers is worth reporting, and it is not always wrong.** It has been measured
both ways.

One agent writing as a bounded service account through its tools and as a superuser through its
shell is a misconfiguration, and a bad one: it is partial, so anyone spot-checking finds the
agent's own name on the half that went through the tools and concludes the setup worked. But a
split is also what you get on a machine somebody deliberately set up so that their own terminal
is theirs and the editor's tools are the agent's — where every alternative is worse, and the only
thing wrong is that a shell command still records the person.

**You cannot tell those two apart from in here**, and neither is simply correct. Which credential
is meant to be where is a fact about how that machine was set up. So name both accounts, say
which surface each came from, and leave the judgement to the person: they cannot act on a split
nobody has told them about, and you are the only one positioned to see it.

**Do not offer `SUBROUTINE_TOKEN_<CONNECTION>` as the remedy, and do not set it.** It is read
*before* the token a plugin sets for itself, and an MCP server inherits the environment its
editor started in — so exporting it does not reconcile two identities, it replaces the agent's
credential with whatever that variable holds. Where the shell holds a person's token, that
silently makes the agent that person, with everything they are allowed to do. Both `whoami`
calls then agree, which reads exactly like the fix having worked.

**The remedy to name is `subroutine agent create <name> --store`, and it is the person's to
run** — it prints a credential, which is not something to do on their behalf. It records the
agent's token beside theirs rather than in place of it, and `subroutine` then resolves the
agent's in a process an agent started and theirs everywhere else, including in `git` hooks,
which no editor setting reaches. Where an agent already has an identity through its tools and
only the shell is wrong, that one command is the whole of it.

**Never read a credential to find out what is stored** — not `credentials.toml`, not a `.env`,
not a variable holding a token. Not to list its keys, not to check that a token is there, not to
count its characters. The program answers all of that without printing a secret:
`subroutine connections` names where each connection's token comes from and which other
tokens are stored beside it, and `subroutine whoami` says which one is in use and what it may
do. **Reading one yourself is exploring credentials, and a harness that watches for it will
refuse the command — rightly**, because nothing can tell a script that lists keys from one that
copies the values. If those two commands cannot answer what you need, say what you were trying to
find out and ask; do not look.

**Ask what can be started, not what exists.** This is the one thing Subroutine answers that a
list of tasks does not:

```
subroutine_list(ready=true, order="-priority_score")
```

`ready` leaves out anything blocked by unfinished work, anything deferred to a later date, and a
question parked on somebody else. One parked on you stays in yours, because answering it is yours
to do.
Without it you get a backlog in priority order, which includes things nobody can act on yet.

**Ask what happened, not only what is left.** A date field takes `.gte`, `.gt`, `.lt` and
`.lte`, and the value is the same date grammar a write accepts:

```
subroutine_list(filter={"created_at.gte": "yesterday"})
subroutine_list(filter={"completed_at.gte": "start_of_week"})
```

Two entries make a range, and it narrows alongside `project`, `ready` and the rest rather than
replacing them. This is the question to ask at the start of a session about work you did in the
last one — `subroutine_changes` answers what *moved*, and this answers what a period contains.

For *worked on* rather than *changed*, ask `touched_at` — it reads the event feed, so a comment
or a status change counts where `updated_at` would say nothing happened:

```
subroutine_list(filter={"touched_at.gte": "yesterday"})
subroutine_list(filter={"touched_at.gte": "start_of_week", "touched_by.eq": "laurence"})
```

**Take the task before you touch anything, and say when you start.** Two calls around the
work, in this order, every time:

```
subroutine_claim(ref=42)                      first, before any other change
subroutine_update(ref=42, status="in_progress")   when you actually begin
                                              … the work …
subroutine_done(ref=42)                       which hands the claim back with it
```

**This used to say "if anybody else works from this list", and that condition is why nobody
ever did it.** An agent alone on an instance reads it as false — and it was, until it was not.
By the time a second worker exists, the habit needed to have been there already. So it is
unconditional now: you cannot see who else is about to pick this up, and that is the whole
reason the mechanism exists.

**A claim and a status are two different facts, and both are worth saying.** The claim says
*somebody has this right now* — a lease, so `ready=true` hides it from other workers and
`claimed_by` shows your name beside the item. The status says *work has begun*, which is not
the same thing: you may claim an item in order to read it and decide it is not for you, and
then nothing was in progress at all. Nobody derives either from the other.

**It expires by itself, and working on it keeps it alive.** Every write to something you are
holding — an edit, a status change, a comment — pushes the lease out, so an agent that is
working never has to think about it and an agent that stopped stops renewing. Nothing is
stranded if your context ends first, which it will. A claim you find on somebody else's item
may already have run out; you are told who holds it and until when, which is the answer to what
you do next.

**Finishing hands it back**, so there is no separate act at the end. That used to be a third
call and it is gone deliberately: an obligation falling at the end of a session is one nobody
attends, because the end of a session is compaction or a killed process rather than a moment
anybody is present for. `subroutine_claim(ref=42, release=true)` is still there for work you are
putting down without finishing.

**If your session keeps a task list of its own, lay a claimed item's steps out in it** and work
them there. The item stays the record, and the session's list is the working surface for the
next hour. Not every session has one, and then there is nothing to do.

**When you cannot go on, hand it back rather than stopping or guessing.** A conversation ends
and takes a question with it; an item does not. *Cannot go on* means a question the item, the
decisions that govern it and the code do not answer — which includes anything resting on taste,
priority, scope, or a word somebody using the product will read — or a permission your
credential does not carry. Hard is not blocked.

**Who it goes back to, in order:**

1. **Whoever assigned it to you** — `subroutine_show` says *assigned by @jo*. Not when that is
   you, and not when it came to you as a question: that one you answer, or pass up.
2. **Otherwise your account parent**, the account yours was created by, which
   `subroutine_whoami` names. Every agent answers to a person in the end, so a question always
   reaches one.
3. **Whoever answers assigns it back to whoever asked.**

```
subroutine_update(ref=42, status="needs_input", assignee="jo")
subroutine_comment(ref=42, body="Which way round should the flag read? Both work; the second
                                 matches the CLI, so I would pick it.")
subroutine_claim(ref=42, release=true)
```

**The comment is the hand-back.** Say what you need, why it is theirs to decide rather than
yours, what you would choose, and — when you are passing a question up — who below you is
waiting on it, because the item names only whoever assigned it last. An assignment with no
question on it is a notification, and it leaves the person you interrupted to work out why.

**How you hand it over says what you mean:**

| You are | Status | Assign it to |
| --- | --- | --- |
| Asking | `needs_input` | the rules above |
| Giving it back, because it is not yours to do | unchanged | the rules above |
| Answering | `open` | whoever asked |
| Finishing | done | nobody new — it stays with you |

**A question that came to you is never sent back unanswered.** Handing back is itself an
assignment, so whoever asked is now named as your assigner, and returning it to them is the one
loop these rules rule out. A back-and-forth — asked, answered, asked again — is not that: its
status changes every time.

**Finishing does not reassign.** Left with you, the item still names who handed it over, and
that is how they find it:

```
subroutine_list(filter={"assigned_by.eq": "me", "completed_at.gte": "yesterday"})
```

If they have to check the work first, give it back open and say it is ready to check.
Cancelling work somebody gave you is their decision, so give it back instead.

**Assign it, or it reaches nobody.** *Waiting on you*, on that person's agenda, holds what is
assigned to them or what they are holding, so a parked item with nobody's name on it sits among
the work nobody has taken. Their answer is on the item when you — or a different agent, days
later — come back to it.

**One hand-back is no reason to stop**: carry on with something else that is ready. **Two in a
row are**, because they say the work you are being given is not clear enough to do, and that is
for the person to put right rather than for you to keep trying. `subroutine explain
handing-back` says all of this at a terminal.

**Parking is a status, on the item you already have, whatever its type — not a new
`question`.** A `question` is work for whoever picks it up; parking says *this one is yours,
and I cannot go on without it*.

**Look before you file.** Searching costs one call and a duplicate costs somebody an afternoon
of wondering which of two items is the real one:

```
subroutine_search(q="deploy script")
```

It reads titles *and* what was written about them, and every word you give has to appear —
in any order, in either field. So a half-remembered description finds it. Use it before
creating anything, and whenever the user refers to something you have no record of — it is
usually already there.

**Read one before acting on it.** A listing is titles; `subroutine_show(ref=42)` is the whole
item, with what it is linked to and everything anybody has recorded against it:

```
subroutine_show(ref=42)
```

That record is the point. Somebody — possibly you, last week — wrote down why this was
attempted and what happened, and reading it is cheaper than repeating it. A ref names a task
*or* a document, so this is also how you read a decision somebody pointed you at.

**Note the version, and send it back when you write.** `subroutine_show` reports one — `version
7` — and both `subroutine_update` and `subroutine_document` take it as `expected_version`:

```
subroutine_update(ref=42, description="...", expected_version=7)
```

If somebody saved while you were reading and thinking, the change is turned down and nothing is
written. Without it your write lands on top of theirs, you are told it succeeded, and nothing
anywhere records that their edit existed.

**This is your problem far more than a person's.** Somebody editing in a browser has the item
in front of them and is told when it moves; you read it, spend two minutes reasoning, and write
into a gap you cannot see. It costs nothing to send — you already have the number from the read
you just did — and a revision is where it matters most, because replacing a body loses whole
paragraphs rather than one field.

**Filing something is a complete act on its own.** Most of the time you file work you are
about to do, so it is visible while it is happening rather than afterwards — but the commoner
case in a working conversation is that somebody has just told you about a problem, and what
they want is for it to be *written down*.

**A report is not an assignment.** When somebody describes a defect, file it and say you have,
with its number. Do not begin fixing it unless they asked you to. The words that most often
get misread are the ones about *importance* — "urgent", "this is a blocker", "we shouldn't
ship with this" — and every one of those is a fact about the tracker rather than an instruction
to act. They tell you what to put in `!4/2`; they say nothing about who does the work or when.

If you genuinely cannot tell whether you were handed a report or a job, file it first and ask.
Filing is cheap and reversible; a change nobody asked for costs somebody a review, and if they
had already given the work to a person or another agent it collides with them silently — a
claim (below) tells you somebody *has* an item, and nothing tells you an item was meant for
them.

One line carries the detail:

```
subroutine_add(text="Fix the deploy script by friday !4/2 ~2h #ops +web")
```

`by friday` is a deadline, `!4/2` is importance and urgency out of five, `~2h` an estimate,
`#ops` a tag, `+web` the project. Whatever it read is echoed back, so check that line — it is
the only confirmation that `+web` was understood rather than left in the title.

**The line is the title. Everything you know that the title cannot hold goes in
`description`, in the same call:**

```
subroutine_add(
    text="Cache the connection roster !3/2 +web",
    description="Measured at 400ms a call, four calls a listing. The roster changes only when config.toml does.",
)
```

Write it while you are filing, not afterwards. You have the most context about a piece of work
at the moment you decide it exists, and a title alone is rarely enough for the next reader —
who is usually you, without any of the session this came from.

**The type decides what mood the title is in.** Get these two agreeing or a listing stops
being scannable — and a reader should be able to tell a plan from a fault from an open question
*by the grammar alone*, without the type column and without knowing the subject. That matters
most in a listing already filtered by type, where the column is dropped.

| Type | The title is | Example |
| --- | --- | --- |
| `task`, `feature`, `chore` | an instruction | *"Highlight the search term where it matched"* |
| `bug` | a symptom, in the present tense | *"A date more than a year away renders as if it were this year"* |
| `question` | a question, ending in a question mark | *"Should search read comments?"* |
| `event` | the thing that happens | *"Solar eclipse"* |
| `decision`, `finding`, `spec` | the conclusion | *"Blocked is tracked; waiting is a defer with a reason"* |

The failure to avoid is two items that read alike: *"A client reads a setting from /v1/meta"*
and *"Every entity a client can change is proved to reach what stores it"* are grammatically
identical, and only one of them is a complaint. Telling them apart means judging which state is
desirable, which is exactly the knowledge the type column exists to spare a reader.

Two more reasons, and the second is the one that decided it:

- Your motivation is not lost by a short instruction, because it belongs in the description —
  which is one field away and is where somebody looks next.
- **An instruction cannot rot.** "The guide's 8 KB budget is exhausted" was true and is not; the
  budget is 15 KB, and that title is on a finished item nobody will re-read. A title stating a
  *condition* goes false when the condition changes, silently and permanently. An instruction
  has no truth value at all, so it can neither go false nor quietly become true and start
  reading as a description of the world rather than of the work.

If you find out later that something is not what you filed it as, say so — `type` is settable
on both `subroutine_add` and `subroutine_update`. What something is often becomes clear only
after it has been looked at, so reclassifying is normal rather than an admission.

**A title has to fit on a card, so aim at about a hundred characters.** Not a number to count
to — a board draws each item as a card about three lines wide, and a title that runs past that
pushes every other item down the page. The person reading it is scanning fifty of them.

The limit is 200 and you will be refused past it, but the limit is a floor of decency rather
than the target: a title needing 180 characters is nearly always two sentences, and the second
one belongs in `description`, which is an argument on the same call.

    ✗  Superintendent v1 is one co-located Starlette service holding the page store, the
       transport and the adapter registry, with the panel served from the same origin so the
       socket needs no CORS, and page files kept beside the composition file
    ✓  Superintendent v1 is one co-located service serving the panel from its own origin

**What you cut is not lost.** It goes in `description`, which is where the next reader looks
and where nothing is competing for width.

**Comment as you go, especially when something fails.** A comment is what happened.

```
subroutine_comment(ref=42, body="Reproduced on 3.11 only. The fix in #38 does not apply here.")
```

A `#38` in the body is a *reference*, not a link: it shows on item 38 under *Referred to
by*, so the two find each other later. Where the item cited is one that governs — a
decision, a specification, a design, a dead end — `subroutine_show` goes further and
offers the typed link for you to confirm, with the call that makes it. Confirm it: an
unconfirmed suggestion is not what `ready=true` reads.

If you wrote one you should not have, `subroutine_comment(ref=42, body="…", remove=true)` takes
it back out — named by some of its words, because a comment has no number of its own. Matching
more than one is refused rather than guessed at. **Do not use it to tidy history**: a comment
that turned out to be wrong is worth more standing beside the correction than removed, because
"we thought X" is half of why the next session should not think X. Withdraw duplication and
mistakes, not the record of having been wrong.

**When what you concluded changes, edit the body — never correct it underneath.** A body says
what is true *now*; a comment says what *happened*. So a superseded plan is replaced:
`subroutine_document(ref=42, body="…")` for a document, `subroutine_update(ref=42,
description="…")` for a task. The test needs no judgement — *would somebody who read only the
body be misled?* If yes, the body is wrong.

**A comment may never open by telling the reader to disregard what is above it.** If you find
yourself writing that sentence, an edit was what you needed. A reader who has to reach the
bottom of a thread to learn the top was wrong has had their time taken, and will not trust the
next item either.

Then say what changed, if it is worth saying — *"replaced the plan with the runbook, because
building it settled three of the open questions"* is what happened, which is exactly what a
comment is for. The change is recorded with the previous text, so nothing is lost by replacing
it.


**Write a document when you conclude something.** A comment is what happened; a document is
what you concluded — and the test is simple: *would the next person need to read it?* Decisions,
findings, designs and dead ends are all worth more than the hour they cost.

```
subroutine_document(title="Why we dropped the queue", type="decision",
                    body="It added an operational surface nobody wanted. …")
```

Dead ends especially. "We tried X and it does not work because Y" is the single most valuable
thing to leave behind, because without it the next session will try X.

**Say which project it belongs in.** Pass `project="web"`. Without one it lands in the Inbox,
which is where things go when nobody decided — fine for a quick capture, wrong for a conclusion
somebody will go looking for. It can be moved later, so this is worth a moment and not worth a
question.

**Here, or on disk?** A conclusion the next *session* needs is a document here. A thing a
*program* reads — a specification whose sections code and tests address by number, a config
file, a README — stays a file. The test is who reads it, not how important it is. And one thing
lives in one place, never both: two copies drift, and nobody can tell which one is stale.

**Ask before writing anything sensitive.** A document in a private project is visible only to
that project's members; one anywhere else is visible to everybody who can reach the workspace.
If a conclusion names a client, a rate, a person or a credential, ask which project it belongs
in rather than choosing for yourself — publishing cannot be undone.

**Finish by leaving the trail.** Before your context ends: comment on what you touched, write a
document for anything you decided, and mark done what is done. `subroutine_done(ref=42)`.

## When the tools do not cover it

`subroutine_call_api(method="PATCH", path="/v1/documents/42", body={"title": "…"})` reaches any
route your credential already allows. Use it for the thing you cannot otherwise do — and reach
for a named tool first, every time you have one.

**That is not politeness, it is the difference between a call that works and a call that is
right.** The tools carry conventions the API does not enforce. `subroutine_add` reads a whole
line — `Fix the boiler by friday !4/2 ~2h #home +sr` — and `POST /v1/tasks` will happily take
`{"title": "Fix the boiler", "importance": 4}` instead. Both succeed. The second quietly stops
using the grammar, sets no deadline because nobody parsed "by friday", and nothing anywhere
reports it. A raw call is the one place this tool surface cannot help you.

So: **the tools are a budget, and the command line is the whole product.** The list you were
given is short because each tool costs context in every session whether you call it or not —
not because the product does only that much. **If `subroutine` is on your `PATH`**, `subroutine --help` and
`subroutine explain <topic>` are complete, and setting anything up — `subroutine token create`
for another agent, for instance — exists only there. Check before recommending one: a session
connected to a server by address has the whole tool surface and no command line at all, and
`subroutine_call_api` is its escape hatch instead.

**Two things to read before constructing a call.** `subroutine://meta` is this workspace's
vocabulary — status keys, item types, what each listing filters and sorts by. The keys are
renameable, so `done` may be called something else here and guessing is how you get a 422.
`subroutine://docs/examples` is a worked request for each common act, every one of them executed
by the project's own test suite.

**Three routes are deliberately out of reach**: creating a workspace, renaming one, and moving a
project. Each is consequential, none can be undone, and the command line counts what will change
and asks first — which a tool call cannot do here yet. The refusal names the command to run.

## Things worth knowing

- **A number means one item, for ever.** `#42` is allocated once and never reused, and it names
  a task *or* a document. Never address anything by its position in a list.

  **The number space is Subroutine's, so do not mint your own.** When you write anything else —
  a review, a design note, a summary in chat — do not number its sections and call them findings
  or items. A reader holding "finding 3" and `#3` cannot tell them apart, and you will not be
  there to explain. Name the sections, and cite the real ref wherever one exists.

  **You usually cannot cite the refs on the first pass**, because you do not know what is worth
  filing until you have written it up. So write it with names, file from it, then go back once
  and put the refs in. That pass is what turns a write-up into the index into the tracker, which
  is what somebody reading it in six months actually wants — and it is the pass that shows you
  which findings you never filed.
- **Blocked is a link, not a status.** Say it with a link rather than by setting a status — a
  link resolves itself when the other side finishes, and it is what `ready` reads.

  **`ref` is the blocker.** `subroutine_link(ref=42, type="blocks", other=43)` means *42 blocks
  43*, so 43 is the one that disappears from `ready`. Read it as the sentence it spells: "42
  blocks 43". If what you have in mind is "this work depends on that work", the thing it depends
  *on* goes in `ref`.
  **`other` takes several.** `subroutine_link(ref=42, type="blocks", other=[43, 44, 45])` makes
  three links in one call — same `ref`, same type, one per target. Laying out a plan is when
  this matters: one measured project needed 37 links, which is 37 round trips one at a time.
  Every number is read before any link is written, so a bad one leaves nothing half-made.

  **A link joins tasks and documents, never projects.** So a milestone that has to follow
  another is an item blocked by the work it stands for, and a feature is a parent task: a plan
  laid out as projects is a set of folders, with no way to say that one comes after another.

- **Waiting on something outside the system is a deferral with a reason**:
  `subroutine_update(ref=42, defer="now+7d")` and a comment saying what you are waiting for.
  The link above resolves itself; an external wait does not, so it needs the reason in prose
  and a date to look again.
- **When you make something a person has to look at, say where it is.** A screenshot, a
  rendered page, a report: put it where they can reach it and give the path if you share a
  filesystem, or the address if there is one. Do not describe it as delivered because a tool
  said it sent it. Whether it arrived is a fact about their screen, not about your call.
- **Do not close somebody else's work** without being asked, and do not edit their comments —
  a comment is attributed prose. Add your own.

## This is a default, not a rule

The practice above is what the tool is shaped for, not a policy anybody has to keep. If the
project has its own conventions — in `CLAUDE.md`, in a contributing guide, or just in what the
user tells you — those win. Say so once and follow them.

Attribution

simonhollidaysimonholliday
View sourceMore from simonholliday →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Caveman

Ultra-compressed communication mode that cuts output tokens while keeping technical accuracy. Levels: lite, full, ultra and the wenyan variants. Use for /caveman, "caveman mode", "talk like caveman", "be brief" or "less tokens".

1066601 votes

Hyperplan

Adversarial multi-agent planning skill. Self-orchestrates 5 hostile category members (unspecified-low, unspecified-high, deep, ultrabrain, artistry) via team-mode for ruthless cross-critique debate, distills only the defensible insights, then MANDATORILY hands the distilled insight bundle to the `plan` agent for executable plan formalization. Use when planning needs maximum rigor and surfacing of weak assumptions, blind spots, and over-engineering. Triggers: 'hyperplan', 'hpp', '/hyperplan', ...

686011 votes

Mcp Code Execution

Routes multi-tool workflows through MCP servers for large datasets and pipelines. Use when Bash tool overhead is limiting throughput on data-heavy tasks.

3351 votes

catchup

Recovers the conversation and failed tool calls of a previous Codex, Claude Code, Antigravity, Cline, Copilot CLI, Cursor, DeepSeek Harness, Kimi, OpenCode, Pi Agent, or ZCode session. Use when the user says "catch up", "what did the last session do", "get me up to speed", "I switched agents", asks to recover/summarize a previous session before continuing, or asks to diagnose or report a catchup failure. Do NOT use for the current conversation, git history, or any non-agent log.

651 votes

math-skill

A comprehensive mathematical reasoning skill for AI assistants — handles arithmetic to research-level problems with rigorous step-by-step reasoning, systematic verification, and transparent uncertainty handling

381 votes
View all in ai-agents →