feat(linear): import Linear issues into threads + deep two-way integration (#3703)#3711
feat(linear): import Linear issues into threads + deep two-way integration (#3703)#3711maslinedwin wants to merge 14 commits into
Conversation
Adds a native Linear integration for the web app (issue pingdotgg#3703): - contracts: Linear issue/auth schemas + 5 WS RPCs - server: LinearApi Effect service (GraphQL over HttpClient), PAT stored via ServerSecretStore with T3CODE_LINEAR_API_TOKEN env fallback - client-runtime/web: Linear atom family + state wiring - Settings -> Linear page to connect/disconnect a personal API key - per-folder Linear icon in the sidebar opening a browse/search/select popover with combine/subtasks toggle - import flow: fetch issues -> format markdown -> new draft thread -> pre-fill composer (formatLinearIssues, unit tested) - docs/integrations/linear.md
…+ link contract - contracts: LinearIssueFilter, paginated LinearListIssues, team/state/project/ label/user metadata, write-mutation inputs, LinearIssueLink; 9 new RPCs - server LinearApi: listIssues (filter+cursor), listTeams/workflowStates/projects/ labels/users, updateIssueState/createComment/createAttachment; ws handlers+scopes - client-runtime: list/metadata query families + mutation commands - orchestration contract: optional linearIssue link on thread/shell + create/meta commands + turn-start bootstrap (persistence wiring follows)
…t, import modes - /linear full-page browser: team/status/assignee filters, cursor pagination (Load more), select-all + multi-select table - import control: one-thread-per-issue (real started threads, each linked) or combine (draft) + destination-folder picker - entry points: command-palette 'Browse Linear issues' + popover 'Browse all' - useLinearImport gains perIssue mode via startTurn bootstrap
- migration 033 + ProjectionThreads schema/SQL store linked_linear_issue_json - thread linearIssue flows through decider → thread.created/meta-updated → projector → projection pipeline → snapshot query (read model + shell + detail) - turn-start bootstrap.createThread carries linearIssue - LinearIssueBadge on sidebar thread rows, colored by workflow-state type (advances In Progress → In Review → Done as status is written back)
- ServerSettings.linear: autoSync + per-transition toggles + comment/attachment toggles + per-team state-mapping override (+ patch) - linearStateMapping resolver: workflow-state type/name → target stateId, with per-team override (unit tested, 7 cases) - LinearSyncReactor: agent start → In Progress; PR open → In Review; merge → Done (via VcsStatusBroadcaster per-thread watch); dedupes per issue, posts optional comment/attachment, reflects state to the thread badge via thread.meta.update. Registered in OrchestrationReactor + server layer graph - Settings UI: Status write-back toggles on the Linear settings page
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
ApprovabilityVerdict: Needs human review This PR introduces a substantial new feature (Linear integration with ~3900 lines of new code) including external API integration, background sync service, database migration, and new UI components. Additionally, there are unresolved high-severity review comments identifying potential state regression bugs and incorrect failure handling that should be addressed. You can customize Macroscope's approvability policy. Learn more. |
- LinearSyncReactor: inline the service interface in Context.Service per Effect conventions (drop standalone LinearSyncReactorShape) - reactor: key PR watchers by worktree so a later/changed worktree re-registers instead of being ignored (High); set appliedRank after the write + badge reflect so a failed issueUpdate can retry - drop the link-attachment write-back (+ setting/UI): it linked the issue to itself; a real thread URL isn't available server-side without OAuth/hosting - LinearBrowser: replace (not merge) rows when the filter changes so stale pages can't leak across filters; only offer 'Load more' when an endCursor exists - perIssue import: attempt all issues and report a summary instead of bailing mid-loop; surface issues that couldn't be loaded
|
Thanks for the automated review — pushed fixes in Macroscope (Effect Service Conventions): Cursor Bugbot (6 of 8 addressed):
Intentionally left:
Full recursive typecheck + lint pass and unit tests are green after the changes. |
Partial perIssue imports (some threads created, or some issues not loaded) now return ok:true with a `warning`, so consumers navigate and show a non-blocking warning toast instead of a full-failure error. Only a total failure returns ok:false. (addresses Bugbot follow-up)
Server settings updates deepMerge (never delete missing keys), so a nested patch could add but not remove team state-mapping overrides. applyServerSettingsPatch now replaces linear.stateMappingByTeam wholesale (like providerInstances), so an empty map clears stale overrides. (addresses Macroscope review)
…rtial import - probeAuth now only swallows LinearAuthError (rejected token → unauthenticated) and lets LinearRequestError propagate, so a transient outage/misconfig isn't shown as 'Not connected' (Settings surfaces the error). Widened setToken/ clearToken error channels + clearToken RPC union accordingly. - useLinearImport returns failedIds; LinearBrowser/LinearBrowsePopover keep just the failed issues selected (and don't navigate/close) on a partial import so they can be retried without re-running the ones that already succeeded. (addresses Macroscope + Bugbot follow-ups)
… on toggle
- setToken/clearToken no longer fail on a post-write probe outage (probeAuth
stays strict for the status query; the writes swallow LinearRequestError and
report a best-effort status). Reverted their error channels + clearToken RPC.
- LinearBrowsePopover surfaces auth query errors ('Couldn't reach Linear')
before the 'not connected' prompt, and prunes selections that leave the
current result set so Import can't act on invisible issues.
- Linear settings toggles send only the changed key via the server settings
command, so stateMappingByTeam is no longer carried (and wiped) on every
toggle; explicit map edits still replace wholesale.
(addresses Macroscope + Bugbot follow-ups on the prior fixes)
There was a problem hiding this comment.
One convention issue: recovering a single known tagged failure should use Effect.catchTags rather than Effect.catch with a manual _tag check. See inline comments.
Posted via Macroscope — Effect Service Conventions
…ection polish - probeAuth/probeAuthLenient use Effect.catchTags to recover specific error tags (Effect Service Conventions) instead of Effect.catch + manual _tag - probeAuth only treats auth-message GraphQL errors as a rejected token; other 200-with-errors responses propagate as LinearRequestError (outage) - Settings save: a stored-but-unverified token (outage) is a saved success with a warning, not a connect failure - import retry-selection: keep only failed issues still visible in the current results/rows selected, so no un-clearable off-screen selection and the popover prune can't drop them
Only narrow the selection to failed issues when the hook actually reports failedIds; a total transient failure (e.g. fetchIssues failed, nothing imported) now keeps the current selection so it can be retried as-is.
|
One more note: I'm genuinely happy to take this in whatever direction the team wants — reshape the scope, split it into smaller stacked PRs, adjust the UX/architecture, drop pieces, or rework anything to fit how you'd want a Linear integration to look in T3 Code. Just point me at the changes and I'll turn them around. More than that, I'd love to actually collaborate on bringing this Linear integration into the platform properly, not just land a one-off PR. If there's appetite for it, I'm keen to help shape and maintain it alongside you all. cc @juliusmarminge — would love your thoughts on whether this is a direction worth pursuing, and how you'd want it scoped. |
…issue done' - perIssue imports now start threads on Claude Opus 4.8 (claudeAgent / claude-opus-4-8) instead of the project/Codex default. - New linear.completeThreadIssue RPC: resolves the team's completed state, writes it back via issueUpdate, and reflects it on the thread (badge → Done) via thread.meta.update. Surfaced as a 'Mark <ID> done in Linear' item in the thread right-click menu, shown only when the thread has a linked issue.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 08fa28b. Configure here.
| cause, | ||
| }), | ||
| ), | ||
| ); |
There was a problem hiding this comment.
RPC fails after Linear updates
High Severity
The completeLinearThreadIssue RPC reports a failure if the internal thread.meta.update dispatch fails, even when the Linear issue has been successfully updated. This causes the client to incorrectly indicate the action failed and potentially display stale UI, despite the issue being marked as done in Linear.
Reviewed by Cursor Bugbot for commit 08fa28b. Configure here.
| ), | ||
| ); | ||
| } | ||
| return { success: true }; |
There was a problem hiding this comment.
Manual done skips reactor rank
High Severity
Marking a linked issue done via completeLinearThreadIssue updates Linear but never advances LinearSyncReactor's per-issue appliedRank. A later agent start can still run the "started" transition and move a Done issue back to In Progress.
Reviewed by Cursor Bugbot for commit 08fa28b. Configure here.
| description: | ||
| result._tag === "Failure" | ||
| ? "Linear rejected the update — check the connection in Settings." | ||
| : "No completed state is configured for this team.", |
There was a problem hiding this comment.
Complete action mislabels failures
Medium Severity
The toast messages for completing a Linear issue are too generic. The client-side logic incorrectly interprets completeLinearIssue results: it maps various server-side failures (like missing Linear links or API rejections) to "No completed state is configured," and some internal failures to "Linear rejected the update." This gives users misleading feedback.
Reviewed by Cursor Bugbot for commit 08fa28b. Configure here.


