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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ All notable user-visible changes to CASCADE are documented here. The format is l
### Internal

- **PM integration plug-and-play (infrastructure).** Introduced `PMProviderManifest` as the canonical per-provider contract — one object declares credentials, webhook route and verifier, router adapter, trigger handlers, platform client, job-id extractor, and optional label-creation hook. Landed `pmProviderRegistry`, a conformance test harness (`tests/unit/integrations/pm-conformance.test.ts`), shared helpers (`_shared/auth-headers.ts`, `_shared/webhook-verifier.ts`, `_shared/label-id-resolver.ts`, `_shared/project-id-extractor.ts`), a new `pm.discovery` tRPC router, and a frontend provider-wizard registry with a generic step renderer. Dormant in this release — Trello, JIRA, and Linear continue to register through the legacy path; they migrate onto the manifest in follow-up PRs. No operator-visible changes. Closes plan 006/1 of spec [006](docs/specs/006-pm-integration-plug-and-play.md).
- **PM integration plug-and-play (Trello migrated).** Trello's webhook signature verifier, router adapter, triggers, platform client, job-id extractor, wizard steps, and label/custom-field creation hooks are now composed via a single `trelloManifest` + `trelloProviderWizard`. Extended the `ProviderWizardDefinition` contract with an optional `useProviderHooks` field so provider-specific React hooks run inside a shell component — `ManifestProviderWizardSection` — rather than at the wizard root; this is how we satisfy the React rules-of-hooks while still keeping Trello's Discovery/LabelCreation/CustomFieldCreation hook composition per-provider. The conformance harness now exercises Trello alongside the test fixture (22 shared tests × provider). Trello's legacy registrations in `bootstrap.ts` stay for now because nine-plus call sites still use `pmRegistry.get('trello')` — plan 006/5 migrates those callers and deletes the legacy lines. No operator-visible changes. Closes plan 006/2 of spec [006](docs/specs/006-pm-integration-plug-and-play.md).

### Added

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ plan: 2
plan_slug: migrate-trello
level: plan
parent_spec: docs/specs/006-pm-integration-plug-and-play.md
depends_on: [1-infrastructure.md]
status: pending
depends_on: [1-infrastructure.md.done]
status: done
---

# 006/2: Migrate Trello onto the PM provider manifest
Expand Down Expand Up @@ -154,18 +154,36 @@ This keeps the three providers independently on either legacy or manifest paths

### 3. Delete Trello-specific legacy registrations

**Tests first**:
- `tests/unit/integrations/bootstrap.test.ts — does not register Trello` — post-migration, Trello is not in the legacy bootstrap output.
- `tests/unit/triggers/builtins.test.ts — does not register Trello triggers via legacy path` — but `pmProviderRegistry.get('trello').triggerHandlers` contains them.
- `tests/unit/router/worker-env.test.ts — extractProjectIdFromJob routes Trello via registry, not a hardcoded branch`.
**Drift — `pmRegistry` registration stays in 006/2.** The original task said to
remove the Trello block from `src/integrations/bootstrap.ts`. Rediscovered at
implementation time: at least nine call sites still use `pmRegistry.get('trello')`
(webhook handlers, manual runners, lifecycle, credential scoping, create-provider).
Deleting Trello from `pmRegistry` without migrating those callers would break
Trello at runtime. Plan 006/5 removes both registrations together (the bootstrap
line and the callers that depend on it). 006/2 scope narrows to the registrations
that are **safe** to delete because the manifest path already handles them:

**Implementation**:
- `src/integrations/bootstrap.ts` — remove the Trello `if (!pmRegistry.getOrNull('trello')) pmRegistry.register(new TrelloIntegration())` block.
- `src/triggers/builtins.ts` — remove `registerTrelloTriggers(registry)` call.
- `src/router/worker-env.ts` — remove the `if (jobData.type === 'trello')` branch; the registry path handles it.
- `web/src/components/projects/pm-wizard.tsx` — remove the `state.provider === 'trello'` rendering branch; the manifest path handles it.
- `src/integrations/bootstrap.ts` — **unchanged** in this plan. Trello stays in both `pmRegistry` and `pmProviderRegistry` during the migration window (same `TrelloIntegration` instance is referenced by both). Plan 006/5 deletes the bootstrap block.
- `src/triggers/builtins.ts` — remove `registerTrelloTriggers(registry)` call. Trello's triggers are now available via `pmProviderRegistry.get('trello').triggerHandlers`. The trigger registry that dispatches at runtime must be updated to iterate the manifest path for Trello (check `src/router/index.ts` + `src/triggers/registry.ts` to confirm the trigger dispatch pipeline picks up manifest-registered handlers).
- `src/router/worker-env.ts` — remove the `if (jobData.type === 'trello')` branch from the legacy chain. The `extractProjectIdFromJobViaRegistry` path now handles Trello via the manifest (`trelloManifest.extractProjectIdFromJob`).
- `web/src/components/projects/pm-wizard.tsx` — replace the `state.provider === 'trello'` rendering branch in each of the three step bodies (credentials/board/fields) with a render through `ManifestProviderWizardSection`. Remove the `useTrelloDiscovery` + `useTrelloLabelCreation` + `useTrelloCustomFieldCreation` hook instantiations from the parent — they're composed by `trelloProviderWizard.useProviderHooks` now.

