linear-tui 0.9.0

A TUI client for Linear.app — manage issues, projects, and cycles from your terminal
# herdr plugin

[herdr](https://herdr.dev/) is a terminal workspace manager for coding agents.
The plugin in `herdr-plugin/` puts linear-tui in a herdr pane and connects it
to the agents running next to it.

linear-tui does not depend on herdr: it builds, runs, and passes its tests
without it. The herdr-specific pieces live in the plugin's scripts.

## Install

```sh
herdr plugin install k1-c/linear-tui/herdr-plugin
```

It needs herdr 0.9.1 or newer, `jq`, and a `linear-tui` that has
`linear-tui paths` (the release that ships this plugin, or newer) on your `PATH`,
or set `LINEAR_TUI_BIN`, below. For a local checkout, link it instead:
`herdr plugin link ./herdr-plugin`.

Bind the action to a key in herdr's `config.toml`:

```toml
[[keys.command]]
key = "prefix+l"
type = "plugin_action"
command = "k1-c.linear-tui.open"
description = "linear-tui"
```

## Actions

| Action | |
| --- | --- |
| `k1-c.linear-tui.open` | Focus the workspace's linear-tui pane, or open one in the focused pane's directory. |
| `k1-c.linear-tui.deliver` | Send what linear-tui left in its outbox. linear-tui invokes it itself; see below. |
| `k1-c.linear-tui.open-link` | Open the Ctrl+clicked Linear issue URL in a new linear-tui pane. |

linear-tui opens where it was last left in that repository (see
[view-snapshot.md](view-snapshot.md)), so an agent in the same workspace can run
`linear-tui context` and see the same view.

**Links.** Ctrl+click on `https://linear.app/<org>/issue/ENG-42/…` in any pane
opens a linear-tui pane on ENG-42 (`linear-tui open <URL>`) instead of the
browser.

**Restarts.** After the herdr server restarts, a startup hook runs linear-tui
again in each pane it was running in. It finds those panes from view snapshots
that name a herdr pane, were not closed, and belong to a process that is gone.
The pane must have come back as a plain shell in the same directory; linear-tui
then reopens its own view.

## Agents on issues

The plugin keeps a list of herdr's agents in `$STATE/herdr/agents.json`,
refreshed on every agent state change, when panes, tabs, workspaces and
worktrees open or close, and at startup. Each agent is tied to the issue named
by its checkout's branch. `me/eng-42-checkout-fails` is ENG-42, as Linear
suggests branch names.

linear-tui, inside herdr, reads the list once a second when it changes:

- an issue row carries the state of the agent working on it: `▲` waiting for
  you, `●` working, `○` idle, `✓` done;
- the issue page's panel names the agent, its state, and its workspace;
- `g w` brings that agent's pane to the front (only inside herdr).

With several agents on one issue, linear-tui shows the one waiting for you,
then the one at work.

```json
{
  "version": 1,
  "updated_at": "2026-09-25T06:01:02Z",
  "agents": [
    { "pane": "w2:p1", "workspace": "w2", "workspace_label": "shop",
      "agent": "claude", "status": "working",
      "cwd": "/home/me/dev/shop-worktrees/eng-42", "issue": "ENG-42" }
  ]
}
```

`status` is herdr's: `working`, `blocked`, `idle`, `done`, or `unknown`.
`issue` is `null` for an agent on a branch that names none.

## Notes for the agent

In linear-tui, `n` notes the issue under the cursor and `Shift+N` the whole
view; `Ctrl+S` sends every note as one prompt. Inside herdr the prompt goes to
an agent in the same workspace: one in the same directory first, then one
that is not busy, then the one that changed state last. Outside herdr it is
copied to the clipboard.

If no agent takes it, the plugin shows a herdr notification and saves the
prompt to `$STATE/herdr/outbox/undelivered/`.

The prompt ends with a line on `linear-tui context` and `linear-tui issue …`,
so an agent that has not heard of linear-tui can look closer. To word it
your own way, put a template in `$(herdr plugin config-dir k1-c.linear-tui)/prompt.md`
(see `herdr-plugin/prompt.example.md`):

| Placeholder | |
| --- | --- |
| `{{notes}}` | The notes as a Markdown list, each naming its issue (`**ENG-42** title`) or `**This view**`. |
| `{{view}}` | Where you are: `Engineering › Issues › ENG-42`. |
| `{{hint}}` | The line on `linear-tui context` and `linear-tui issue …`. |

### The outbox

linear-tui runs no herdr commands of its own beyond invoking `deliver`. It
leaves one file per request in `$STATE/herdr/outbox/<millis>-<pid>.json`
(`$STATE` from `linear-tui paths`), owner-only:

```json
{
  "version": 1,
  "from": { "pane": "w2:p3", "workspace": "w2", "cwd": "/home/me/dev/shop" },
  "kind": "prompt",
  "text": "My notes on what I am looking at in linear-tui (Engineering › Issues):\n\n- **ENG-42** Checkout fails: …\n\n(…)",
  "notes": "- **ENG-42** Checkout fails: …",
  "view": "Engineering › Issues",
  "hint": "`linear-tui context` shows this view …"
}
```

`g w` leaves `{ "version": 1, "from": { … }, "kind": "focus", "pane": "w2:p1" }`,
which `deliver` answers with `herdr agent focus`.

`deliver` claims each file by renaming it, handles it, and deletes it. `from`
is taken from `HERDR_PANE_ID`, `HERDR_WORKSPACE_ID`, and the working directory.

## Configuration

`$(herdr plugin config-dir k1-c.linear-tui)/config.env`, plain `KEY=value`
lines, read on every action, with no reload needed. For example, linear-tui in the
right 40% of the pane you open it from:

```sh
PLACEMENT=split
DIRECTION=right
SIZE=40
```


| Key | Default | |
| --- | --- | --- |
| `PLACEMENT` | `overlay` | How `open` places the pane: `overlay`, `split`, `tab`, or `zoomed`. |
| `DIRECTION` | `right` | Where a `split` goes: `right` or `down`. |
| `SIZE` | half | For a `split`, linear-tui's share of the pane it splits, in percent (`1`–`99`). |
| `LINEAR_TUI_BIN` | `linear-tui` on `PATH` | The binary to run. herdr starts plugins with a short `PATH`; the plugin adds `~/.cargo/bin`, `~/.local/bin`, Nix and Homebrew locations. |
| `NOTIFY_AGENTS` | `0` | `1` tells each newly started agent, once, how to use `linear-tui context` and `linear-tui issue …`. |

## How agents learn about linear-tui

An agent has to know `linear-tui context` exists before you say "fix this
one". In order of preference:

- **The agent plugin** ([agent-plugin.md]agent-plugin.md), for Claude Code
  and Codex. A SessionStart hook adds a few lines to the session's context in
  repositories where you use linear-tui. No prompt is sent, so nothing below
  applies to it. Works with or without herdr.
- **Your agent's instructions**, for agents without hooks, a line like this in
  `AGENTS.md` or `CLAUDE.md`:

  ```markdown
  The user browses Linear in linear-tui. `linear-tui context` shows what they
  are looking at (open issue, list, cursor); `linear-tui issue show|comment|status|create`
  act on Linear with their credentials. Only move an issue to Done when told to.
  ```

- **`NOTIFY_AGENTS=1`**, the fallback: when an agent has started and is
  waiting for its first message, this plugin sends it `herdr-plugin/notice.md`
  once per agent session. It is off by default, because a prompt is a message
  from the user: it takes the agent's first turn (tokens, and history that stays
  for the whole session), Claude Code names the session after it, it can land
  while you are typing your first message, it reaches every agent in every
  repository, and the agent reads it as something you said.

