Skip to content

feat(linear): optional Linear Project scope for PM integration#1116

Merged
zbigniewsobiecki merged 3 commits intodevfrom
feat/005-linear-project-scope
Apr 15, 2026
Merged

feat(linear): optional Linear Project scope for PM integration#1116
zbigniewsobiecki merged 3 commits intodevfrom
feat/005-linear-project-scope

Conversation

@zbigniewsobiecki
Copy link
Copy Markdown
Member

Summary

Extends the Linear PM integration so operators can optionally narrow a CASCADE project's scope to a specific Linear Project (initiative) via the PM wizard's "Board / Project Selection" step. When set, CASCADE only responds to issues that belong to that Linear Project; when empty, behavior is unchanged from today.

  • Backend: LinearConfig.projectId? threaded through listIssues/createIssue/provider adapter; LinearRouterAdapter.parseWebhook() drops out-of-scope Issue / Comment / IssueLabel events with a structured logger.info entry; tRPC linearProjects / linearProjectsByProject discovery procedures; linearClient.getTeamProjects(teamId, first = 250).
  • UI: Optional "Linear Project" SearchableSelect in the wizard under the Team selector, populated by the new discovery endpoint, disabled (hidden) until a team is chosen; native placeholder option doubles as clear; helper copy explicit about the optional nature. LinearWebhookInfoPanel gains a short callout explaining that the filter runs on CASCADE's side — Linear webhook config stays team-scoped and unchanged.
  • Docs: src/integrations/README.md operator-setup paragraph + single operator-facing CHANGELOG entry.