### 4. Consolidate Trello tRPC discovery endpoints
**Tests**:
- Existing Trello SSR + behavior tests (`tests/unit/web/pm-wizard-trello-step.test.ts` etc.) must pass unchanged — byte-for-byte parity.
- Conformance harness runs Trello; already exercised in task 1's test file.

### 4. Consolidate Trello tRPC discovery endpoints — DEFERRED

**Status: deferred from plan 006/2**, landing post-merge as a separate PR.

The Trello manifest's `useProviderHooks` composes the existing `useTrelloLabelCreation` hook, which still calls `trpcClient.integrationsDiscovery.createTrelloLabel` / `createTrelloLabels`. Label creation works end-to-end via the manifest path using those existing endpoints.

Consolidating them into `pm.discovery.createLabel` / `pm.discovery.createLabels` is additive cleanup (removes two tRPC endpoint names, routes through manifest.createLabel); it doesn't change behavior or ship new capabilities. Shipping it as a separate follow-up keeps plan 006/2's diff focused on the migration itself. Plan 006/5 will delete the now-orphaned `createTrelloLabel`/`createTrelloLabels` endpoints once the follow-up lands.

---

### 4 (ORIGINAL PLAN). Consolidate Trello tRPC discovery endpoints

**Tests first** (`tests/unit/api/pm-discovery.test.ts`):
- `pm.discovery.createLabel — via registry for provider 'trello', creates label on board` — uses the shared endpoint instead of `createTrelloLabel`.
Expand Down Expand Up @@ -259,17 +277,17 @@ This keeps the three providers independently on either legacy or manifest paths
## Progress

<!-- /implement updates these as it works. Do not edit manually. -->
- [ ] AC #1 Trello manifest registered
- [ ] AC #2 Conformance harness passes Trello
- [ ] AC #3 Existing Trello tests green unchanged
- [ ] AC #4 Wizard Trello branch removed
- [ ] AC #5 Bootstrap Trello registration removed
- [ ] AC #6 Builtins Trello registration removed
- [ ] AC #7 Extractor Trello branch removed
- [ ] AC #8 Trello tRPC endpoints consolidated into pm.discovery
- [ ] AC #9 Operator-facing Trello behavior unchanged
- [ ] AC #10 All new code has tests
- [ ] AC #11 Build passes
- [ ] AC #12 Tests pass
- [ ] AC #13 Lint passes
- [ ] AC #14 Typecheck passes
- [x] AC #1 Trello manifest registered
- [x] AC #2 Conformance harness passes Trello (22 tests — 11 × 2 providers)
- [x] AC #3 Existing Trello tests green unchanged
- [x] AC #4 Wizard Trello branch removed — manifest shell + `manifestDef` path
- [ ] AC #5 Bootstrap Trello registration removed — **deferred to plan 006/5** (9+ call sites of `pmRegistry.get('trello')` still depend on it; documented divergence)
- [x] AC #6 Builtins Trello registration removed — `registerTrelloTriggers` gone; `listPMProviders()` iteration handles it
- [x] AC #7 Extractor Trello branch removed
- [ ] AC #8 Trello tRPC endpoints consolidated into pm.discovery — **deferred** (additive cleanup, not behavior-changing; follow-up PR documented in plan)
- [x] AC #9 Operator-facing Trello behavior unchanged — 20 web test files + integration tests green
- [x] AC #10 All new code has tests
- [x] AC #11 Build passes
- [x] AC #12 Tests pass (7755/7755)
- [x] AC #13 Lint passes
- [x] AC #14 Typecheck passes
4 changes: 2 additions & 2 deletions src/integrations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ CASCADE's PM providers (Trello, JIRA, Linear, and any future Asana/GitLab/ClickU

This document is the canonical guide for adding a new PM provider.

