Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 47 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand All @@ -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 |

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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

Expand All @@ -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`)
Expand Down Expand Up @@ -380,7 +381,7 @@ cascade projects trigger-set <project-id> --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
Expand Down Expand Up @@ -453,6 +454,44 @@ cascade projects update <project-id> --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 <project-id> --key LINEAR_API_KEY --value lin_api_...
cascade projects credentials-set <project-id> --key LINEAR_WEBHOOK_SECRET --value <secret> # optional
```

### Configuration

Configure a project to use Linear as its PM provider:

```bash
cascade projects integration-set <project-id> --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://<your-cascade-host>/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.
Expand Down
21 changes: 16 additions & 5 deletions docs/architecture/06-integration-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(projectId: string, fn: () => Promise<T>): Promise<T>;
Expand Down Expand Up @@ -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
```

Expand All @@ -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` |

Expand All @@ -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/<org>/issue/<identifier>`

### GitHub (`src/github/`)

- `GitHubSCMIntegration` implements `SCMIntegration`
Expand Down
9 changes: 5 additions & 4 deletions src/integrations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```
Expand All @@ -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) |
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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/` |
84 changes: 84 additions & 0 deletions tests/unit/agents/shared/promptContext.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading