Skip to content

Release: dev → main (Linear integration + wizard/trigger fixes)#1122

Merged
zbigniewsobiecki merged 57 commits intomainfrom
dev
Apr 16, 2026
Merged

Release: dev → main (Linear integration + wizard/trigger fixes)#1122
zbigniewsobiecki merged 57 commits intomainfrom
dev

Conversation

@zbigniewsobiecki
Copy link
Copy Markdown
Member

Summary

Promotes 57 commits from dev to main570 files, +24k/-6.5k. The bulk is the new Linear PM integration landing end-to-end, plus today's fix chain (wizard state-ID mapping, worker credentials, Linear API auth header, full label parity) and a pile of refactors, dashboard fixes, and dependabot bumps.

Headlines

Linear PM integration (end-to-end)

  • New LinearPMProvider, GraphQL client, discovery (teams/states/labels/projects), webhook ingestion, dispatch + trigger handlers (status-changed, label-added, comment-mention), worker wiring.
  • Dashboard PM wizard exposes Linear as a first-class option with ID-backed dropdowns for states and labels (new in today's fixes) and create-label affordances matching Trello's UX.
  • Optional project scope filter: CASCADE projects can narrow to a specific Linear Project within a team; out-of-scope events are dropped at the router.
  • Reaction on Comment.create webhook events (Linear-style acknowledgment).
  • CLI + dashboard now show Linear webhook setup info, and webhook logs record Linear events.

Today's fix chain (newly landed; see PRs #1117, #1118, #1119, #1121)

  • fix(linear): store state IDs, not names, in status mappings — wizard now persists workflow-state UUIDs; webhooks' strict-equality match actually fires.
  • fix(linear): pass projectId to extractProjectIdFromJob for linear jobs — the router now resolves + injects credentials into the worker for Linear jobs.
  • fix(linear): send personal API keys without Bearer prefix — Linear personal API keys must be sent bare; the router's divergent call sites (ack comment + bot-identity resolver) were using OAuth-style headers and failing with HTTP 400.
  • feat(linear): full label parity with Trello — label mappings now use an ID-backed dropdown sourced from the team's labels + per-slot and batch "Create" buttons that provision the CASCADE defaults in one click.

JIRA / PM parity

Review agent (#1111)

  • External-fork PR checkout fixed.
  • Pre-fetch replaced with compact per-file diffs + SKIPPED FILES contract (200k-token budget, per-file 10% cap).
  • Fix: dedupe GitHub review dispatches by delivery id and head sha.

Dashboard

Agent / trigger refactors

Dependency bumps

Vite, picomatch, axios, hono, @hono/node-server, follow-redirects (all minor/patch).

Operator action required after deploy

  1. Linear-backed projects on prod need a one-time PM wizard re-run. The wizard's state and label mappings now store UUIDs (not names). Saved configs from before this release have names stored, so the new dropdowns will appear empty. Re-pick each Linear workflow state in Status Mappings, and either re-pick or click Create All Missing in Label Mappings.

  2. Prod Caddy already routes /linear/* and /sentry/* to cascade-router — committed to mongrel-intelligence/infra main earlier, already deployed to bauer. No action needed.

  3. Prod router + worker + dashboard images will be rebuilt and redeployed automatically by .github/workflows/deploy.yml on merge to main.

Test plan

🤖 Generated with Claude Code

zbigniewsobiecki and others added 30 commits April 1, 2026 19:12
Add a hub-and-spoke architecture documentation suite covering all backend
subsystems (excluding tests and dashboard web frontend). The hub document
provides a system overview with Mermaid diagrams and links to 10 deep-dive
documents covering services, webhook pipeline, triggers, agents, engines,
integrations, gadgets, config/credentials, database, and resilience.

Also adds 36 unit tests validating doc structure and cross-references,
and removes a stale biome suppression comment in project-harness-form.tsx.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(crypto): validate CREDENTIAL_MASTER_KEY format at startup

* fix(deps): resolve high-severity lodash-es vulnerability via overrides

Add npm overrides to force lodash-es>=4.18.1, resolving GHSA-r5fr-rjxr-66jc
(Code Injection via _.template) and GHSA-f23m-r3pf-42rh (Prototype Pollution)
which affected transitive deps chevrotain/js-toml/llmist. Also updates
brace-expansion to 2.0.3 to resolve moderate GHSA-f886-m6hf-6m8v.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Cascade Bot <bot@cascade.dev>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): invalidate user sessions on password change

* fix(deps): add overrides to resolve high-severity npm audit vulnerabilities

Add npm overrides for lodash@^4.18.1, lodash-es@^4.18.1, and
brace-expansion@^2.0.3 to address high-severity CVEs in transitive
dependencies (archiver → lodash, @llmist/cli → chevrotain → lodash-es).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Cascade Bot <bot@cascade.dev>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: zbigniew sobiecki <zbigniew@sobiecki.name>
…ompts (#1074)

Co-authored-by: Cascade Bot <bot@cascade.dev>
Co-authored-by: Cascade Bot <bot@cascade.dev>
…ges (#1076)

Co-authored-by: Cascade Bot <bot@cascade.dev>
…te legacy functions (#1077)

Co-authored-by: Cascade Bot <bot@cascade.dev>
…grations/bootstrap.ts (#1078)

* fix(pm): consolidate PM registration to single canonical path in integrations/bootstrap.ts

* fix(test): add bootstrap import to pm-provider-switching integration test

After removing the side-effect registration from src/pm/index.ts, the
pm-provider-switching integration test no longer had pmRegistry populated,
causing the pmRegistry assertions and createPMProvider calls to fail.
Add the canonical bootstrap import (matching integration-validation.test.ts).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Cascade Bot <bot@cascade.dev>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…0.2.91 (#1079)

Co-authored-by: Cascade Bot <bot@cascade.dev>
…nd (#1080)

Co-authored-by: Cascade Bot <bot@cascade.dev>
…oncurrency (#1081)

* refactor(triggers): extract shared webhook utilities and add Sentry concurrency

* fix(triggers): move startWatchdog inside concurrency callback and fix README

- Move startWatchdog() inside the execute closure (GitHub) and inside the
  concurrency callback (Sentry) so it only fires when an agent actually runs.
  Previously the watchdog timer started before withAgentTypeConcurrency, so
  a concurrency-blocked job would tick a timer that could fire process.exit(1)
  after the container finished its work.
- Fix Worker-Side Handler Comparison table: GitHub now uses withAgentTypeConcurrency
  (not checkAgentTypeConcurrency directly).
- Fix Shared Utilities table: remove PM from the "Used By" column for concurrency.ts
  since src/pm/webhook-handler.ts still uses raw checkAgentTypeConcurrency.
- Update flow diagrams to show startWatchdog nested inside withAgentTypeConcurrency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(triggers): fix inaccurate "Used By" entries and module comment

- trigger-resolution.ts: correct "Used By" to "Sentry (GitHub and PM use
  inline logic)" — GitHub handler uses inline if/else, not resolveTriggerResult
- pm-ack.ts: correct "Used By" to "GitHub worker handler" — the router adapter
  has its own local postPMAck and does not import the shared utility
- pm-ack.ts module comment: remove false claim about router-side usage; add
  note clarifying the router adapter has its own local function
- GitHub webhook flow diagram: replace resolveTriggerResult with accurate
  inline dispatch description

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Cascade Bot <bot@cascade.dev>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…e-driven alternatives (#1082)

Co-authored-by: Cascade Bot <bot@cascade.dev>
…recated alternatives (#1083)

Co-authored-by: Cascade Bot <bot@cascade.dev>
…to capability strings (#1084)

Co-authored-by: Cascade Bot <bot@cascade.dev>
…rm.tsx (#1085)

Co-authored-by: Cascade Bot <bot@cascade.dev>
…ocedure (#1086)

Co-authored-by: Cascade Bot <bot@cascade.dev>
* chore: remove Squint codebase intelligence support

* chore: remove leftover SQUINT_DB_PATH entries from engine env allowlists

Remove dead `SQUINT_DB_PATH` entries from the engine environment allowlists
in claude-code, codex, and opencode backends (both env.ts and index.ts), and
remove the misleading documentation example in docs/adding-engines.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Cascade Bot <bot@cascade.dev>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(pm): add Linear PM provider adapter and integration

* fix(linear): address review feedback on Linear PM provider adapter

- 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>

---------

Co-authored-by: Cascade Bot <bot@cascade.dev>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Security patches for two advisories blocked on Dependabot:

- vite 6.4.1 → 6.4.2: path traversal in optimize deps sourcemap handler,
  server.fs check for env transport (vitejs/vite#22161, #22159)
- picomatch 4.0.3 → 4.0.4: CVE-2026-33671, CVE-2026-33672

Replaces #1088 and #1058, which were stuck on CI because Dependabot's
lockfile regeneration produced a divergent lockfile vs. dev (dropped
@trpc/server and react-is resolved entries, added platform-specific
tailwindcss-oxide-wasm32-wasi nested entries). Rather than iterate on
@dependabot recreate, bundled both bumps into a single manual PR with
a lockfile regenerated from dev's current state.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(linear): add Linear webhook ingestion in router

* fix(linear): implement isSelfAuthored to prevent infinite comment loop

Add resolveLinearBotUserId() using the Linear GraphQL viewer query,
mirroring the JIRA bot identity pattern. isSelfAuthored() now compares
the comment payload's userId against the bot's user ID so bot-posted
ack comments are filtered before trigger dispatch, preventing an infinite
create/Comment webhook loop when triggers match comment creation events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Cascade Bot <bot@cascade.dev>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(linear): add Linear worker dispatch and trigger handlers

* fix(linear): address review feedback on duplication and dead code

- Extract STATUS_TO_AGENT to src/triggers/shared/status-to-agent.ts and
  re-export from jira/types.ts and linear/types.ts to eliminate duplication
- Replace inline Linear viewer query in comment-mention.ts with the existing
  resolveLinearBotUserId() from bot-identity-resolvers.ts (cached, no raw fetch)
- Clean up hasMention(): remove dead code branch and fix inconsistent JSDoc

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Cascade Bot <bot@cascade.dev>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
aaight and others added 27 commits April 14, 2026 22:53
Co-authored-by: Cascade Bot <bot@cascade.dev>
…ntegration (#1102)

Co-authored-by: Cascade Bot <bot@cascade.dev>
…overy methods (#1104)

Co-authored-by: Cascade Bot <bot@cascade.dev>
Co-authored-by: Cascade Bot <bot@cascade.dev>
…vents (#1106)

Co-authored-by: Cascade Bot <bot@cascade.dev>
… tool (#1108)

Co-authored-by: Cascade Bot <bot@cascade.dev>
…ponents (#1110)

Co-authored-by: Cascade Bot <bot@cascade.dev>
…mpact diffs, add SKIPPED FILES contract (#1111)

Spec [001 (pr-review-correctness)](docs/specs/001-pr-review-correctness.md.done) implemented end-to-end. Root cause of the PR #1092 incident: silent `git checkout` failure on external-fork PRs + a 25k-token full-file pre-fetch that fit only 20 of 129 changed files into the agent's view. This PR fixes both, plus bundles in-flight Linear/PM-wizard work and clears 3 pre-existing biome cognitive-complexity warnings.

Highlights:
- Plan 001/1: `refs/pull/N/head` checkout (works for forks), fail-loud setup, HEAD-SHA verification, full pagination of 7 GitHub list endpoints.
- Plan 001/2: Compact per-file diffs replace full-file pre-fetch; structured self-documenting `SKIPPED FILES` injection benefits all 5 agents that consume `prContext`.
- Bundled: pm-wizard provider-switch credential cleanup; `web/tsconfig.json` extends include for shared `getCredentialRoles`.
- Cleanup: `users.ts`/`webhooks.ts`/`pm-wizard.tsx` complexity warnings cleared via focused helpers.

Tests: 7538/7538 unit pass (+54 new), typecheck/lint/build clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
#1112)

* fix(linear): remove Bearer prefix from API key auth header + fix biome config

**Bug fix:**
Linear personal API keys (lin_api_*) must be sent as bare Authorization
tokens — not Bearer tokens. Bearer is for OAuth access tokens only.
Every outbound Linear API call (getMe, getTeams, createIssue, etc.) was
broken for any project using a personal key.

Reproducer: curl -H "Authorization: Bearer lin_api_..." returned
"Remove the Bearer prefix from the Authorization header."

**Also included:**
- Add logger.error in wrapIntegrationCall so integration failures are
  visible in server logs (previously swallowed silently)
- Add getMe unit tests (the method that was failing in the wizard)
- Fix biome.json: schema ref was 2.4.10 but installed biome is 1.9.4;
  also renamed assist→organizeImports, includes→ignore (1.9.x keys)
- Run biome check --write to apply all import-sort and style fixes
  across the entire codebase (pre-existing issues, now unblocked)
- Add biome override to keep noDelete off for src/ and tests/ —
  delete process.env.X is semantically required (= undefined coerces
  to the string "undefined" in Node.js process.env)
- Remove stale biome-ignore comment in llm-call-list.tsx

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(lint): align biome.json with installed version 2.4.10

Local node_modules had biome 1.9.4 (stale from before #1075), so the
previous commit wrote a 1.9.4 config that broke CI (which installs fresh
2.4.10). After running npm install locally, re-wrote config for 2.4.10:
- Restore assist/files.includes keys (valid 2.x API)
- Keep noDelete: off override (src/ + tests/) — delete process.env.X
  is semantically required; = undefined coerces to "undefined" string
- Add noArrayIndexKey suppression back to llm-call-list (2.4.10 triggers
  the rule even for compound keys; suppression is warranted — read-only list)
- Re-apply biome 2.4.10 import sort across modified files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(plans): add spec 002 and plans; lock plan 002/1

Spec 002 (linear-webhook-setup-ux) introduces the Linear wizard UX
improvements and save-path fix. Plan 1 locked for execution.

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

* feat(dashboard): structured error logging for tRPC + Hono

Adds src/api/errorLogging.ts with formatters that surface pg driver
fields (code, detail, constraint, table) onto server-side logs for
both Hono app.onError and the tRPC errorFormatter. Client responses
for INTERNAL_SERVER_ERROR now carry a generic message; real details
remain in cascade-dashboard-* stdout for operators to grep.

plan 002/1 task 1.

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

* fix(db): allow provider=linear under category=pm in integration check constraint

The chk_integration_category_provider check constraint (from
0047_add_alerting_integration.sql) only permitted trello/jira for
pm-category integrations. Linear support was introduced without a
matching constraint update, so every projects.integrations.upsert
for category=pm + provider=linear failed with SQLSTATE 23514 and
surfaced as HTTP 500 from the dashboard Linear wizard.

Migration 0049 drops and re-creates the constraint with linear added
to the pm branch. Forward-only, no data migration required.
Reproduced with two new integration tests against the test DB.

plan 002/1 tasks 2-3 (diagnose + fix).

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

* test(api): assert plaintext credentials never leak into server logs

Integration tests that spy on console.{log,error,warn} around
writeProjectCredential, upsertProjectIntegration (happy + FK-violation
paths), and the formatTRPCErrorLog formatter. Asserts the sentinel
credential value never appears in captured output, while confirming
the real PG error (SQLSTATE + constraint name) IS present on the
failure path — proving diagnosability without leakage.

plan 002/1 task 4.

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

* docs(plans): plan 002/1 (save-path-fix) done — unblock Linear wizard Save

All 11 per-plan ACs pass. Spec ACs #5-8 delivered:
- Linear upsert succeeds on fresh projects and re-configuration
- DB error diagnostics (SQLSTATE, constraint, detail) surface in server logs
- tRPC clients receive generic 'Internal server error' for unexpected throws
- Plaintext credentials never leak into captured stdout/stderr

Caveat: CLAUDE.md addition deferred to avoid conflicting with the
user's in-progress rewrite of that file. Doc impact met via CHANGELOG.

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

* docs(plans): update plan 002/1 progress checkboxes to done

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

* docs(plans): lock plan 002/2 + record testing-approach divergence

Plan 2 originally assumed @testing-library/react in web/; project has no
such infrastructure and the web/ package has no test script. Plan edited
in place to:
- use react-dom/server.renderToStaticMarkup (available; no new deps)
- test under the existing root unit-core project
- drop interactive-mutation tests (would need jsdom); ProjectSecretField
  internals are out-of-scope per spec non-goal

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

* docs(plans): plan 002/2 frontmatter status -> wip

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

* feat(dashboard): linear wizard UX — accurate events list and inline signing secret

Plan 002/2 (wizard-webhooks-step).

Linear PM wizard Webhooks step now:
- lists the three event families CASCADE consumes (Issues, Comments,
  Issue Labels), each with a one-line rationale tracing to
  src/triggers/linear/
- renders a ProjectSecretField bound to LINEAR_WEBHOOK_SECRET directly
  beneath the webhook URL, matching the Sentry alerting tab pattern
- drops the 'store as LINEAR_WEBHOOK_SECRET in project credentials'
  trailing bullet (replaced by the inline input)

PMWizard threads projectId + the masked LINEAR_WEBHOOK_SECRET credential
meta from projects.credentials.list through WebhookStep, so the field
reflects its own stored value on mount and stays in sync with the
Credentials tab.

Tests: 17 new SSR tests using react-dom/server — no React testing
library required. Interactive mutation firing is out of scope (would
need jsdom); ProjectSecretField itself is untested today under the
same constraint and is spec non-goal to modify.

vitest.config.ts gains @/components, @/lib, @/hooks aliases pointing at
web/src/ for test-time resolution.

src/integrations/README.md Linear section defers to the dashboard
wizard as the authoritative setup source.

Plan 2 closes out spec 002.

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

* docs(claude): streamline CLAUDE.md to essentials

Compacts the project's CLAUDE.md from ~820 to ~163 lines by dropping
dated setup minutiae, duplicated process docs, and legacy sections.
Keeps the load-bearing context: architecture, PR checkout gotcha,
testing commands, Zod policy, migrations, GitHub dual-persona rules,
trigger system, engines, environment, and git hooks.

No code changes.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(plans): add spec 003 + plan; lock plan 003/1

Spec 003 introduces PM status mapping parity across Linear and JIRA.
Plan 1 (status-parity) locked for execution.

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

* docs(plans): plan 003/1 frontmatter status -> wip

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

* feat(pm): status mapping parity across Linear and JIRA

Plan 003/1 (status-parity).

Linear PM wizard Field Mapping step now exposes all 8 CASCADE stages
(backlog, splitting, planning, todo, inProgress, inReview, done,
merged) in lifecycle order instead of only 4. Linear operators can
now map workflow states to splitting/planning/todo/merged and have
the corresponding agents dispatch on issue transitions.

JIRA's silent-drop bug in resolveLifecycleConfig fixed: splitting/
planning/todo mappings the JIRA wizard already accepted now surface
through to PMLifecycleManager and GitHub PR triggers. No operator
action required.

Canonical ProjectPMConfig.statuses widens to declare the full 9-stage
vocabulary (including debug, reserved for future trigger), so
providers can no longer silently drift from the trigger layer.

Existing Linear integrations upgrade in place: new slots render as
'not set' on next wizard visit. No migration.

Tests: 9 new unit tests (type shape + Linear + JIRA integration + SSR
wizard). Integration coverage for spec ACs #8/#9 provided by existing
linear-status-changed and jira-status-changed trigger handler tests —
discovered during Phase 4 that handlers read provider-specific config
directly (not resolveLifecycleConfig), so dispatch was never blocked
for handlers; the drop was in downstream PMLifecycleManager callers.

Totals: 7597 unit + 522 integration all green. Lint + typecheck clean.

Closes spec 003.

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

* docs(specs): spec 003 (linear-status-mapping-parity) done — all plans complete

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(plans): add spec 004 + plan; lock plan 004/1

Spec 004 targets the credential role-resolution ambiguity that breaks
Linear wizard reopens and silently skips Linear webhook signature
verification in the router.

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

* docs(plans): plan 004/1 frontmatter status -> wip

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

* feat(config): disambiguate credential role resolution by provider

Plan 004/1 (disambiguate-role-resolver).

The credential-resolution helper iterated all providers in a category
looking for a role-name match and returned the first hit. Because the
built-in PM registration order is trello → jira → linear, this caused:

1. Dashboard: Linear wizard reopens demanded a re-typed API key every
   time. ('pm', 'api_key') resolved to TRELLO_API_KEY on a Linear-only
   project, which isn't configured, so 'Linear credentials not
   configured' surfaced in the Board/Project Selection step.

2. Router (production): Linear webhook signature verification silently
   returned the JIRA webhook secret (or null on Linear-only projects),
   causing signatures to silently skip verification.

Fix:
- roleToEnvVarKey(category, role) -> roleToEnvVarKey(category, provider, role)
- getIntegrationCredential[OrNull] now take (projectId, category,
  provider, role) — compiler rejects callers that omit provider.
- 42 call sites updated across 13 src/ files to pass the explicit
  provider.
- *ByProject dashboard endpoints look up project_integrations.provider
  first; throw NOT_FOUND with distinguishable message on missing row.
- resolveWebhookSecret branches pass the explicit provider per branch.

New tests:
- tests/unit/config/role-resolver.test.ts (9 tests)
- tests/unit/router/resolveWebhookSecret.test.ts (6 tests)
- integrationsDiscovery tests augmented with default provider mock.

Totals: 7612 unit + 522 integration all green. Lint + typecheck clean.

Closes spec 004.

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

* docs(specs): spec 004 (credential-role-provider-disambiguation) done — all plans complete

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
…oject-scope

feat(linear): optional Linear Project scope for PM integration
Linear webhooks deliver workflow-state UUIDs in data.stateId, but the
PM wizard's status-mapping dropdown was saving display names as the
option value. The status-changed trigger does strict-equality matching
on the saved value, so every Linear status transition since the
integration landed has been silently no-oping.

JIRA gets away with names because JIRA webhooks deliver names; Linear
does not.

Changed the option value to s.id (s.name remains as the label so the
dropdown still reads naturally). Added a regression test asserting
every state ID appears as an option value and no state name does, plus
a WHY comment to anchor the contract for future maintainers.

Operator action required after deploy: re-run the Status Mappings
wizard step on existing Linear-backed projects. Each saved name will
no longer match a value, so dropdowns will appear empty — re-pick from
the dropdown and save. New mappings persist as UUIDs.

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

fix(linear): store state IDs in status mappings
Linear jobs were not handled by extractProjectIdFromJob, so the router
returned null and skipped credential resolution. Workers spawned with
no credentials, then fell back to the DB and died with "Credential is
encrypted but CREDENTIAL_MASTER_KEY is not set" because workers
intentionally don't receive the master key.

Linear jobs already carry projectId on their payload (set by
LinearRouterAdapter.buildJob), so the fix is a single branch in the
projectId extractor.

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

fix(linear): pass projectId for credential resolution to worker
Linear personal API keys (lin_api_*) are sent bare in the Authorization
header — the `Bearer` prefix is OAuth-only, and using it with personal
keys triggers HTTP 400. The router's two Linear API call sites
(platformClients/linear.ts, bot-identity-resolvers.ts) had diverged
from the canonical client (src/linear/client.ts) and used the OAuth
pattern, breaking acknowledgment-comment posting and silently
disabling the Linear bot-identity self-loop check.

Also:
- Fix the misleading docblock at src/linear/client.ts:8 that documented
  `Bearer <api_key>` while the code correctly used a bare key — future
  maintainers would have copied the doc and reintroduced the bug.
- Improve linearGraphQL error messages in both the canonical and the
  router-side helpers to include the response body. Without it the
  failure surface was just an HTTP status, which made this very bug
  invisible until source-comparison.

Test coverage:
- Asserts each of postComment / deleteComment / updateComment sends
  bare API key, no Bearer prefix.
- Asserts the GraphQL mutation body and variables.
- Asserts the warning on HTTP failure includes the response body so
  diagnostics aren't lost again.

Out of scope (separate follow-up): linearLabels are stored as
free-text names but Linear's issueUpdate.labelIds requires UUIDs, so
the `cascade-processing` label isn't being applied. Same shape as the
status-mapping bug — needs a wizard UX change to fetch + present a
label dropdown.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Required because linearGraphQL now reads response.text() to include the
body in HTTP-error messages. The factory was incomplete (only mocked
ok/status/json) and would have failed for any real Response.text() call.

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

fix(linear): send personal API keys without Bearer prefix
The Linear PM wizard stored label mappings as free-text names, but
Linear's issueUpdate.labelIds rejects names and requires UUIDs — so the
cascade-processing / cascade-processed / cascade-error / cascade-ready /
cascade-auto labels were silently never applied to issues.

Brings Linear to the same UX as Trello:

Backend
- linearClient.createLabel(teamId, name, color?) wraps the issueLabelCreate
  GraphQL mutation, returning the new label's { id, name, color }.
- integrationsDiscovery.createLinearLabel + createLinearLabels tRPC
  mutations mirror the Trello equivalents (single + batch with
  per-label error reporting).

Wizard
- LinearFieldMappingStep replaces the free-text label inputs with a
  dropdown sourced from linearTeamDetails.labels (already fetched by
  discovery). Saves label UUIDs, not names.
- Per-slot "Create" button and a batch "Create All Missing" button use
  LINEAR_LABEL_DEFAULTS (name + hex color per slot).
- useLinearLabelCreation hook mirrors useTrelloLabelCreation (single +
  batch mutation, ADD_LINEAR_TEAM_LABEL dispatch on success).

Adapter hardening
- LinearPMProvider.resolveLabelId validates the resolved value is UUID-
  shaped. If the mapping is missing or holds a name (from a pre-existing
  configuration), addLabel/removeLabel short-circuit with a diagnostic
  warn instead of passing a bad value to Linear and failing opaquely.
- createWorkItem filters out non-UUID label values (same warn).

Operator note: projects configured before this PR have names stored
(e.g. processing: "cascade-processing"). Re-open the PM wizard → Label
Mappings step; dropdowns will show empty because the saved name no
longer matches a dropdown value. Pick from the dropdown — or click
"Create All Missing" to provision defaults in one click.

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

feat(linear): full label parity with Trello — dropdown + create-label UX
@zbigniewsobiecki zbigniewsobiecki merged commit 244b32d into main Apr 16, 2026
14 of 15 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