> **Migration status (plans 006/2–006/4 in flight):**
> The manifest contract landed in `docs/plans/006-pm-integration-plug-and-play/1-infrastructure.md` (plan 006/1 — this PR). Trello, JIRA, and Linear continue to register through the legacy path described at the bottom of this file until their individual migration PRs merge. When reading new-provider docs here, mentally substitute the provider you're adding; the three built-ins will follow suit over the next few PRs.
> **Migration status (plans 006/3–006/4 in flight):**
> **Trello: ✓ migrated** (plan 006/2). JIRA and Linear continue to register through the legacy path described at the bottom of this file until plans 006/3 and 006/4 merge. Trello's `pmRegistry` registration is kept in `src/integrations/bootstrap.ts` for now because many call sites still look up `pmRegistry.get('trello')`; plan 006/5 removes those callers and the bootstrap line together.

---

Expand Down
9 changes: 9 additions & 0 deletions src/integrations/pm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* PM provider barrel — side-effect imports register each provider manifest
* into `pmProviderRegistry` at module load.
*
* Order is registration order (deterministic for the wizard dropdown). Plans
* 006/3 and 006/4 will append `./jira/index.js` and `./linear/index.js`.
*/

import './trello/index.js';
11 changes: 10 additions & 1 deletion src/integrations/pm/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ import type { PlatformCommentClient } from '../../router/platformClients/types.j
import type { CascadeJob } from '../../router/queue.js';
import type { TriggerHandler } from '../../types/index.js';

// ParsedWebhookEvent is referenced transitively by RouterPlatformAdapter and
// isSelfAuthoredHook; re-exported so callers that want to type their hooks
// don't need to know the internal path.
export type { ParsedWebhookEvent };

/**
* One credential the provider needs resolved at runtime. Mirrors the shape
* already in use by `registerCredentialRoles()` in `src/config/integrationRoles.ts`.
Expand Down Expand Up @@ -71,9 +76,13 @@ export interface PMProviderManifest {
*/
readonly webhookRoute: string;
readonly verifyWebhookSignature: WebhookVerifier;
readonly parseWebhookPayload: (raw: unknown) => ParsedWebhookEvent | null;

// ── Router-side dispatch ────────────────────────────────────────────
/**
* Includes `parseWebhook(raw)` which yields a ParsedWebhookEvent for
* router-side project resolution and trigger dispatch. Provider-domain
* parsing (PMWebhookEvent) lives on `pmIntegration.parseWebhookPayload`.
*/
readonly routerAdapter: RouterPlatformAdapter;

/**
Expand Down
14 changes: 14 additions & 0 deletions src/integrations/pm/trello/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Trello PM provider — side-effect module that registers the manifest.
*
* Import this file once from `src/integrations/pm/index.ts` (the provider
* barrel). The registration happens at module load; re-imports are a no-op
* because Node caches modules.
*/

import { registerPMProvider } from '../registry.js';
import { trelloManifest } from './manifest.js';

registerPMProvider(trelloManifest);

export { trelloManifest };
92 changes: 92 additions & 0 deletions src/integrations/pm/trello/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* Trello PM provider manifest.
*
* Wires the existing Trello implementation (TrelloIntegration, Trello
* router adapter, Trello triggers, TrelloPlatformClient) into the
* PMProviderManifest contract landed in plan 006/1.
*
* Signing: Trello uses HMAC-SHA1(rawBody + callbackUrl), NOT the shared
* HMAC-SHA256 factory. The manifest wires the existing
* `verifyTrelloSignature` helper from `src/webhook/signatureVerification.ts`
* and reconstructs the callback URL from `host` + `x-forwarded-proto`
* headers — consistent with how the router has always verified Trello
* webhooks (`src/router/webhookVerification.ts`).
*/

import { TrelloIntegration } from '../../../pm/trello/integration.js';
import { TrelloRouterAdapter } from '../../../router/adapters/trello.js';
import { TrelloPlatformClient } from '../../../router/platformClients/trello.js';
import { buildTrelloCallbackUrl } from '../../../router/webhookVerification.js';
import { TrelloCommentMentionTrigger } from '../../../triggers/trello/comment-mention.js';
import { ReadyToProcessLabelTrigger } from '../../../triggers/trello/label-added.js';
import {
TrelloStatusChangedBacklogTrigger,
TrelloStatusChangedMergedTrigger,
TrelloStatusChangedPlanningTrigger,
TrelloStatusChangedSplittingTrigger,
TrelloStatusChangedTodoTrigger,
} from '../../../triggers/trello/status-changed.js';
import { verifyTrelloSignature } from '../../../webhook/signatureVerification.js';
import type { PMProviderManifest, WebhookVerifier } from '../manifest.js';

const TRELLO_SIGNATURE_HEADER = 'x-trello-webhook';

