worktender

Reference

Exit codes, errors, keybindings, and the behaviours that do not belong anywhere else.

Finding out what is wrong

$ worktender doctor
version  0.5.0 @f074c65  warn  origin/main is at @a1b2c3d; run `worktender update`
herdr    0.7.5          ok
gh       authenticated  ok
events   unset          off

repos
  worktender    3 worktrees  1 working
  house         7 worktrees  1 idle, 1 working

Four of this plugin's failures are environmental, silent, and shaped exactly like ordinary operation, so doctor exists to name them without being asked the right question first:

It is read-only, takes no lock, and works from outside a repository: someone who cannot tell what is wrong often cannot tell where they are either. The repository list is herdr's open worktree workspaces rather than wherever the caller is standing.

doctor is a command rather than an action, and deliberately: an action's output lands in the plugin log, which is exactly the indirection a diagnostic should not have.

Staying current

$ worktender update
install: /Users/you/.config/herdr/plugins/github/steig.worktender-3ebd1704d63b
origin/main is at @a1b2c3d; fetching
0.5.0 @f074c65 -> 0.6.0 @a1b2c3d

herdr still records @f074c65 for this plugin, so `herdr plugin list` will keep
naming a commit that is no longer installed.
nothing here can correct that record; a reinstall re-clones and re-records it:
  herdr plugin install steig/worktender

herdr has no plugin update — its plugin subcommands are install, uninstall, link, unlink, enable, disable, list, config-dir, action, log and pane — so this command exists because nothing else can move an install forward.

Three things worth knowing:

The staging is a request, though: update runs the build script from the checkout it just fetched, and every release before this one writes bin/worktender regardless. That cannot be prevented from here, so the live binary is compared before and after and the failure says which happened — replaced in place, or never built at all. The two look identical from the staged file alone, and calling the first the second would assert a state nobody checked. - herdr plugin list will report the pre-update commit afterwards. herdr records the commit at install time and never re-reads the checkout — the manifest version it re-reads, so the two disagree. Nothing in this plugin can correct that record; update says so, and doctor repeats it on every run.

It refuses two checkouts: one on a branch — that is what herdr plugin link leaves, and it is yours to move with git — and one with uncommitted changes, which a hard reset would destroy.

Like doctor, it is a command rather than an action. An action's output lands in the plugin log, and herdr running it as an action would be herdr executing the binary the rebuild replaces.

Getting onto it the first time

update arrived in 0.6.0, so no install that predates it can reach it by running it — which is every install that existed when it shipped:

$ worktender update
worktender: unknown command "update"; usage: worktender <ls|doctor|sync|dispatch|prune|prune-apply|report|gate|on-event|startup>

Reinstall instead:

herdr plugin install steig/worktender

That is the one to reach for first, and not only because the alternative is longer: it re-clones and re-records, so it is also the only thing that corrects the commit herdr holds. The hand path — fetch one commit deep, reset onto FETCH_HEAD, rebuild — is exactly what update performs, and it leaves herdr plugin list naming the commit the install started on; one checkout still read @8ef0de9 after two updates by hand, which doctor now reports as a warn.

This is once per install rather than a standing tax. Any install carrying update moves forward with update.

Exit codes and errors

Every failure prints worktender: <error> on stderr and exits non-zero. The code says what to do next, not what went wrong — a coordinating agent has four responses available, and one code per response is what lets it pick without matching on the message.

Code Meaning What the caller does
0 It happened. Proceed.
1 Usage: an unknown flag, a missing argument, a repository you declined to name, a target that cannot report. Fix the invocation. Retrying unchanged fails identically.
2 Environment: herdr's socket unreachable, git failing, gh unauthenticated. Also every unclassified failure. Fix the machine, then retry.
3 Needs a human: a worker reported blocked, or start briefed an agent that never took it up. Escalate. No retry clears it.
4 No answer arrived: a gate timed out, a pane died before reporting. The work may simply be unfinished; redispatching is reasonable.

