diff --git a/CLAUDE.md b/CLAUDE.md index b4c68f24..b6cc7127 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,12 +26,12 @@ CASCADE runs as three services (no monolithic server mode): The extensible trigger system routes events to agents: ``` -Trello/JIRA/Sentry/GitHub Webhook → Router → Redis/BullMQ → Worker → TriggerRegistry → Agent → Code Changes → PR +Trello/JIRA/Linear/Sentry/GitHub Webhook → Router → Redis/BullMQ → Worker → TriggerRegistry → Agent → Code Changes → PR ``` - `src/router/` - Webhook receiver (enqueues jobs to Redis) - `src/webhook/` - Shared webhook handler factory, parsers, and logging -- `src/triggers/` - Event handlers (Trello/JIRA card moves, labels, GitHub PRs, Sentry alerts) +- `src/triggers/` - Event handlers (Trello/JIRA/Linear issue moves, labels, GitHub PRs, Sentry alerts) - `src/agents/` - AI agents (splitting, planning, implementation, review, debug, alerting, backlog-manager, resolve-conflicts) - `src/gadgets/` - Tools agents can use (PM/SCM/alerting operations, Tmux, Todo, file system) @@ -123,6 +123,7 @@ Lefthook runs pre-commit (lint, typecheck) and pre-push (unit tests, integration - `src/github/` - GitHub client, dual-persona model (personas.ts) - `src/trello/` - Trello API client - `src/jira/` - JIRA API client +- `src/linear/` - Linear API client - `src/sentry/` - Sentry API client and integration - `src/utils/` - Utilities (logging, repo cloning, lifecycle) - `web/` - Dashboard frontend (React 19, Vite, Tailwind v4, TanStack Router) @@ -140,7 +141,7 @@ provider-specific branching. | Category | Interface | Example providers | |----------|-----------|-------------------| -| `pm` | `PMIntegration` (extends `IntegrationModule`) | Trello, JIRA | +| `pm` | `PMIntegration` (extends `IntegrationModule`) | Trello, JIRA, Linear | | `scm` | `SCMIntegration` (extends `IntegrationModule`) | GitHub | | `alerting` | `AlertingIntegration` (extends `IntegrationModule`) | Sentry | @@ -178,7 +179,7 @@ for `hasIntegration()` to return `true`. ### Bootstrap -`src/integrations/bootstrap.ts` is the single registration point for all four built-in +`src/integrations/bootstrap.ts` is the single registration point for all five built-in integrations. It is safe to import from both the router and worker — it does not pull in the agent execution pipeline or template files. @@ -208,7 +209,7 @@ Optional (infrastructure): - `SENTRY_RELEASE` - Release identifier for source maps (e.g., git SHA) - `SENTRY_TRACES_SAMPLE_RATE` - Trace sampling rate 0.0-1.0 (default: 0.1) -**Project credentials** (`GITHUB_TOKEN_IMPLEMENTER`, `GITHUB_TOKEN_REVIEWER`, `TRELLO_API_KEY`, `TRELLO_TOKEN`, LLM API keys) are stored in the `project_credentials` table — project-scoped, encrypted at rest when `CREDENTIAL_MASTER_KEY` is set. All credentials (integration tokens and LLM keys) use the same `project_credentials` table keyed by `(projectId, envVarKey)`. There is no env var fallback — the database is the sole source of truth for project-scoped secrets. +**Project credentials** (`GITHUB_TOKEN_IMPLEMENTER`, `GITHUB_TOKEN_REVIEWER`, `TRELLO_API_KEY`, `TRELLO_TOKEN`, `LINEAR_API_KEY`, `LINEAR_WEBHOOK_SECRET`, LLM API keys) are stored in the `project_credentials` table — project-scoped, encrypted at rest when `CREDENTIAL_MASTER_KEY` is set. All credentials (integration tokens and LLM keys) use the same `project_credentials` table keyed by `(projectId, envVarKey)`. There is no env var fallback — the database is the sole source of truth for project-scoped secrets. ## Database Configuration @@ -218,8 +219,8 @@ CASCADE stores all project configuration in PostgreSQL. The `config/projects.jso - `organizations` - Organization definitions (multi-tenant support) - `projects` - Per-project config (repo, base branch, budget, engine, and per-project overrides for model, iterations, timeouts, progress model/interval, `run_links_enabled`, `max_in_flight_items`) -- `project_integrations` - Integration configs per project with `category` (pm/scm), `provider` (trello/jira/github), `config` JSONB, and `triggers` JSONB. One PM + one SCM per project (enforced by unique constraint) -- `project_credentials` - Project-scoped credentials keyed by `(projectId, envVarKey)`. Stores all credential types (GitHub tokens, Trello keys, JIRA tokens, LLM API keys). Encrypted at rest when `CREDENTIAL_MASTER_KEY` is set +- `project_integrations` - Integration configs per project with `category` (pm/scm), `provider` (trello/jira/linear/github), `config` JSONB, and `triggers` JSONB. One PM + one SCM per project (enforced by unique constraint) +- `project_credentials` - Project-scoped credentials keyed by `(projectId, envVarKey)`. Stores all credential types (GitHub tokens, Trello keys, JIRA tokens, Linear API keys, LLM API keys). Encrypted at rest when `CREDENTIAL_MASTER_KEY` is set - `agent_configs` - Per-agent-type overrides (model, iterations, engine, `agent_engine_settings`, max_concurrency, `system_prompt`, `task_prompt`), project-scoped only (`project_id NOT NULL`) - `agent_definitions` - Agent YAML definitions (built-in and custom). Each row stores the full definition JSONB, keyed by `agent_type` - `agent_trigger_configs` - Configured trigger events per project/agent pair (replaces legacy `project_integrations.triggers`) @@ -380,7 +381,7 @@ cascade projects trigger-set --agent review --event scm:review-requ | Event | Providers | Description | |-------|-----------|-------------| -| `pm:status-changed` | Trello, JIRA | Trigger when card/issue moves to agent's target status | +| `pm:status-changed` | Trello, JIRA, Linear | Trigger when card/issue moves to agent's target status | | `pm:label-added` | All | Trigger when Ready to Process label is added | ```bash @@ -453,6 +454,44 @@ cascade projects update --agent-engine opencode The OpenCode engine is implemented in `src/backends/opencode/`. Configure with `cascade agents create --engine opencode` or via the Agent Configs tab in the dashboard. +## Linear Integration + +CASCADE supports Linear as a PM provider. When Linear issues change state or labels are applied, they are routed to the appropriate agents. + +- **Linear client**: `src/linear/` — API client and type definitions +- **PM integration**: `src/pm/linear/integration.ts` — implements `PMIntegration` +- **Triggers**: `src/triggers/linear/` — `status-changed.ts`, `label-added.ts`, `comment-mention.ts` +- **Webhook route**: `/linear/webhook` in `src/router/index.ts` + +### Credentials + +Store Linear credentials via the dashboard (Project Settings > Credentials tab) or CLI: + +```bash +cascade projects credentials-set --key LINEAR_API_KEY --value lin_api_... +cascade projects credentials-set --key LINEAR_WEBHOOK_SECRET --value # optional +``` + +### Configuration + +Configure a project to use Linear as its PM provider: + +```bash +cascade projects integration-set --category pm --provider linear \ + --config '{"teamId":"TEAM_ID","statuses":{"todo":"Todo","inProgress":"In Progress","inReview":"In Review","done":"Done"}}' +``` + +Linear uses **issue identifiers** (`TEAM-123` format) as work item IDs. Issues belong to **teams** (equivalent to Trello boards or JIRA projects). + +### Webhook Setup + +Register your CASCADE webhook URL with Linear in your team settings: +``` +https:///linear/webhook +``` + +If `LINEAR_WEBHOOK_SECRET` is configured, the router verifies the `Linear-Signature` header on incoming payloads. + ## Sentry / Alerting Integration CASCADE integrates with Sentry for alert-driven automation. When Sentry issues or metric alerts arrive, they are routed to the `alerting` agent. diff --git a/docs/architecture/06-integration-layer.md b/docs/architecture/06-integration-layer.md index 8cc8bf4d..c9939274 100644 --- a/docs/architecture/06-integration-layer.md +++ b/docs/architecture/06-integration-layer.md @@ -10,7 +10,7 @@ The base contract for all integrations: ```typescript interface IntegrationModule { - readonly type: string; // 'trello', 'jira', 'github', 'sentry' + readonly type: string; // 'trello', 'jira', 'linear', 'github', 'sentry' readonly category: IntegrationCategory; // 'pm' | 'scm' | 'alerting' withCredentials(projectId: string, fn: () => Promise): Promise; @@ -74,12 +74,13 @@ const integrationRegistry: IntegrationRegistry; // singleton `src/integrations/bootstrap.ts` -Single, idempotent registration point for all four built-in integrations. Safe to import from router, worker, and dashboard — it does not pull in the agent execution pipeline or template files. +Single, idempotent registration point for all five built-in integrations. Safe to import from router, worker, and dashboard — it does not pull in the agent execution pipeline or template files. ``` -TrelloIntegration → integrationRegistry + pmRegistry -JiraIntegration → integrationRegistry + pmRegistry -GitHubSCMIntegration → integrationRegistry +TrelloIntegration → integrationRegistry + pmRegistry +JiraIntegration → integrationRegistry + pmRegistry +LinearIntegration → integrationRegistry + pmRegistry +GitHubSCMIntegration → integrationRegistry SentryAlertingIntegration → integrationRegistry ``` @@ -93,6 +94,7 @@ Each provider declares its credential roles — the mapping from logical role na |----------|----------|---------------|----------------| | Trello | pm | `api_key` → `TRELLO_API_KEY`, `token` → `TRELLO_TOKEN` | `api_secret` | | JIRA | pm | `email` → `JIRA_EMAIL`, `api_token` → `JIRA_API_TOKEN` | `webhook_secret` | +| Linear | pm | `api_key` → `LINEAR_API_KEY` | `webhook_secret` → `LINEAR_WEBHOOK_SECRET` | | GitHub | scm | `implementer_token` → `GITHUB_TOKEN_IMPLEMENTER`, `reviewer_token` → `GITHUB_TOKEN_REVIEWER` | `webhook_secret` | | Sentry | alerting | `api_token` → `SENTRY_API_TOKEN` | `webhook_secret` | @@ -115,6 +117,15 @@ Each provider declares its credential roles — the mapping from logical role na - Status transitions via JIRA transition ID lookup - Issue key extraction via regex: `[A-Z][A-Z0-9]+-\d+` +### Linear (`src/pm/linear/`, `src/linear/`) + +- `LinearIntegration` implements `PMIntegration` +- `LinearPMProvider` implements `PMProvider` (issue CRUD, comments, labels, state transitions) +- `linearClient` — GraphQL/REST client with AsyncLocalStorage credential scoping +- Status transitions via Linear state ID lookup +- Issue identifier extraction via regex: `[A-Z][A-Z0-9]*-\d+` (e.g. `TEAM-123`) +- Work item URL format: `https://linear.app//issue/` + ### GitHub (`src/github/`) - `GitHubSCMIntegration` implements `SCMIntegration` diff --git a/src/integrations/README.md b/src/integrations/README.md index af38d22c..9a1b6d0c 100644 --- a/src/integrations/README.md +++ b/src/integrations/README.md @@ -14,7 +14,7 @@ branching in shared code. ``` IntegrationModule (base contract) -├── PMIntegration — project management (Trello, JIRA) +├── PMIntegration — project management (Trello, JIRA, Linear) ├── SCMIntegration — source control (GitHub) └── AlertingIntegration — monitoring/alerting (Sentry) ``` @@ -27,7 +27,7 @@ IntegrationModule (base contract) | `src/integrations/registry.ts` | `IntegrationRegistry` class + `integrationRegistry` singleton | | `src/integrations/scm.ts` | `SCMIntegration` interface (SCM-specific extension) | | `src/integrations/alerting.ts` | `AlertingIntegration` interface (alerting-specific extension) | -| `src/integrations/bootstrap.ts` | **One place** — registers all 4 built-in integrations | +| `src/integrations/bootstrap.ts` | **One place** — registers all 5 built-in integrations | | `src/integrations/index.ts` | Public barrel exports | | `src/pm/integration.ts` | `PMIntegration` interface (PM-specific extension) | | `src/pm/registry.ts` | `PMIntegrationRegistry` singleton (PM-specific; backward compat) | @@ -95,8 +95,8 @@ Implementation: `src/sentry/alerting-integration.ts` (`SentryAlertingIntegration ## Adding a new integration — step by step -The example below adds a hypothetical **Linear** PM integration. Adapt the names for your actual -provider and category. +The example below uses **Linear** as a PM integration (already implemented — see +`src/pm/linear/integration.ts`). Adapt the names for your actual provider and category. ### Step 1 — Implement the interface @@ -458,5 +458,6 @@ Before submitting a new integration: |----------|----------|-------------|---------|---------| | `trello` | pm | `src/pm/trello/integration.ts` | `src/router/adapters/trello.ts` | `src/triggers/trello/` | | `jira` | pm | `src/pm/jira/integration.ts` | `src/router/adapters/jira.ts` | `src/triggers/jira/` | +| `linear` | pm | `src/pm/linear/integration.ts` | `src/router/adapters/linear.ts` | `src/triggers/linear/` | | `github` | scm | `src/github/scm-integration.ts` | `src/router/adapters/github.ts` | `src/triggers/github/` | | `sentry` | alerting | `src/sentry/alerting-integration.ts` | `src/router/adapters/sentry.ts` | `src/triggers/sentry/` | diff --git a/tests/unit/agents/shared/promptContext.test.ts b/tests/unit/agents/shared/promptContext.test.ts index d445094a..c7ce91c2 100644 --- a/tests/unit/agents/shared/promptContext.test.ts +++ b/tests/unit/agents/shared/promptContext.test.ts @@ -199,6 +199,90 @@ describe('buildPromptContext', () => { }); }); + describe('with Linear provider', () => { + beforeEach(() => { + const mockProvider = createMockPMProvider(); + mockProvider.type = 'linear' as never; + mockProvider.getWorkItemUrl = vi.fn((id: string) => `https://linear.app/myorg/issue/${id}`); + mockGetPMProvider.mockReturnValue(mockProvider); + }); + + it('sets workItemNoun to "issue" for Linear', () => { + const ctx = buildPromptContext('TEAM-123', makeProject() as never); + expect(ctx.workItemNoun).toBe('issue'); + }); + + it('sets workItemNounPlural to "issues" for Linear', () => { + const ctx = buildPromptContext('TEAM-123', makeProject() as never); + expect(ctx.workItemNounPlural).toBe('issues'); + }); + + it('sets workItemNounCap to "Issue" for Linear', () => { + const ctx = buildPromptContext('TEAM-123', makeProject() as never); + expect(ctx.workItemNounCap).toBe('Issue'); + }); + + it('sets workItemNounPluralCap to "Issues" for Linear', () => { + const ctx = buildPromptContext('TEAM-123', makeProject() as never); + expect(ctx.workItemNounPluralCap).toBe('Issues'); + }); + + it('sets pmName to "Linear"', () => { + const ctx = buildPromptContext('TEAM-123', makeProject() as never); + expect(ctx.pmName).toBe('Linear'); + }); + + it('sets pmType to "linear"', () => { + const ctx = buildPromptContext('TEAM-123', makeProject() as never); + expect(ctx.pmType).toBe('linear'); + }); + + it('generates workItemUrl from provider using Linear issue URL format', () => { + const ctx = buildPromptContext('TEAM-123', makeProject() as never); + expect(ctx.workItemUrl).toBe('https://linear.app/myorg/issue/TEAM-123'); + }); + + it('sets pipeline list IDs from Linear statuses', () => { + const linearProject = makeProject({ + trello: undefined, + pm: { type: 'linear' }, + linear: { + teamId: 'team-abc', + statuses: { + backlog: 'Backlog', + todo: 'Todo', + inProgress: 'In Progress', + inReview: 'In Review', + done: 'Done', + merged: 'Merged', + }, + }, + }); + const ctx = buildPromptContext('TEAM-1', linearProject as never); + expect(ctx.backlogListId).toBe('Backlog'); + expect(ctx.todoListId).toBe('Todo'); + expect(ctx.inProgressListId).toBe('In Progress'); + expect(ctx.inReviewListId).toBe('In Review'); + expect(ctx.mergedListId).toBe('Merged'); + }); + + it('leaves pipeline list IDs undefined when Linear statuses are missing', () => { + const linearProject = makeProject({ + trello: undefined, + pm: { type: 'linear' }, + linear: { + teamId: 'team-abc', + statuses: {}, + }, + }); + const ctx = buildPromptContext('TEAM-1', linearProject as never); + expect(ctx.backlogListId).toBeUndefined(); + expect(ctx.todoListId).toBeUndefined(); + expect(ctx.inProgressListId).toBeUndefined(); + expect(ctx.inReviewListId).toBeUndefined(); + }); + }); + describe('with prContext', () => { beforeEach(() => { const mockProvider = createMockPMProvider();