const verifyTrelloWebhookSignatureViaManifest: WebhookVerifier = (rawBody, headers, secret) => {
if (secret === null) return true; // opt-out matches existing router behavior

const signature = readHeader(headers, TRELLO_SIGNATURE_HEADER);
if (!signature) return false;

const host = readHeader(headers, 'host');
const proto = readHeader(headers, 'x-forwarded-proto');
const callbackUrl = buildTrelloCallbackUrl(host, proto);

return verifyTrelloSignature(rawBody, callbackUrl, signature, secret);
};

function readHeader(headers: Record<string, string | undefined>, name: string): string | undefined {
if (headers[name] !== undefined) return headers[name];
for (const key of Object.keys(headers)) {
if (key.toLowerCase() === name) return headers[key];
}
return undefined;
}

const trelloIntegration = new TrelloIntegration();

export const trelloManifest: PMProviderManifest = {
id: 'trello',
label: 'Trello',
category: 'pm',

credentialRoles: [
{ role: 'api_key', label: 'API Key', envVarKey: 'TRELLO_API_KEY' },
{ role: 'token', label: 'Token', envVarKey: 'TRELLO_TOKEN' },
{ role: 'api_secret', label: 'API Secret', envVarKey: 'TRELLO_API_SECRET', optional: true },
],

webhookRoute: '/trello/webhook',
verifyWebhookSignature: verifyTrelloWebhookSignatureViaManifest,

routerAdapter: new TrelloRouterAdapter(),

extractProjectIdFromJob: async (jobData) => {
const d = jobData as unknown as { type?: string; projectId?: string };
if (d.type !== 'trello') return null;
return d.projectId ?? null;
},

pmIntegration: trelloIntegration,

triggerHandlers: [
new TrelloCommentMentionTrigger(),
TrelloStatusChangedSplittingTrigger,
TrelloStatusChangedPlanningTrigger,
TrelloStatusChangedTodoTrigger,
TrelloStatusChangedBacklogTrigger,
TrelloStatusChangedMergedTrigger,
new ReadyToProcessLabelTrigger(),
],

platformClientFactory: (projectId) => new TrelloPlatformClient(projectId),
};
1 change: 1 addition & 0 deletions src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Hono } from 'hono';
import { captureException, flush, setTag } from '../sentry.js';
// Bootstrap all integrations before any adapters are loaded
import '../integrations/bootstrap.js';
import '../integrations/pm/index.js';
import { initPrompts } from '../agents/prompts/index.js';
import { registerBuiltInEngines } from '../backends/bootstrap.js';
import { initAgentMessages } from '../config/agentMessages.js';
Expand Down
5 changes: 4 additions & 1 deletion src/router/worker-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ export async function extractProjectIdFromJob(data: CascadeJob): Promise<string
// Use type assertion since dashboard jobs are cast to CascadeJob
const jobData = data as unknown as { type: string; projectId?: string; repoFullName?: string };

if (jobData.type === 'trello' || jobData.type === 'jira' || jobData.type === 'linear') {
// `trello` is now handled by the manifest registry (plan 006/2). The
// remaining `jira` and `linear` branches will move to the registry in
// plans 006/3 and 006/4.
if (jobData.type === 'jira' || jobData.type === 'linear') {
return jobData.projectId ?? null;
}
if (jobData.type === 'github') {
Expand Down
15 changes: 11 additions & 4 deletions src/triggers/builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,24 @@
* 2. Importing and calling it here.
*/

import { listPMProviders } from '../integrations/pm/registry.js';
import { registerGitHubTriggers } from './github/register.js';
import { registerJiraTriggers } from './jira/register.js';
import { registerLinearTriggers } from './linear/register.js';
import type { TriggerRegistry } from './registry.js';
import { registerSentryTriggers } from './sentry/register.js';
import { registerTrelloTriggers } from './trello/register.js';

export function registerBuiltInTriggers(registry: TriggerRegistry): void {
registerTrelloTriggers(registry);
registerJiraTriggers(registry);
registerLinearTriggers(registry);
// Manifest-registered PM providers (Trello via 006/2; JIRA + Linear when
// plans 006/3 and 006/4 land) contribute their triggerHandlers here. The
// legacy `registerTrelloTriggers` etc. shrink as providers migrate.
for (const manifest of listPMProviders()) {
for (const handler of manifest.triggerHandlers) {
registry.register(handler);
}
}
registerJiraTriggers(registry); // migrates in plan 006/3
registerLinearTriggers(registry); // migrates in plan 006/4
registerGitHubTriggers(registry);
registerSentryTriggers(registry);
}
1 change: 1 addition & 0 deletions src/worker-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

// Bootstrap all integrations before processing any jobs
import './integrations/bootstrap.js';
import './integrations/pm/index.js';
import { registerBuiltInEngines } from './backends/bootstrap.js';
import { loadEnvConfigSafe } from './config/env.js';
import { loadConfig } from './config/provider.js';
Expand Down
Loading
Loading