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: extracting the issue ID from a branch name, resolving the live state IDs for each issue's team, 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 |
Fallback Linear team name, used only when an issue's get_issue result carries no team. States are otherwise resolved against each issue's own team. 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 lead a branch name — every team whose issues this repo carries, (e.g. ["R"]). The branch issue-ID regex is built from these. |
["R"] |
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 — extract the branch's issue ID, resolve its team's state 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.
Extracting the issue ID from the branch
A branch is named <id>-<slug>, so its issue is the leading token only.
Mirror the canonical, tested buildBranchIssueRe / extractBranchIssueId in
the repo-root lib/issue-keys.mjs (this skill has
no scripts, so it mirrors the helper rather than vendoring it — ADR-0004):
- 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:^(?:C|R)-\d+\b. The naive join^R|A-\d+\bis wrong — it parses as^RorA-\d+\b. A single key needs no wrapper:^R-\d+\b. - Anchor at the start (
^) and match once (nogflag). A slug token such as the-c-2-inr-40-add-c-2-supportmust never resolve to an issue — in a repo configured with["C", "R"]it would otherwise transition an unrelated Cadogan issue. - Guard the empty case. With no configured keys, match nothing — never build an empty alternation.
Match against the upper-cased branch name — branches like r-7-as-acquired
carry the key in lower case, and a flow such as --issue=R-7 produces upper-case
branch names like R-7-as-acquired. So c-12-fix-r-3-thing → C-12,
r-2651-2-agent-skills → R-2651, R-123/demo → R-123, and
feature/r-1-x → no issue. A branch that predates a team-key rename resolves only if its old key is
still listed in issueKeys. A bogus or
malformed ID simply errors on lookup and is 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.
Resolving the target state (per issue's team)
A repo can carry issues from several Linear teams — e.g. c- (Cadogan),
r- (Rheged) branches in one repo. Workflow state IDs are per-team, so never resolve them once for
a configured team. For each issue (mirroring planTransitions in the repo-root
lib/linear-states.mjs):
- Call
mcp__linear-server__get_issueand read the issue's team name (and its current state — see Transition rules). Fall back tolinearTeamNameonly when the result carries no team; with neither, skip the issue and say why. - Call
mcp__linear-server__list_issue_statuseswithteam: <that team name>to fetch its live workflow states — once per team per run: cache the result and reuse it for every later issue in the same team. Each state carries a stabletype(triage/backlog/unstarted/started/completed/canceled), a displayname, anid, and aposition. - Resolve the target among that team's states (
resolveTargetState).
Resolve the target by type, not by display name. Display names are
customisable — a team may rename In Progress → Doing or In Review →
Code Review — so matching the literal name silently fails to find the state.
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. If a team has no state of
the target's type, skip its issues and say so.
Resolve teams by name, not key. Use the team name get_issue returns (or
linearTeamName). A team's key can be renamed over its lifetime (e.g. CAT → WTF
→ AKW → ASW → A → R), so a hardcoded key goes stale; the name does not move.
Transition rules
For each issue, use the get_issue result from the previous step 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 with the issue's team (e.g.
R-7 (Rheged Studio): Todo → In Progress (would apply) /
C-9 (Cadogan): Code 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 — extract the branch's issue ID, resolve the target state for its team, 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
(get_issue, list_issue_statuses, save_issue). The rules it follows are
pinned by tests against the repo-root reference helpers
lib/issue-keys.mjs (branch issue-ID extraction)
and lib/linear-states.mjs (per-team state
resolution and the forward-only transition rule). The only repo-specific inputs
are the issue-ID prefixes and the fallback team name, both read from
config.json.