Design highlights (read-before-merge)

  • Backwards-compat is zero-effort. Every code path gates on LinearConfig.projectId being truthy. Existing installations without the field: no migration, no behavior change, no env var.
  • Cross-team Linear Projects use intersection. If a Linear Project spans teams A and B and a CASCADE project is configured for Team A × Project X, sibling-team issues inside the same project are ignored (dropped by the existing teamId lookup before the new filter runs). Multi-team project scoping is explicitly out-of-scope for v1 and will be a future spec.
  • Webhook filter is info-level. logger.info('LinearRouterAdapter: dropping event outside project scope', {...}) on every out-of-scope event. This is a deliberate choice — operators need "why isn't CASCADE picking up my issue?" visible at the default log level. Can be escalated to debug later if noise complaints arrive.
  • GraphQL pagination capped at 250. getTeamProjects ships with first: 250 (Linear's max). Large orgs are covered; very-large-team fallback would need cursor pagination (not warranted today).
  • Stateless per-event evaluation. The filter inspects each webhook's current projectId against the currently-configured scope. No cached "tracked issues" set — matches how the team-only filter works today and naturally handles issues being moved in/out of the scoped project.
  • Project selector fires alongside the existing details mutation in handleTeamSelect. Each has its own error/retry surface; if one fails the UI shows the mixed state rather than blocking. Deliberate: both are independent fetches.

Commits

Three commits: docs(005)feat(linear) backendfeat(linear) UI. Each reviewable in isolation:

  1. Spec + plans + coverage map (audit trail for the /specify → /plan → /implement workflow).
  2. Backend — config type, client/provider outbound scope, router-side drop filter, tRPC discovery, 33 new unit tests.
  3. UI — wizard state + hook + component, save payload, webhook-panel copy, operator docs, 21 new unit tests.

Test plan

  • npm run typecheck — clean
  • npm run lint — clean (biome)
  • npm test — 7668 tests pass (54 new)
  • npm run build — passes
  • Manual smoke (post-merge, operator responsibility — plan 3 AC fix: redis startup fails due to nologin shell #15):
    • Configure a fresh Linear integration without a project scope → confirm today's behavior unchanged
    • Add a project scope, trigger a Linear webhook for an issue outside the project → observe the drop log entry, no agent invocation, no ack comment
    • Trigger a webhook for an issue inside the scope → observe normal processing
    • Clear the project scope, re-save → confirm full-team processing resumes on next event

Spec / plans

  • Spec: docs/specs/005-linear-project-scope.md.done
  • Plans: docs/plans/005-linear-project-scope/{1-scope-config-and-outbound,2-webhook-scope-filter-and-discovery,3-wizard-ui}.md.done + _coverage.md

🤖 Generated with Claude Code

zbigniewsobiecki and others added 3 commits April 15, 2026 22:58
Captures the spec, three execution plans, and coverage map for adding
an optional Linear Project scope to the Linear PM integration. All three
plans are .done; the code landing in the next two commits implements
them. See docs/specs/005-linear-project-scope.md.done for the feature-
level spec.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…very)

Delivers the backend half of the optional Linear Project scope feature
(see docs/specs/005-linear-project-scope.md.done).

Config + outbound operations:
- `LinearConfig.projectId?: string` — new optional field on the PM
  config JSONB. Absence preserves existing full-team behavior.
- `linearClient.listIssues()` accepts `projectId` and translates it to
  a `project.id.eq` GraphQL filter.
- `linearClient.createIssue()` passes `projectId` through to the
  mutation input via `LinearCreateIssueInput.projectId`.
- `LinearPMProvider.listWorkItems()`, `createWorkItem()`, and
  `addChecklistItem()` propagate `config.projectId` to every outbound
  call, so sub-issues stay in scope.

Router-side webhook filter:
- `RouterProjectConfig.linear.projectId?` + `loadProjectConfig()` copy
  the field from `LinearConfig` into the router-side projection.
- `LinearRouterAdapter.parseWebhook()` drops Issue, Comment, and
  IssueLabel events whose issue's projectId differs from the configured
  one (or has no project), returning null so no downstream step fires.
  A structured `logger.info('LinearRouterAdapter: dropping event
  outside project scope', { reason, configuredProjectId,
  issueProjectId, issueId, teamId, projectId, eventType })` entry
  records the drop so operators can debug "why isn't CASCADE picking
  up my issue?" — info-level is a deliberate choice.

Wizard discovery endpoint:
- `linearClient.getTeamProjects(teamId, first = 250)` — new GraphQL
  method + `LinearProject` type. Pagination cap prevents truncation
  for large teams.
- `linearProjects` + `linearProjectsByProject` tRPC procedures mirror
  the `linearTeams` / `linearTeamsByProject` pattern so the wizard UI
  can populate a project dropdown.

Cross-team intersection is enforced: sibling-team issues in a shared
Linear project are dropped by the existing teamId lookup before the
new filter runs. Integrations without `projectId` set see zero
behavior change; no migration.

Tests: 33 new unit tests across `tests/unit/linear/client.test.ts`
(new file, 9 tests), `tests/unit/pm/linear-adapter.test.ts` (new file,
7 tests), `tests/unit/pm/linear-integration.test.ts` (4 new tests),
`tests/unit/router/adapters/linear.test.ts` (8 tests for the scope
filter), and `tests/unit/api/routers/integrationsDiscovery.test.ts`
(8 tests for the new procedures). `unit-core` project now includes
`tests/unit/linear/**`.

Spec: docs/specs/005-linear-project-scope.md.done
Plans: 005/1 (scope-config-and-outbound) + 005/2 (webhook-scope-
filter-and-discovery).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes spec 005 by shipping the operator-facing half. The backend
(previous commit) now has a UI driver that writes `projectId` into
the Linear integration config.

Wizard state:
- `pm-wizard-state.ts` — `linearProjectId` + `linearProjects` fields,
  `SET_LINEAR_PROJECTS` + `SET_LINEAR_PROJECT_ID` actions, and a
  reset-on-team-change hook (a new team invalidates the project list).
- `buildEditState()` hydrates `linearProjectId` from a saved config so
  reopening the wizard for an edited project pre-selects the scope.
- `buildLinearIntegrationConfig(state)` — new pure save-payload
  builder. Keeps the save mutation thin and gives the payload shape a
  direct unit-test surface without a React runtime.

Discovery hook:
- `useLinearDiscovery()` — adds `linearProjectsMutation` mirroring the
  existing `linearDetailsMutation` pattern (byProject + raw-creds
  variants; fires after team selection and on editing-mount when a
  team is already stored).

UI:
- `LinearTeamStep` — renders a SearchableSelect for "Linear Project
  (optional)" under the Team selector, but only when a team is
  selected. Native placeholder `<option value="">` doubles as the
  clear control. Helper copy explicitly marks the field optional and
  names the fallback behavior.
- `LinearWebhookInfoPanel` — adds a one-paragraph callout clarifying
  that project-scope filtering happens on CASCADE's side — Linear
  webhook config stays team-scoped and unchanged. Pre-empts the
  predictable support question.
- Save payload now includes `projectId` exactly when the selector has
  a value; omitted when empty. Clearing persists as "no project scope".

Docs:
- `src/integrations/README.md` — Linear operator-setup paragraph
  now mentions the optional project scope and where it lives in the
  wizard.
- `CHANGELOG.md` — single operator-facing Unreleased entry covering
  the whole feature.

Tests: 21 new unit tests across pm-wizard-state (reducer + hydration
+ save-payload builder), linear-team-step (new SSR component tests
covering render gating, options population, edit-mode pre-selection,
clear behavior, helper copy), and linear-webhook-info-panel (regression
+ new callout copy).

AC #15 of plan 3 (manual end-to-end smoke test with a live Linear
workspace + webhook delivery) is an operator post-merge verification
step — not executable in CI.

Spec: docs/specs/005-linear-project-scope.md.done
Plan: 005/3 (wizard-ui).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 15, 2026

Codecov Report

❌ Patch coverage is 95.55556% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/linear/client.ts 82.35% 3 Missing ⚠️
src/router/config.ts 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@zbigniewsobiecki zbigniewsobiecki merged commit 12c578b into dev Apr 15, 2026
9 checks passed
@zbigniewsobiecki zbigniewsobiecki deleted the feat/005-linear-project-scope branch April 15, 2026 21:04
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.

1 participant