| name | lisa-setup-linear |
| description | Configure Linear as the… |
| allowed-tools | ["Bash","Read","Write","Edit","Skill","AskUserQuestion","mcp__linear-server__authenticate","mcp__linear-server__complete_authentication"] |
Setup Linear: $ARGUMENTS
Make Linear a tracker, a PRD source, or both for this project. After this skill, .lisa.config.json contains linear.workspace (+ linear.teamKey when Linear is the tracker), the team carries the lifecycle states and label namespaces lisa needs, and (optionally) tracker / source point at Linear.
The two lifecycles run on different primitives, and conflating them is the most common setup error:
- Build queue → native workflow STATES, read from
linear.workflow.*. Not labels. See "Why Linear uses states, not labels" in config-resolution, and Step 3a below. lisa-linear-build-intake reads only these.
- PRD lifecycle → PROJECT labels (
prd-*), because a PRD is a Linear Project.
Project labels and issue labels are distinct namespaces in Linear and are NOT interchangeable — creating an issue label named prd-ready will not work for the PRD flow. The one issue label this skill creates is the sentinel feedback marker, which belongs to the PRD flow despite being an issue label (Linear's MCP has no project-level comments — see linear-prd-intake).
A status:* issue-label namespace is no longer scaffolded or read. It was the pre-state-model build lane; see "Migrating a project that predates the state model" below for what to do with a config that still carries it.
Workflow
Step 0 — Pick a setup path AND what Linear is for
Ask two things via AskUserQuestion.
Access path:
How should lisa talk to Linear for this project?
- MCP-only (simplest) — authenticate the Linear MCP once via browser OAuth; lisa uses it for every operation. Best for single-workspace developers on a personal laptop.
- MCP + API key (recommended for teams) — MCP for interactive dev, a personal API key in keychain for headless / CI. Continue through key-store steps.
- API-key-only (headless / CI) — store a personal API key in the OS keychain; lisa uses curl against the Linear GraphQL API. Best for pipelines / containers.
Role (multiSelect):
What should lisa use Linear for?
- Destination tracker — lisa writes Epics→Projects, Stories→Issues, Sub-tasks→Sub-issues; the build queue runs off native workflow states (
linear.workflow), not labels. Sets tracker: "linear". (Requires a team key.)
- PRD source — humans flag Linear projects with
prd-ready; /lisa:intake scans and ticketes them off the prd-* project-label namespace. Sets source: "linear".
The role answer drives Step 3 (states for the tracker lane, prd-* project labels for the PRD lane) and whether teamKey is required (tracker → yes).
Step 1 — Establish Linear access
MCP path (1 or 2)
Verify the Linear MCP is authenticated to the right workspace by listing teams:
lisa-linear-access operation: list-teams({})
If it errors / returns nothing, run mcp__linear-server__authenticate and have the user complete OAuth in the browser, then mcp__linear-server__complete_authentication, then re-list. A non-empty team list confirms the MCP is authed to a readable workspace.
API-key path (2 or 3)
Linear personal API keys are created at Linear → Settings → Security & access → Personal API keys (or https://linear.app/<workspace>/settings/account/security). Store the key in the OS keychain keyed by the workspace slug, using the clipboard-pipe pattern (key never enters chat), mirroring setup-notion:
case "$(uname -s)" in
Darwin)
cat <<EOF
1. Copy the Linear API key (starts with 'lin_api_').
2. Run this single line (leading space keeps it out of zsh history):
security delete-generic-password -s lisa-linear -a "$WORKSPACE" 2>/dev/null; TOK="\$(pbpaste)"; security add-generic-password -U -s lisa-linear -a "$WORKSPACE" -w "\$TOK"; unset TOK
EOF
;;
Linux)
cat <<EOF
secret-tool clear service lisa-linear account "$WORKSPACE" 2>/dev/null; printf '%s' "\$(wl-paste 2>/dev/null || xclip -selection clipboard -o 2>/dev/null || xsel --clipboard --output 2>/dev/null)" | secret-tool store --label="Lisa Linear ($WORKSPACE)" service lisa-linear account "$WORKSPACE"
(no clipboard tool? the command reads stdin — paste, then Ctrl-D. Or env-var fallback: export LINEAR_API_KEY_$(echo "$WORKSPACE" | tr '[:upper:]-' '[:lower:]_')="<key>")
EOF
;;
MINGW*|MSYS*|CYGWIN*)
cat <<EOF
PowerShell: \$tok = Get-Clipboard; cmdkey /generic:"lisa-linear-$WORKSPACE" /user:"$WORKSPACE" /pass:"\$tok"; Remove-Variable tok
EOF
;;
esac
Never accept the key via this skill's chat or stdin. After the user confirms storage, retrieve via the lookup ladder (env → workspace-suffixed env → keychain) and validate against the GraphQL API:
read_linear_key() {
local ws="$1"
[ -n "$LINEAR_API_KEY" ] && { echo "$LINEAR_API_KEY"; return; }
local slug; slug=$(echo "$ws" | tr '[:upper:]-' '[:lower:]_')
local varname="LINEAR_API_KEY_${slug}"
[ -n "${!varname}" ] && { echo "${!varname}"; return; }
candidates=()
[ -n ];
candidates+=()
[ -n ];
candidates+=()
candidates+=(node_modules/@codyswann/lisa/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs)
resolver
tried=()
resolver ;
tried+=()
[ -f ];
via_lisa
via_lisa=$(node get LINEAR_API_KEY 2>/dev/null) \
&& [ -n ] && { ; ; }
from_keychain=
Darwin) from_keychain=$(security find-generic-password -s lisa-linear -a -w 2>/dev/null) ;;
Linux) -v secret-tool >/dev/null && from_keychain=$(secret-tool lookup service lisa-linear account 2>/dev/null) ;;
MINGW*|MSYS*|CYGWIN*)
from_keychain=$(LISA_CRED_TARGET= powershell.exe -NoProfile -NonInteractive -Command 2>/dev/null | -d ) ;;
[ -n ] && { ; ; }
>&2
>&2
>&2
>&2
1
}
KEY=$(read_linear_key )
[ -z ] && { >&2; 1; }
VIEWER=$(curl -s -X POST https://api.linear.app/graphql \
-H -H \
-d )
! | jq -e >/dev/null 2>&1;
>&2
1
Step 2 — Resolve workspace slug + team key
- Workspace slug: honor
--workspace=<slug>. Otherwise derive from the validated identity — the GraphQL organization.urlKey (API path) or the team list's workspace (MCP path). Confirm with the user; this slug is the keychain account key and the multi-workspace disambiguator.
- Team key (required when Linear is the tracker): honor
--team=<KEY>. Otherwise enumerate teams via lisa-linear-access operation: list-teams({}) (or the GraphQL teams query) and present them via AskUserQuestion (label = team key, description = team name) for the user to pick the team that owns lisa's destination Issues. If Linear is source-only, teamKey is optional — skip unless the user wants to pin a team scope.
Step 3 — Scaffold the lifecycle namespaces
Read role → label with the default-fallback ladder the intake skills use, so scaffolded labels match exactly what they query.
read_role() {
local ns="$1" role="$2" default="$3" local_v global_v
local_v=$(jq -r ".linear.labels.${ns}.${role} // empty" .lisa.config.local.json 2>/dev/null)
global_v=$(jq -r ".linear.labels.${ns}.${role} // empty" .lisa.config.json 2>/dev/null)
echo "${local_v:-${global_v:-$default}}"
}
3a. Build-queue lifecycle — WORKFLOW STATES (only if Linear is the tracker)
The build lane resolves to native workflow states, not labels — see "Why Linear uses states, not labels" in config-resolution. Read role → state name with the same ladder, against linear.workflow:
read_state() {
local role="$1" default="$2" local_v global_v
local_v=$(jq -r ".linear.workflow.${role} // empty" .lisa.config.local.json 2>/dev/null)
global_v=$(jq -r ".linear.workflow.${role} // empty" .lisa.config.json 2>/dev/null)
echo "${local_v:-${global_v:-$default}}"
}
Enumerate the team's states with lisa-linear-access operation: list-workflow-states (each carries id, name, type, position). For each role, resolve in this order — the same cascade lisa-setup-jira uses, with one extra rung Linear affords that JIRA does not:
- Exact name match → resolved, nothing to do. For
ready, additionally refuse to resolve onto the team's DEFAULT state (Todo on a stock team): that inverts the gate from "a human flipped this" to "nobody has touched this" and makes every untouched backlog item claimable. Offer to create a dedicated state instead.
- A plausible existing state of the right
type (ready → unstarted, claimed/review → started, blocked → started or unstarted, terminal done → completed) → present the team's state list via AskUserQuestion and let the user pick which state means this role. Record the choice as a config override in Step 4.
- Nothing plausible → offer to create the state via
lisa-linear-access operation: create-workflow-state (name, type, position, colour), showing the exact name and type first. Linear's API permits this where JIRA's workflow editing is admin-gated — which is why this rung exists here and not there.
- User declines creation → stop and say which role is unresolvable and that the lifecycle cannot run without it. Never silently fall back to a state whose meaning differs, and never invent a name in config that does not exist in the team.
| Role | Default state | type | Ships with a stock team? |
|---|
ready | Ready | unstarted | no — must be created or mapped |
claimed | In Progress | started | yes |
review | In Review | started | yes |
blocked | Blocked | unstarted | no — must be created or mapped |
done.dev | On Dev | started | no — must be created or mapped |
done.staging | On Stg | started | no — must be created or mapped |
done.production | Done | completed | yes |
The env rungs are deliberately started, not completed. On Dev and On Stg mean "merged and deployed that far" — work that is emphatically not finished. Typing them completed would make Linear treat them as closed: they would leave the active board, stop counting in cycles, and re-create the exact premature-closure problem this model exists to fix. Only done.production is completed.
Position them between In Review and Done so the board reads left-to-right in real lifecycle order. A team that orders its board differently can pass its own position.
Turn off the team's merge → Done git automation. Linear's per-team git automations (Settings → Team → Workflow, or the gitAutomationStates API) auto-complete an Issue on merge to any branch. With this model that automation is an unwanted second writer: it jumps an Issue straight to Done at a dev merge, skipping On Dev / On Stg and asserting production-done. Lisa itself moves the state at each rung, so the automation is redundant as well as wrong. Detect it and offer to delete it; leave start and review alone — those assert non-terminal states and are harmless.
3b. PRD-lifecycle labels — PROJECT labels (only if Linear is the PRD source)
Probe with lisa-linear-access operation: list-project-labels. Create missing ones via lisa-linear-access operation: create-project-label. This probe-then-create is find-or-create per label: a label already present is reused untouched, so re-running never duplicates prd-*. These are a separate label kind from issue labels — creating an issue label of the same name will NOT work for the PRD flow.
prd-verified is the terminal lifecycle state after prd-shipped (the verified role from config-resolution, #591): /lisa:verify-prd transitions a Linear PRD project into it once the shipped product has been empirically verified against the PRD. Scaffold it through the same find-or-create path as every other prd-* row.
| Role | Default | Kind |
|---|
draft | prd-draft | project label |
ready | prd-ready | project label |
in_review | prd-in-review | project label |
blocked | prd-blocked | project label |
ticketed | prd-ticketed | project label |
shipped | prd-shipped | project label |
verified | prd-verified | project label |
sentinel | prd-intake-feedback | issue label (marks the sentinel feedback issue — create via create_issue_label) |
3c. Handle name collisions / renames
If the team already uses a differently-named label for a role, do not create a duplicate — present the existing labels via AskUserQuestion, map the role to the existing label, and record the mapping as a config override (Step 4).
Step 4 — Write .lisa.config.json
linear.workspace (and linear.teamKey when tracker) are project-wide → committed. Write only label keys that differ from defaults.
touch .lisa.config.json
[ -s .lisa.config.json ] || echo '{}' > .lisa.config.json
jq --arg ws "$WORKSPACE" \
'.linear = ((.linear // {}) | .workspace = $ws)' \
.lisa.config.json > .lisa.config.json.tmp && mv .lisa.config.json.tmp .lisa.config.json
if [ -n "$TEAM_KEY" ]; then
jq --arg tk "$TEAM_KEY" '.linear.teamKey = $tk' \
.lisa.config.json > .lisa.config.json.tmp && mv .lisa.config.json.tmp .lisa.config.json
fi
if [ -n "$LABEL_OVERRIDES_JSON" ] && [ "$LABEL_OVERRIDES_JSON" != "{}" ]; then
jq --argjson o "$LABEL_OVERRIDES_JSON" \
'.linear.labels = ((.linear.labels // {}) * $o)' \
.lisa.config.json > .lisa.config.json.tmp && mv .lisa.config.json.tmp .lisa.config.json
fi
if [ -n "$WORKFLOW_OVERRIDES_JSON" ] && [ "$WORKFLOW_OVERRIDES_JSON" != "{}" ]; then
jq --argjson w "$WORKFLOW_OVERRIDES_JSON" \
\
.lisa.config.json > .lisa.config.json.tmp && .lisa.config.json.tmp .lisa.config.json
Migrating a project that predates the state model. A config carrying
linear.labels.build.{ready,claimed,review,blocked,done} was written against the
old label-driven lane. Those keys are inert now — nothing reads them — but
leaving them in place reads as configuration and will mislead the next person.
Migrate in one pass, and do it before the first intake cycle runs, or that cycle
sees an empty queue:
- Resolve each build role to a state per 3a, writing
linear.workflow.
- Backfill live Issues: for every Issue carrying a
status:* label, set its
workflow state to the role that label encoded. Do this before deleting
anything — the labels are the only record of where each Issue sits.
- Drop
build.{ready,claimed,review,blocked,done} from linear.labels, keeping
build.human_needed and the whole prd map.
- Leave the
status:* labels themselves in the workspace, unapplied, until the
first intake cycle after the migration has run green. They are the rollback.
No secrets in config — the API key stays in keychain / LINEAR_API_KEY, the MCP session in its own store.
Step 5 — Offer to set top-level tracker / source
For each role selected in Step 0, offer the matching top-level flag (skip if already pointing at Linear).
If tracker selected and .tracker ≠ "linear": ask "Set top-level tracker: \"linear\" so vendor-neutral skills write Issues here?" → jq '.tracker = "linear"'.
If source selected and .source ≠ "linear": ask "Set top-level source: \"linear\" so /lisa:intake (no args) scans this workspace for prd-ready projects?" → jq '.source = "linear"'.
Both are project-wide — never set without explicit confirmation.
Step 6 — Verify
jq -e '.linear.workspace' .lisa.config.json >/dev/null
[ "$(jq -r '.tracker // empty' .lisa.config.json)" = "linear" ] && jq -e '.linear.teamKey' .lisa.config.json >/dev/null
Confirm what was scaffolded is present: list-workflow-states for every build role when Linear is the tracker, list_project_labels for prd-* (including the terminal prd-verified) and list_issue_labels for the sentinel when Linear is the PRD source. Do NOT expect a status:* namespace — it is not part of this model. Report success with the resolved workspace, team key (if any), which namespaces were scaffolded (created vs. already existed), any non-default overrides, and whether tracker / source were set. Direct the user to /lisa:intake to test.
Idempotency
- Re-running merges the
linear section's fields rather than appending — jq merge throughout.
- Label creation is find-or-create per kind; existing labels are left untouched, so re-runs never churn human-customized labels.
- Re-running does not re-prompt for
tracker / source if they already point at Linear. The keychain store in Step 1 is the user's manual action — they re-run the same security / secret-tool / cmdkey command.
Rules
- Never write the API key to
.lisa.config.json. It stays in keychain or LINEAR_API_KEY.
- Never accept the API key via this skill's stdin/chat — always the platform clipboard-pipe pattern, so the value never enters the LLM context.
- Never conflate the two label kinds: build labels are issue labels, PRD labels are project labels. The sentinel is an issue label. Creating the wrong kind silently breaks the corresponding intake flow.
- Never create a duplicate label for a role that already has a (differently-named) label — map and record an override instead.
- Never set
tracker / source without explicit confirmation — they're project-wide switches.
- Never invent a workspace slug or team key. Derive from the validated identity / team list and confirm; if resolution fails, ask the user.