Skip to content

feat(linear): import Linear issues into threads + deep two-way integration (#3703)#3711

Open
maslinedwin wants to merge 14 commits into
pingdotgg:mainfrom
maslinedwin:feat/linear-issue-import
Open

feat(linear): import Linear issues into threads + deep two-way integration (#3703)#3711
maslinedwin wants to merge 14 commits into
pingdotgg:mainfrom
maslinedwin:feat/linear-issue-import

Conversation

@maslinedwin

@maslinedwin maslinedwin commented Jul 5, 2026

Copy link
Copy Markdown

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 a T3CODE_LINEAR_API_TOKEN env fallback). No OAuth, no public webhook URL, no new dependencies.

Features

  • Connect Linear — Settings → Linear: paste a personal API key; connection status shows the authenticated account. Token is redacted/never returned to the client.
  • Import from a folder — a Linear icon on each sidebar folder row opens a browse/search popover to import 1+ issues into a new thread (title, description/acceptance criteria, labels, priority, assignee, sub-issues, linked PRs, comments, attachments), with a combine / subtasks toggle.
  • Bulk browser (/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.
  • Thread ↔ issue link + badge — imported threads remember their Linear issue; a status pill on the chat/sidebar row reflects the issue's live workflow state, colored by state type.
  • Status write-backLinearSyncReactor moves the issue In Progress on agent start → In Review on PR open → Done on merge (via issueUpdate, 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)

  • Server LinearApi mirrors the Bitbucket source-control provider: an Effect Context.Service using HttpClient from effect/unstable/http for GraphQL, a tagged-error family, and T3CODE_-prefixed config. Read ops (search/list/fetch/metadata) + write mutations (issueUpdate/commentCreate/attachmentCreate).
  • Contracts are effect/Schema in packages/contracts; 14 RPCs registered in the existing WsRpcGroup, handled in ws.ts with proper auth scopes (reads → read scope, writes → operate scope).
  • The thread↔issue link is threaded through the event-sourcing stack (migration 033ProjectionThreads → decider → projector → pipeline → snapshot query → read model/shell), following the existing archived_at column-add pattern.
  • LinearSyncReactor is modeled on ThreadDeletionReactor (subscribes to streamDomainEvents; watches PR state via VcsStatusBroadcaster per linked thread's worktree), registered in OrchestrationReactor and the server layer graph. It reflects state changes back to the UI via a thread.meta.update domain event, dedupes per issue, and never regresses a stage.
  • Auth is behind a small token-provider seam so OAuth + the Linear Agent SDK ("assign/@mention T3 Code in Linear") can be added later without rework.

Commits (each self-contained)

  1. feat(linear): import Linear issues into threads — Phase 0: connect + per-folder import + formatter.
  2. feat(linear): deep-integration foundation — list/filter/metadata + mutation API + LinearIssueLink contract.
  3. feat(linear): Phase 1 bulk browser — filters, pagination, multi-select, import modes.
  4. feat(linear): Phase 2 — persist thread↔issue link + sidebar badge.
  5. feat(linear): Phase 3 — status write-back (LinearSyncReactor + settings).

Testing

  • vp check and vp run typecheck pass (all 15 packages, 0 errors); no lint errors introduced.
  • Unit tests: formatLinearIssues (single/combine/subtasks/omit-empty) and the workflow-state resolver (type/name/override/fallback) — added, green.
  • Live end-to-end against a real Linear workspace: connected a PAT; browsed/filtered issues in /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. The issueUpdate mutation was also validated directly (state change + revert).

Known limitations / follow-ups (called out honestly)

  • PR-open → In Review and merge → Done watch a thread's worktree; a thread created without a worktree won't auto-advance from a PR (the issueUpdate path 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.
  • Per-team workflow-state mapping is wired end-to-end but the settings page currently exposes the toggles; mapping falls back to sensible type/name-based defaults.
  • OAuth + 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 /linear page, and import selected issues into threads with rich markdown context (combine vs one-thread-per-issue).

Threads can carry a persisted linearIssue link (new linked_linear_issue_json projection column + migration 033), surfaced in the UI with a status badge and optional Mark done in Linear from the thread menu.

A new LinearApi GraphQL client and 14 linear.* WebSocket RPCs power auth, listing/filtering, fetch, and mutations (issueUpdate, comments, attachments). LinearSyncReactor listens to orchestration and VCS PR events to advance linked issues (start → PR open → merge) using resolveTargetStateId and server Settings → Linear toggles, then reflects state back via thread.meta.update.

Client wiring includes linearEnvironment atoms, useLinearImport, settings at /settings/linear, command-palette entry, and docs/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

  • Adds a Linear API client, WebSocket RPC endpoints (auth, search, list, mutate), and a LinearSyncReactor that automatically transitions Linear issue states when threads start, PRs open, or PRs merge
  • Adds a LinearBrowsePopover and full LinearBrowser UI at /linear to search, select, and import Linear issues into threads — either one thread per issue or combined into a single draft
  • Adds /settings/linear where users can connect a personal API key and configure auto-sync toggles (transition on start, PR open, PR merge, post comments)
  • Persists the linked Linear issue on threads via a new linked_linear_issue_json column (migration 033) and surfaces it as a badge in the sidebar and a context-menu action to mark the issue done
  • Risk: the LinearSyncReactor caches workflow states in memory for the process lifetime; a server restart is required to pick up Linear workflow state changes

Macroscope summarized 08fa28b.

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
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ae05a075-a896-4f7f-b966-93be7f34501d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jul 5, 2026
Comment thread apps/server/src/orchestration/Layers/LinearSyncReactor.ts Outdated
Comment thread apps/server/src/orchestration/Layers/LinearSyncReactor.ts
Comment thread apps/web/src/hooks/useLinearImport.ts
Comment thread apps/server/src/orchestration/Layers/LinearSyncReactor.ts Outdated
Comment thread apps/web/src/hooks/useLinearImport.ts
Comment thread apps/web/src/components/linear/LinearBrowser.tsx Outdated
Comment thread apps/server/src/orchestration/Layers/LinearSyncReactor.ts
Comment thread apps/web/src/components/linear/LinearBrowser.tsx Outdated
Comment thread apps/server/src/linear/linearStateMapping.ts
Comment thread apps/server/src/linear/LinearApi.ts
Comment thread apps/web/src/hooks/useLinearImport.ts
Comment thread apps/web/src/components/linear/LinearBrowser.tsx
Comment thread apps/server/src/linear/LinearApi.ts
Comment thread apps/server/src/orchestration/Layers/LinearSyncReactor.ts
Comment thread apps/web/src/components/settings/LinearSettings.tsx
Comment thread apps/web/src/hooks/useLinearImport.ts
Comment thread apps/web/src/components/linear/LinearBrowser.tsx
Comment thread packages/client-runtime/src/state/linear.ts
@macroscopeapp

macroscopeapp Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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
@maslinedwin

Copy link
Copy Markdown
Author

Thanks for the automated review — pushed fixes in 1f04d05.

Macroscope (Effect Service Conventions): LinearSyncReactor now inlines its interface in Context.Service and references LinearSyncReactor["Service"]["start"] instead of a standalone …Shape type.

Cursor Bugbot (6 of 8 addressed):

  • PR watcher ignores worktree changes (High) — watchers are now keyed by threadId:worktreePath, so a thread that later gets/changes a worktree registers a fresh watcher.
  • Wrong URL on link attachment — removed the attachment write-back (and its setting/UI); it linked the issue to itself, and a real thread URL isn't available server-side without OAuth/hosting. Left as a follow-up for the OAuth phase.
  • Filter change merges stale page — rows now replace (not merge) when the filter key changes, via a ref tracking which filter the accumulated rows belong to.
  • Load more without cursor repeats page — "Load more" only shows / advances when the API returned an endCursor.
  • Partial import leaves orphan threadsperIssue now attempts every issue and returns a summary (created N of M, plus any that failed / couldn't be loaded) instead of bailing mid-loop.
  • Rank set before badge updateappliedRank is now set after the write and the best-effort badge reflect.

Intentionally left:

  • Same issue rank blocks second thread — per-issue monotonic state is deliberate: a Linear issue has one workflow state, so we don't want two threads racing it backward.
  • Silent drop of missing issue IDs (combine mode)perIssue now surfaces missing issues; combine still imports what Linear returns, which is the expected behavior.

Full recursive typecheck + lint pass and unit tests are green after the changes.

Comment thread packages/contracts/src/settings.ts
Comment thread apps/web/src/components/linear/LinearBrowser.tsx
Comment thread apps/web/src/hooks/useLinearImport.ts
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)
Comment thread apps/web/src/components/linear/LinearBrowser.tsx
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)
Comment thread apps/web/src/components/LinearBrowsePopover.tsx
Comment thread apps/web/src/components/LinearBrowsePopover.tsx
Comment thread apps/server/src/linear/LinearApi.ts Outdated
Comment thread packages/shared/src/serverSettings.ts
… 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)
Comment thread apps/web/src/components/settings/LinearSettings.tsx
Comment thread apps/web/src/components/LinearBrowsePopover.tsx
Comment thread apps/web/src/components/LinearBrowsePopover.tsx Outdated
Comment thread apps/server/src/linear/LinearApi.ts

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/server/src/linear/LinearApi.ts Outdated
Comment thread apps/server/src/linear/LinearApi.ts Outdated
…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
Comment thread apps/web/src/components/LinearBrowsePopover.tsx
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.
@maslinedwin

Copy link
Copy Markdown
Author

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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ 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.

Comment thread apps/server/src/ws.ts
cause,
}),
),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 08fa28b. Configure here.

Comment thread apps/server/src/ws.ts
),
);
}
return { success: true };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 08fa28b. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant