Skip to content

feat(pm): add Linear PM provider adapter and integration#1096

Merged
aaight merged 2 commits intodevfrom
feature/linear-pm-provider-adapter
Apr 14, 2026
Merged

feat(pm): add Linear PM provider adapter and integration#1096
aaight merged 2 commits intodevfrom
feature/linear-pm-provider-adapter

Conversation

@aaight
Copy link
Copy Markdown
Collaborator

@aaight aaight commented Apr 14, 2026

Summary

  • Implements LinearPMProvider in src/pm/linear/adapter.ts — a full PMProvider implementation wrapping the existing Linear GraphQL client
  • Implements LinearIntegration in src/pm/linear/integration.ts — a full PMIntegration implementation with credential scoping, webhook parsing, ack comments, reactions, project lookup, and work item ID extraction
  • Extends PMType union in src/pm/types.ts to include 'linear'
  • Extends pm.type enum in src/config/schema.ts to z.enum(['trello', 'jira', 'linear'])
  • Adds LinearConfig type and getLinearConfig() accessor in src/pm/config.ts
  • Registers LINEAR_API_KEY (required) and LINEAR_WEBHOOK_SECRET (optional) credential roles in src/config/integrationRoles.ts
  • Exports LinearPMProvider from src/pm/index.ts
  • Updates RouterProjectConfig.pmType, ProjectContext.pmType, and PromptContext.pmType to include 'linear'
  • Adds unit tests: tests/unit/pm/linear/adapter.test.ts (33 tests) and tests/unit/pm/linear/integration.test.ts (32 tests)

Trello card: https://trello.com/c/69de7f4c5b9eca332747b69d

Key decisions

  • Checklists via sub-issues: Linear has no native checklists; sub-issues are used instead, following the same pattern as JIRA subtasks. createChecklist() returns a synthetic checklist object; addChecklistItem() creates a child issue via linearClient.createIssue()
  • Credential self-registration: LinearIntegration calls registerCredentialRoles('linear', 'pm', [...]) at module load time (idempotent), and the roles are also pre-registered in the hardcoded _rolesRegistry in integrationRoles.ts for consistency
  • lookupProject() returns null: Linear-by-teamId project lookup requires a DB repository function not yet implemented (separate story). Returning null falls through to other lookup mechanisms
  • getWorkItemUrl() constructs generic URL: Uses https://linear.app/issue/<id> since the full org-scoped URL requires fetching the issue from the API. The actual issue.url from getWorkItem() is always correct
  • No binary file upload: Linear's API doesn't support binary file uploads; addAttachmentFile() posts a comment as a fallback, mirroring the JIRA pattern for URLs

Test plan

  • All unit tests pass (372 test files, 7313 tests)
  • TypeScript compiles cleanly (npm run typecheck)
  • Lint passes with no errors (npm run lint)
  • LinearPMProvider adapter tests cover all PMProvider methods
  • LinearIntegration tests cover hasIntegration, createProvider, withCredentials, resolveLifecycleConfig, parseWebhookPayload, isSelfAuthored, lookupProject, and extractWorkItemId

🤖 Generated with Claude Code

🕵️ claude-code · claude-sonnet-4-6 · run details

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 14, 2026

Codecov Report

❌ Patch coverage is 76.28866% with 115 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/pm/linear/adapter.ts 79.35% 48 Missing and 3 partials ⚠️
src/pm/linear/integration.ts 70.51% 46 Missing ⚠️
src/agents/shared/promptContext.ts 71.42% 3 Missing and 5 partials ⚠️
src/pm/config.ts 28.57% 4 Missing and 1 partial ⚠️
src/router/config.ts 28.57% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Collaborator

@nhopeatall nhopeatall left a comment

Choose a reason for hiding this comment

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

Summary

Solid implementation that follows existing patterns (JIRA adapter/integration). Three issues need fixing before merge: LinearIntegration is never registered in bootstrap.ts, addChecklistItem creates standalone issues instead of sub-issues, and isSelfAuthored will never work because it calls linearClient.getMe() without credentials in scope.

Architecture & Design

  • [BLOCKING] Missing bootstrap registration: LinearIntegration is not registered in src/integrations/bootstrap.ts. Without this, pmRegistry.get('linear') will throw at runtime when a Linear project receives a webhook or a worker tries to create a provider. The self-registration via registerCredentialRoles() at module load handles credential roles only, but the actual integration object is never instantiated and registered into pmRegistry or integrationRegistry. Add a block following the Trello/JIRA pattern in bootstrap.ts.

  • [SHOULD_FIX] getPromptTerminology() doesn't handle Linear: src/agents/shared/promptContext.ts:23-32 uses a binary isJira check — Linear projects will get Trello terminology (card, cards, Trello) instead of issue, issues, Linear. This will cause confusing agent prompts for Linear projects.

  • [SHOULD_FIX] getListIds() doesn't read Linear config: src/agents/shared/promptContext.ts:6-21 only reads Trello and JIRA configs. Linear projects won't have status IDs populated in prompt context, which means lifecycle status references in agent prompts will be undefined.

Code Issues

