Linear Setup
The Linear board is the control plane the skills read from, and they match on exact names: /spec, /start, and
/auto write the states Planned and Ready for Release; next-candidates.sh gates on specified, hides
needs decision / human / solo / keeper, and boosts security / bug; the saved views are how a stakeholder sees
the board without the fleet's own technical issues. A team missing any of it fails quietly. This skill makes a team match
the model in one converging pass instead of a hand-edited checklist.
The model is assets/model.json, exported from basefund/BF (team name Product). It is
portable: the source team's id is stored as ${TEAM_ID} and its name as ${TEAM_NAME}, both substituted for the
target at check/apply time — so BF's Product: Simple becomes Ops: Simple on a team named Ops. The model covers:
| Kind |
What is synced |
Not synced (report only, or out of scope) |
| Statuses |
every workflow state (name, type, color, position, description), the team's triage toggle, its default issue state |
extra states the target has (never deleted — rename or archive by hand); a state whose type differs (immutable — archive and re-run) |
| Labels |
every workspace-level issue label (name, color, description, groups + parents); required: true marks the load-bearing ones |
team-scoped labels (BF has none; a same-named one on the target is reported as a conflict) |
| Views |
every shared Issue view about the team — owned by it, or workspace-level with a filter naming it — whose filter references only the team and label names; each with its shared display preferences (layout, grouping, ordering, shown fields) and whether the exporting user has it favorited. Applied as workspace-level shared views (a team-scoped match is moved there) and favorited for the authenticated user |
views filtering on assignees, projects, or other teams (workspace-specific ids — skipped at export); personal/unshared views; workspace views whose filter does not name the team; Project views; per-user display preferences; id-bearing preference keys (hidden columns, column order, label-group grouping); favorite ordering |
| Team |
triageEnabled, defaultIssueState |
cycles, estimates, templates, members, auto-archive |
All logic lives in scripts/linear-setup.sh (the diff itself in scripts/plan.jq); this
skill dispatches to it and narrates. Every mutation is raw GraphQL through linear-cli api mutate — the dedicated
views create / labels create commands cannot carry filterData, icons, or descriptions, and there is no
statuses create at all.
Arguments
/linear-setup [check | apply | rename FROM TO | export] [TEAM] [--profile P]
- (none) or
check — read-only diff of the team against the model. Default.
apply — create/update the gaps, then re-check. Converges: a second run plans zero mutations.
rename FROM TO — rename one workflow state. Made for Linear's default Todo → Planned.
export — refresh the model from a live team. Only meaningful against the model team (see Refreshing the model).
TEAM defaults to LINEAR_TEAM; --profile selects a linear-cli workspace profile and defaults to linear-cli's own
selection (LINEAR_CLI_PROFILE, then the config's current).
Workflow
Resolve the target. Team key from the argument or LINEAR_TEAM; profile from the project's
.claude/settings.local.json (env.LINEAR_CLI_PROFILE) when the project is not in the default workspace. Confirm
the key reaches the right workspace before anything else:
linear-cli --profile <P> whoami -o json | jq -c '{name, email, admin, url}'
Account disabled (HTTP 400, FORBIDDEN) means the stored key belongs to a suspended account, not a CLI fault —
the user mints a new personal API key while logged into that workspace, then
linear-cli --profile <P> auth login --key <key>. Stop here until it answers; nothing below can run.
Check. Run it and show the table verbatim — every row is KIND op [*] name — detail, * marking a load-bearing
item:
~/.claude/skills/linear-setup/scripts/linear-setup.sh check --team <KEY> --profile <P>
Exit 0 is converged; 1 means gaps; 2 is an error. Read the header line — it prints the workspace the API actually
answered for and the user whose favorites are being checked (favorites for <email>; favorites are per user, so a
teammate running the same check sees their own FAVORITE gaps). On a triage-off team expect both TEAM update triageEnabled and STATE create Triage: apply flips
the toggle first and Linear mints the state, so the create resolves to an update on the re-plan.
Resolve what apply will not do for you, before applying:
STATE extra Todo on a fresh team → rename --team <KEY> Todo Planned first. Applying first creates Planned
beside Todo, and then Todo has to be archived by hand in Linear with any issues moved.
conflict rows — a state whose type differs, a team-scoped label with a model name — are fixed in the Linear UI;
the detail column says how. Apply proceeds around them and exits 1 until they are gone.
extra rows are informational: nothing is ever deleted. Duplicate-named views are flagged as extras with their owner.
Confirm the header's workspace/team with the user, then apply (--dry-run shows the plan and runs nothing).
Applying BF's model to the wrong workspace is reversible but noisy — labels and views appear for everyone there.
~/.claude/skills/linear-setup/scripts/linear-setup.sh apply --team <KEY> --profile <P>
It re-snapshots between phases (triage → states → default state → label groups → labels + views → favorites), prints each
mutation, clears linear-cli's statuses cache (linear skill gotcha #23: a state minted seconds ago is otherwise
invisible to mark-ready-for-release.sh and friends), and ends with the post-apply table. Exit 0 = converged.
Wire the project. In the project repo, .claude/settings.json sets env.LINEAR_TEAM to the key (committed —
every team-resolving skill reads it). If the workspace is not the machine's default profile, .claude/settings.local.json
sets env.LINEAR_CLI_PROFILE to the profile name (gitignored, machine-local; an unknown profile fails closed rather
than falling back to the default workspace). Add a one-line note to the project's CLAUDE.md naming the workspace
and the profile variable, as bfp-control-panel does.
Report: the final table, what was created, what remains manual (extras, conflicts), and the wiring written.
Refreshing the model
BF is the model. When its statuses, labels, or views change deliberately, re-export from BF and review the diff —
the check against BF itself doubles as a drift detector (the header says this team IS the model's source):
~/.claude/skills/linear-setup/scripts/linear-setup.sh export --team BF --profile basefund
git -C ~/.claude diff -- skills/linear-setup/assets/model.json
Export rules: shared Issue views about the team only — owned by it, or workspace-level with a filter naming it (the
base ${TEAM_NAME} board is the latter) — each with its shared display preferences and a favorite flag saying whether
the exporting user had it favorited; a view whose filter still contains any UUID after the team-id
substitution is skipped and listed under skipped.views with its reason; two views sharing a name keep the oldest;
labels are the workspace-level set with required stamped from the roster in the script (REQUIRED_LABELS — the
names linear-for-stakeholders.md says never to rename, plus epic). Team-name substitution is a literal replace of
the source team's name in view names and descriptions (skipped for names under three characters), so a team whose name
is an ordinary word will see that word templated wherever it appears in a view description. Commit the refreshed model
through the keeper flow like any other ~/.claude change; other machines pick it up on /update.
Gotchas
- Order matters once: rename before apply. Everything else converges in any order.
- Triage may need a paid plan. If
teamUpdate {triageEnabled} is refused, the Triage state cannot exist either;
apply reports the error and stops — tell the user, do not retry.
- Every model view is a workspace-level view. A team-scoped view carrying a model name is
scope drift, and apply
moves it — customViewUpdate with teamId: null, measured 2026-09-04 on bfpnext. That puts the base ${TEAM_NAME}
board and the label views in everyone's workspace Views, not under one team.
- A preferences write replaces the whole object. Measured 2026-09-04 on bfpnext:
viewPreferencesUpdate carrying
only layout nulled showTriageIssues. The script always sends the full model object; the check compares only the
keys the model names, so a key Linear adds on its own is not drift.
- Favorites are per user. Apply favorites for whoever the profile's API key belongs to — the header names them.
Nothing un-favorites, and sidebar order is not managed.
- View match is by exact substituted name. Renaming a view in Linear makes the model re-create it under the model
name and flag the renamed one as
extra; rename it back or delete the extra.
filterData is stored verbatim (measured: byte-identical round-trip on basefund), so a filterData drift row is a
real difference, not API normalization.
- This is provisioning, not migration. It never moves issues, deletes anything, or touches members, cycles, or
templates. A team that already has issues in
Todo needs the rename, not a new Planned.
- Tests:
scripts/linear-setup.test.sh runs the script against a stateful linear-cli shim and is wired into
~/.claude's pnpm test; run it after editing the script or plan.jq.
1---2name: linear-setup3description: Bring a Linear team's issue statuses, issue labels, and saved views to the house model — the exact set the basefund `BF` team runs on, exported into this skill. Checks a team for gaps, applies the missing or drifted pieces idempotently through the Linear API (never deletes) — views land as workspace-level shared views with their board layout and other display preferences, favorited for the running user — renames Linear's default `Todo` to `Planned`, wires the project's `LINEAR_TEAM`/`LINEAR_CLI_PROFILE`, and re-exports the model when BF changes. Use when the user says 'linear setup', 'set up Linear for this project/workspace/team', 'replicate BF's statuses/labels/views', 'check the Linear config', 'new Linear workspace', 'bootstrap a Linear team', or invokes /linear-setup.4---56# Linear Setup78The Linear board is the control plane the skills read from, and they match on **exact names**: `/spec`, `/start`, and9`/auto` write the states `Planned` and `Ready for Release`; `next-candidates.sh` gates on `specified`, hides10`needs decision` / `human` / `solo` / `keeper`, and boosts `security` / `bug`; the saved views are how a stakeholder sees11the board without the fleet's own technical issues. A team missing any of it fails quietly. This skill makes a team match12the model in one converging pass instead of a hand-edited checklist.1314**The model** is [assets/model.json](assets/model.json), exported from `basefund/BF` (team name `Product`). It is15portable: the source team's id is stored as `${TEAM_ID}` and its name as `${TEAM_NAME}`, both substituted for the16target at check/apply time — so BF's `Product: Simple` becomes `Ops: Simple` on a team named `Ops`. The model covers:1718| Kind | What is synced | Not synced (report only, or out of scope) |19| --- | --- | --- |20| Statuses | every workflow state (name, type, color, position, description), the team's triage toggle, its default issue state | extra states the target has (never deleted — `rename` or archive by hand); a state whose **type** differs (immutable — archive and re-run) |21| Labels | every workspace-level issue label (name, color, description, groups + parents); `required: true` marks the load-bearing ones | team-scoped labels (BF has none; a same-named one on the target is reported as a conflict) |22| Views | every **shared** Issue view about the team — owned by it, or workspace-level with a filter naming it — whose filter references only the team and label names; each with its shared display preferences (layout, grouping, ordering, shown fields) and whether the exporting user has it favorited. Applied as **workspace-level** shared views (a team-scoped match is moved there) and favorited for the authenticated user | views filtering on assignees, projects, or other teams (workspace-specific ids — skipped at export); personal/unshared views; workspace views whose filter does not name the team; Project views; per-user display preferences; id-bearing preference keys (hidden columns, column order, label-group grouping); favorite ordering |23| Team | `triageEnabled`, `defaultIssueState` | cycles, estimates, templates, members, auto-archive |2425All logic lives in [scripts/linear-setup.sh](scripts/linear-setup.sh) (the diff itself in [scripts/plan.jq](scripts/plan.jq)); this26skill dispatches to it and narrates. Every mutation is raw GraphQL through `linear-cli api mutate` — the dedicated27`views create` / `labels create` commands cannot carry `filterData`, icons, or descriptions, and there is no28`statuses create` at all.2930## Arguments3132`/linear-setup [check | apply | rename FROM TO | export] [TEAM] [--profile P]`3334- **(none)** or `check` — read-only diff of the team against the model. Default.35- `apply` — create/update the gaps, then re-check. Converges: a second run plans zero mutations.36- `rename FROM TO` — rename one workflow state. Made for Linear's default `Todo` → `Planned`.37- `export` — refresh the model from a live team. Only meaningful against the model team (see *Refreshing the model*).3839`TEAM` defaults to `LINEAR_TEAM`; `--profile` selects a `linear-cli` workspace profile and defaults to linear-cli's own40selection (`LINEAR_CLI_PROFILE`, then the config's `current`).4142## Workflow43441. **Resolve the target.** Team key from the argument or `LINEAR_TEAM`; profile from the project's45 `.claude/settings.local.json` (`env.LINEAR_CLI_PROFILE`) when the project is not in the default workspace. Confirm46 the key reaches the right workspace before anything else:4748 ```bash49 linear-cli --profile <P> whoami -o json | jq -c '{name, email, admin, url}'50 ```5152 `Account disabled` (HTTP 400, `FORBIDDEN`) means the stored key belongs to a suspended account, not a CLI fault —53 the user mints a new personal API key while logged into **that** workspace, then54 `linear-cli --profile <P> auth login --key <key>`. Stop here until it answers; nothing below can run.55562. **Check.** Run it and show the table verbatim — every row is `KIND op [*] name — detail`, `*` marking a load-bearing57 item:5859 ```bash60 ~/.claude/skills/linear-setup/scripts/linear-setup.sh check --team <KEY> --profile <P>61 ```6263 Exit 0 is converged; 1 means gaps; 2 is an error. Read the **header line** — it prints the workspace the API actually64 answered for and the user whose favorites are being checked (`favorites for <email>`; favorites are per user, so a65 teammate running the same check sees their own `FAVORITE` gaps). On a triage-off team expect both `TEAM update triageEnabled` and `STATE create Triage`: apply flips66 the toggle first and Linear mints the state, so the create resolves to an update on the re-plan.67683. **Resolve what apply will not do for you**, before applying:69 - `STATE extra Todo` on a fresh team → `rename --team <KEY> Todo Planned` first. Applying first creates `Planned`70 beside `Todo`, and then `Todo` has to be archived by hand in Linear with any issues moved.71 - `conflict` rows — a state whose type differs, a team-scoped label with a model name — are fixed in the Linear UI;72 the detail column says how. Apply proceeds around them and exits 1 until they are gone.73 - `extra` rows are informational: nothing is ever deleted. Duplicate-named views are flagged as extras with their owner.74754. **Confirm the header's workspace/team with the user, then apply** (`--dry-run` shows the plan and runs nothing).76 Applying BF's model to the wrong workspace is reversible but noisy — labels and views appear for everyone there.7778 ```bash79 ~/.claude/skills/linear-setup/scripts/linear-setup.sh apply --team <KEY> --profile <P>80 ```8182 It re-snapshots between phases (triage → states → default state → label groups → labels + views → favorites), prints each83 mutation, clears linear-cli's statuses cache (linear skill gotcha #23: a state minted seconds ago is otherwise84 invisible to `mark-ready-for-release.sh` and friends), and ends with the post-apply table. Exit 0 = converged.85865. **Wire the project.** In the project repo, `.claude/settings.json` sets `env.LINEAR_TEAM` to the key (committed —87 every team-resolving skill reads it). If the workspace is not the machine's default profile, `.claude/settings.local.json`88 sets `env.LINEAR_CLI_PROFILE` to the profile name (gitignored, machine-local; an unknown profile fails closed rather89 than falling back to the default workspace). Add a one-line note to the project's `CLAUDE.md` naming the workspace90 and the profile variable, as `bfp-control-panel` does.91926. **Report**: the final table, what was created, what remains manual (extras, conflicts), and the wiring written.9394## Refreshing the model9596BF is the model. When its statuses, labels, or views change deliberately, re-export **from BF** and review the diff —97the check against BF itself doubles as a drift detector (the header says `this team IS the model's source`):9899```bash100~/.claude/skills/linear-setup/scripts/linear-setup.sh export --team BF --profile basefund101git -C ~/.claude diff -- skills/linear-setup/assets/model.json102```103104Export rules: shared Issue views about the team only — owned by it, or workspace-level with a filter naming it (the105base `${TEAM_NAME}` board is the latter) — each with its shared display preferences and a `favorite` flag saying whether106the exporting user had it favorited; a view whose filter still contains any UUID after the team-id107substitution is skipped and listed under `skipped.views` with its reason; two views sharing a name keep the oldest;108labels are the workspace-level set with `required` stamped from the roster in the script (`REQUIRED_LABELS` — the109names `linear-for-stakeholders.md` says never to rename, plus `epic`). Team-name substitution is a literal replace of110the source team's name in view names and descriptions (skipped for names under three characters), so a team whose name111is an ordinary word will see that word templated wherever it appears in a view description. Commit the refreshed model112through the keeper flow like any other `~/.claude` change; other machines pick it up on `/update`.113114## Gotchas115116- **Order matters once: rename before apply.** Everything else converges in any order.117- **Triage may need a paid plan.** If `teamUpdate {triageEnabled}` is refused, the `Triage` state cannot exist either;118 apply reports the error and stops — tell the user, do not retry.119- **Every model view is a workspace-level view.** A team-scoped view carrying a model name is `scope` drift, and apply120 moves it — `customViewUpdate` with `teamId: null`, measured 2026-09-04 on bfpnext. That puts the base `${TEAM_NAME}`121 board and the label views in everyone's workspace Views, not under one team.122- **A preferences write replaces the whole object.** Measured 2026-09-04 on bfpnext: `viewPreferencesUpdate` carrying123 only `layout` nulled `showTriageIssues`. The script always sends the full model object; the check compares only the124 keys the model names, so a key Linear adds on its own is not drift.125- **Favorites are per user.** Apply favorites for whoever the profile's API key belongs to — the header names them.126 Nothing un-favorites, and sidebar order is not managed.127- **View match is by exact substituted name.** Renaming a view in Linear makes the model re-create it under the model128 name and flag the renamed one as `extra`; rename it back or delete the extra.129- **`filterData` is stored verbatim** (measured: byte-identical round-trip on basefund), so a `filterData` drift row is a130 real difference, not API normalization.131- **This is provisioning, not migration.** It never moves issues, deletes anything, or touches members, cycles, or132 templates. A team that already has issues in `Todo` needs the `rename`, not a new `Planned`.133- **Tests**: `scripts/linear-setup.test.sh` runs the script against a stateful `linear-cli` shim and is wired into134 `~/.claude`'s `pnpm test`; run it after editing the script or `plan.jq`.