| name | lisa-setup-linear |
| description | Configure Linear as the destination tracker and/or the PRD source for this project. Verifies Linear access (MCP OAuth or a personal API key in OS keychain), resolves the workspace slug and team key, scaffolds the build-queue issue-label namespace (`status:*`) when Linear is the tracker and/or the PRD-lifecycle project-label namespace (`prd-*` + issue-level sentinel) when Linear is the PRD source, writes the `linear` section into `.lisa.config.json`, and offers to set top-level `tracker: "linear"` and/or `source: "linear"`. Idempotent — re-running updates the existing section and reuses existing labels. No /lisa:setup:atlassian prerequisite. |
| 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 label namespaces lisa needs, and (optionally) tracker / source point at Linear.
Linear's data model splits labels into two kinds that matter here: issue labels (drive the build queue, status:*) and project labels (drive the PRD lifecycle, prd-*). They are distinct namespaces in Linear and are NOT interchangeable — the build lifecycle lives on Issues, the PRD lifecycle lives on Projects. The sentinel feedback marker is an issue label even though it belongs to the PRD flow (Linear's MCP has no project-level comments — see linear-prd-intake).
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 the
status:* issue-label namespace. 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 (which label namespaces to scaffold) 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; }
case "$(uname -s)" in
Darwin) security find-generic-password -s lisa-linear -a "$ws" -w 2>/dev/null ;;
Linux) command -v secret-tool >/dev/null && secret-tool lookup service lisa-linear account "$ws" 2>/dev/null ;;
MINGW*|MSYS*|CYGWIN*)
LISA_CRED_TARGET="lisa-linear-${ws}" powershell.exe -NoProfile -NonInteractive -Command '
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public static class LisaCred {
[StructLayout(LayoutKind.Sequential)]
private struct CREDENTIAL {
public int Flags; public int Type; public IntPtr TargetName; public IntPtr Comment;
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
public int CredentialBlobSize; public IntPtr CredentialBlob; public int Persist;
public int AttributeCount; public IntPtr Attributes; public IntPtr TargetAlias; public IntPtr UserName;
}
[DllImport("advapi32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
private static extern bool CredRead(string target, int type, int flags, out IntPtr credential);
[DllImport("advapi32.dll")] private static extern void CredFree(IntPtr cred);
public static string Read(string target) {
IntPtr p;
if (!CredRead(target, 1, 0, out p)) { return null; }
try {
CREDENTIAL c = (CREDENTIAL)Marshal.PtrToStructure(p, typeof(CREDENTIAL));
if (c.CredentialBlobSize == 0) { return String.Empty; }
return Marshal.PtrToStringUni(c.CredentialBlob, c.CredentialBlobSize / 2);
} finally { CredFree(p); }
}
}
"@
[LisaCred]::Read($env:LISA_CRED_TARGET)' 2>/dev/null | -d ;;
}
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 label 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 labels — ISSUE labels (only if Linear is the tracker)
Probe with lisa-linear-access operation: list-issue-labels (scoped to the team). For each role's resolved name, create it via lisa-linear-access operation: create-issue-label only if absent. The done role is env-keyed — create all three defaults; collapse to a single string in config later if the project's terminal state is env-independent.
| Role | Default |
|---|
ready | status:ready |
claimed | status:in-progress |
review | status:code-review |
blocked | status:blocked |
done.dev | status:on-dev |
done.staging | status:on-stg |
done.production | status:done |
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
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 the scaffolded labels are present (list_issue_labels for status:* + the sentinel; list_project_labels for prd-*, including the terminal prd-verified). 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.