Implements #3703 (I filed that issue and offered to help implement it).
Heads-up on size & scope
I've read
CONTRIBUTING.md— I know this is a large feature PR and that you're not actively taking contributions. It's ~3.6k lines because it's a full two-way integration, but it's cleanly split into 5 self-contained commits (Phase 0 → Phase 3) and I'm very happy to break it into stacked PRs (e.g. land just the read-only import first) or shrink scope to whatever you'd actually consider. Just say the word. UI screenshots / a short screen recording can be added on request.What this does & why
Today, turning a Linear ticket into an agent task means copy-pasting context by hand. This adds a native Linear integration so you can browse/search issues, bulk-import them into threads with full context pre-filled, and — as the agent works — write status back to Linear automatically. Research into Conductor's integration showed it's deliberately launch-only (no write-back), so a two-way integration is the differentiator.
Everything runs on a Linear personal API key (stored in the existing encrypted
ServerSecretStore, with aT3CODE_LINEAR_API_TOKENenv fallback). No OAuth, no public webhook URL, no new dependencies.Features
/linear) — a full-page table with team / status / assignee / project / label filters, cursor pagination, select-all + multi-select. Import as one thread per issue (parallel agents, each linked) or combined into one thread; pick the destination folder.LinearSyncReactormoves the issue In Progress on agent start → In Review on PR open → Done on merge (viaissueUpdate, mapped per-team by workflow-state type so it survives renamed states), plus optional progress comment and an idempotent "T3 Code" attachment link. Fully configurable/toggleable in Settings; defaults on.Architecture (follows existing conventions)
LinearApimirrors the Bitbucket source-control provider: an EffectContext.ServiceusingHttpClientfromeffect/unstable/httpfor GraphQL, a tagged-error family, andT3CODE_-prefixed config. Read ops (search/list/fetch/metadata) + write mutations (issueUpdate/commentCreate/attachmentCreate).effect/Schemainpackages/contracts; 14 RPCs registered in the existingWsRpcGroup, handled inws.tswith proper auth scopes (reads → read scope, writes → operate scope).033→ProjectionThreads→ decider → projector → pipeline → snapshot query → read model/shell), following the existingarchived_atcolumn-add pattern.LinearSyncReactoris modeled onThreadDeletionReactor(subscribes tostreamDomainEvents; watches PR state viaVcsStatusBroadcasterper linked thread's worktree), registered inOrchestrationReactorand the server layer graph. It reflects state changes back to the UI via athread.meta.updatedomain event, dedupes per issue, and never regresses a stage.Commits (each self-contained)
feat(linear): import Linear issues into threads— Phase 0: connect + per-folder import + formatter.feat(linear): deep-integration foundation— list/filter/metadata + mutation API +LinearIssueLinkcontract.feat(linear): Phase 1 bulk browser— filters, pagination, multi-select, import modes.feat(linear): Phase 2 — persist thread↔issue link + sidebar badge.feat(linear): Phase 3 — status write-back (LinearSyncReactor + settings).Testing
vp checkandvp run typecheckpass (all 15 packages, 0 errors); no lint errors introduced.formatLinearIssues(single/combine/subtasks/omit-empty) and the workflow-state resolver (type/name/override/fallback) — added, green./linear; imported an issue (one-thread-per-issue) → the thread was created & linked, the agent started, and the issue flipped Todo → In Progress in Linear, with the thread badge reflecting "In Progress" live. TheissueUpdatemutation was also validated directly (state change + revert).Known limitations / follow-ups (called out honestly)
issueUpdatepath itself is validated). The live E2E confirmed the start transition; the PR/merge transitions are unit-tested + typechecked but weren't driven with a real merge.actor=app+ the Linear Agent Interaction SDK (delegate/@mention T3 Code inside Linear) are intentionally deferred; the code is structured to add them without rework.Docs: added
docs/integrations/linear.md. Happy to adjust scope, split this up, or add screenshots/video — whatever makes it reviewable for you.Note
Medium Risk
Stores Linear tokens and performs automatic external issue mutations from a background reactor tied to thread/VCS lifecycle; scope is large but gated by auth scopes, user toggles, and idempotent stage tracking.
Overview
Adds an end-to-end Linear integration: connect via a server-stored personal API key (or env fallback), browse/search issues from the sidebar popover and a new
/linearpage, and import selected issues into threads with rich markdown context (combine vs one-thread-per-issue).Threads can carry a persisted
linearIssuelink (newlinked_linear_issue_jsonprojection column + migration 033), surfaced in the UI with a status badge and optional Mark done in Linear from the thread menu.A new
LinearApiGraphQL client and 14linear.*WebSocket RPCs power auth, listing/filtering, fetch, and mutations (issueUpdate, comments, attachments).LinearSyncReactorlistens to orchestration and VCS PR events to advance linked issues (start → PR open → merge) usingresolveTargetStateIdand server Settings → Linear toggles, then reflects state back viathread.meta.update.Client wiring includes
linearEnvironmentatoms,useLinearImport, settings at/settings/linear, command-palette entry, anddocs/integrations/linear.md.Reviewed by Cursor Bugbot for commit 08fa28b. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add two-way Linear integration with issue import, status sync, and settings UI
LinearSyncReactorthat automatically transitions Linear issue states when threads start, PRs open, or PRs mergeLinearBrowsePopoverand fullLinearBrowserUI at/linearto search, select, and import Linear issues into threads — either one thread per issue or combined into a single draft/settings/linearwhere users can connect a personal API key and configure auto-sync toggles (transition on start, PR open, PR merge, post comments)linked_linear_issue_jsoncolumn (migration 033) and surfaces it as a badge in the sidebar and a context-menu action to mark the issue doneLinearSyncReactorcaches workflow states in memory for the process lifetime; a server restart is required to pick up Linear workflow state changesMacroscope summarized 08fa28b.