linear-sync
Move the Linear issues linked to the current branch through their workflow states. This skill is the single source of truth for how issues are transitioned: resolving the live state IDs, extracting issue IDs from a branch name, and the per-state transition rules. Callers decide when and whether to fire it; the mechanics live here once so the rules don't drift across the ship flow, branch cleanup, and the start-of-work transition.
Configuration
Two knobs live in config.json beside this file. Read it at the
start of a run and use its values throughout. Edit your copied config.json to
match the consuming repo:
| Key | Meaning | Default |
|---|---|---|
linearTeamName |
Linear team name used to resolve the live state IDs. Use the name, not the key — the key is renamed over time but the name is stable. | "Rheged Studio" |
issueKeys |
Team-key prefixes that may appear in branch names. The issue-ID regex is built from these. | ["A"] |
A neutral config.example.json ships alongside it as a
template — copy it over config.json and fill in your values, or edit
config.json directly.
Usage modes
--dry-run is passed through $ARGUMENTS (the agent reads it), matching the
--dry-run "preview, change nothing" convention used across the other skills.
Normal — resolve state IDs, extract the branch's issue IDs, apply the transition, and report what moved:
linear-sync
Dry run — resolve state IDs and each issue's current state, report the
intended transition (or skip reason) per issue, and exit without any
save_issue call:
linear-sync --dry-run
Under --dry-run the resolve + read steps still run (they are read-only), so the
preview is accurate; only the save_issue write in the transition step is
skipped. End the report with DRY RUN — no issues were changed.
Resolving the target state (do this once per run)
Call mcp__linear-server__list_issue_statuses with team: <linearTeamName>
once to fetch the team's live workflow states. Each carries a stable type
(triage / backlog / unstarted / started / completed / canceled), a
display name, an id, and a position.
Resolve the target by type, not by display name. Display names are
customisable — a consumer may rename In Progress → Doing or In Review →
Code Review — so matching the literal name silently fails to find the state (the
biggest correctness gap for adopters). Map each transition to a concrete state:
- In Progress → the
startedstate named "In Progress" (case-insensitive); else the earlieststartedbyposition. - In Review → the
startedstate whose name matches "In Review" / "Review"; else, when there are ≥2startedstates, the lateststartedbyposition; else the In Progress state. - Done → the
completedstate named "Done"; else the earliestcompletedbyposition.
started covers both In Progress and In Review, so the name match (then position)
is what separates them; a team with a single started state resolves both targets
to it. Use the resolved id in the save_issue call.
Pass the team name, not the key. Linear state IDs are per-team, and a
workspace's team can be renamed over its lifetime (e.g. CAT → WTF → AKW → ASW),
so a hardcoded key goes stale. The team name (linearTeamName) does not move.
This is the canonical gotcha for adopters — resolve by name, every run.
Extracting issue IDs from the branch
Build the issue-ID regex deterministically — mirror the canonical, tested
buildIssueRe in the repo-root lib/issue-keys.mjs,
which pnpm vendor:sync copies into each consuming bundle (ADR-0004) and which
exists for exactly this job:
- Escape regex metacharacters in each key (a configured key such as
C++would otherwise throw or silently widen the match). - Group the alternation. Wrap the keys in
(?:…)whenever there is more than one, so the-\d+binds to the whole alternation:\b(?:A|B)-\d+\b. The naive join\bA|B-\d+\bis wrong — it parses as\bAorB-\d+\b, matching a bareAand missingA-7. A single key needs no wrapper:\bA-\d+\b. - Guard the empty case. With no configured keys, match nothing — never build
an empty alternation (it would match the empty string before every
-<digits>and inject bogus IDs like-2).
Match the result (with the g flag) against the upper-cased branch name —
branches like asw-7-as-acquired carry the key in lower case, and a flow such as
--issue=A-7 produces upper-case branch names like A-7-as-acquired. Keeping the
legacy keys means leftover branches from before a team-key rename are still
recognised. Deduplicate the matches. Bogus or malformed IDs simply error on lookup
and are skipped with a warning — no separate validation pass.
When a caller already has an issues list to hand (e.g. a changelog step emits
one), use that instead of re-extracting.
Transition rules
For each issue ID, call mcp__linear-server__get_issue to read its current state
(use its type, not its display name). Decide using the progression order of
state types:
triage < backlog < unstarted < started < completed
canceled (which also covers a Duplicate state) is terminal and sits outside the
line. Within started, order by position, so In Progress precedes In Review. All
transitions are idempotent: apply only when the current state is earlier in
the progression than the resolved target; skip silently when the issue is
already at or past it, or when its current type is completed or canceled
(terminal states are never advanced automatically).
- In Progress (fired when starting work on an issue) — apply from
triage/backlog/unstarted; skip from anystarted,completed, orcanceled. - In Review (fired on PR open/update inside a ship flow) — apply from
triage/backlog/unstarted, or from astartedstate earlier bypositionthan the resolved In Review (e.g. In Progress); skip once at or past In Review, orcompleted/canceled. - Done (fired on branch cleanup) — apply from
triage/backlog/unstarted/started; skip fromcompleted/canceled.
Apply a transition with mcp__linear-server__save_issue using the resolved state
id — it is unambiguous across renames. The display-name form
state: "<target>" works only when the consumer kept Linear's default state names.
Under --dry-run, skip this save_issue call. Still read each issue's
current state with get_issue and decide whether it would transition, but
report the intended move (e.g. A-7: Todo → In Progress (would apply) /
A-9: In Review (would skip — already at/past target)) instead of writing it.
Canceledis the Linear API's own US spelling — keep it as-is when referenced in code or config.
Caller responsibilities (when / whether to fire)
The skill owns the mechanics; each caller owns the policy:
- Start of work — transition to
In Progresswhen work begins on an issue (unless already In Progress or further along). Run automatically; no prompt. - Ship flow (PR open/update) — transition linked issues to
In Reviewautomatically after the PR is created or updated. No prompt. - Branch cleanup — transition orphaned issues to
Doneonly after explicit confirmation, default no. Linear's GitHub integration normally handles theDonetransition on PR merge, so this prompt exists only for the rare case where the integration didn't fire (e.g. the issue ID was added after merge).
Standalone vs inside a caller
- Standalone — resolve the target state, extract the branch's issue IDs, apply the transition, and report which issues moved and which were skipped. The default target is In Progress (the start-of-work transition that has no other home).
- Inside a caller — the caller supplies the target (and often the
issueslist) and decides whether to prompt; the mechanics above are unchanged.
Implementation
No supporting scripts — the skill drives the Linear MCP tools directly
(list_issue_statuses, get_issue, save_issue). The only repo-specific inputs
are the team name and the issue-ID prefixes, both read from config.json.