Blocking

  • src/pm/linear/adapter.ts:201addChecklistItem extracts parentId from the checklist ID but never passes it to createIssue(). The LinearCreateIssueInput type doesn't include parentId, so the created issue is standalone with no parent relationship. Either add parentId to the type and pass it, or document as a known limitation.

  • src/pm/linear/integration.ts:148isSelfAuthored calls linearClient.getMe() without withLinearCredentials(). This always throws (caught silently), meaning the bot can never detect its own comments — feedback loop risk. The _projectId parameter (unused, underscore-prefixed) should be used to scope credentials.

Should Fix

  • src/pm/linear/adapter.ts:23 — Local LinearConfig type has labels?: Record<string, string> but src/pm/config.ts has labels?: { processing?: string; ... }. Import the canonical type instead of redeclaring.

  • src/pm/linear/integration.ts:186sendReaction is a no-op but extracts commentId and checks event type before returning. Either implement using linearClient.createReaction() or remove the dead code.

  • src/router/config.ts:84-103loadProjectConfig only populates Trello and JIRA config in RouterProjectConfig. Linear projects will have pmType: 'linear' but no provider-specific config.

🕵️ claude-code · claude-opus-4-6 · run details

@nhopeatall
Copy link
Copy Markdown
Collaborator

Inline Review Notes

src/pm/linear/adapter.ts:201 — addChecklistItem doesn't set parentId

parentId is extracted from the checklist ID (line 195) but never passed to linearClient.createIssue(). The LinearCreateIssueInput type in src/linear/types.ts needs a parentId?: string field, and the createIssue call should include it:

await linearClient.createIssue({
    teamId: this.config.teamId,
    title: name,
    description,
    parentId, // <-- missing
});

src/pm/linear/integration.ts:148 — isSelfAuthored has no credentials in scope

linearClient.getMe() requires credentials via AsyncLocalStorage. Without withLinearCredentials(), this always throws and falls through to return false. The _projectId parameter on line 133 is unused but needed:

async isSelfAuthored(event: PMWebhookEvent, projectId: string): Promise<boolean> {
    // ...
    const apiKey = await getIntegrationCredential(projectId, 'pm', 'api_key');
    const me = await withLinearCredentials({ apiKey }, () => linearClient.getMe());
    return me.id === commentUserId;
}

src/pm/linear/adapter.ts:23 — Local LinearConfig type diverges from canonical type

The local LinearConfig declares labels?: Record<string, string> but src/pm/config.ts's LinearConfig uses labels?: { processing?: string; ... }. Import from ../config.js instead of redeclaring to prevent drift.

🕵️ claude-code · claude-opus-4-6 · run details

- Register LinearIntegration in bootstrap.ts following Trello/JIRA pattern
- Fix addChecklistItem to pass parentId to createIssue() for proper sub-issue relationship
- Add parentId to LinearCreateIssueInput type in linear/types.ts
- Fix isSelfAuthored to use withLinearCredentials() with project credentials
- Remove dead code from sendReaction (no-op with dead extraction logic)
- Import canonical LinearConfig from pm/config.ts instead of redeclaring local type
- Fix getPromptTerminology() to return issue/Linear for Linear projects
- Fix getListIds() to read Linear statuses config for prompt context
- Add Linear config to RouterProjectConfig and loadProjectConfig()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@nhopeatall nhopeatall left a comment

Choose a reason for hiding this comment

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

Summary

Clean, well-structured implementation that follows the established JIRA integration pattern closely. All CI checks pass. The adapter and integration files are consistent with existing patterns and the code handles edge cases (null users, missing config, empty identifiers) correctly.

Should Fix

  • src/agents/prompts/index.ts:309getTemplateVariables() description says 'PM type: trello or jira' but the pmType field now also accepts 'linear'. Minor doc inconsistency. Should say trello, jira, or linear to match.

  • src/triggers/github/utils.ts:96-102 — Not part of this PR's diff, but the extractWorkItemId() function only handles jira and falls through to Trello for all other PM types. For Linear projects, this would attempt Trello URL extraction. This function appears to be dead code (no imports found), but if it's ever reactivated, Linear projects would silently fail to extract work item IDs from PR bodies. Worth a follow-up to either delete this dead code or add Linear support.

Notes

  • The getWorkItemUrl() returning https://linear.app/issue/<id> (missing workspace slug) is acknowledged in the PR description as a known limitation — the actual issue.url from getWorkItem() is always correct. Acceptable trade-off.
  • addChecklistItem regex correctly handles Linear identifiers containing hyphens (e.g., TEAM-123), matching the same pattern used by JIRA.
  • The extractWorkItemId regex matches the identical regex in JIRA's integration, which is correct since both use the same identifier format. The system routes to the correct integration's extractWorkItemId based on project PM type, so no collision risk.
  • Dual registration of credential roles (hardcoded in integrationRoles.ts + self-register in integration.ts module load) is intentional and consistent with the PR description's "Credential self-registration" decision.

🕵️ claude-code · claude-opus-4-6 · run details

@aaight aaight merged commit 73c646c into dev Apr 14, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants