feat(linear): first-class Linear integration (PR 1/4)#185
Merged
Conversation
added 2 commits
June 2, 2026 15:23
Adds Linear as a CLI provider following the established Sentry/Cloudflare recipe — task/project/cycle management end-to-end so agents can read, triage, and mutate Linear without leaving Clanker. internal/linear/ — GraphQL client targeting https://api.linear.app/graphql. Auth header is the raw Personal API Key with NO Bearer prefix (Linear's #1 footgun, tested explicitly). Honours X-RateLimit-Requests-Remaining; backoff with jitter on 429s. Cursor pagination matches Linear's Relay shape (first/after/pageInfo). Resource coverage: - Workspaces, Teams (with inlined WorkflowStates for the kanban path) - Issues — list with rich filters (state.type, team, project, cycle, label, assignee, priority), get by UUID or identifier, create with full input, update with partial pointer-patch, comment - Projects — list/get/create/update (state, lead, dates, team scoping) - Cycles — list with active/future filters, get, create, update - Labels — list (used for the infra:<type>:<id> annotation lookup planned for PR2), find-by-name, create - Users — list + find-by-display-name for the assign CLI - Documents — read-only list/get CLI surface: - `clanker linear list {issues|projects|teams|cycles|labels|users|docs}` with all filter flags - `clanker linear get {issue|project|cycle|doc|team}` with id-or-identifier - `clanker linear resolve <id>...` (moves to first 'completed' state on the issue's team) - `clanker linear assign`, `comment`, `create issue|project|cycle`, `update issue`, `label create` - `clanker linear ask "..."` — LLM-powered triage with parallel context gathering via errgroup (issues + projects + cycles + teams + labels fetched concurrently); per-workspace history at ~/.clanker/linear-{workspaceID}.json with safeSlug path-traversal guard MCP tools (registered alongside Sentry/Tencent in mcp.go): - Read (ReadOnlyHintAnnotation=true): _ask, _list_issues, _get_issue, _list_projects, _list_cycles, _list_teams, _search_by_label - Write (no hint — destructive): _create_issue, _update_issue, _comment_issue, _create_project, _update_project Tests: client (happy path, no-Bearer auth header, 429 retry-after, GraphQL errors envelope, IssueFilter→GraphQL mapping, partial-patch serialisation), conversation history round-trip + path traversal. Docs: .clanker.example.yaml section explains the no-Bearer-prefix gotcha and scope inheritance. PR1 of 4 toward full Linear+Notion across CLI and desktop app — desktop PR (provider window + kanban + infra-annotations) follows.
…filter
Pre-merge review found four real bugs.
Backend (internal/linear):
- issues.go: AssigneeMe was declared but never read in toGraphQL —
callers setting it got silent no-filtering. Renamed to AssigneeIsMe
and wired to `assignee.isMe.eq: true`. AssigneeID takes precedence
when both are set.
- issues.go: ResolveIssueID resolves human identifiers (ENG-42) to the
UUIDs Linear's mutations actually require. The mutation endpoints
silently 4xx on identifiers — agents will pass them, so resolve at
the boundary.
- client.go: parseAPIError caps the raw body at 512 bytes so a WAF/CDN
HTML response (10KB+ of marketing) doesn't bloat every log line that
prints err.Error().
- issues.go: queryIssueComments doc says "newest first" but the query
orders ASC. Fixed the docstring to match the query.
CLI (cmd/linear.go + internal/linear/static_commands.go):
- linear.go: dropped the local --api-key / --workspace / --team flags
that were shadowing the parent's persistent flags. `clanker linear
--api-key X ask "q"` was silently ignoring the explicit flag because
Cobra resolves the local declaration first.
- linear.go: gatherLinearContext switched from substring match
("my" matched "myql"/"company") to a word-boundary regex helper.
- static_commands.go: assign / comment / update issue / resolve all
pipe user input through ResolveIssueID first. The resolve command
had an additional bug — used the original `id` argument in the
UpdateIssue call instead of issue.ID after a successful GetIssue,
so `resolve ENG-42` would GET-OK then PUT-fail.
- static_commands.go: updateIssuesToStateType caches the team→state
lookup so a batch like `resolve ENG-1 ENG-2 ENG-3` makes one
GetTeam call, not three.
- static_commands.go: buildLabelCommand uses a named createCmd
variable instead of the fragile lc.Commands()[0] indexing.
MCP (cmd/mcp_linear.go):
- update_issue and comment_issue handlers resolve the issueId before
calling Linear so agents passing "ENG-42" work end-to-end.
- All 5 mutation tools now carry mcp.WithDestructiveHintAnnotation(true)
so MCP clients prompt for confirmation. Sentry's resolve/ignore is
the same level of impact; Linear's writes create user-visible
changes (assignment notifications, comments) and deserve the gate.
Docs (.clanker.example.yaml):
- Document the infra:<type>:<id> annotation label convention that the
desktop PR will use to bridge cloud resources to Linear issues.
- Note the conversation-history file path so operators can clear it.
Tests:
- TestFilterToGraphQL_AssigneeIsMe covers the new clause + the
AssigneeID precedence rule.
- TestResolveIssueID exercises UUID passthrough vs ENG-42 lookup.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First-class Linear CLI provider following the Sentry/Cloudflare recipe. Task/project/cycle management end-to-end so agents can read, triage, and mutate Linear without leaving Clanker. PR 1 of 4 in the Linear + Notion roadmap.
What lands
Tests
Happy-path GraphQL + the no-Bearer-prefix invariant + 429 retry-after + the GraphQL `errors` envelope + IssueFilter→GraphQL input mapping + partial-patch pointer serialisation + history round-trip + safeSlug path traversal.
Test plan
Out of scope (later PRs)