What it does
jira-issue-tracker is one of three tracker adapters. .agents/tracker-contract.md states the
five things the pipeline requires of any tracker. This file is the HOW for Jira: status as
transitions, the two coordinates a session cannot discover, issue links and their invertible
direction, whole-field description writes, and what only a human can change. The project half
stays in the project: the site, the cloudId, the project key and the transition-id table live
in that project’s docs/agents/issue-tracker.md. I reach Jira through the Atlassian MCP tools.
Everything here was measured on a live team-managed project that moved from Linear to Jira on
2026-08-21.
The single biggest difference from every other tracker is that you do not assign a status, you
take a transition. Transitions are identified by numeric ids that are project-specific,
non-sequential and not guessable, and states added later take low ids. So a remembered transition
id is a valid write to a state you did not mean: it succeeds, nothing errors, and no query flags
it. Read the transitions every time, because the extra call is the guard, and it costs less
than the class of bug it prevents. In exchange, Jira’s advantage is real: Blocks is expressible
in one JQL query, so the frontier is one call rather than the N+1 loop GitHub forces.
When Thomas reaches for it
| What is in front of you | Reach for |
|---|---|
The project’s issue-tracker.md names Jira |
Skill(skill: "jira-issue-tracker"), and no other adapter |
| A status change | getTransitionsForJiraIssue, then transitionJiraIssue with an id from that response |
| A description to patch | Read it, substitute, send the whole field back, because there is no partial update |
| A state the project does not have | Stop. Adding one is an owner action in the Jira UI, and the MCP tools cannot touch a workflow |
| A duplicate | A Duplicate link plus the cancelled state, never a status of its own |
Prerequisites
- The
cloudIdis written down. It appears in no URL a human pastes, so without it every session starts with a lookup throughgetAccessibleAtlassianResources. - The project key is quoted in JQL. A key that is also a JQL word makes
project = <KEY>fail to parse, and the error does not say to quote it. - You know whether the project is team-managed or company-managed, because that decides whether the statuses are the board or map to it.
- The accountId used for the claim is resolved once with
lookupJiraAccountIdand then carried in the project’sissue-tracker.md.
What it leaves behind
| What happened | Where it lands |
|---|---|
| The claim | The assignee field, set to your own accountId |
| The Builder identity | .astraler/state/dispatch-record.json, because assignee takes an accountId and cannot hold builder/<ticket-id> |
| The frontier | A single JQL query: unassigned, not done, minus anything with an unfinished is blocked by link |
| The blocking graph | Native issue links, whose rendered wording is the thing to read back |
| Migration provenance | A provenance label and a first description line pointing back, neither of which is ever stripped |
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. The readiness state exists for the human, and an agent will not notice it missing
because it re-derives readiness on demand. The contract fixes this by writing the computed
answer back as state, in the same action that closes the last blocker, and by never reading a
readiness label as a blocker. Readiness plus an open
is blocked bylink is a contradiction, so move the ticket back in the same action that adds the link. - AST-074: a tracker measured only against itself cannot detect its own drift. Four tickets
stayed claimed and in-progress after their code had merged, nothing errored, and the drift
lived in the tracker’s content, where no reachability check can see it. The fix is
reconcile-tracker, which measures the tracker against git. It is read-only by a recorded ruling, because the join key is a ticket id in a commit subject and a citation is not a completion.
It’s working if
- Every transition id came from a fresh
getTransitionsForJiraIssue, never from a table or from memory. - Every new link was verified by reading the rendered wording back, so a backwards
Blockscannot corrupt the frontier silently. - No description was retyped from memory, and every JSON payload was built programmatically.
Relateslinks were kept out, because a mention in the body carries the same information where it is readable.- A blocker that is not an issue was given an issue. A decision ticket is first-class, is never dispatched to an agent, and closes when the owner answers.
Where it fits
thomas.md reads the project’s issue-tracker.md at session start → that file names this
adapter → the frontier is one JQL query → dispatch-ticket claims the winner by setting the
assignee, then git decides any same-second race → merge takes the Done transition and moves
whatever it unblocked into the readiness state → reconcile-tracker measures the result against
git. The two adapters in the same group are github-issue-tracker and linear-issue-tracker,
and .agents/tracker-contract.md sits above all three.