### Design notes (#47)

Checked against herdr 0.9.1 and Claude Code 2.1:

- `[[events]] on` accepts every event of the socket API's `events.subscribe`
  (`pane.agent_detected`, `pane.agent_status_changed`, `pane.created`, …);
  herdr warns only about names it does not know. The payload arrives in
  `HERDR_PLUGIN_EVENT_JSON` as `{ "event": "pane_agent_status_changed", "data":
  { "pane_id", "workspace_id", "agent", "agent_status" } }`. herdr-hunk-diff
  learns about agent states the same way, from `pane.agent_status_changed`.
- `pane.agent_detected` fires as soon as the agent's process is recognised,
  often while it still shows a startup dialog (a trust prompt), reported as
  `blocked`. `herdr agent prompt` refuses a blocked agent, and typing into the
  dialog would answer it. The right moment is the agent's first `idle`, before
  it has completed any turn (`completion_seq` absent in `herdr agent get`).
- A prompt sent then arrives before the user's first message and is answered
  in a few seconds. It has a cost: it is the agent's first turn, and Claude
  Code titles the session after it. Hence opt-in.
- Revisited in #71: the "instructions" option was first set aside as needing
  per-agent setup, but Claude Code and Codex both load plugins with
  SessionStart hooks whose output becomes session context, the way linear-flow
  and herdr's own integrations reach agents. Packaged as a plugin it is one
  install and avoids the prompt's costs, so it is now the default way; the
  prompt notice stays as a fallback.
- An MCP server needs registering per agent and contradicts "no dependency on
  Linear MCP". A note inside each prompt linear-tui sends covers only the
  human-to-agent direction (it is still done: every prompt of notes ends with
  the `{{hint}}` line).

## Files

- `herdr-plugin.toml`, the manifest.
- `lib.sh`, shared helpers: config, `PATH`, the `linear-tui` binary.
- `open.sh`, the `open` and `open-link` actions.
- `deliver.sh`, `prompt.example.md`, the `deliver` action, and a prompt template to start from.
- `restore.sh`, the startup hook that restarts linear-tui in its panes.
- `agents.sh`, keeps `agents.json` current.
- `event.sh`, `notice.md`, the opt-in agent notice.

Failures go to herdr's plugin log: `herdr plugin log list --plugin k1-c.linear-tui`.