Skip to content

Integration: Add Linear issues as TaskSpawner source type for developer-team-driven agent workflows #764

@kelos-bot

Description

@kelos-bot

🤖 Kelos Strategist Agent @gjkim42

Area: Integration Opportunities

Summary

Kelos currently supports four source types: githubIssues, githubPullRequests, cron, and jira. The Jira integration (added in PR #556) proved the pattern for non-GitHub issue trackers works cleanly — the Source interface (internal/source/source.go:37) maps naturally to any system that produces work items. However, a major gap remains: Linear, the issue tracker most popular among the exact developer demographic most likely to adopt Kelos (startup and scale-up engineering teams running Kubernetes).

This proposal adds a linear source type to TaskSpawner, enabling teams that use Linear for project management to drive autonomous AI agents from their existing workflow — without migrating to GitHub Issues or Jira.

Why Linear specifically?

1. Market alignment with Kelos's target audience

Linear's user base heavily overlaps with Kelos's ideal adopters:

  • Kubernetes-native teams: Linear is dominant among startups and scale-ups that already run on Kubernetes
  • Automation-minded engineers: Linear users tend to be early adopters of developer tooling and AI-assisted workflows
  • Fast-moving teams: Linear's speed-focused design attracts teams that would benefit most from autonomous agents

Jira targets enterprise; GitHub Issues targets open-source. Linear targets the growth-stage engineering teams that are currently underserved by Kelos's source types.

2. Significant adoption numbers

Linear has crossed 10,000+ companies including many well-known engineering organizations. Among YC-backed startups and developer-tools companies, Linear is often the default choice over Jira.

3. Clean API that maps directly to the Source interface

Linear provides a well-documented GraphQL API that maps cleanly to Kelos's WorkItem struct:

Linear concept WorkItem field Notes
issue.identifier (e.g., ENG-42) ID Unique within workspace
issue.number Number Numeric identifier
issue.title Title Issue title
issue.description Body Markdown description
issue.url URL Linear web URL
issue.labels[].name Labels Linear labels
issue.comments[].body Comments Comment text
"Issue" Kind Always "Issue" for Linear

4. Comment-based workflow control works identically

Linear supports comments on issues, so Kelos's existing commentPolicy pattern (triggerComment, excludeComments, allowedUsers) translates directly. A team can use /kelos trigger comments in Linear just like they do in GitHub Issues.

Proposed API

Add linear to the When struct

type When struct {
    GitHubIssues       *GitHubIssues       `json:"githubIssues,omitempty"`
    GitHubPullRequests *GitHubPullRequests  `json:"githubPullRequests,omitempty"`
    Cron               *Cron               `json:"cron,omitempty"`
    Jira               *Jira               `json:"jira,omitempty"`
    // NEW
    Linear             *Linear             `json:"linear,omitempty"`
}

New Linear source type

// Linear discovers issues from a Linear team and spawns one Task per matched issue.
// Authentication uses a Linear API key provided via secretRef.
type Linear struct {
    // TeamKey is the Linear team identifier (e.g., "ENG", "PLATFORM").
    // Issues are discovered from this team only.
    // +kubebuilder:validation:Required
    // +kubebuilder:validation:MinLength=1
    TeamKey string `json:"teamKey"`

    // States filters issues by workflow state names (e.g., ["Todo", "In Progress"]).
    // When empty, issues in all non-terminal states are discovered.
    // +optional
    States []string `json:"states,omitempty"`

    // Labels filters issues that have ALL of the specified label names.
    // When empty, no label filtering is applied.
    // +optional
    Labels []string `json:"labels,omitempty"`

    // ExcludeLabels excludes issues that have ANY of the specified labels.
    // +optional
    ExcludeLabels []string `json:"excludeLabels,omitempty"`

    // Assignee filters issues assigned to a specific user (Linear display name or email).
    // When empty, no assignee filtering is applied.
    // +optional
    Assignee string `json:"assignee,omitempty"`

    // Priority filters issues by Linear priority level.
    // Linear priorities: 0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low.
    // When set, only issues at this priority or higher (lower number) are discovered.
    // +optional
    // +kubebuilder:validation:Minimum=0
    // +kubebuilder:validation:Maximum=4
    MaxPriority *int `json:"maxPriority,omitempty"`

    // Project filters issues belonging to a specific Linear project name.
    // When empty, issues from all projects within the team are discovered.
    // +optional
    Project string `json:"project,omitempty"`

    // CommentPolicy configures comment-based workflow control.
    // Works identically to GitHub Issues commentPolicy.
    // +optional
    CommentPolicy *LinearCommentPolicy `json:"commentPolicy,omitempty"`

    // Reporting configures status reporting back to Linear issues.
    // When enabled, the spawner posts comments on issues when tasks start/complete.
    // +optional
    Reporting *LinearReporting `json:"reporting,omitempty"`

    // SecretRef references a Secret containing a "LINEAR_API_KEY" key.
    // The API key must have read access to the team's issues.
    // Create one at: Linear Settings → API → Personal API keys.
    // +kubebuilder:validation:Required
    SecretRef SecretReference `json:"secretRef"`

    // PollInterval overrides spec.pollInterval for this source (e.g., "30s", "5m").
    // When empty, spec.pollInterval is used.
    // +optional
    PollInterval string `json:"pollInterval,omitempty"`
}

// LinearCommentPolicy mirrors GitHubCommentPolicy for Linear issues.
type LinearCommentPolicy struct {
    // TriggerComment is the command that must appear in a comment to include the issue.
    // When set, only issues with a matching comment are discovered.
    // +optional
    TriggerComment string `json:"triggerComment,omitempty"`

    // ExcludeComments lists commands that exclude an issue from discovery.
    // +optional
    ExcludeComments []string `json:"excludeComments,omitempty"`
}

// LinearReporting configures status comments posted back to Linear issues.
type LinearReporting struct {
    // Enabled controls whether the spawner posts status comments.
    // +kubebuilder:default=false
    Enabled bool `json:"enabled"`
}

Implementation approach

Source implementation (internal/source/linear.go)

Linear's GraphQL API makes the implementation straightforward. The core query:

query($teamKey: String!, $after: String) {
  team(key: $teamKey) {
    issues(
      first: 50
      after: $after
      filter: {
        state: { name: { in: $states } }
        labels: { name: { in: $labels } }
      }
    ) {
      nodes {
        identifier
        number
        title
        description
        url
        priority
        state { name }
        assignee { name email }
        labels { nodes { name } }
        comments { nodes { body user { name } } }
        project { name }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
}

The LinearSource struct follows the same pattern as JiraSource:

type LinearSource struct {
    TeamKey       string
    States        []string
    Labels        []string
    ExcludeLabels []string
    Assignee      string
    MaxPriority   *int
    Project       string
    CommentPolicy *LinearCommentPolicy
    APIKey        string
    Client        *http.Client
}

func (s *LinearSource) Discover(ctx context.Context) ([]WorkItem, error) {
    issues, err := s.fetchAllIssues(ctx)
    if err != nil {
        return nil, err
    }

    var items []WorkItem
    for _, issue := range issues {
        // Apply client-side filters (excludeLabels, assignee, priority, project)
        if s.shouldExclude(issue) {
            continue
        }

        items = append(items, WorkItem{
            ID:       issue.Identifier,  // e.g., "ENG-42"
            Number:   issue.Number,
            Title:    issue.Title,
            Body:     issue.Description,
            URL:      issue.URL,
            Labels:   extractLinearLabels(issue),
            Comments: extractLinearComments(issue),
            Kind:     "Issue",
        })
    }

    return items, nil
}

Spawner wiring (cmd/kelos-spawner/main.go)

Add --linear-api-key-file flag and extend buildSource():

case ts.Spec.When.Linear != nil:
    lin := ts.Spec.When.Linear
    apiKey, err := readLinearAPIKey(linearAPIKeyFile)
    if err != nil {
        return nil, err
    }
    return &source.LinearSource{
        TeamKey:       lin.TeamKey,
        States:        lin.States,
        Labels:        lin.Labels,
        ExcludeLabels: lin.ExcludeLabels,
        Assignee:      lin.Assignee,
        MaxPriority:   lin.MaxPriority,
        Project:       lin.Project,
        CommentPolicy: convertLinearCommentPolicy(lin.CommentPolicy),
        APIKey:        apiKey,
        Client:        httpClient,
    }, nil

Controller changes (internal/controller/taskspawner_deployment_builder.go)

Add Linear secret volume mount and spawner arguments, following the same pattern as Jira:

if ts.Spec.When.Linear != nil {
    args = append(args, "--linear-api-key-file=/secrets/linear/LINEAR_API_KEY")
    volumes = append(volumes, linearSecretVolume(ts.Spec.When.Linear.SecretRef))
}

Files to modify

File Change
api/v1alpha1/taskspawner_types.go Add Linear struct, LinearCommentPolicy, LinearReporting, When.Linear field
internal/source/linear.go New: LinearSource implementing Source interface (~200 lines)
internal/source/linear_test.go New: Unit tests with mock GraphQL responses (~300 lines)
cmd/kelos-spawner/main.go Add --linear-api-key-file flag, extend buildSource()
cmd/kelos-spawner/main_test.go Add buildSource test for Linear config
internal/controller/taskspawner_deployment_builder.go Add Linear secret volume mount and spawner args
internal/controller/taskspawner_controller.go No changes needed (Linear uses polling like Jira)

Example TaskSpawner configs

Example 1: Engineering team bug triage agent

apiVersion: kelos.dev/v1alpha1
kind: TaskSpawner
metadata:
  name: linear-bug-triage
spec:
  when:
    linear:
      teamKey: ENG
      states: ["Triage"]
      labels: ["bug"]
      secretRef:
        name: linear-api-key
      pollInterval: "5m"
  maxConcurrency: 3
  taskTemplate:
    type: claude-code
    model: sonnet
    credentials:
      type: oauth
      secretRef:
        name: claude-credentials
    workspaceRef:
      name: main-app
    branch: "fix/{{.ID}}"
    ttlSecondsAfterFinished: 3600
    promptTemplate: |
      Linear issue {{.ID}}: {{.Title}}

      Description:
      {{.Body}}

      Labels: {{.Labels}}

      Instructions:
      1. Analyze the bug report and reproduce the issue
      2. Identify the root cause
      3. Implement a fix with tests
      4. Create a PR titled "Fix: {{.Title}}"

Example 2: Feature spec to implementation

apiVersion: kelos.dev/v1alpha1
kind: TaskSpawner
metadata:
  name: linear-feature-impl
spec:
  when:
    linear:
      teamKey: PLATFORM
      states: ["Ready for Development"]
      maxPriority: 2  # Urgent and High priority only
      commentPolicy:
        triggerComment: "/kelos implement"
      secretRef:
        name: linear-api-key
  maxConcurrency: 2
  taskTemplate:
    type: claude-code
    credentials:
      type: oauth
      secretRef:
        name: claude-credentials
    workspaceRef:
      name: platform-service
    branch: "feature/{{.ID}}"
    ttlSecondsAfterFinished: 7200
    promptTemplate: |
      Implement the feature described in Linear issue {{.ID}}.

      Title: {{.Title}}
      Priority: High/Urgent

      Specification:
      {{.Body}}

      Discussion:
      {{.Comments}}

      Create a PR with the implementation. Include tests.

Example 3: Cron + Linear for stale issue cleanup

apiVersion: kelos.dev/v1alpha1
kind: TaskSpawner
metadata:
  name: linear-stale-review
spec:
  when:
    linear:
      teamKey: ENG
      states: ["In Progress"]
      labels: ["needs-review"]
      reporting:
        enabled: true
      secretRef:
        name: linear-api-key
      pollInterval: "24h"
  maxConcurrency: 5
  taskTemplate:
    type: claude-code
    model: haiku
    credentials:
      type: api-key
      secretRef:
        name: anthropic-key
    workspaceRef:
      name: main-app
    promptTemplate: |
      Review the status of Linear issue {{.ID}}: {{.Title}}

      Current state: In Progress, labeled "needs-review"
      Description: {{.Body}}

      Check if there is an associated PR branch. If so, review
      the code changes and post a summary comment on the issue.
      If no branch exists, comment noting the issue appears stale.

Why this is strategically important for Kelos

  1. Opens a new market segment: Linear-using teams (thousands of companies) are currently excluded from issue-driven Kelos workflows. This is arguably a larger addressable market than Jira for Kelos's typical user profile.

  2. Low implementation cost, high adoption potential: The Jira source implementation (internal/source/jira.go) is ~270 lines. A Linear source would be similar in size. The Source interface and spawner architecture already handle all the orchestration complexity.

  3. Reinforces Kelos as platform-agnostic: With GitHub, Jira, and Linear support, Kelos becomes the clear choice for "bring your own issue tracker" agent orchestration. This positions Kelos ahead of tools that only integrate with GitHub.

  4. Complements the GitLab proposal (Integration: Add GitLab issues and merge requests as TaskSpawner source types for multi-platform agent workflows #701): Together with GitLab support, Linear support would cover the three most common toolchains:

Relationship to existing issues

Issue Relationship
#701 (GitLab source) Complementary: GitLab is a code hosting integration; Linear is a project management integration. Together they complete the non-GitHub toolchain story.
#687 (Webhook source) Alternative trigger path: Linear supports webhooks. A generic webhook source could theoretically receive Linear events, but a native source provides filtering, comment policy, reporting, and a better UX.
#595 (Slack source) Different layer: Slack is for ChatOps-style ad-hoc commands; Linear is for structured issue-driven workflows. Different use cases.
#664 (githubRepositories) Independent: Repository discovery vs. issue tracking are orthogonal dimensions.

/kind feature

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions