What it does
linear-issue-tracker is one of three tracker adapters. .agents/tracker-contract.md states the
five things the pipeline requires of any tracker, and this file is the HOW for Linear. It is the
thin one, and the reason is a compliment to the product: native workflow states, a real status
field, a board that follows it with no second write, native relations, native assignee. All five
requirements are met, so most of what the other two adapters spend their length on does not exist
here. I reach Linear through the linear-server MCP tools rather than a CLI. Everything here was
measured on a live project that ran Linear until 2026-08-21.
Two things are worth knowing before choosing Linear, and the first is not about the data model.
The free tier stops accepting new issues. save_issue returns “You’ve exceeded the free
issue limit”. That error does not degrade a pipeline, it stops one, because the first step of
the method is create. A project hit this and had to migrate the same day. Requirements 1 to 5
are about a tracker’s model, but a tracker also has to accept a write, so check the ceiling of
the tier you are on first. The second is the claim protocol, which is weaker here than the method
assumes, and git is what carries it.
When Thomas reaches for it
| What is in front of you | Reach for |
|---|---|
The project’s issue-tracker.md names Linear |
Skill(skill: "linear-issue-tracker"), and no other adapter |
| Creating, reading, labelling, relating or closing | save_issue, which is nearly all of the surface |
| A map document for one effort | save_document on the Linear project, a first-class primitive rather than the GitHub workaround |
| A ticket that reads ready while its parent epic is blocked | Filter on parent status too, because blocking is not inherited |
| Searching your own issues | list_issues with query; search_documentation is Linear’s product docs |
Prerequisites
- The tier accepts writes. Check the issue ceiling before the pipeline depends on
create. - The project’s
issue-tracker.mdcarries the workspace, team name and key, project name and id, the ticket prefix, any id ambiguity from its own history, and which seats exist, since the claim protocol depends on it. - The state vocabulary is mapped onto the method’s language: open is anything not
Done,CanceledorDuplicate; in flight isIn ProgressorIn Review. Todois treated as a state that must be written, not as a state that will happen.
What it leaves behind
| What happened | Where it lands |
|---|---|
| The claim | assignee: "me" plus state In Progress, as the session’s first write |
| The Builder identity | The dispatch record and a comment, because assignee resolves to a real workspace member |
| The frontier, materialised | The Todo state, written when the last blocker closes |
| The blocking graph | Native relations via save_issue’s blockedBy |
| The effort’s map | A Linear project document holding Notes, Decisions-so-far and Fog, one per effort |
Known failures
Pulled from harness/.agents/memory/recurring-failure-modes.md. Both are marked promoted.
- AST-057: a frontier that is only computed is invisible to the one person who cannot
compute it. Measured on this workspace: zero issues had ever entered
Todo, and a ticket sat inBacklogfor hours after both its blockers merged. The upstream cause is a plugin skill writing a readiness label at creation and never revisiting it, so two representations of readiness sit side by side and neither answers the dispatcher’s question. The contract fixes this by writing the computed answer back as state, and by never reading a readiness label as a blocker. - AST-074: frontier promotion computed from blocking edges alone is over-inclusive. That was
measured again on Linear when three tickets surfaced as claimable during an earlier phase,
because a sub-issue with zero blockers reads ready even when its parent epic is blocked. The
fix keeps promotion as the router’s judgement, stated directly in
thomas.md, rather than a query result.
It’s working if
- A ticket entered
Todoin the same action that closed its last blocker, rather than skipping fromBacklogtoIn Progress. - Every invisible blocker, whether a deploy, a credential or a decision, was given an issue and an edge, because a blocker that is not an issue is invisible and the graph lies confidently.
- The claim was released by confirming the branch and worktree are gone, not by reading the assignee back, which cannot tell whose claim it read.
- Parent status was part of the readiness filter, not just
blockedBy. - Nobody treated
list_agent_skillsas writable, because that shelf is authored by the owner in Linear’s own interface.
Where it fits
thomas.md reads the project’s issue-tracker.md at session start → that file names this
adapter → the frontier is list_issues with no assignee, a non-terminal state and no unfinished
blockedBy → dispatch-ticket claims the winner with assignee: "me", and git worktree add -b
decides any same-second race → merge moves the ticket to Done and whatever it unblocked to
Todo → reconcile-tracker measures the result against git. The two adapters in the same group
are github-issue-tracker and jira-issue-tracker, and .agents/tracker-contract.md sits above
all three.