2 is the catch-all on purpose. The alternatives are worse: defaulting to 1 tells a coordinator to rewrite a correct invocation, and defaulting to 3 wakes somebody for a bug. An unrecognised failure routed to 2 costs at most a retry.

The distinction 3 and 4 draw is the one this replaced two codes for. A gate returning 1 used to mean the worker is blocked and needs a person or that herdr's socket had died, and those demand opposite actions.

Everything fails loudly on purpose. herdr records a plugin action that exits 0 as "succeeded", so a command that reports a problem and exits 0 is a silent failure — which is why sync and prune exit non-zero with %d of %d action(s) failed rather than printing a warning and returning success.

Errors you are most likely to meet:

Message Means
refusing to guess which repository to change You ran a changing command outside herdr. ls and prune allow it; sync, prune-apply and the event paths do not.
--repo: not inside a git repository: <path> prune/prune-apply were given a path that is not one. Never a fallback — naming a repository exists to stop the resolution wandering.
another worktender reconcile has held X for more than 30s A concurrent pass. Retry.
WORKTENDER_EVENTS="ture" is not a value this gate recognises Events stay off. Fix the value yourself; nothing here rewrites it.
MUSTER_EVENTS is set, but it was renamed A superseded opt-in enabling nothing. So is HERDR_WT_EVENTS.
--note is N characters; the limit is 200 Refused, never truncated. Shorten and report again.
this checkout is on branch X rather than the detached HEAD herdr installs update was pointed at a linked development checkout. Move that one with git.
no herdr-plugin.toml beside X; update only works from an installed plugin binary The binary is not sitting in an install's bin/.
the worker reported blocked after Ns The gate failed fast rather than waiting out its clock. With --any it names which worker, and the others are left running.
no new report reached status done within Ns Timed out. The message accounts for every worker waited on, quoting what its pane already held when the gate opened, which it ignored as a previous task's answer.
b is the same worker (pane w2:p1); name it once A gate was given an agent's name and its pane id, which herdr resolves to one worker. Watching it twice would let one report look like two.

Smaller things worth knowing

The digest is not decoration. herdr's agent namespace is global and it refuses a duplicate outright (agent_name_taken), so two repositories with a worktree called api — or an issue #12 each — would ask for one name and whichever was staffed second would get no agent. It also separates two long branches the 32-character limit would otherwise truncate onto each other. So do not retype the name from the branch: start prints the exact gate line for what it started, and sync prints the name it staffed under. - Adoption does not focus the workspace, so adopting a batch does not drag you through every one of them. - The repository lock is fail-open. It lives under HERDR_PLUGIN_STATE_DIR, and if that is absent or unwritable it degrades to a lock that excludes nothing, silently. Any lock held longer than five minutes is taken. It stops two reconciles duplicating work; it is not a safety control — the guards that are re-checked immediately before removal are.

Binding a key to an action

herdr has no plugin_action keybinding type — its key commands are command, pane, popup and split. So a binding runs the invoke CLI like any other command, in your own config.toml:

[[keys.command]]
key = "prefix+alt+s"
type = "popup"
command = "herdr plugin action invoke sync --plugin steig.worktender"
width = "70%"
height = "50%"

Bind sync and prune, never prune-apply. A key that can reach a removal is exactly what splitting those two actions was meant to prevent.

This plugin deliberately ships no [[keys.command]] entries of its own. An install that silently claims prefix+alt+s is the same class of surprise as one that edits your agent's configuration — and you may already have that key.

That is the whole of the herdr action surface, and it is a subset of the binary's: every action runs ./bin/worktender <id>, so ls, sync, prune and prune-apply are subcommands you can call directly — and should, since the action path returns an invocation record rather than the output. report and gate have no action equivalent at all: they take arguments and gate blocks, neither of which an action can do.