Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/agents/prompts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export interface PromptContext {
projectId?: string;

// PM vocabulary (computed from pmType)
pmType?: 'trello' | 'jira';
pmType?: 'trello' | 'jira' | 'linear';
workItemNoun?: string; // "card" or "issue"
workItemNounPlural?: string; // "cards" or "issues"
workItemNounCap?: string; // "Card" or "Issue"
Expand Down
41 changes: 28 additions & 13 deletions src/agents/shared/promptContext.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,49 @@
import { getJiraConfig, getTrelloConfig } from '../../pm/config.js';
import { getJiraConfig, getLinearConfig, getTrelloConfig } from '../../pm/config.js';
import { getPMProviderOrNull } from '../../pm/index.js';
import type { ProjectConfig } from '../../types/index.js';
import type { PromptContext } from '../prompts/index.js';

function getListIds(project: ProjectConfig) {
const trelloConfig = getTrelloConfig(project);
const jiraConfig = getJiraConfig(project);
const linearConfig = getLinearConfig(project);

return {
backlogListId: trelloConfig?.lists?.backlog ?? jiraConfig?.statuses?.backlog,
todoListId: trelloConfig?.lists?.todo ?? jiraConfig?.statuses?.todo,
inProgressListId: trelloConfig?.lists?.inProgress ?? jiraConfig?.statuses?.inProgress,
inReviewListId: trelloConfig?.lists?.inReview ?? jiraConfig?.statuses?.inReview,
doneListId: trelloConfig?.lists?.done ?? jiraConfig?.statuses?.done,
mergedListId: trelloConfig?.lists?.merged ?? jiraConfig?.statuses?.merged,
backlogListId:
trelloConfig?.lists?.backlog ??
jiraConfig?.statuses?.backlog ??
linearConfig?.statuses?.backlog,
todoListId:
trelloConfig?.lists?.todo ?? jiraConfig?.statuses?.todo ?? linearConfig?.statuses?.todo,
inProgressListId:
trelloConfig?.lists?.inProgress ??
jiraConfig?.statuses?.inProgress ??
linearConfig?.statuses?.inProgress,
inReviewListId:
trelloConfig?.lists?.inReview ??
jiraConfig?.statuses?.inReview ??
linearConfig?.statuses?.inReview,
doneListId:
trelloConfig?.lists?.done ?? jiraConfig?.statuses?.done ?? linearConfig?.statuses?.done,
mergedListId:
trelloConfig?.lists?.merged ?? jiraConfig?.statuses?.merged ?? linearConfig?.statuses?.merged,
debugListId: trelloConfig?.lists?.debug,
processedLabelId: trelloConfig?.labels?.processed,
autoLabelId: trelloConfig?.labels?.auto ?? jiraConfig?.labels?.auto,
autoLabelId:
trelloConfig?.labels?.auto ?? jiraConfig?.labels?.auto ?? linearConfig?.labels?.auto,
};
}

function getPromptTerminology(pmType: string | undefined) {
const isJira = pmType === 'jira';
const isLinear = pmType === 'linear';

return {
workItemNoun: isJira ? 'issue' : 'card',
workItemNounPlural: isJira ? 'issues' : 'cards',
workItemNounCap: isJira ? 'Issue' : 'Card',
workItemNounPluralCap: isJira ? 'Issues' : 'Cards',
pmName: isJira ? 'JIRA' : 'Trello',
workItemNoun: isJira || isLinear ? 'issue' : 'card',
workItemNounPlural: isJira || isLinear ? 'issues' : 'cards',
workItemNounCap: isJira || isLinear ? 'Issue' : 'Card',
workItemNounPluralCap: isJira || isLinear ? 'Issues' : 'Cards',
pmName: isJira ? 'JIRA' : isLinear ? 'Linear' : 'Trello',
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/api/routers/webhooks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export interface ProjectContext {
projectId: string;
orgId: string;
repo?: string;
pmType: 'trello' | 'jira';
pmType: 'trello' | 'jira' | 'linear';
boardId?: string;
jiraBaseUrl?: string;
jiraProjectKey?: string;
Expand Down
15 changes: 14 additions & 1 deletion src/config/integrationRoles.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type IntegrationCategory = 'pm' | 'scm' | 'alerting';
export type IntegrationProvider = 'trello' | 'jira' | 'github' | 'sentry';
export type IntegrationProvider = 'trello' | 'jira' | 'linear' | 'github' | 'sentry';

export interface CredentialRoleDef {
role: string;
Expand Down Expand Up @@ -35,6 +35,18 @@ const _rolesRegistry = new Map<string, CredentialRoleDef[]>([
},
],
],
[
'linear',
[
{ role: 'api_key', label: 'API Key', envVarKey: 'LINEAR_API_KEY' },
{
role: 'webhook_secret',
label: 'Webhook Secret',
envVarKey: 'LINEAR_WEBHOOK_SECRET',
optional: true,
},
],
],
[
'github',
[
Expand Down Expand Up @@ -69,6 +81,7 @@ const _rolesRegistry = new Map<string, CredentialRoleDef[]>([
const _categoryRegistry = new Map<string, IntegrationCategory>([
['trello', 'pm'],
['jira', 'pm'],
['linear', 'pm'],
['github', 'scm'],
['sentry', 'alerting'],
]);
Expand Down
23 changes: 22 additions & 1 deletion src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@ const JiraConfigSchema = z.object({
.optional(),
});

const LinearConfigSchema = z.object({
teamId: z.string().min(1),
statuses: z.record(z.string()), // CASCADE status names → Linear state IDs
labels: z
.object({
processing: z.string().optional(),
processed: z.string().optional(),
error: z.string().optional(),
readyToProcess: z.string().optional(),
auto: z.string().optional(),
})
.optional(),
customFields: z
.object({
cost: z.string().optional(),
})
.optional(),
});

export const ProjectConfigSchema = z.object({
id: z.string().min(1),
orgId: z.string().min(1),
Expand All @@ -49,7 +68,7 @@ export const ProjectConfigSchema = z.object({

pm: z
.object({
type: z.enum(['trello', 'jira']).default('trello'),
type: z.enum(['trello', 'jira', 'linear']).default('trello'),
})
.default({ type: 'trello' }),

Expand All @@ -68,6 +87,8 @@ export const ProjectConfigSchema = z.object({

jira: JiraConfigSchema.optional(),

linear: LinearConfigSchema.optional(),

model: z.string().default(PROJECT_DEFAULTS.model),
agentModels: z.record(z.string()).optional(),
maxIterations: z.number().int().positive().default(PROJECT_DEFAULTS.maxIterations),
Expand Down
9 changes: 8 additions & 1 deletion src/integrations/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/**
* Unified integration bootstrap — canonical registration point for all integrations.
*
* Registers all 4 built-in integrations into the `integrationRegistry`:
* Registers all 5 built-in integrations into the `integrationRegistry`:
* - TrelloIntegration (PM)
* - JiraIntegration (PM)
* - LinearIntegration (PM)
* - GitHubSCMIntegration (SCM)
* - SentryAlertingIntegration (Alerting)
*
Expand All @@ -26,6 +27,7 @@
import { GitHubSCMIntegration } from '../github/scm-integration.js';
import { integrationRegistry } from '../integrations/registry.js';
import { JiraIntegration } from '../pm/jira/integration.js';
import { LinearIntegration } from '../pm/linear/integration.js';
import { pmRegistry } from '../pm/registry.js';
import { TrelloIntegration } from '../pm/trello/integration.js';
import { SentryAlertingIntegration } from '../sentry/alerting-integration.js';
Expand All @@ -40,6 +42,11 @@ if (!pmRegistry.getOrNull('jira')) {
pmRegistry.register(jira);
if (!integrationRegistry.getOrNull('jira')) integrationRegistry.register(jira);
}
if (!pmRegistry.getOrNull('linear')) {
const linear = new LinearIntegration();
pmRegistry.register(linear);
if (!integrationRegistry.getOrNull('linear')) integrationRegistry.register(linear);
}
if (!integrationRegistry.getOrNull('github')) {
integrationRegistry.register(new GitHubSCMIntegration());
}
Expand Down
1 change: 1 addition & 0 deletions src/linear/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export interface LinearCreateIssueInput {
title: string;
description?: string;
teamId: string;
parentId?: string;
assigneeId?: string;
stateId?: string;
priority?: number;
Expand Down
26 changes: 26 additions & 0 deletions src/pm/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,38 @@ export function getJiraConfig(project: ProjectConfig): JiraConfig | undefined {
return project.jira as JiraConfig | undefined;
}

/** Linear-specific configuration (from project_integrations JSONB) */
export interface LinearConfig {
teamId: string;
statuses: Record<string, string>;
labels?: {
processing?: string;
processed?: string;
error?: string;
readyToProcess?: string;
auto?: string;
};
customFields?: { cost?: string };
}

/**
* Get the Linear config for a project.
* Returns the config or undefined if this is not a Linear project.
*/
export function getLinearConfig(project: ProjectConfig): LinearConfig | undefined {
if (project.pm?.type !== 'linear') return undefined;
return project.linear as LinearConfig | undefined;
}

/**
* Get the cost custom field ID for a project, regardless of PM type.
*/
export function getCostFieldId(project: ProjectConfig): string | undefined {
if (project.pm?.type === 'jira') {
return getJiraConfig(project)?.customFields?.cost;
}
if (project.pm?.type === 'linear') {
return getLinearConfig(project)?.customFields?.cost;
}
return getTrelloConfig(project)?.customFields?.cost;
}
1 change: 1 addition & 0 deletions src/pm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type { PMIntegration, PMWebhookEvent } from './integration.js';
export { JiraPMProvider } from './jira/adapter.js';
export type { ProjectPMConfig } from './lifecycle.js';
export { hasAutoLabel, PMLifecycleManager, resolveProjectPMConfig } from './lifecycle.js';
export { LinearPMProvider } from './linear/adapter.js';
export {
extractMarkdownImages,
filterImageMedia,
Expand Down
Loading
Loading