feat(linear): optional Linear Project scope for PM integration#1116
Merged
zbigniewsobiecki merged 3 commits intodevfrom Apr 15, 2026
Merged
feat(linear): optional Linear Project scope for PM integration#1116zbigniewsobiecki merged 3 commits intodevfrom
zbigniewsobiecki merged 3 commits intodevfrom
Conversation
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 Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
LinearConfig.projectId?threaded throughlistIssues/createIssue/provider adapter;LinearRouterAdapter.parseWebhook()drops out-of-scope Issue / Comment / IssueLabel events with a structuredlogger.infoentry; tRPClinearProjects/linearProjectsByProjectdiscovery procedures;linearClient.getTeamProjects(teamId, first = 250).SearchableSelectin 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.LinearWebhookInfoPanelgains a short callout explaining that the filter runs on CASCADE's side — Linear webhook config stays team-scoped and unchanged.src/integrations/README.mdoperator-setup paragraph + single operator-facing CHANGELOG entry.Design highlights (read-before-merge)
LinearConfig.projectIdbeing truthy. Existing installations without the field: no migration, no behavior change, no env var.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.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.getTeamProjectsships withfirst: 250(Linear's max). Large orgs are covered; very-large-team fallback would need cursor pagination (not warranted today).projectIdagainst 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.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) backend→feat(linear) UI. Each reviewable in isolation:Test plan
npm run typecheck— cleannpm run lint— clean (biome)npm test— 7668 tests pass (54 new)npm run build— passesSpec / plans
docs/specs/005-linear-project-scope.md.donedocs/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