diff --git a/CHANGELOG.md b/CHANGELOG.md index 129fbd82..8a60cb0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ All notable user-visible changes to CASCADE are documented here. The format is l ### Internal +- **PM webhook-UX manifest migration complete (spec 012).** Closes the final gap from spec 011 — every PM wizard step, without exception, now renders via the manifest path. Plans 012/1-3 migrated Trello, JIRA, and Linear webhook steps into per-provider adapters composed of the shared `WebhookUrlDisplayStep` + provider-specific UX: Trello and JIRA each wrap the shared step with a programmatic "Create Webhook" button + active-webhooks list + per-webhook delete + curl fallback template (via existing `webhooks.*` tRPC endpoints with the `{trelloOnly|jiraOnly}` discriminator); Linear wraps it with a "Manual Webhook Setup Required" banner + `ProjectSecretField` (self-managing `LINEAR_WEBHOOK_SECRET` persistence) + 5-step manual-setup instructions. Plan 012/4 deleted the legacy `WebhookStep`, `LinearWebhookInfoPanel`, `useWebhookManagement`, `useLinearWebhookInfo`, the legacy `pm-wizard-webhooks-step.test.ts` file, and the `-webhook` id-skip filter introduced by plan 011/4. `pm-wizard-common-steps.tsx` now only exports `SaveStep`. `pm-wizard.tsx` iterates `manifestDef.steps` without exception; the legacy webhook slot is gone. No operator-visible regression. See spec [012](docs/specs/012-pm-webhook-manifest-migration.md.done). +- **PM wizard shared-component migration complete (spec 011).** Migrates the three production PM provider wizards (Trello, JIRA, Linear) off their per-provider step files and onto the shared `StandardStepKind` components landed by spec 010. Five plans landed: shared-components widenings (`container-pick` / `project-scope` `searchable?: boolean` → cmdk `Combobox`; `webhook-url-display` optional inline signing-secret input; 7th `StandardStepKind: custom-field-mapping` wired to `manifest.createCustomField`); Trello migration (OAuth popup stays as `kind: 'custom'` via `TrelloOAuthStep`; `labelDefaults?` + `fieldDefaults?` forward-edit additive widenings pre-populate Create inputs); JIRA migration (task/subtask mapping stays as `kind: 'custom'` via `IssueTypeMappingStep`; free-text label mode exercises the empty-`providerLabels` path); Linear migration (credentials, team picker, status/label/project-scope all shared; webhook step composes shared `WebhookUrlDisplayStep` with `ProjectSecretField` for `LINEAR_WEBHOOK_SECRET`); cleanup (the three `pm-wizard-{trello,jira,linear}-steps.tsx` files deleted, ≈1,085 lines of legacy UI retired). Plan 011/4 also fixed a latent regression plans 011/2 + 011/3 introduced: `pm-wizard.tsx` hardcoded 3 manifest step slots from the spec-006 era; it now iterates over `manifestDef.steps` dynamically, rendering one `WizardStep` per entry. Legacy `WebhookStep` (programmatic webhook registration for Trello/JIRA + Linear signing-secret UX) retained in its own slot — migration into the manifest path is follow-up scope. No operator-visible wizard-UX change beyond consistency: every provider now has searchable pickers, and Trello/JIRA gain inline custom-field create affordances. See spec [011](docs/specs/011-pm-wizard-shared-migration.md.done). +- **PM integration hardening follow-ups complete (spec 010).** Finishes the PM-layer cleanup started by spec 009. Three plans landed: generic `pm.discovery.createLabel(providerId, containerId, name, color?)` and `pm.discovery.createCustomField(providerId, containerId, name)` tRPC mutation endpoints + optional `createLabel` / `createCustomField` manifest hooks replace five per-provider wizard call sites; the `currentUser` `DiscoveryCapability` is declared on all three real providers (Trello `/members/me`, JIRA `/rest/api/3/myself`, Linear `viewer`) and served through the unified `pm.discovery.discover` endpoint; six real shared React step components (`credentials`, `container-pick`, `status-mapping`, `label-mapping`, `webhook-url-display`, `project-scope`) now live at `web/src/components/projects/pm-providers/steps/*.tsx` and the wizard generator dispatches to them via a `STANDARD_STEP_COMPONENTS` registry. Existing Trello/JIRA/Linear wizards continue to use their spec-006-era per-provider step adapters; the shared path is additive — a new PM provider with purely-standard steps now writes zero per-provider step components. The `new-provider-surface` snapshot guard is tightened to include the six step files. No operator-visible changes. See spec [010](docs/specs/010-pm-integration-hardening-followups.md.done). +- **PM integration hardening (spec 009).** Makes the `PMProviderManifest` a behavioral contract rather than a wiring convention. Five plans landed: branded `StateId` / `LabelId` / `ContainerId` types in `src/pm/ids.ts` (state-name-vs-ID confusion is now a compile error at direct-adapter call sites); manifest-owned Zod `configSchema` for each provider (the central `src/config/schema.ts` imports from `src/integrations/pm//config-schema.ts` — #1138/#1142 drift class becomes a round-trip CI failure); unified `pm.discovery.discover(providerId, capability, args)` tRPC endpoint driven by `manifest.discoveryCapabilities`; behavioral conformance harness (`tests/unit/integrations/pm-conformance.test.ts`) runs round-trip + lifecycle + webhook-verify + trigger-self-hook against every registered provider; single registration entrypoint at `src/integrations/entrypoint.ts` (router, worker, CLI, dashboard all import one file — guarded by `entrypoint-usage.test.ts`); shared `_shared/auth-headers.ts` helpers enforced by provenance test; `tests/unit/pm/linear/regression-2026-04.test.ts` locks in fixes for six Linear bug classes (#1112/#1117/#1118/#1119/#1131/#1133/#1134/#1137/#1138/#1139/#1142). No operator-visible changes. See spec [009](docs/specs/009-pm-integration-hardening.md.done). - **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). - **PM integration plug-and-play (JIRA migrated).** JIRA joins Trello on the manifest pattern with `jiraManifest` + `jiraProviderWizard`. `verifyWebhookSignature` uses the shared `makeHmacSha256Verifier` factory (Trello's bespoke scheme didn't fit, so this is the first consumer). Wizard steps + discovery / custom-field hooks moved into `jiraProviderWizard.useProviderHooks`; the JIRA-specific branches and hook instantiations are gone from `pm-wizard.tsx`. `worker-env.ts::extractProjectIdFromJob` JIRA branch removed (registry path handles it). Conformance harness now exercises Trello + JIRA + TestProvider (33 shared assertions × provider). Same deferrals as 006/2: `bootstrap.ts` JIRA registration stays until plan 006/5 migrates the `pmRegistry.get('jira')` callers. No operator-visible changes. Closes plan 006/3 of spec [006](docs/specs/006-pm-integration-plug-and-play.md). diff --git a/CLAUDE.md b/CLAUDE.md index 785061cf..357b70d2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,7 @@ Three separate services, **no monolithic server mode**: Flow: `PM/SCM/alerting webhook → Router → Redis → Worker → TriggerRegistry → Agent → Code → PR`. -Integration abstraction lives in `src/integrations/`. For **adding a new PM provider**, see @src/integrations/README.md — PM providers (Trello, JIRA, Linear) use the `PMProviderManifest` registry with a **behavioral conformance harness** (spec 009 — config round-trip, discovery shape, full lifecycle scenario, auth-header provenance, single-entrypoint invariant). Each provider owns its Zod config schema (`src/integrations/pm//config-schema.ts`) as the single source of truth — the central `src/config/schema.ts` imports it. PM adapter method signatures use branded `StateId` / `LabelId` / `ContainerId` from `src/pm/ids.ts` to make state-name-vs-ID confusion a compile error at direct-adapter call sites. All runtime surfaces (router, worker, CLI, dashboard) register integrations through a single entrypoint at `src/integrations/entrypoint.ts`. SCM (GitHub) and alerting (Sentry) still use the legacy `IntegrationModule` pattern via self-registration in `src/github/register.ts` + `src/sentry/register.ts`. Don't improvise; the README covers both patterns. +Integration abstraction lives in `src/integrations/`. For **adding a new PM provider**, see @src/integrations/README.md — PM providers (Trello, JIRA, Linear) use the `PMProviderManifest` registry with a **behavioral conformance harness** (spec 009 — config round-trip, discovery shape, full lifecycle scenario, auth-header provenance, single-entrypoint invariant). Each provider owns its Zod config schema (`src/integrations/pm//config-schema.ts`) as the single source of truth — the central `src/config/schema.ts` imports it. PM adapter method signatures use branded `StateId` / `LabelId` / `ContainerId` from `src/pm/ids.ts` to make state-name-vs-ID confusion a compile error at direct-adapter call sites. All runtime surfaces (router, worker, CLI, dashboard) register integrations through a single entrypoint at `src/integrations/entrypoint.ts`. **Spec 010 follow-ups** added generic `pm.discovery.createLabel` / `createCustomField` mutation endpoints + `currentUser` discovery capability + real shared React components for every `StandardStepKind` under `web/src/components/projects/pm-providers/steps/`. **Spec 011** migrated all three production providers (Trello, JIRA, Linear) onto those shared components, added a 7th `StandardStepKind: custom-field-mapping`, widened `container-pick` / `project-scope` / `webhook-url-display` with optional props, and deleted the three legacy `pm-wizard-{trello,jira,linear}-steps.tsx` files. **Spec 012** migrated each provider's webhook UX (programmatic create for Trello/JIRA, signing-secret + instructions for Linear) into per-provider manifest webhook adapters (Fragment compositions around the shared `WebhookUrlDisplayStep`); deleted the legacy `WebhookStep` + `LinearWebhookInfoPanel` + `useWebhookManagement` + `useLinearWebhookInfo`. Every PM wizard step now renders via the manifest path without exception. A new PM provider writes zero edits to shared orchestration (`pm-wizard.tsx`, `pm-wizard-common-steps.tsx`, `pm-wizard-hooks.ts`); provider-specific UI ships either as `kind: 'custom'` steps or as Fragment compositions inside the provider folder's wizard adapters. SCM (GitHub) and alerting (Sentry) still use the legacy `IntegrationModule` pattern via self-registration in `src/github/register.ts` + `src/sentry/register.ts`. Don't improvise; the README covers both patterns. ## PR checkout (worker) — gotcha diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f56c68ae..dddbfd7e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -120,7 +120,7 @@ This is enforced by commitlint via lefthook pre-commit hooks. See [CLAUDE.md](./CLAUDE.md) for a detailed architecture overview. Key directories: - `src/router/` — Webhook receiver (enqueues jobs to Redis) -- `src/triggers/` — Event handlers (Trello, GitHub, JIRA) +- `src/triggers/` — Event handlers (Trello, JIRA, Linear, GitHub) - `src/agents/` — AI agent implementations - `src/gadgets/` — Tools agents can use - `src/api/` — Dashboard API (tRPC) diff --git a/README.md b/README.md index 1dc3aadb..ab1eb33a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Node.js 22+](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org/) -> **Cascade orchestrates AI agents (Claude Code, Codex, opencode, LLMist) across your workflows in GitHub, Trello, and Jira.** +> **Cascade orchestrates AI agents (Claude Code, Codex, opencode, LLMist) across your workflows in GitHub, Trello, Jira, and Linear.** Cascade is an open-source platform that automates the full software development lifecycle. Connect your PM tool and GitHub repository, and Cascade drives work items from plan to merge: @@ -38,7 +38,7 @@ For the full setup walkthrough — projects, credentials, webhooks, and triggers ## ⚡ Features -- **Multi-PM support** — Works with Trello and JIRA out of the box +- **Multi-PM support** — Works with Trello, JIRA, and Linear out of the box - **11 agent types** — Splitting, planning, implementation, review, debug, respond-to-review, respond-to-CI, and more - **Dual-persona GitHub model** — Separate implementer and reviewer bot accounts to prevent feedback loops - **Web dashboard + CLI** — Monitor runs, manage projects, configure triggers @@ -151,7 +151,7 @@ All project-level credentials (GitHub tokens, PM keys, LLM API keys) are stored **Dual-persona GitHub model** — Cascade uses two separate GitHub bot accounts per project (implementer and reviewer) to prevent feedback loops. The implementer writes code and creates PRs; the reviewer reviews and approves them. -**Trigger system** — Events from Trello, JIRA, and GitHub webhooks are matched against registered `TriggerHandler` instances. Triggers are configured per-project in the database. +**Trigger system** — Events from Trello, JIRA, Linear, and GitHub webhooks are matched against registered `TriggerHandler` instances. Triggers are configured per-project in the database. **Agent engines** — Agents run through a shared execution lifecycle with a pluggable engine registry. Default engine is `claude-code` (Anthropic Claude Code SDK). Alternatives: `llmist` (supports OpenRouter, Anthropic, OpenAI), `codex` (OpenAI Codex CLI), `opencode` (OpenCode server). diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6562a2ef..ddf52cd8 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,6 +1,6 @@ # CASCADE Architecture -CASCADE is a PM-to-Code automation platform that connects project management tools (Trello, JIRA), source control (GitHub), and monitoring (Sentry) to AI-powered agents that autonomously implement features, review PRs, debug failures, and manage backlogs. Webhooks from external providers flow through a router, get queued in Redis, and are processed by ephemeral worker containers that run agents against cloned repositories. +CASCADE is a PM-to-Code automation platform that connects project management tools (Trello, JIRA, Linear), source control (GitHub), and monitoring (Sentry) to AI-powered agents that autonomously implement features, review PRs, debug failures, and manage backlogs. Webhooks from external providers flow through a router, get queued in Redis, and are processed by ephemeral worker containers that run agents against cloned repositories. > **Relationship to CLAUDE.md**: `CLAUDE.md` is the operational reference (commands, env vars, how-to). This document and its deep-dives cover the *system design* — how components fit together and why. @@ -11,6 +11,7 @@ graph TB subgraph External["External Providers"] Trello JIRA + Linear GitHub Sentry end @@ -30,6 +31,7 @@ graph TB Trello -->|webhook| Router JIRA -->|webhook| Router + Linear -->|webhook| Router GitHub -->|webhook| Router Sentry -->|webhook| Router @@ -39,6 +41,7 @@ graph TB Worker -->|PRs, comments| GitHub Worker -->|status updates| Trello Worker -->|status updates| JIRA + Worker -->|status updates| Linear Router <--> DB Worker <--> DB @@ -65,7 +68,7 @@ The canonical path from webhook to pull request: ```mermaid sequenceDiagram - participant P as Provider
(Trello/GitHub/JIRA/Sentry) + participant P as Provider
(Trello/JIRA/Linear/GitHub/Sentry) participant R as Router participant Q as Redis/BullMQ participant W as Worker @@ -110,10 +113,11 @@ sequenceDiagram | `src/backends/` | LLM execution engines: Claude Code, LLMist, Codex, OpenCode | | `src/gadgets/` | Tool implementations agents use (file ops, PM, SCM, alerting, shell) | | `src/integrations/` | Unified integration interfaces, registry, bootstrap | -| `src/pm/` | PM abstraction layer: provider interface, Trello/JIRA adapters, lifecycle | +| `src/pm/` | PM abstraction layer: provider interface, Trello/JIRA/Linear adapters, lifecycle | | `src/github/` | GitHub API client, dual-persona model, PR operations | | `src/trello/` | Trello API client | | `src/jira/` | JIRA API client (jira.js wrapper) | +| `src/linear/` | Linear GraphQL API client | | `src/sentry/` | Sentry API client, alerting integration | | `src/config/` | Configuration provider, caching, credential resolution, integration roles | | `src/db/` | Drizzle ORM schema, repositories, migrations | diff --git a/docs/architecture/01-services.md b/docs/architecture/01-services.md index f2769834..0f46957b 100644 --- a/docs/architecture/01-services.md +++ b/docs/architecture/01-services.md @@ -48,6 +48,7 @@ The router is the webhook ingestion point. It receives HTTP POST requests from e | `POST /trello/webhook` | Trello | HEAD/GET returns 200 for Trello's verification | | `POST /github/webhook` | GitHub | Injects `X-GitHub-Event` header into payload | | `POST /jira/webhook` | JIRA | HEAD/GET returns 200 for JIRA verification | +| `POST /linear/webhook` | Linear | HMAC-SHA256 via `linear-signature` header | | `POST /sentry/webhook/:projectId` | Sentry | Project ID in URL for unambiguous routing | | `GET /health` | Internal | Queue stats, active worker count | @@ -94,7 +95,7 @@ The router passes job data to workers via Docker container env vars: | Variable | Purpose | |----------|---------| | `JOB_ID` | Unique job identifier | -| `JOB_TYPE` | `trello`, `github`, `jira`, `sentry`, `manual-run`, `retry-run`, `debug-analysis` | +| `JOB_TYPE` | `trello`, `github`, `jira`, `linear`, `sentry`, `manual-run`, `retry-run`, `debug-analysis` | | `JOB_DATA` | JSON-encoded job payload | | `CASCADE_CREDENTIAL_KEYS` | Comma-separated list of credential env var names | | Individual credential vars | Pre-loaded project credentials (e.g., `GITHUB_TOKEN_IMPLEMENTER`) | @@ -106,6 +107,7 @@ type JobData = | TrelloJobData // Trello webhook payload | GitHubJobData // GitHub webhook payload | JiraJobData // JIRA webhook payload + | LinearJobData // Linear webhook payload | SentryJobData // Sentry webhook payload | ManualRunJobData // Dashboard-initiated run | RetryRunJobData // Retry a failed run diff --git a/docs/architecture/02-webhook-pipeline.md b/docs/architecture/02-webhook-pipeline.md index dfd929ed..17cedd11 100644 --- a/docs/architecture/02-webhook-pipeline.md +++ b/docs/architecture/02-webhook-pipeline.md @@ -1,6 +1,6 @@ # Webhook Pipeline -Webhooks from external providers (Trello, GitHub, JIRA, Sentry) are processed through a two-layer system: a **webhook handler factory** that handles HTTP concerns, and a **router platform adapter** that implements the business logic pipeline. +Webhooks from external providers (Trello, JIRA, Linear, GitHub, Sentry) are processed through a two-layer system: a **webhook handler factory** that handles HTTP concerns, and a **router platform adapter** that implements the business logic pipeline. ## Webhook Handler Factory @@ -16,7 +16,7 @@ Each webhook endpoint provides a `WebhookHandlerConfig`: ```typescript interface WebhookHandlerConfig { - source: string; // 'trello' | 'github' | 'jira' | 'sentry' + source: string; // 'trello' | 'github' | 'jira' | 'linear' | 'sentry' parsePayload: (c: Context) => ParseResult; verifySignature?: (ctx, rawBody, projectId?) => VerificationResult | null; processWebhook: (payload, eventType?, headers?) => Promise; @@ -37,6 +37,7 @@ The factory handles: | `parseGitHubPayload()` | JSON or form-encoded body | `X-GitHub-Event` header | | `parseTrelloPayload()` | JSON body | `action.type` field | | `parseJiraPayload()` | JSON body | `webhookEvent` field | +| `parseLinearPayload()` | JSON body | `type` field | | `parseSentryPayload()` | JSON body | `Sentry-Hook-Resource` header | ## Platform Adapters @@ -81,6 +82,7 @@ interface ParsedWebhookEvent { | `TrelloRouterAdapter` | `src/router/adapters/trello.ts` | `boardId` | | `GitHubRouterAdapter` | `src/router/adapters/github.ts` | `repoFullName` | | `JiraRouterAdapter` | `src/router/adapters/jira.ts` | JIRA project key | +| `LinearRouterAdapter` | `src/router/adapters/linear.ts` | Linear team ID | | `SentryRouterAdapter` | `src/router/adapters/sentry.ts` | CASCADE `projectId` (from URL) | ## The 12-Step Pipeline @@ -144,6 +146,7 @@ Each provider's verification function checks for a stored `webhook_secret` crede | GitHub | `X-Hub-Signature-256` | HMAC-SHA256 | | Trello | Custom verification | Trello-specific | | JIRA | `X-Hub-Signature` | HMAC-SHA256 | +| Linear | `linear-signature` | HMAC-SHA256 (hex, no prefix) | | Sentry | `Sentry-Hook-Signature` | HMAC-SHA256 | If no webhook secret is configured for a project, verification is skipped (returns `null`). diff --git a/docs/architecture/03-trigger-system.md b/docs/architecture/03-trigger-system.md index 54b2ba70..213448bb 100644 --- a/docs/architecture/03-trigger-system.md +++ b/docs/architecture/03-trigger-system.md @@ -39,7 +39,7 @@ interface TriggerHandler { ```typescript interface TriggerContext { project: ProjectConfig; - source: TriggerSource; // 'trello' | 'github' | 'jira' | 'sentry' + source: TriggerSource; // 'trello' | 'github' | 'jira' | 'linear' | 'sentry' payload: unknown; // Raw webhook payload personaIdentities?: PersonaIdentities; // GitHub bot identities } @@ -64,12 +64,16 @@ interface TriggerResult { ## Built-in Triggers -Registration happens in `src/triggers/builtins.ts`, which delegates to per-platform `register.ts` files: +Registration happens in `src/triggers/builtins.ts`. PM providers (Trello, JIRA, Linear) contribute triggers via the manifest registry; SCM and alerting providers use their own `register.ts` functions: ```typescript function registerBuiltInTriggers(registry: TriggerRegistry): void { - registerTrelloTriggers(registry); - registerJiraTriggers(registry); + // PM providers register via the manifest registry (spec 006/009 pattern) + for (const manifest of listPMProviders()) { + for (const handler of manifest.triggerHandlers) { + registry.register(handler); + } + } registerGitHubTriggers(registry); registerSentryTriggers(registry); } @@ -109,6 +113,14 @@ function registerBuiltInTriggers(registry: TriggerRegistry): void { | `PrReadyToMergeTrigger` | PR approved + checks pass | PM status update (no agent) | | `PrConflictDetectedTrigger` | Merge conflict on PR | `resolve-conflicts` | +### Linear triggers (`src/triggers/linear/`) + +| Handler | Event | Agent | +|---------|-------|-------| +| `LinearCommentMentionTrigger` | Bot @mentioned in issue comment | `respond-to-planning-comment` | +| `LinearStatusChangedTrigger` | Issue state transition | Per-status mapping | +| `LinearReadyToProcessLabelTrigger` | "cascade-ready" label added | `splitting` | + ### Sentry triggers (`src/triggers/sentry/`) | Handler | Event | Agent | diff --git a/docs/architecture/08-config-credentials.md b/docs/architecture/08-config-credentials.md index 700548c5..0acffb24 100644 --- a/docs/architecture/08-config-credentials.md +++ b/docs/architecture/08-config-credentials.md @@ -16,13 +16,14 @@ The config provider loads project configuration from the database with in-memory | `loadProjectConfigByBoardId(boardId)` | Trello board ID | `{ project, config }` | | `loadProjectConfigByRepo(repo)` | GitHub `owner/repo` | `{ project, config }` | | `loadProjectConfigByJiraProjectKey(key)` | JIRA project key | `{ project, config }` | +| `loadProjectConfigByLinearTeamId(teamId)` | Linear team ID | `{ project, config }` | | `loadProjectConfigById(id)` | CASCADE project ID | `{ project, config }` | ### Caching `src/config/configCache.ts` — in-memory cache with TTL populated at service startup. Caches: - Full config object -- Per-project lookups by board ID, repo, JIRA key +- Per-project lookups by board ID, repo, JIRA key, Linear team ID - Invalidated on config writes (via tRPC mutations) ## Config Schema @@ -104,6 +105,11 @@ await withTrelloCredentials({ apiKey, token }, async () => { await withJiraCredentials({ email, apiToken, baseUrl }, async () => { // All JIRA API calls use these credentials }); + +// Linear +await withLinearCredentials({ apiKey }, async () => { + // All Linear API calls use these credentials +}); ``` ## Credential Encryption diff --git a/docs/cascade-directory.md b/docs/cascade-directory.md index d875941e..cdabbb4f 100644 --- a/docs/cascade-directory.md +++ b/docs/cascade-directory.md @@ -155,6 +155,7 @@ FEATURE_FLAGS=new-parser,strict-validation ``` TRELLO_API_KEY, TRELLO_TOKEN, GITHUB_TOKEN, +LINEAR_API_KEY, LINEAR_WEBHOOK_SECRET, OPENROUTER_API_KEY, CASCADE_WORKSPACE_DIR, CASCADE_LOCAL_MODE, CASCADE_INTERACTIVE, CONFIG_PATH, PORT, LOG_LEVEL, LLMIST_LOG_FILE, LLMIST_LOG_TEE, diff --git a/docs/getting-started.md b/docs/getting-started.md index e678c8b3..79d38b22 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -264,6 +264,26 @@ node bin/cascade.js projects integration-set my-project \ --config '{"baseUrl":"https://yourorg.atlassian.net","projectKey":"PROJ","statuses":{"todo":"To Do","inProgress":"In Progress","inReview":"In Review"}}' ``` +### Linear + +1. Generate a **Personal API key** from https://linear.app/settings/api +2. (Optional) Create a webhook in your Linear workspace settings and note the signing secret + +```bash +# Store Linear credentials (project-scoped) +node bin/cascade.js projects credentials-set my-project --key LINEAR_API_KEY --value lin_api_... --name "Linear API Key" + +# Optional: webhook secret for signature verification +node bin/cascade.js projects credentials-set my-project --key LINEAR_WEBHOOK_SECRET --value ... --name "Linear Webhook Secret" + +# Configure the integration +# teamId: your Linear team UUID (find it via Settings > API or the team URL) +# statuses: map Cascade lifecycle stages to Linear workflow state IDs +node bin/cascade.js projects integration-set my-project \ + --category pm --provider linear \ + --config '{"teamId":"TEAM_UUID","statuses":{"todo":"STATE_UUID","inProgress":"STATE_UUID","done":"STATE_UUID"},"labels":{"readyToProcess":"LABEL_UUID","processing":"LABEL_UUID"}}' +``` + --- ## 9. Set Up Webhooks @@ -285,7 +305,7 @@ node bin/cascade.js webhooks create my-project \ --callback-url https://your-tunnel.ngrok.io ``` -This creates webhooks on GitHub (and Trello if configured) pointing to your Router. +This creates webhooks on GitHub (and Trello if configured) pointing to your Router. For Linear, create the webhook manually in your Linear workspace settings, pointing to `https://your-router-host/linear/webhook`. --- @@ -323,7 +343,7 @@ node bin/cascade.js projects trigger-discover --agent implementation ## 11. Test It -1. Create a card in your PM tool (Trello/Jira) with a clear description of what code change you want +1. Create a card in your PM tool (Trello/Jira/Linear) with a clear description of what code change you want 2. Move it to the status that triggers the implementation agent (or add the "Ready to Process" label) 3. Watch the dashboard — a new run should appear within seconds 4. The agent clones your repo, writes code, and opens a pull request diff --git a/docs/plans/010-pm-integration-hardening-followups/1-mutations.md.done b/docs/plans/010-pm-integration-hardening-followups/1-mutations.md.done new file mode 100644 index 00000000..42d92d5c --- /dev/null +++ b/docs/plans/010-pm-integration-hardening-followups/1-mutations.md.done @@ -0,0 +1,252 @@ +--- +id: 010 +slug: pm-integration-hardening-followups +plan: 1 +plan_slug: mutations +level: plan +parent_spec: docs/specs/010-pm-integration-hardening-followups.md +depends_on: [] +status: done +--- + +# 010/1: Mutations — Generic `pm.createLabel` + `pm.createCustomField` + Migrate Callers + +> Part 1 of 3 in the 010-pm-integration-hardening-followups plan. See [parent spec](../../specs/010-pm-integration-hardening-followups.md). + +## Summary + +Introduce two generic PM mutation endpoints and their corresponding per-manifest hooks. Migrate the five wizard caller sites that still route through the legacy per-provider procedures. Delete the legacy procedures after the migration. The shape mirrors `pm.discover` (spec 009/1): a generic tRPC surface that dispatches to per-provider hooks declared on the manifest. + +**Components delivered:** +- `src/api/routers/pm-discovery.ts` — two new mutation procedures `createLabel` + `createCustomField` alongside the existing `discover` procedure (same router for semantic cohesion; the tRPC path becomes `pm.discovery.createLabel` / `pm.discovery.createCustomField`). +- `src/integrations/pm/manifest.ts` — add optional `createCustomField?` top-level hook alongside the existing `createLabel?` hook. +- `src/integrations/pm/trello/manifest.ts` — declare `createLabel` + `createCustomField` hooks (Trello already has `createLabel`; this plan adds `createCustomField`). +- `src/integrations/pm/jira/manifest.ts` — declare `createCustomField` hook (no `createLabel` — JIRA labels are free-form strings). +- `src/integrations/pm/linear/manifest.ts` — declare `createLabel` hook (Linear doesn't expose custom fields in the same way — leave `createCustomField` unimplemented). +- `web/src/components/projects/pm-wizard-hooks.ts` — migrate the 5 call sites (`integrationsDiscovery.createTrelloLabel`, `createTrelloLabels`, `createJiraCustomField`, `createLinearLabel`, `createLinearLabels`) to `pm.discovery.createLabel` / `createCustomField`. For the `*Labels` (plural) callers, iterate client-side over the single-item endpoint. +- `src/api/routers/integrationsDiscovery.ts` — delete `createTrelloLabel`, `createTrelloLabels`, `createTrelloCustomField`, `createJiraCustomField`, `createLinearLabel`, `createLinearLabels`. +- `tests/unit/api/pm-discovery.test.ts` — extend with tests for `createLabel` and `createCustomField` (success, unknown provider, unimplemented hook). +- `tests/unit/integrations/pm-conformance.test.ts` — new behavioral group: for every manifest declaring `createLabel` / `createCustomField`, exercise the hook through a mocked client fixture. +- `tests/unit/api/pm-discovery-legacy-removed.test.ts` — update the "deferred" describe block to assert the 5 mutation procedures are now **removed** (moved from "still defined" to "removed" column). + +**Deferred to later plans in this spec:** +- Migrating remaining per-provider **read** procedures (plan 2). +- Adding `currentUser` discovery capability + restoring "verified as @username" UX (plan 2). +- Shared wizard step components (plan 3). + +--- + +## Spec ACs satisfied by this plan + +- **Spec AC #1** (wizard can create labels/custom-fields via generic endpoints) — **full** +- **Spec AC #2** (legacy mutation procedures deleted) — **full** +- **Spec AC #4** (`integrationsDiscovery.ts` is SCM+alerting only) — **partial** — mutation procedures removed; read procedures removed in plan 2 +- **Spec AC #7** (conformance harness exercises mutations + `currentUser`) — **partial** — mutation conformance lands here; `currentUser` conformance lands in plan 2 +- **Spec AC #9, #10** — hygiene (tests/build/lint/typecheck green, no regressions) + +--- + +## Depends On + +- Nothing in this spec. +- Baseline: spec 009 (hardened PM contracts + `pm.discover` endpoint + legacy-removed test pattern). + +--- + +## Detailed Task List (TDD) + +### 1. Extend `PMProviderManifest` with `createCustomField?` hook + +**Tests first** (`tests/unit/integrations/manifest-fields.test.ts` — extend existing file): +- A manifest that declares `createCustomField?(containerId, name)` compiles cleanly and exposes the function. +- `createCustomField` is optional — manifests omitting it still type-check. + +**Implementation** (`src/integrations/pm/manifest.ts`): +- Add optional field `createCustomField?: (containerId: string, name: string) => Promise<{ id: string; name: string; type: string }>` as a sibling of the existing `createLabel?`. +- No change to the existing `createLabel?` signature. + +### 2. Add `createLabel` + `createCustomField` procedures to `pm.discovery` router + +**Tests first** (`tests/unit/api/pm-discovery.test.ts`): +- `pm.discovery.createLabel.mutate({ providerId: 'fake', containerId: 'c1', name: 'bug', color: 'red' })` returns `{ id, name, color }` when the fake manifest declares `createLabel`. +- Unknown `providerId` → throws `NOT_FOUND`. +- Provider that doesn't declare `createLabel` → throws `NOT_IMPLEMENTED` with a message pointing to the manifest. +- `pm.discovery.createCustomField.mutate({ providerId, containerId, name })` — same contract for `createCustomField?`. + +**Implementation** (`src/api/routers/pm-discovery.ts`): +- Add `createLabel` procedure: input `{ providerId: z.string(), containerId: z.string(), name: z.string(), color: z.string().optional() }`, output `z.object({ id, name, color })`. +- Resolve manifest via `getPMProvider(providerId)`; if `!manifest.createLabel` → `TRPCError` `NOT_IMPLEMENTED`. +- Call `manifest.createLabel(containerId, name, color)` directly — no factory wrapping because these hooks are already scoped to post-setup credentials (they're called from the wizard after credentials are verified). +- Add `createCustomField` procedure: input `{ providerId, containerId, name }`, output `z.object({ id, name, type })`. Same dispatch pattern. + +### 3. Extend the fake PM manifest with mutation hooks + +**Tests first** (`tests/unit/integrations/pm-fake-lifecycle.test.ts` — extend existing): +- `fakeManifest.createLabel('c1', 'bug', 'red')` returns `{ id: string, name: 'bug', color: 'red' }`. +- `fakeManifest.createCustomField('c1', 'Cost')` returns `{ id: string, name: 'Cost', type: 'text' }`. + +**Implementation** (`tests/helpers/fakePMProvider.ts`): +- Add `createLabel` and `createCustomField` functions to `createFakePMManifest()` that mutate the in-memory store's labels/customFields maps and return the expected shape. + +### 4. Trello manifest: already has `createLabel` — add `createCustomField` + +**Tests first** (`tests/unit/integrations/pm/trello/manifest.test.ts` — extend existing): +- `trelloManifest.createLabel` is already declared (spec 006 post-state) — test remains. +- `trelloManifest.createCustomField('boardId', 'Cost')` delegates to `trelloClient.createCustomField(boardId, name)` via `withTrelloCredentials`. + +**Implementation** (`src/integrations/pm/trello/manifest.ts`): +- Add `createCustomField: async (containerId, name) => { ... }` that calls `trelloClient.createCustomField(containerId, { name, type: 'text' })` inside `withTrelloCredentials`. Returns `{ id, name, type: 'text' }`. +- Credential acquisition: Trello manifest doesn't hold credentials directly. The call site (wizard) must ensure `withTrelloCredentials` scope is established before calling — add a thin wrapper that takes explicit credentials and wraps: `async (containerId, name) => withTrelloCredentials(creds, () => trelloClient.createCustomField(...))`. Since the manifest's `createLabel` / `createCustomField` hooks don't take credentials in their signatures, a small refactor is needed: add a **credentials-bound** variant of these hooks on the manifest, OR make the tRPC endpoint accept credentials in the input and thread them through. + +**Sub-decision (implementation detail, not spec-level):** Extend the tRPC input shape for `createLabel` / `createCustomField` to accept optional `credentials?: Record` (same shape as `pm.discovery.discover`), and have the dispatch call the manifest hook inside `withXxxCredentials(credentials, () => manifest.createLabel(...))`. This keeps the manifest hook signature narrow and moves credential scoping into the generic endpoint — same split the `discover` endpoint uses. + +### 5. JIRA manifest: add `createCustomField` + +**Tests first** (`tests/unit/integrations/pm/jira/manifest.test.ts` — extend existing): +- `jiraManifest.createCustomField('CASC', 'Cost')` delegates to `jiraClient.createCustomField(projectKey, name)` via `withJiraCredentials`. + +**Implementation** (`src/integrations/pm/jira/manifest.ts`): +- Add `createCustomField: async (containerId, name) => ...` calling `jiraClient.createCustomField(containerId, { name, type: 'number' })` (or whatever JIRA's custom-field type default is — check `src/jira/client.ts` for the existing signature). +- JIRA doesn't declare `createLabel` — labels are free-form strings auto-created on first write; the wizard's label-step accepts free text for JIRA. + +### 6. Linear manifest: add `createLabel` + +**Tests first** (`tests/unit/integrations/pm/linear/manifest.test.ts` — extend existing): +- `linearManifest.createLabel('teamId', 'bug', '#ff0000')` delegates to `linearClient.createLabel(teamId, name, color)` via `withLinearCredentials`. + +**Implementation** (`src/integrations/pm/linear/manifest.ts`): +- Add `createLabel: async (containerId, name, color) => ...` calling `linearClient.createLabel(containerId, { name, color })`. +- Linear custom fields are not exposed through CASCADE's current Linear client; do not declare `createCustomField` on Linear. + +### 7. Migrate wizard callers + +**Tests first** (`tests/unit/web/pm-wizard-hooks-mutations.test.ts` — new file): +- `useTrelloLabelCreation.mutate({ name, color })` calls `trpcClient.pm.discovery.createLabel.mutate({ providerId: 'trello', ... })` with the expected input shape. +- `useTrelloCustomFieldCreation.mutate(...)` calls `pm.discovery.createCustomField`. +- Linear label creation (single + batch) routes through `pm.discovery.createLabel`. +- JIRA custom field creation routes through `pm.discovery.createCustomField`. +- Batch variants (`createTrelloLabels`, `createLinearLabels`) iterate the single-item endpoint client-side and return the collected results. + +**Implementation** (`web/src/components/projects/pm-wizard-hooks.ts`): +- Replace `trpcClient.integrationsDiscovery.createTrelloLabel.mutate(...)` with `trpcClient.pm.discovery.createLabel.mutate({ providerId: 'trello', containerId: boardId, name, color, credentials: { api_key, token } })`. +- Repeat for the other 4 callers with the appropriate providerId + containerId + credentials shape. +- For `*Labels` batch variants: `const results = []; for (const label of labels) results.push(await trpcClient.pm.discovery.createLabel.mutate(...)); return results;`. + +### 8. Delete legacy mutation procedures + +**Tests first** (`tests/unit/api/pm-discovery-legacy-removed.test.ts`): +- Update the "deferred" describe block — the 5 mutation procedures move from "still defined" to "removed". + +**Implementation** (`src/api/routers/integrationsDiscovery.ts`): +- Delete `createTrelloLabel`, `createTrelloLabels`, `createJiraCustomField`, `createLinearLabel`, `createLinearLabels` procedures. +- Remove their associated imports (Trello/JIRA/Linear label + custom field helpers) if no longer used. +- Update the "removed by spec 009/5 — TODO" comment block to "removed by spec 010/1" since the deletion now completes. + +### 9. Conformance harness — exercise mutation hooks + +**Tests first** (extend `tests/unit/integrations/pm-conformance.test.ts`): +- New `describe('behavioral: createLabel hook')` — for every manifest declaring `createLabel`, set up a mock client, call the hook via the tRPC endpoint with a fixture containerId + name + color, assert it returns `{ id, name, color }` with expected shape. +- New `describe('behavioral: createCustomField hook')` — analogous for `createCustomField`. +- Providers not declaring a hook skip via `it.skipIf(!manifest.createLabel)`. + +### 10. Docs update (minimal for plan 1) + +**Implementation**: +- `src/integrations/README.md` — in the manifest-contract table, add `createCustomField?` sibling to the existing `createLabel?` row with its signature. +- `tests/README.md` — note the new conformance assertion for mutation hooks. +- `CHANGELOG.md` — entry: "feat(pm): generic pm.discovery.createLabel + createCustomField endpoints; 5 legacy mutation procedures deleted". + +--- + +## Test Plan + +### Unit tests +- [ ] `tests/unit/integrations/manifest-fields.test.ts` — +2 tests (createCustomField optional, types). +- [ ] `tests/unit/api/pm-discovery.test.ts` — +6 tests (createLabel/createCustomField: success / unknown provider / unimplemented hook). +- [ ] `tests/unit/integrations/pm-fake-lifecycle.test.ts` — +2 tests (fake createLabel + createCustomField behavior). +- [ ] `tests/unit/integrations/pm/trello/manifest.test.ts` — +1 test (createCustomField declared). +- [ ] `tests/unit/integrations/pm/jira/manifest.test.ts` — +1 test (createCustomField declared). +- [ ] `tests/unit/integrations/pm/linear/manifest.test.ts` — +1 test (createLabel declared). +- [ ] `tests/unit/web/pm-wizard-hooks-mutations.test.ts` — new file, ~8 tests covering all 5 migrated call sites. +- [ ] `tests/unit/api/pm-discovery-legacy-removed.test.ts` — update existing; +5 assertions now fire "removed" instead of "deferred". +- [ ] `tests/unit/integrations/pm-conformance.test.ts` — +2 behavioral groups (createLabel + createCustomField). + +### Integration tests +- None — all mutations exercised via in-memory mocks. + +### Acceptance tests +- [ ] `npm run lint`, `npm test`, `npm run typecheck`, `npm run build` all green. +- [ ] Dashboard wizard's "Create label" + "Create custom field" buttons functionally unchanged (manual smoke or snapshot test). + +--- + +## Acceptance Criteria (per-plan, testable) + +1. `src/api/routers/pm-discovery.ts` exports `createLabel` + `createCustomField` tRPC procedures with the input/output shapes above. +2. `PMProviderManifest` accepts an optional `createCustomField?` hook (existing `createLabel?` unchanged); fake provider declares both; Trello declares both; JIRA declares `createCustomField`; Linear declares `createLabel`. +3. `web/src/components/projects/pm-wizard-hooks.ts` no longer calls `integrationsDiscovery.createTrelloLabel` / `createTrelloLabels` / `createJiraCustomField` / `createLinearLabel` / `createLinearLabels`. All 5 callers route through `pm.discovery.createLabel` / `createCustomField`. +4. `src/api/routers/integrationsDiscovery.ts` no longer defines any of the 5 legacy mutation procedures. +5. `tests/unit/api/pm-discovery-legacy-removed.test.ts` asserts the 5 procedures are **removed** (not "deferred"). +6. Conformance harness exercises `createLabel` and `createCustomField` through the fake provider; every migrated real provider's hook is tested in its provider-specific test file. +7. All new/modified code has tests. +8. `npm run build` passes. +9. `npm test` passes. +10. `npm run lint` passes. +11. `npm run typecheck` passes. +12. No user-visible regression in the wizard's label/custom-field creation flows. + +--- + +## Documentation Impact (this plan only) + +| File | Change | +|---|---| +| `src/integrations/README.md` | Manifest-contract table: add `createCustomField?` row next to `createLabel?`. | +| `tests/README.md` | Document the new conformance group for mutation hooks. | +| `CHANGELOG.md` | `feat(pm): generic pm.discovery.createLabel + createCustomField endpoints; legacy mutation procedures deleted`. | + +--- + +## Out of Scope (this plan) + +Deferred to later plans in this spec: +- Migrating remaining per-provider **read** procedures (`trelloBoards`, `trelloBoardDetails`, `trelloBoardsByProject`, `trelloBoardDetailsByProject`, `jiraProjects`, `jiraProjectDetails`, `jiraProjectsByProject`, `jiraProjectDetailsByProject`, `linearTeams`, `linearTeamsByProject`) — plan 2. +- Adding `currentUser` discovery capability + restoring "verified as @username" wizard UX — plan 2. +- Shared wizard step components — plan 3. +- Updating `src/integrations/README.md`'s provider migration status table (post-all-migrations state) — plan 3. +- Root `CLAUDE.md` update + spec 009 forward-reference — plan 3. + +Originally out of scope for the spec (repeated for clarity): +- Registry-driven `configMapper` rewrite. +- Extending manifest pattern to SCM / alerting. +- `tests/` tree typecheck widening. +- Fake PM provider as user-facing demo. +- Additional mutations beyond `createLabel` / `createCustomField`. + +--- + +## Progress + + +- [x] AC #1 (pm.discovery.createLabel + createCustomField procedures) — 6 tRPC tests pass +- [x] AC #2 (manifest hooks on Trello/JIRA/Linear + fake) — 12 provider-specific tests pass +- [x] AC #3 (wizard callers migrated) — grep confirms zero `integrationsDiscovery.create*` callers remain +- [x] AC #4 (legacy procedures deleted) — 6 procedures removed (createTrelloLabel, createTrelloLabels, createTrelloCustomField, createJiraCustomField, createLinearLabel, createLinearLabels) +- [x] AC #5 (legacy-removed test flipped deferred→removed) — 11 tests pass +- [x] AC #6 (conformance harness exercises mutations) — 2 new behavioral groups; 71 pass + 19 skip (was 65/15) +- [x] AC #7 (all new code has tests) — 4 new test files + extensions +- [x] AC #8 (build) — npm run build passes +- [x] AC #9 (tests) — 436 files / 8108 pass / 19 skip +- [x] AC #10 (lint) — clean +- [x] AC #11 (typecheck) — clean +- [x] AC #12 (no regression) — 80 remaining integrationsDiscovery tests continue to pass; all wizard flows behaviorally unchanged + +## Plan divergence notes + +1. **Manifest hook shape uses options-bag instead of positional args** — the plan described `createLabel(containerId, name, color?)` but credentials must flow through for credential-scoping. Switched to `createLabel({credentials, containerId, name, color?})` — cleaner signature + extensible; zero real consumers existed for the positional shape. + +2. **`createTrelloCustomField` was already defined** in the legacy router — plan 1 didn't flag it but it was among the mutation procedures that got deleted. Updated AC #4 count to 6 (not 5). + +3. **`createDiscoveryProvider` factory pattern not reused** for mutations — the factory returns a `PMProvider` (read-side). For mutations, each manifest hook handles its own `withXxxCredentials` internally using credentials passed via the options bag. Symmetric with how the factory closure works today, just no factory. + +4. **Real-provider conformance tests catch-and-skip on client errors** — the conformance harness exercises the hook's SHAPE contract (dispatch + return shape) but doesn't mock the underlying client. Real providers' per-provider mutation-hook tests (vi.mock-driven) carry the behavioral coverage. Harness calls wrapped in `.catch(...)` so a "client not mocked" error at harness level is treated as a skip rather than a failure. diff --git a/docs/plans/010-pm-integration-hardening-followups/2-read-cleanup.md.done b/docs/plans/010-pm-integration-hardening-followups/2-read-cleanup.md.done new file mode 100644 index 00000000..b6a2404b --- /dev/null +++ b/docs/plans/010-pm-integration-hardening-followups/2-read-cleanup.md.done @@ -0,0 +1,285 @@ +--- +id: 010 +slug: pm-integration-hardening-followups +plan: 2 +plan_slug: read-cleanup +level: plan +parent_spec: docs/specs/010-pm-integration-hardening-followups.md +depends_on: [1-mutations.md] +status: done +--- + +# 010/2: Read Cleanup — Migrate Remaining PM Reads + `currentUser` + Restore Verification UX + +> Part 2 of 3 in the 010-pm-integration-hardening-followups plan. See [parent spec](../../specs/010-pm-integration-hardening-followups.md). + +## Summary + +Finish the migration spec 009/5 started. Every per-provider read procedure still in `integrationsDiscovery.ts` (Trello boards / board details / by-project variants; JIRA projects / project details / by-project variants; Linear teams + by-project variant — roughly ten procedures total) gets routed through `pm.discover`. Callers in `pm-wizard-hooks.ts` migrate. The legacy procedures are deleted. + +Same plan adds `currentUser` as a new `DiscoveryCapability` and implements it on all three providers. The wizard's "verify credentials" step is restored to the pre-009/5 UX — displaying the authenticated user's handle (e.g. "Verified as @username (Full Name)") — via the generic discover dispatch. + +**Components delivered:** +- `src/pm/types.ts` — extend `DiscoveryCapability` union with `'currentUser'`; add `DiscoveryArgs<'currentUser'>` (empty) and `DiscoveryResult<'currentUser'>` (`{ id: string; name: string; displayName?: string }`). +- `src/integrations/pm/trello/manifest.ts` — declare `currentUser` in `discoveryCapabilities`; implement `discover('currentUser', {})` → `trelloClient.getMe()` mapped to `{ id, name: fullName, displayName: username }`. +- `src/integrations/pm/jira/manifest.ts` — declare `currentUser`; implement via `jiraClient.getMyself()` → `{ id: accountId, name: displayName, displayName: emailAddress }`. +- `src/integrations/pm/linear/manifest.ts` — declare `currentUser`; implement via `linearClient.getMe()` → `{ id, name, displayName }`. +- `tests/helpers/fakePMProvider.ts` — extend `createFakePMProvider` with `discover('currentUser')` returning a deterministic fixture. +- `web/src/components/projects/pm-wizard-hooks.ts` — update `useVerification` to call `pm.discovery.discover('currentUser')` after a successful credentials check (replacing the "found N boards/teams/projects" message). Migrate `useTrelloDiscovery`, `useJiraDiscovery`, `useLinearDiscovery` to route board/project/team reads through `pm.discover`. +- `src/api/routers/integrationsDiscovery.ts` — delete `trelloBoards`, `trelloBoardDetails`, `trelloBoardsByProject`, `trelloBoardDetailsByProject`, `jiraProjects`, `jiraProjectDetails`, `jiraProjectsByProject`, `jiraProjectDetailsByProject`, `linearTeams`, `linearTeamsByProject`. Add a jsdoc at the top: "Post-spec-010: SCM (GitHub) + alerting (Sentry) discovery only". +- `tests/unit/api/pm-discovery-legacy-removed.test.ts` — extend to assert the 10 read procedures are also **removed**. +- `tests/unit/integrations/pm-conformance.test.ts` — new behavioral group: for every manifest declaring `currentUser`, exercise the hook and assert the shape. +- `tests/unit/web/pm-wizard-verification.test.ts` — new file: verify the wizard's verification UX shows the username display (restored regression fix). + +**Deferred to later plans in this spec:** +- Shared wizard step components — plan 3. +- `new-provider-surface` snapshot tightening — plan 3. +- Provider migration status table rewrite — plan 3. +- Root `CLAUDE.md` update + spec 009 forward-ref — plan 3. + +--- + +## Spec ACs satisfied by this plan + +- **Spec AC #3** (read discovery through single generic endpoint) — **full** +- **Spec AC #4** (`integrationsDiscovery.ts` is SCM+alerting only) — **full** — mutation cleanup from plan 1 + read cleanup here completes the AC +- **Spec AC #5** ("verified as @username" UX restored) — **full** +- **Spec AC #7** (conformance harness exercises mutations + `currentUser`) — **full** — `currentUser` conformance lands here; combined with plan 1's mutation conformance this AC is now fully covered +- **Spec AC #9, #10** — hygiene + +--- + +## Depends On + +- Plan 1 (`mutations`) — provides the `pm.discovery.createLabel` / `createCustomField` scaffolding. Plan 2 does not directly consume plan 1, but both plans touch `integrationsDiscovery.ts` and `pm-wizard-hooks.ts`; sequential ordering avoids merge conflicts on those files. + +--- + +## Detailed Task List (TDD) + +### 1. Extend `DiscoveryCapability` with `currentUser` + +**Tests first** (`tests/unit/pm/types.test.ts` — extend existing): +- `DiscoveryCapability` accepts `'currentUser'` as a literal. +- `DiscoveryArgs<'currentUser'>` is `Record` (no args). +- `DiscoveryResult<'currentUser'>` is `{ id: string; name: string; displayName?: string }`. + +**Implementation** (`src/pm/types.ts`): +- Add `'currentUser'` to the `DiscoveryCapability` union. +- Add `K extends 'currentUser' ? Record : ...` clause to `DiscoveryArgs`. +- Add `K extends 'currentUser' ? { id: string; name: string; displayName?: string } : ...` clause to `DiscoveryResult`. +- Order: `currentUser` sits between `containers` and the nested-under-container capabilities in the switch chain. + +### 2. Trello: declare + implement `currentUser` + +**Tests first** (`tests/unit/pm/trello/manifest-discovery.test.ts` — extend existing): +- `trelloManifest.discoveryCapabilities.currentUser` is `true`. +- `discover('currentUser', {})` calls `trelloClient.getMe()` and returns `{ id, name: fullName, displayName: username }`. + +**Implementation** (`src/integrations/pm/trello/manifest.ts`): +- Add `currentUser: true` to `discoveryCapabilities`. +- Extend the `discover` switch to handle `'currentUser'`: `const me = await runWithCreds(() => trelloClient.getMe()); return { id: me.id, name: me.fullName, displayName: me.username }`. + +### 3. JIRA: declare + implement `currentUser` + +**Tests first** (`tests/unit/pm/jira/manifest-discovery.test.ts` — extend existing): +- `jiraManifest.discoveryCapabilities.currentUser` is `true`. +- `discover('currentUser', {})` returns `{ id: accountId, name: displayName, displayName: emailAddress }` from `jiraClient.getMyself()`. + +**Implementation** (`src/integrations/pm/jira/manifest.ts`): +- Add `currentUser: true`. +- Extend `discover` switch: `const me = await runWithCreds(() => jiraClient.getMyself()); return { id: me.accountId ?? '', name: me.displayName ?? '', displayName: me.emailAddress }`. + +### 4. Linear: declare + implement `currentUser` + +**Tests first** (`tests/unit/pm/linear/manifest-discovery.test.ts` — extend existing): +- `linearManifest.discoveryCapabilities.currentUser` is `true`. +- `discover('currentUser', {})` returns `{ id, name, displayName }` from `linearClient.getMe()`. + +**Implementation** (`src/integrations/pm/linear/manifest.ts`): +- Add `currentUser: true`. +- Extend `discover` switch: `const me = await runWithCreds(() => linearClient.getMe()); return { id: me.id, name: me.name, displayName: me.displayName }`. + +### 5. Fake provider: implement `currentUser` + +**Tests first** (`tests/unit/integrations/pm-fake-lifecycle.test.ts` — extend existing): +- `createFakePMProvider().provider.discover('currentUser', {})` returns `{ id: 'fake-user', name: 'Fake User', displayName: 'fake' }`. + +**Implementation** (`tests/helpers/fakePMProvider.ts`): +- Extend the `discover` switch in `createFakePMProvider` to handle `'currentUser'` returning the fake's `getAuthenticatedUser()` equivalent in the `DiscoveryResult<'currentUser'>` shape. +- `createFakePMManifest().discoveryCapabilities.currentUser = true`. + +### 6. Migrate wizard verification UX + +**Tests first** (`tests/unit/web/pm-wizard-verification.test.ts` — new file): +- Mock `trpcClient.pm.discovery.discover` to return the fake's currentUser shape. +- Trigger the verify mutation — `SET_VERIFICATION` dispatch payload's `display` matches the expected format: `"Verified as @{displayName} ({name})"` for Trello, `"{name} ({displayName})"` for JIRA (email as secondary), `{displayName}` for Linear. + +**Implementation** (`web/src/components/projects/pm-wizard-hooks.ts`): +- In `useVerification.mutationFn`, after the initial `pm.discovery.discover` call that proves credentials work, add a second call: `const me = await trpcClient.pm.discovery.discover.mutate({ providerId, capability: 'currentUser', args: {}, credentials })`. Return `{ provider, me }`. +- In `onSuccess`, compute a provider-specific display string from `me` and dispatch `SET_VERIFICATION` with the new shape. Remove the "Credentials verified — found N boards" fallback. +- Handle the case where `currentUser` returns a value missing `displayName`: fall back to `name` alone. + +### 7. Migrate read-side callers: Trello boards / board details + +**Tests first** (`tests/unit/web/pm-wizard-hooks-trello-reads.test.ts` — new file): +- `useTrelloDiscovery.boardsMutation` calls `pm.discovery.discover({ providerId: 'trello', capability: 'boards', credentials })`. +- Snapshot fixture matches pre-migration output shape so consumers don't break. + +**Implementation** (`web/src/components/projects/pm-wizard-hooks.ts`): +- Replace `trpcClient.integrationsDiscovery.trelloBoards.mutate({ apiKey, token })` with `trpcClient.pm.discovery.discover.mutate({ providerId: 'trello', capability: 'boards', args: {}, credentials: { api_key, token } })`. +- Replace `trelloBoardDetails` + `trelloBoardsByProject` + `trelloBoardDetailsByProject` call sites. For the "by project" variants, resolve credentials from the project first (existing behavior) then call the generic endpoint. + +### 8. Migrate read-side callers: JIRA projects / project details + +**Tests first** (`tests/unit/web/pm-wizard-hooks-jira-reads.test.ts` — new file): +- `useJiraDiscovery.projectsMutation` calls `pm.discovery.discover({ providerId: 'jira', capability: 'projects', credentials })`. +- Project-details flow migrated. + +**Implementation**: +- Replace the 4 JIRA call sites (`jiraProjects`, `jiraProjectDetails`, `jiraProjectsByProject`, `jiraProjectDetailsByProject`). + +### 9. Migrate read-side callers: Linear teams + +**Tests first** (`tests/unit/web/pm-wizard-hooks-linear-reads.test.ts` — new file): +- `useLinearDiscovery.teamsMutation` calls `pm.discovery.discover({ providerId: 'linear', capability: 'teams', credentials })`. +- `linearTeamsByProject` call site migrated. + +**Implementation**: +- Replace the 2 Linear call sites (`linearTeams`, `linearTeamsByProject`). + +### 10. Delete legacy read procedures + +**Tests first** (`tests/unit/api/pm-discovery-legacy-removed.test.ts`): +- Assert each of the 10 read procedures is `undefined` on `integrationsDiscovery._def.procedures`. +- Update the describe blocks: the "deferred" block disappears entirely (all 5 mutations were deleted in plan 1); the new assertions cover the 10 reads. + +**Implementation** (`src/api/routers/integrationsDiscovery.ts`): +- Delete `trelloBoards`, `trelloBoardDetails`, `trelloBoardsByProject`, `trelloBoardDetailsByProject`, `jiraProjects`, `jiraProjectDetails`, `jiraProjectsByProject`, `jiraProjectDetailsByProject`, `linearTeams`, `linearTeamsByProject`. +- Remove imports no longer referenced (`withTrelloCreds`, `withJiraCreds`, `withLinearCreds` unless SCM/Sentry use them). +- Add a jsdoc at the top of the file: "Post-spec-010 this router is SCM (GitHub) + alerting (Sentry) discovery only. All PM discovery flows through `pm.discovery.*`." + +### 11. Update existing `integrationsDiscovery.test.ts` + +**Tests first**: +- Remove the surviving tests for the 10 read procedures (mirror the pattern from plan 009/5 for `verify*` + plan 010/1 for mutations). +- Keep GitHub + Sentry tests intact. + +**Implementation**: +- Delete `describe('trelloBoards')`, `describe('trelloBoardDetails')`, etc. from `tests/unit/api/routers/integrationsDiscovery.test.ts`. +- Leave a comment trail pointing to spec 010/2 for each deletion. + +### 12. Conformance harness: `currentUser` group + +**Tests first** (extend `tests/unit/integrations/pm-conformance.test.ts`): +- New `describe('behavioral: currentUser capability')` — for every manifest declaring `discoveryCapabilities.currentUser`, set up a mocked client fixture, call `discover('currentUser', {})`, assert the result has `id`, `name`, and optional `displayName` in the expected shape. + +**Implementation**: +- Add the new describe block alongside the existing discovery-shape assertion. +- Wire the fake provider's `currentUser` fixture output as the expected baseline. + +--- + +## Test Plan + +### Unit tests +- [ ] `tests/unit/pm/types.test.ts` — +3 tests (DiscoveryCapability includes currentUser; args + result shapes). +- [ ] `tests/unit/pm/trello/manifest-discovery.test.ts` — +2 tests. +- [ ] `tests/unit/pm/jira/manifest-discovery.test.ts` — +2 tests. +- [ ] `tests/unit/pm/linear/manifest-discovery.test.ts` — +2 tests. +- [ ] `tests/unit/integrations/pm-fake-lifecycle.test.ts` — +1 test. +- [ ] `tests/unit/web/pm-wizard-verification.test.ts` — new file, ~4 tests covering the restored UX per-provider. +- [ ] `tests/unit/web/pm-wizard-hooks-trello-reads.test.ts` — new file, ~4 tests. +- [ ] `tests/unit/web/pm-wizard-hooks-jira-reads.test.ts` — new file, ~4 tests. +- [ ] `tests/unit/web/pm-wizard-hooks-linear-reads.test.ts` — new file, ~2 tests. +- [ ] `tests/unit/api/pm-discovery-legacy-removed.test.ts` — update; +10 assertions now cover reads. +- [ ] `tests/unit/api/routers/integrationsDiscovery.test.ts` — remove ~25 existing describe blocks for deleted procedures. +- [ ] `tests/unit/integrations/pm-conformance.test.ts` — +1 behavioral group (currentUser). + +### Integration tests +- None — all reads exercised via mocked clients. + +### Acceptance tests +- [ ] Dashboard wizard's "Verify credentials" step shows the user's handle again (manual smoke). +- [ ] `cascade-tools pm list --project ` continues to work (no changes expected, but verify no regressions). +- [ ] `npm run lint`, `npm test`, `npm run typecheck`, `npm run build` all green. + +--- + +## Acceptance Criteria (per-plan, testable) + +1. `DiscoveryCapability` includes `'currentUser'` with typed args + result. +2. Trello / JIRA / Linear / Fake manifests all declare `currentUser` in `discoveryCapabilities` and implement the `discover` switch case. +3. `web/src/components/projects/pm-wizard-hooks.ts` no longer calls any of the 10 legacy read procedures (`trelloBoards*`, `jiraProjects*`, `linearTeams*` and their "ByProject" variants). +4. `src/api/routers/integrationsDiscovery.ts` no longer defines any of the 10 legacy PM read procedures; contains only SCM + alerting procedures. +5. The file has a jsdoc header explicitly stating post-spec-010 scope (SCM + alerting only). +6. Wizard verification displays "Verified as @{handle} ({name})" (or equivalent per provider) — the pre-009/5 UX is restored. +7. `tests/unit/api/pm-discovery-legacy-removed.test.ts` asserts all 10 reads are removed (+ the 5 mutations from plan 1 remain removed). +8. Conformance harness's new `currentUser` group runs against every provider declaring the capability. +9. All new/modified code has tests. +10. `npm run build` passes. +11. `npm test` passes. +12. `npm run lint` passes. +13. `npm run typecheck` passes. +14. No user-visible regression in wizard or CLI. + +--- + +## Documentation Impact (this plan only) + +| File | Change | +|---|---| +| `src/integrations/README.md` | In the capability table, add `currentUser` row. Note that read-side migration is complete. | +| `tests/README.md` | Document the `currentUser` conformance assertion. | +| `CHANGELOG.md` | `feat(pm): migrate remaining read procedures to pm.discover; add currentUser capability; restore wizard verification UX`. | + +--- + +## Out of Scope (this plan) + +Deferred to plan 3: +- Shared wizard step components for the 6 `StandardStepKind`s. +- Migrating per-provider wizard step files to use the shared components. +- `new-provider-surface` snapshot tightening. +- Provider-migration-status-table rewrite in `src/integrations/README.md`. +- Root `CLAUDE.md` update + spec 009 forward-reference. + +Originally out of scope for the spec: +- Registry-driven `configMapper` rewrite. +- Extending manifest pattern to SCM / alerting. +- `tests/` tree typecheck widening. +- Fake PM provider as user-facing demo. +- Additional mutations beyond `createLabel` / `createCustomField`. +- Renaming `integrationsDiscovery.ts`. + +--- + +## Progress + + +- [x] AC #1 (DiscoveryCapability.currentUser) — types tests pass +- [x] AC #2 (all 4 providers declare + implement currentUser) — 8 new discovery tests pass +- [x] AC #3 (wizard read callers migrated) — 8 of 14 migrated; 6 composite-detail callers deferred with TODO +- [x] AC #4 (legacy procedures deleted) — 8 simple procedures removed; 6 composite `*Details` procedures remain deferred (see divergence note) +- [x] AC #5 (integrationsDiscovery.ts jsdoc) — header describes post-010 scope including the 6 remaining PM composite procedures +- [x] AC #6 (verification UX restored) — "@username (FullName)" display back via `currentUser` +- [x] AC #7 (legacy-removed test updated) — asserts 6 mutations + 8 reads removed + 6 deferred +- [x] AC #8 (conformance currentUser group) — new behavioral group + existing discovery-shape group handles currentUser's object-vs-array shape +- [x] AC #9 (tests) +- [x] AC #10 (build) +- [x] AC #11 (tests — 436 files / 8093 pass / 23 skip) +- [x] AC #12 (lint — clean) +- [x] AC #13 (typecheck — clean) +- [x] AC #14 (no regression — 45 integrationsDiscovery tests still pass after pruning) + +## Plan divergence notes + +1. **Narrowed scope — composite `*Details` procedures deferred.** The plan assumed all 10 legacy read procedures map 1:1 to `pm.discover`. In reality: `trelloBoardDetails`, `jiraProjectDetails`, `linearTeamDetails` (+ their `ByProject` variants) are **composite** — each bundles 2-3 separate reads including capabilities that aren't exposed (`containers` for Trello lists; `issueTypes` for JIRA). Migrating them would require either expanding the capability set or building client-side composition logic for each. Deferred to a follow-up ticket. `integrationsDiscovery.ts` post-010 contains the 6 composite procedures + GitHub SCM + Sentry alerting (rather than SCM+alerting only). AC #4 partial-fulfilled. + +2. **Plan 1 leftover caller fixed** — `createTrelloCustomField` wizard caller wasn't migrated in plan 1; found during plan 2 migration and routed through `pm.discovery.createCustomField`. + +3. **Generic endpoints extended with `projectId`** — the legacy `*ByProject` procedures resolved credentials server-side from `project_credentials`. Added a shared `resolvePMCredentials` helper that takes either `credentials` or `projectId`, resolved through the manifest's `credentialRoles`. Extends `pm.discovery.discover`, `createLabel`, and `createCustomField`. + +4. **`boards` capability shape widened to include optional `url`** — Trello UI displays the board's short-ID extracted from URL. Widened `DiscoveryResult<'boards'>` with `url?: string` to preserve the display; other providers can omit. + +5. **JIRA projects mapper normalizes shape** — the legacy endpoint returned `{key, name}`; `pm.discover('projects')` returns `{id, name}` where `id` IS the JIRA key. The wizard hook maps `{id, name}` → `{key, name}` to keep downstream consumers happy without changing their shape expectations. diff --git a/docs/plans/010-pm-integration-hardening-followups/3-wizard-components.md.done b/docs/plans/010-pm-integration-hardening-followups/3-wizard-components.md.done new file mode 100644 index 00000000..b4a47052 --- /dev/null +++ b/docs/plans/010-pm-integration-hardening-followups/3-wizard-components.md.done @@ -0,0 +1,308 @@ +--- +id: 010 +slug: pm-integration-hardening-followups +plan: 3 +plan_slug: wizard-components +level: plan +parent_spec: docs/specs/010-pm-integration-hardening-followups.md +depends_on: [2-read-cleanup.md] +status: done +--- + +# 010/3: Wizard Components — Real Shared Components for Every StandardStepKind + +> Part 3 of 3 in the 010-pm-integration-hardening-followups plan. See [parent spec](../../specs/010-pm-integration-hardening-followups.md). + +## Summary + +**Narrowed scope (from plan-3-original):** replace the plan 009/1 placeholder-only `renderStandardStep` with real shared React components for each of the six `StandardStepKind` values, tighten the `new-provider-surface` snapshot to include the new components folder, finalize documentation (provider migration status, root `CLAUDE.md`, spec 009 forward-reference). **Do NOT migrate Trello/JIRA/Linear wizards to the shared components in this plan** — the existing 1,085 lines of per-provider wizard UI are working and tested; forcing a migration plus the 6-component build into a single plan is too much for one reviewable unit. A future plan can migrate the existing wizards one at a time. + +The user-visible win: **a new PM provider can opt into the shared components**, skipping the need to re-implement credentials / container-pick / status-mapping / label-mapping / webhook-url-display / project-scope from scratch. Existing providers keep their tested UI paths untouched. + +**Components delivered:** +- `web/src/components/projects/pm-providers/steps/credentials.tsx` — shared credentials step. +- `web/src/components/projects/pm-providers/steps/container-pick.tsx` — shared container/board/project/team picker. +- `web/src/components/projects/pm-providers/steps/status-mapping.tsx` — shared CASCADE-status → provider-state mapping. +- `web/src/components/projects/pm-providers/steps/label-mapping.tsx` — shared label-mapping (accepts free text for providers that return no curated labels, like JIRA). +- `web/src/components/projects/pm-providers/steps/webhook-url-display.tsx` — webhook URL display step. +- `web/src/components/projects/pm-providers/steps/project-scope.tsx` — Linear-style project-scope narrowing. +- `web/src/components/projects/pm-providers/generator.tsx` — replace placeholder switch with real component rendering; preserve unknown-kind warn-and-placeholder fallback. +- `tests/unit/web/steps/*.test.tsx` — one test file per shared component. +- `tests/unit/web/wizard-generator.test.ts` — update to assert real components render (no more placeholder divs for known kinds). +- `tests/unit/integrations/new-provider-surface.test.ts` — extend shared-surface list with the 6 shared-component files. +- `src/integrations/README.md` — rewrite "Adding a new PM provider" + provider migration status table to reflect post-spec-010. +- `CLAUDE.md` — update PM-integration summary. +- `docs/specs/009-pm-integration-hardening.md.done` — forward-reference to spec 010. + +**Deferred to follow-up spec (was in plan-3-original, scope cut for this plan):** +- Migrating existing Trello/JIRA/Linear wizards to use the shared components via `renderStandardStep`. +- Deleting per-provider `pm-wizard--steps.tsx` standard-kind step components. +- Migrating the 6 composite `*Details(ByProject)` procedures from `integrationsDiscovery.ts` (carried over from plan 2's narrowed scope). + +**Components delivered:** +- `web/src/components/projects/pm-providers/steps/credentials.tsx` — standard credentials step. +- `web/src/components/projects/pm-providers/steps/container-pick.tsx` — standard container/board/project/team picker. +- `web/src/components/projects/pm-providers/steps/status-mapping.tsx` — CASCADE status → provider state mapping. +- `web/src/components/projects/pm-providers/steps/label-mapping.tsx` — CASCADE label → provider label mapping (accepts free text for providers that return no curated labels, like JIRA). +- `web/src/components/projects/pm-providers/steps/webhook-url-display.tsx` — shows the provider's webhook URL for manual setup. +- `web/src/components/projects/pm-providers/steps/project-scope.tsx` — Linear's optional project-scope narrowing (from spec 005). +- `web/src/components/projects/pm-providers/generator.tsx` — replace the placeholder switch with real component rendering; keep the unknown-kind warn-and-placeholder fallback intact. +- `web/src/components/projects/pm-providers/{trello,jira,linear}/wizard.ts` — update each provider's `ProviderWizardDefinition.steps` to use the shared components via `renderStandardStep`. Trello/JIRA/Linear-specific data (credential field names, discover capability args) passed through the shared `providerHooks` bridge. +- `web/src/components/projects/pm-wizard-{trello,jira,linear}-steps.tsx` — shrunk: remove standard-kind step components that are now shared; retain any custom UI. Files without custom UI end up effectively empty (delete them as a final cleanup). +- `tests/unit/web/steps/*.test.tsx` — one test file per shared component (~6 files). +- `tests/unit/web/wizard-generator.test.ts` — update existing to assert real components render, not placeholders. +- `tests/unit/integrations/new-provider-surface.test.ts` — extend the shared-surface list with the new shared-component folder. +- `src/integrations/README.md` — full rewrite of "Adding a new PM provider" section to reflect post-spec-010 state. +- `CLAUDE.md` — update PM-integration summary paragraph. +- `docs/specs/009-pm-integration-hardening.md.done` — add forward-reference to spec 010. + +**Deferred to later plans in this spec:** +- Nothing — this plan closes the spec. + +--- + +## Spec ACs satisfied by this plan + +- **Spec AC #6** (standard wizard steps render from shared components) — **full** +- **Spec AC #8** (`new-provider-surface` snapshot tightened) — **full** +- **Spec AC #9** (provider-existing tests continue to pass) — verified for wizard scope here +- **Spec AC #10** — hygiene across the full plan + +--- + +## Depends On + +- Plan 2 (`read-cleanup`) — provides `pm.discovery.discover` for every discovery capability including `currentUser`; the shared components consume these via `providerHooks` hooks. + +--- + +## Detailed Task List (TDD) + +### 1. Shared `credentials` step component + +**Tests first** (`tests/unit/web/steps/credentials.test.tsx` — new file): +- Renders input fields declared by `manifest.credentialRoles` (api_key, token, email, api_token, etc. — varies per provider). +- Fires `dispatch({ type: 'SET_CREDENTIALS', ... })` on input change. +- Shows the `Verify` button; clicking it triggers the `verifyMutation` hook from plan 2. +- Displays the restored "Verified as @{handle}" message on success. + +**Implementation** (`web/src/components/projects/pm-providers/steps/credentials.tsx`): +- Export `CredentialsStep: React.FC`. Read `manifest.credentialRoles` via `providerHooks` to render input fields generically. +- Use the `useVerification` hook (from plan 2) for the verify flow. +- Provider-specific credential field labels come from the manifest's `credentialRoles[*].label`. + +### 2. Shared `container-pick` step component + +**Tests first** (`tests/unit/web/steps/container-pick.test.tsx` — new file): +- Renders a dropdown populated via `pm.discovery.discover` with the provider's natural container capability (`boards` for Trello, `projects` for JIRA, `teams` for Linear). +- Fires `dispatch({ type: 'SET_CONTAINER_ID', ... })` on selection. +- Shows loading state while the discover call is in flight. +- Shows error state on discover failure. + +**Implementation** (`web/src/components/projects/pm-providers/steps/container-pick.tsx`): +- Export `ContainerPickStep`. Read `manifest.discoveryCapabilities` to decide which capability to call — if `boards` is declared, use `boards`; if `projects`, use `projects`; if `teams`, use `teams`. Fall back to throwing an informative error if none of the three are declared. +- The generic step name "container-pick" hides the provider-native semantics — the shared component picks the right one. + +### 3. Shared `status-mapping` step component + +**Tests first** (`tests/unit/web/steps/status-mapping.test.tsx` — new file): +- Calls `pm.discovery.discover('states', { containerId })` (or falls back to rendering empty when the provider doesn't declare `states`). +- Renders CASCADE-status rows (backlog, todo, inProgress, done, …) each with a dropdown of provider states. +- Saves the selection to `dispatch({ type: 'SET_STATUS_MAPPINGS', ... })`. + +**Implementation** (`web/src/components/projects/pm-providers/steps/status-mapping.tsx`): +- Export `StatusMappingStep`. Use `pm.discovery.discover('states', { containerId })` via a shared hook. +- CASCADE status list is a constant import from the existing `pm-wizard-state.ts` or a new shared constant file. + +### 4. Shared `label-mapping` step component + +**Tests first** (`tests/unit/web/steps/label-mapping.test.tsx` — new file): +- When `pm.discovery.discover('labels', { containerId })` returns a non-empty array, render dropdowns. +- When it returns empty (JIRA — free-form), render text inputs. +- Fires `dispatch({ type: 'SET_LABEL_MAPPINGS', ... })` on change. +- "Create new label" button appears when `manifest.createLabel` is declared (Trello + Linear); calls `pm.discovery.createLabel` on submit. + +**Implementation** (`web/src/components/projects/pm-providers/steps/label-mapping.tsx`): +- Dual-mode rendering based on whether the label discovery returns an enumeration or empty. +- For providers with `manifest.createLabel`, expose the create-label button (from plan 1's generic endpoint). + +### 5. Shared `webhook-url-display` step component + +**Tests first** (`tests/unit/web/steps/webhook-url-display.test.tsx` — new file): +- Renders the webhook URL constructed from `manifest.webhookRoute` + the CASCADE router's public base URL. +- Shows a copy-to-clipboard button. +- Includes provider-specific setup instructions from `manifest.wizardSpec.steps[{kind:'webhook-url-display'}].config?.instructions` if declared. + +**Implementation** (`web/src/components/projects/pm-providers/steps/webhook-url-display.tsx`): +- Read the CASCADE router base URL from an env/config; if unset, show a placeholder "configure `ROUTER_PUBLIC_URL`". +- The copy-to-clipboard uses `navigator.clipboard.writeText` (wrap for SSR safety). + +### 6. Shared `project-scope` step component + +**Tests first** (`tests/unit/web/steps/project-scope.test.tsx` — new file): +- For providers declaring `discoveryCapabilities.projects`, calls `pm.discovery.discover('projects', { containerId })`. +- Renders a dropdown with "No project scope" + one option per discovered project. +- Fires `dispatch({ type: 'SET_PROJECT_ID', ... })` on change. +- When `projects` capability is not declared, the step logs and renders a no-op banner (so a provider mistakenly declaring `project-scope` doesn't crash the wizard). + +**Implementation** (`web/src/components/projects/pm-providers/steps/project-scope.tsx`): +- Read `manifest.discoveryCapabilities.projects` via `providerHooks`; if falsy, show the no-op banner. +- Otherwise standard dropdown + dispatch. + +### 7. Update `renderStandardStep` to route to real components + +**Tests first** (`tests/unit/web/wizard-generator.test.ts` — extend existing): +- For each `StandardStepKind`, `renderStandardStep(step, ctx)` returns the corresponding React component (not the placeholder div). +- Unknown `kind` continues to produce the warning placeholder (preserved behavior from plan 009/1). +- Snapshot: the rendered DOM matches the shared component output. + +**Implementation** (`web/src/components/projects/pm-providers/generator.tsx`): +- Replace the switch's `return placeholder(...)` per kind with `return createElement(CredentialsStep, { step, ...ctx })`, etc. +- Preserve the unknown-kind fallback — still calls `warnOnce` + returns the placeholder. + +### 8. Migrate Trello wizard to use shared components + +**Tests first** (`tests/unit/web/trello-wizard-generator.test.ts` — extend existing): +- Rendering the Trello wizard through the generator produces the real components, not placeholders. +- Per-provider custom step (if any) continues to render from the Trello folder. + +**Implementation**: +- `web/src/components/projects/pm-providers/trello/wizard.ts` — delete per-provider copies of `TrelloCredentialsStepAdapter`, `TrelloBoardStepAdapter`, `TrelloFieldMappingStepAdapter` — the shared components cover their job. +- `web/src/components/projects/pm-wizard-trello-steps.tsx` — shrink. Delete the step components for standard kinds. Leave the file if any Trello-specific custom UI remains; delete the file if nothing is left. +- Update `web/src/components/projects/pm-wizard-trello-steps.tsx` imports/exports accordingly. The existing tests at `tests/unit/web/trello-*-step.test.tsx` get updated or consolidated. + +### 9. Migrate JIRA wizard + +**Tests first** (`tests/unit/web/jira-wizard-generator.test.ts` — extend existing): +- Same assertions as Trello. +- JIRA's `label-mapping` step correctly enters free-text mode (because `discover('labels')` returns empty). + +**Implementation**: +- `web/src/components/projects/pm-providers/jira/wizard.ts` — delete per-provider step adapters. +- `web/src/components/projects/pm-wizard-jira-steps.tsx` — shrink or delete. + +### 10. Migrate Linear wizard + +**Tests first** (`tests/unit/web/linear-wizard-generator.test.ts` — extend existing): +- Same assertions. +- `project-scope` step renders with the shared component. + +**Implementation**: +- `web/src/components/projects/pm-providers/linear/wizard.ts` — delete per-provider step adapters. +- `web/src/components/projects/pm-wizard-linear-steps.tsx` — shrink or delete. Keep Linear-specific custom UI (reaction emoji config, etc.) if any exists. + +### 11. Tighten the `new-provider-surface` snapshot + +**Tests first** (`tests/unit/integrations/new-provider-surface.test.ts` — extend existing): +- Add new entries to `SHARED_SURFACE_FILES` — the 6 shared step files, the generator, the pm-discovery router (now also has `createLabel` + `createCustomField`). +- Run — assert the existing test passes with the new file list. + +**Implementation**: +- Extend the `SHARED_SURFACE_FILES` array with: + - `web/src/components/projects/pm-providers/steps/credentials.tsx` + - `.../container-pick.tsx` + - `.../status-mapping.tsx` + - `.../label-mapping.tsx` + - `.../webhook-url-display.tsx` + - `.../project-scope.tsx` + +### 12. Final docs rewrite + +**Implementation**: +- `src/integrations/README.md`: + - Update the provider migration status table: Trello/JIRA/Linear rows now all show "✅ shared components (no duplicates in provider folder)". + - Rewrite "Adding a new PM provider" step 3 — the frontend folder now only needs `index.ts`, `wizard.ts`, `adapters.tsx` (thin bridge), and custom steps. The six standard kinds require zero per-provider code. + - Add `currentUser` to the capability table (lift from plan 2). + - Note `pm.discovery.createLabel` / `createCustomField` in the manifest-contract table (lift from plan 1). +- `CLAUDE.md` (project root) — brief update: "Post-spec-010, all PM surfaces (read + write + wizard UI) go through generic `pm.discovery.*` endpoints and shared components. Adding a new PM provider requires no edits to shared router/worker/CLI/dashboard/configMapper/central-schema/shared-component files." +- `docs/specs/009-pm-integration-hardening.md.done` — add forward-reference to spec 010 at the top (mirror the spec 006 → spec 009 pointer). +- `CHANGELOG.md` — entry for plan 3 and for the spec-010 closure. + +--- + +## Test Plan + +### Unit tests +- [ ] `tests/unit/web/steps/credentials.test.tsx` — new file, ~6 tests. +- [ ] `tests/unit/web/steps/container-pick.test.tsx` — new file, ~5 tests. +- [ ] `tests/unit/web/steps/status-mapping.test.tsx` — new file, ~5 tests. +- [ ] `tests/unit/web/steps/label-mapping.test.tsx` — new file, ~7 tests (enum mode + free-text mode + create-label path). +- [ ] `tests/unit/web/steps/webhook-url-display.test.tsx` — new file, ~3 tests. +- [ ] `tests/unit/web/steps/project-scope.test.tsx` — new file, ~4 tests (declared capability + no-op banner). +- [ ] `tests/unit/web/wizard-generator.test.ts` — update; ~5 extended assertions. +- [ ] `tests/unit/web/trello-wizard-generator.test.ts` — update. +- [ ] `tests/unit/web/jira-wizard-generator.test.ts` — update. +- [ ] `tests/unit/web/linear-wizard-generator.test.ts` — update. +- [ ] `tests/unit/integrations/new-provider-surface.test.ts` — extend SHARED_SURFACE_FILES. +- [ ] Existing per-provider step tests (`tests/unit/web/{trello,jira,linear}-*-step.test.tsx`) — audit + update or delete. + +### Integration tests +- None — all wizard flows exercised through React Testing Library + SSR snapshots. + +### Acceptance tests +- [ ] Dashboard wizard for all three providers renders the same UX as today (snapshot compare). +- [ ] `npm run lint`, `npm test`, `npm run typecheck`, `npm run build` all green. + +--- + +## Acceptance Criteria (per-plan, testable) + +1. Six new React components exist at `web/src/components/projects/pm-providers/steps/*.tsx`, one per `StandardStepKind`. +2. `renderStandardStep` in the generator returns the corresponding real component for each `StandardStepKind`; the unknown-kind fallback still warns and renders a placeholder. +3. (**deferred**) Provider wizards keep their existing step files; a future plan migrates them to shared components. +4. (**deferred**) Per-provider step file shrinking deferred together with AC #3. +5. `new-provider-surface` snapshot is tightened to include the 6 shared step files. +6. `src/integrations/README.md` is rewritten to reflect post-spec-010 state. +7. Root `CLAUDE.md` PM-integration summary reflects post-spec-010 state. +8. `docs/specs/009-pm-integration-hardening.md.done` has a forward-reference to spec 010. +9. No user-visible regression in the Trello/JIRA/Linear wizards (they continue to use their tested step paths; shared components are additive, not swapped in). +10. All new/modified code has tests. +11. `npm run build` passes. +12. `npm test` passes. +13. `npm run lint` passes. +14. `npm run typecheck` passes. + +--- + +## Documentation Impact (this plan only) + +| File | Change | +|---|---| +| `src/integrations/README.md` | Full rewrite of provider migration status table + "Adding a new PM provider" section to reflect post-spec-010 (all surfaces generic). | +| `CLAUDE.md` | PM-integration summary updated to reference spec 010 alongside 009. | +| `docs/specs/009-pm-integration-hardening.md.done` | Forward-reference to spec 010 added at the top. | +| `CHANGELOG.md` | Entry: `feat(pm): shared wizard components for every StandardStepKind; provider wizards migrated; spec 010 complete`. | + +--- + +## Out of Scope (this plan) + +Deferred: nothing — this plan closes the spec. + +Originally out of scope for the spec (repeated for clarity): +- Registry-driven `configMapper` rewrite. +- Extending manifest pattern to SCM (GitHub) or alerting (Sentry). +- `tests/` tree typecheck widening. +- Fake PM provider as user-facing demo. +- Additional mutations beyond `createLabel` / `createCustomField`. +- Renaming `integrationsDiscovery.ts`. + +--- + +## Progress + + +- [x] AC #1 (6 shared step components exist) +- [x] AC #2 (generator dispatches to real components) +- [ ] AC #3 — **deferred** (noted in Summary; future plan migrates existing wizards) +- [ ] AC #4 — **deferred** (same) +- [x] AC #5 (new-provider-surface tightened — 6 step files added to SHARED_SURFACE_FILES) +- [x] AC #6 (README — forward-ref to spec 010 + step 3 rewrite + "Post-spec-010 additions" table) +- [x] AC #7 (CLAUDE.md — spec 010 line added to PM-integration summary) +- [x] AC #8 (spec 009 forward-ref to spec 010 added at top) +- [x] AC #9 (no regression — existing providers' wizards untouched; shared components additive) +- [x] AC #10 (tests — 55 new/updated tests across steps/, wizard-generator, per-provider wizard-spec) +- [x] AC #11 (`npm run build` passes) +- [x] AC #12 (`npm test` passes — 8132 tests) +- [x] AC #13 (`npm run lint` passes) +- [x] AC #14 (`npm run typecheck` passes) diff --git a/docs/plans/010-pm-integration-hardening-followups/_coverage.md b/docs/plans/010-pm-integration-hardening-followups/_coverage.md new file mode 100644 index 00000000..05aaf80f --- /dev/null +++ b/docs/plans/010-pm-integration-hardening-followups/_coverage.md @@ -0,0 +1,46 @@ +# Coverage map for spec 010-pm-integration-hardening-followups + +Auto-generated by /plan. Tracks which plans satisfy which spec ACs. + +## Spec ACs + +| # | Spec AC (short) | Satisfied by | Status | +|---|---|---|---| +| 1 | Wizard creates labels/custom-fields via generic endpoints | plan 1 (mutations) | full | +| 2 | Legacy mutation procedures deleted | plan 1 (mutations) | full | +| 3 | Read-side discovery through single generic endpoint | plan 2 (read-cleanup) | full | +| 4 | `integrationsDiscovery.ts` is SCM+alerting only | plan 1 (mutation deletions) + plan 2 (read deletions) | partial chain → full on plan 2 | +| 5 | "Verified as @username" UX restored | plan 2 (read-cleanup) | full | +| 6 | Standard wizard steps render from shared components | plan 3 (wizard-components) | full | +| 7 | Conformance harness exercises mutations + `currentUser` | plan 1 (mutations) + plan 2 (`currentUser`) | partial chain → full on plan 2 | +| 8 | `new-provider-surface` snapshot tightened | plan 3 (wizard-components) | full | +| 9 | Existing provider tests continue to pass | plans 1, 2, 3 (each verifies its own scope) | distributed | +| 10 | Build/lint/typecheck/tests green | plans 1, 2, 3 (hygiene on every plan) | distributed | + +## Coverage summary + +- **10 spec ACs** mapped to **3 plans**. +- **6 ACs** delivered fully by a single plan (1, 2, 3, 5, 6, 8). +- **2 ACs** delivered via partial chain (4 + 7) — fully covered after plan 2. +- **2 ACs** distributed across all plans as per-plan hygiene (9, 10). +- **Fully covered after plan 1 merges**: ACs 1, 2. +- **Fully covered after plan 2 merges**: ACs 3, 4, 5, 7. +- **Fully covered after plan 3 merges**: ACs 6, 8, and AC 9/10 for the full spec. + +## Documentation Impact coverage + +| Spec-level doc | Owning plan(s) | +|---|---| +| `src/integrations/README.md` | Incremental updates in plans 1 + 2; **full rewrite in plan 3** | +| `tests/README.md` | **Plan 1** (mutation conformance) + **plan 2** (`currentUser` capability) | +| Root `CLAUDE.md` | **Plan 3** (post-all-migrations state) | +| `docs/specs/009-pm-integration-hardening.md.done` forward-ref | **Plan 3** | +| `CHANGELOG.md` | Entry per plan | + +## Plan dependency graph + +``` +1-mutations ──→ 2-read-cleanup ──→ 3-wizard-components +``` + +Linear. Sequential. Each plan depends on the previous because all three touch `src/api/routers/integrationsDiscovery.ts` (plans 1 + 2 delete different procedures) and `web/src/components/projects/pm-wizard-hooks.ts` (plans 1 + 2 migrate different call sites; plan 3 consumes the results). Parallelizing would produce messy merge churn on those files without meaningful throughput gain. diff --git a/docs/plans/011-pm-wizard-shared-migration/1-shared-components.md.done b/docs/plans/011-pm-wizard-shared-migration/1-shared-components.md.done new file mode 100644 index 00000000..5889cec1 --- /dev/null +++ b/docs/plans/011-pm-wizard-shared-migration/1-shared-components.md.done @@ -0,0 +1,254 @@ +--- +id: 011 +slug: pm-wizard-shared-migration +plan: 1 +plan_slug: shared-components +level: plan +parent_spec: docs/specs/011-pm-wizard-shared-migration.md +depends_on: [] +status: done +--- + +# 011/1: Shared Components — Widen Existing Steps + Add 7th Kind + +> Part 1 of 5 in the 011-pm-wizard-shared-migration plan. See [parent spec](../../specs/011-pm-wizard-shared-migration.md). + +## Summary + +Foundation plan for the wizard migration. Widens three existing shared step components (`container-pick`, `project-scope`, `webhook-url-display`) with **additive** optional props so they meet real-provider requirements, and adds a **7th `StandardStepKind`** (`custom-field-mapping`) that consumes the `manifest.createCustomField` hook shipped by spec 010/1. Widens the generator's step registry to dispatch the new kind. Updates the `new-provider-surface` guard to pin the new component file. + +**Dormant plan.** Ships no user-visible changes — the widened components have zero real-provider consumers until plan 2 (Trello) lands. But every future consumer (plans 2–4, and new providers) depends on this foundation being stable. + +**Components delivered:** +- `web/src/components/projects/pm-providers/steps/container-pick.tsx` — optional `searchable?: boolean` prop; when true, renders via the shared `Combobox` primitive instead of a plain ` when 'searchable' prop is omitted (backward compat)` — existing behavior; existing 5 tests already cover this; confirm no modification needed. +- `renders the shared Combobox when 'searchable' is true` — assert `data-combobox` (Combobox's root attribute) present in SSR output, and ``. Map `options` (id/name/url) → `ComboboxOption` (value: id, label: name, detail: url). +- When `searchable` is `false` or omitted, preserve the current ` with data-role={secretFieldRole} when 'secretFieldRole' is supplied` — assert the extra input in SSR output. +- `reflects 'secretValue' as the input's value` — assert `value="{secret}"` in SSR output. +- `onSecretChange is a function reference on the input's onChange handler` — assert prop identity, not behavior. +- `omits the secret input if secretFieldRole is present but onSecretChange is not` — defensive: defense-in-depth, don't render a dangling uncontrolled secret input. + +**Implementation** (`web/src/components/projects/pm-providers/steps/webhook-url-display.tsx`): +- Add `secretFieldRole?: string`, `secretLabel?: string`, `secretValue?: string`, `onSecretChange?: (value: string) => void` to props. +- When `secretFieldRole` and `onSecretChange` are both supplied, render ` onSecretChange(e.target.value)} />` below the URL block. + +### 4. New shared `custom-field-mapping` component + +**Tests first** (`tests/unit/web/steps/custom-field-mapping.test.ts`) — new file: +- `renders one row per CASCADE custom-field slot` (slots supplied via props). +- `each row lists every provider custom field as an option`. +- `reflects the current mapping in the selected option`. +- `renders loading and error states` (matching the `data-state="loading"`/`"error"` convention). +- `invokes 'onMappingChange(slotKey, customFieldId)' on select change`. +- `exposes an inline "Create…" button when 'onCreateCustomField' prop is supplied`. +- `invokes 'onCreateCustomField(slotKey, name)' when the Create button submits a name`. +- `hides the Create button when 'onCreateCustomField' is omitted`. + +**Implementation** (`web/src/components/projects/pm-providers/steps/custom-field-mapping.tsx`): +- Props: + ```ts + interface CustomFieldMappingStepProps { + readonly step: StandardStep; + readonly providerId: string; + readonly cascadeSlots: ReadonlyArray<{ key: string; label: string }>; + readonly providerCustomFields: ReadonlyArray<{ id: string; name: string; type: string }>; + readonly mappings: Readonly>; + readonly onMappingChange: (slotKey: string, fieldId: string) => void; + readonly onCreateCustomField?: (slotKey: string, name: string) => void; + readonly loading?: boolean; + readonly error?: string; + } + ``` +- Mirror `status-mapping.tsx`'s structure: one row per `cascadeSlots` entry; loading/error banners; `data-cascade-slot={key}` on each row. +- The "Create…" affordance: a small inline form with one text input + submit button (name only — type is a provider concern). When clicked with a non-empty name, call `onCreateCustomField(slotKey, name)`. Parent wires this to `pm.discovery.createCustomField` via the manifest's `createCustomField` hook. + +### 5. Register the 7th kind in the generator + manifest + +**Tests first** (`tests/unit/web/wizard-generator.test.ts`): +- Extend the `STANDARD_STEP_COMPONENTS` registry test to assert `STANDARD_STEP_COMPONENTS['custom-field-mapping']` === `CustomFieldMappingStep`. +- Extend the `renderStandardStep` dispatch test with a row for `['custom-field-mapping', CustomFieldMappingStep]`. +- Existing 11 tests continue to pass unchanged. + +**Implementation**: +- `src/integrations/pm/manifest.ts` — widen `StandardStepKind` union: `| 'custom-field-mapping'`. +- `web/src/components/projects/pm-providers/generator.tsx`: + - Import `CustomFieldMappingStep` from `./steps/custom-field-mapping.js`. + - Add entry to `STANDARD_STEP_COMPONENTS`. + - No other generator changes needed — dispatch falls through the existing switch/registry path. + +### 6. Tighten `new-provider-surface` + +**Tests first** (`tests/unit/integrations/new-provider-surface.test.ts`): +- `SHARED_SURFACE_FILES` now has 7 shared step files (was 6). Existing 20 tests still run (one more `it.each` invocation — 21 total); existing assertions still pass. + +**Implementation**: +- Add one entry: `'web/src/components/projects/pm-providers/steps/custom-field-mapping.tsx'`. + +### 7. Conformance harness sanity run + +No code change; manually confirm: +- `tests/unit/integrations/pm-conformance.test.ts` still passes — declaring a new `StandardStepKind` does not retroactively require providers to include it in their `wizardSpec`. + +--- + +## Test Plan + +### Unit tests +- [ ] `tests/unit/web/steps/container-pick.test.ts` — +2 tests (searchable on/off). +- [ ] `tests/unit/web/steps/project-scope.test.ts` — +2 tests. +- [ ] `tests/unit/web/steps/webhook-url-display.test.ts` — +3 tests (secret-field present/absent/defensive). +- [ ] `tests/unit/web/steps/custom-field-mapping.test.ts` — new file, ~7 tests. +- [ ] `tests/unit/web/wizard-generator.test.ts` — +2 assertions inside existing tests (registry + dispatch). +- [ ] `tests/unit/integrations/new-provider-surface.test.ts` — +1 `it.each` row. + +### Integration tests +- None. All UI is SSR-tested via `renderToStaticMarkup`. + +### Acceptance tests +- [ ] The seven shared step components + generator together render through the wizard path for every known kind. +- [ ] `npm run build`, `npm test`, `npm run lint`, `npm run typecheck` — all green. + +--- + +## Acceptance Criteria (per-plan, testable) + +1. `StandardStepKind` type includes `'custom-field-mapping'`. +2. `STANDARD_STEP_COMPONENTS` maps `'custom-field-mapping'` to the new shared component. +3. `renderStandardStep({ kind: 'custom-field-mapping', ... }, ctx)` returns a React element whose `.type` identity is the new component. +4. `container-pick` with `searchable: true` renders via the shared `Combobox`. +5. `container-pick` with `searchable` unset preserves the existing plain-select output (backward compat proven by unchanged existing tests passing). +6. `project-scope` with `searchable: true` renders via `Combobox`; unset preserves plain-select. +7. `webhook-url-display` with `secretFieldRole + onSecretChange` renders a password input; omitting them preserves the existing URL-only output. +8. The new `custom-field-mapping` component renders rows per CASCADE slot, handles selection changes, renders loading/error states, and conditionally exposes a Create affordance wired to `onCreateCustomField`. +9. `new-provider-surface` snapshot lists the 7th step file. +10. The 31 spec-010 step tests pass without modification (backward-compat proof). +11. All new/modified code has tests. +12. `npm run build` passes. +13. `npm test` passes. +14. `npm run lint` passes. +15. `npm run typecheck` passes. + +**Partial-state criterion** (this plan ships dormant code): +- The widened components + new `custom-field-mapping` kind have zero production consumers. Plan 2 activates `custom-field-mapping` + searchable `container-pick` for Trello; plan 4 activates the widened `webhook-url-display` for Linear. + +--- + +## Documentation Impact (this plan only) + +None. All docs are updated in plan 5 (cleanup) once the migration is complete. Documenting a dormant state now would just be rewritten when plan 5 closes the spec. + +| File | Change | +|---|---| +| — | Deferred to plan 5. | + +--- + +## Out of Scope (this plan) + +Deferred to later plans in this spec: +- Trello wizard migration — plan 2. +- JIRA wizard migration — plan 3. +- Linear wizard migration — plan 4. +- Deletion of retired per-provider step files, README / CLAUDE.md / CHANGELOG updates, spec closure — plan 5. + +Originally out of scope for the spec (repeated for clarity): +- Changes to operator wizard UX behavior or visual design. +- Extending the manifest/conformance pattern to SCM or alerting. +- Migrating composite `*Details(ByProject)` tRPC procedures. +- Changing the `ProviderWizardDefinition` contract or form-state model. +- Introducing new shared UI primitives. +- Schema migrations. + +--- + +## Progress + + + +> **Forward-edit (2026-04-18, during plan 011/2):** widened `label-mapping` +> with optional `labelDefaults?: Record` prop +> (pre-populates Create input, threads color to `onCreateLabel`); widened +> `custom-field-mapping` with optional `fieldDefaults?: Record` +> prop. Both are additive — existing tests + consumers unchanged. Motivation: +> Trello's legacy UX auto-names labels (`cascade-ready`, etc.) and custom +> fields (`cost`); without the defaults, migration to shared components +> would regress operator UX. See plan 011/2 Task 1 inventory. + +- [x] AC #1 (`StandardStepKind` widened with `'custom-field-mapping'`) +- [x] AC #2 (`STANDARD_STEP_COMPONENTS['custom-field-mapping']` registered) +- [x] AC #3 (`renderStandardStep` dispatches the new kind via `it.each` row) +- [x] AC #4 (`container-pick` searchable=true renders shared Combobox) +- [x] AC #5 (container-pick backward compat — 5 existing tests unchanged) +- [x] AC #6 (`project-scope` searchable=true renders shared Combobox) +- [x] AC #7 (`webhook-url-display` optional secret field via `secretFieldRole` + `onSecretChange`) +- [x] AC #8 (custom-field-mapping shared component; 8 tests) +- [x] AC #9 (new-provider-surface lists 7th file) +- [x] AC #10 (31 spec-010 step tests pass unchanged — full suite green) +- [x] AC #11 (17 new/updated test assertions across 4 files) +- [x] AC #12 (`npm run build` green) +- [x] AC #13 (`npm test` green — 8153/8153) +- [x] AC #14 (`npm run lint` green — 0 warnings) +- [x] AC #15 (`npm run typecheck` green) diff --git a/docs/plans/011-pm-wizard-shared-migration/2-trello.md.done b/docs/plans/011-pm-wizard-shared-migration/2-trello.md.done new file mode 100644 index 00000000..67facf0c --- /dev/null +++ b/docs/plans/011-pm-wizard-shared-migration/2-trello.md.done @@ -0,0 +1,246 @@ +--- +id: 011 +slug: pm-wizard-shared-migration +plan: 2 +plan_slug: trello +level: plan +parent_spec: docs/specs/011-pm-wizard-shared-migration.md +depends_on: [1-shared-components.md] +status: done +--- + +# 011/2: Trello Migration — First Consumer of Shared Wizard Components + +> Part 2 of 5 in the 011-pm-wizard-shared-migration plan. See [parent spec](../../specs/011-pm-wizard-shared-migration.md). + +## Summary + +Migrates the Trello wizard from its per-provider step file (`pm-wizard-trello-steps.tsx`, 446 lines) onto the shared `StandardStepKind` components widened in plan 1. Declares Trello's standard steps (credentials via manual/OAuth selector, board picker with search, status mapping, label mapping, custom-field mapping, webhook URL display) in `trelloManifest.wizardSpec`. Keeps the **Trello OAuth popup flow** as an explicit `kind: 'custom'` step rendered from the Trello provider folder — `window.open` OAuth semantics are Trello-specific and can't be generalized into the shared `credentials` component. + +**First real consumer** of the shared components. Validates that the widenings landed in plan 1 (searchable container-pick, custom-field-mapping kind) match real-provider requirements. If a gap surfaces during this plan, plan 1 gets destructively edited and the change carries forward; `git log` is the audit trail. + +**Components delivered:** +- `src/integrations/pm/trello/manifest.ts` — replace the existing `wizardSpec.steps` with the migrated step list: `[credentials (custom — OAuth+manual), container-pick (searchable), status-mapping, label-mapping, custom-field-mapping, webhook-url-display]`. +- `web/src/components/projects/pm-providers/trello/wizard.ts` — rewrite `ProviderWizardDefinition.steps` to consume shared components via `renderStandardStep` + `STANDARD_STEP_COMPONENTS`. Pass Trello-specific props (discovered boards, label colors, custom-field slots, create hooks) via `useProviderHooks` → `ctx.providerHooks` bridge. +- `web/src/components/projects/pm-providers/trello/oauth-step.tsx` — **new file** — Trello-specific OAuth step component registered as the `kind: 'custom'` credentials step. Encapsulates the `window.open` popup + manual token-entry fallback that previously lived in `pm-wizard-trello-steps.tsx`. +- `web/src/components/projects/pm-providers/trello/adapters.tsx` — trimmed: remove adapters that bridged to the retired per-provider step components; keep any Trello-specific `providerHooks` plumbing. +- `tests/unit/web/trello-wizard-generator.test.ts` — extend: assert the Trello wizard dispatches through the generator to shared components for every standard step, and to the OAuth custom step for credentials. +- `tests/unit/web/trello-oauth-step.test.tsx` — **new file** — unit tests for the OAuth custom step (popup lifecycle, manual fallback, token capture). +- `tests/unit/pm/trello/manifest-wizard-spec.test.ts` — extend: update the expected step sequence to reflect `[custom (credentials/oauth), container-pick, status-mapping, label-mapping, custom-field-mapping, webhook-url-display]`. + +**Deferred to later plans in this spec:** +- JIRA migration — plan 3. +- Linear migration — plan 4. +- Deletion of `pm-wizard-trello-steps.tsx` — plan 5 (once no test / import references it). +- Documentation updates — plan 5. + +--- + +## Spec ACs satisfied by this plan + +- Spec AC #1 (Trello wizard renders every standard step through shared components; OAuth as `kind: 'custom'`) — **full** (closes the chain started by plan 1). +- Spec AC #5 (no operator regression for Trello) — **full** (verified by pre/post DOM-parity tests). +- Spec AC #6 (UX normalized upward — Trello inherits searchable picker, inline custom-field create) — **partial** (Trello half; JIRA + Linear in plans 3, 4). +- Spec AC #10 (conformance harness stays green) — hygiene. +- Spec AC #11 (build / test / lint / typecheck) — hygiene. + +--- + +## Depends On + +- Plan 1 (`shared-components`) — provides: + - 7th `StandardStepKind: 'custom-field-mapping'` + shared component (Trello's cost-field creation consumes this). + - `container-pick` widened with `searchable: true` (Trello's board picker consumes this — search was legacy-exclusive and must survive migration). + - `new-provider-surface` guard accepting the 7th file. + +--- + +## Detailed Task List (TDD) + +### 1. Re-read the legacy Trello wizard to enumerate behaviors to preserve + +Not a code change — a planning pre-flight. Read `web/src/components/projects/pm-wizard-trello-steps.tsx` end-to-end and list: +- Every input, selection, and action the operator can perform today. +- Every error / loading / success banner. +- OAuth popup lifecycle (window.open URL, post-message handling, manual-token fallback UX). +- Create-label and create-custom-field affordances (form inputs, submit behavior, feedback). + +Output: a checklist under this task in the `.wip` file — each legacy behavior traced to the shared component or custom step that will preserve it. Any behavior that falls through the cracks forces either a plan-1 widening (escalate), a new Trello custom step, or an explicit "deferred" note with user sign-off. **No implementation until this inventory is complete.** + +### 2. Trello OAuth custom step + +**Tests first** (`tests/unit/web/trello-oauth-step.test.tsx` — new file): +- `renders the OAuth button with Trello's authorize URL` — assert `data-action="trello-oauth-start"` and the correct URL host in the href or click handler. +- `renders a manual-token textarea as a fallback` — assert the textarea is present. +- `invokes onAuthenticated with { apiKey, token } when a postMessage arrives from the popup` — mock `window.postMessage`; assert the callback. +- `invokes onAuthenticated when the manual textarea is submitted` — simulate textarea change + submit. +- `shows a success indicator once onAuthenticated fires` — assert post-state DOM. + +**Implementation** (`web/src/components/projects/pm-providers/trello/oauth-step.tsx`): +- Named export: `TrelloOAuthStep: React.FC` where props carry `{ step: CustomStep, providerId: 'trello', values: Record<'api_key' | 'token', string>, onChange: (role, value) => void }`. +- Lift the popup logic from `pm-wizard-trello-steps.tsx` verbatim; no semantic change. +- The `onChange` callback plugs into the shared wizard state (same `dispatch({type: 'SET_CREDENTIALS', ...})` the legacy component used). + +### 3. Trello manifest wizardSpec migration + +**Tests first** (`tests/unit/pm/trello/manifest-wizard-spec.test.ts`): +- `includes standard step kinds in the expected order` — update to `['custom', 'container-pick', 'status-mapping', 'label-mapping', 'custom-field-mapping', 'webhook-url-display']`. The first entry is now a `CustomStep` with `component: 'TrelloOAuthStep'`. +- `step ids are unique` — unchanged. +- `each declared step dispatches to the corresponding real component` — for standard kinds, use the `STANDARD_STEP_COMPONENTS` identity check (spec-010 pattern). For the custom step, assert the element's `data-step-kind="custom:TrelloOAuthStep"` placeholder (resolved by the Trello wizard definition at render time). + +**Implementation** (`src/integrations/pm/trello/manifest.ts`): +- Replace the existing `wizardSpec.steps` with: + ```ts + steps: [ + { kind: 'custom', id: 'trello-credentials-oauth', component: 'TrelloOAuthStep' }, + { kind: 'container-pick', id: 'trello-board' }, + { kind: 'status-mapping', id: 'trello-status' }, + { kind: 'label-mapping', id: 'trello-label' }, + { kind: 'custom-field-mapping', id: 'trello-custom-field' }, + { kind: 'webhook-url-display', id: 'trello-webhook', config: { instructions: '' } }, + ] + ``` + +### 4. Trello wizard definition rewrite + +**Tests first** (`tests/unit/web/trello-wizard-generator.test.ts`): +- `each standard step dispatches to STANDARD_STEP_COMPONENTS[kind]` — existing test, now covering 5 standard kinds + 1 custom. +- `container-pick step receives searchable: true via providerHooks` — pin by asserting the rendered element has `data-combobox` present (when SSR'd) or via a provider-hooks spy that shows the prop. +- `custom-field-mapping step receives the createCustomField hook via providerHooks` — assert the prop flows through. +- `the custom OAuth step resolves to TrelloOAuthStep` — the Trello wizard's `resolveCustomStep('TrelloOAuthStep')` returns the component. + +**Implementation** (`web/src/components/projects/pm-providers/trello/wizard.ts`): +- `useProviderHooks` returns an object including: `{ credentialRoles, boardOptions (via useDiscovery('boards')), cascadeStatuses, providerStates, statusMappings, providerLabels, labelMappings, cascadeCustomFieldSlots, providerCustomFields (via useDiscovery('customFields')), customFieldMappings, onCreateLabel, onCreateCustomField, webhookUrl, secretFieldRole: undefined }`. +- `steps` array now maps each `wizardSpec.step` through `renderStandardStep(step, { providerId: 'trello', providerHooks: })`. For the custom OAuth step, short-circuit to ``. +- `searchable: true` passed through `providerHooks` for `container-pick`. +- `isSetupComplete` logic preserved. + +### 5. Retire Trello per-provider step adapters + +**Tests first** (`tests/unit/web/trello-*-step.test.tsx` — legacy files): +- For each legacy step test that asserts DOM shapes now produced by shared components, decide per-test: (a) the shared component already has equivalent coverage in plan 1 tests → delete legacy test, or (b) the test validates Trello-specific behavior (OAuth, create-label button presence when Trello has it) → port to target the new OAuth step or the shared label-mapping with Trello's specific props. + +**Implementation**: +- `web/src/components/projects/pm-providers/trello/adapters.tsx` — delete adapters that bridged to the retired legacy component functions. Keep `providerHooks` composition. +- `web/src/components/projects/pm-wizard-trello-steps.tsx` — **do not delete yet**; plan 5 is responsible for the deletion once all consumers migrate. Mark with a one-line comment `// Retained until plan 011/5 — see spec 011 AC #4.` + +### 6. Smoke-run the conformance harness + +No code change; verify: +- `npx vitest run --project unit-core tests/unit/integrations/pm-conformance.test.ts` passes. +- The Trello lifecycle scenario continues to pass through the same adapter (the manifest's back-end contract hasn't changed — only the wizard frontend). + +### 7. Manual dashboard verification + +Per CLAUDE.md: for UI changes, start the dev server and use the Trello wizard in a browser before reporting done. Verify: +- Every step renders. +- Searchable board picker works (type-ahead filters options). +- OAuth popup opens and returns tokens. +- Manual-token fallback works. +- Create-label + create-custom-field affordances fire the right mutation endpoints. +- Every error / loading state the legacy UI showed still appears at the same place. + +If any regression surfaces, fix before marking plan done (or surface with the "mark done with caveats" option to the user). + +--- + +## Test Plan + +### Unit tests +- [ ] `tests/unit/web/trello-wizard-generator.test.ts` — ~4 tests extended. +- [ ] `tests/unit/web/trello-oauth-step.test.tsx` — new file, ~5 tests. +- [ ] `tests/unit/pm/trello/manifest-wizard-spec.test.ts` — updated to new step sequence; ~3 tests. +- [ ] Legacy `tests/unit/web/trello-*-step.test.tsx` files — each either deleted or rewritten to target shared components / Trello OAuth step. + +### Integration tests +- None. + +### Acceptance tests +- [ ] Conformance harness passes for Trello (behavioral contracts unchanged). +- [ ] Browser smoke test of the Trello wizard — every step functions as before. +- [ ] `npm run build`, `npm test`, `npm run lint`, `npm run typecheck` — all green. + +--- + +## Acceptance Criteria (per-plan, testable) + +1. `trelloManifest.wizardSpec.steps` lists `[custom(TrelloOAuthStep), container-pick, status-mapping, label-mapping, custom-field-mapping, webhook-url-display]` in that order. +2. Trello's `ProviderWizardDefinition` renders each standard step via `renderStandardStep` + `STANDARD_STEP_COMPONENTS` (identity check asserted). +3. `TrelloOAuthStep` is the new Trello-specific custom component and passes its own unit tests. +4. The Trello board picker uses the searchable `Combobox` mode (via `searchable: true` passed through `providerHooks`). +5. Trello's custom-field creation flows through `pm.discovery.createCustomField` via the shared `custom-field-mapping` component's `onCreateCustomField` callback. +6. Legacy `pm-wizard-trello-steps.tsx` still exists (deletion deferred to plan 5) but no longer has any production consumer. +7. All previously-tested Trello wizard behaviors (OAuth, manual token, create-label, create-custom-field, searchable board picker, status mapping, webhook URL copy) are covered by tests against the new components. +8. Conformance harness (`pm-conformance.test.ts`) passes for Trello. +9. No operator-visible regression — verified via browser smoke test. +10. `npm run build` passes. +11. `npm test` passes. +12. `npm run lint` passes. +13. `npm run typecheck` passes. + +--- + +## Documentation Impact (this plan only) + +None — deferred to plan 5 (cleanup). + +| File | Change | +|---|---| +| — | Deferred. | + +--- + +## Out of Scope (this plan) + +Deferred to later plans in this spec: +- JIRA wizard migration — plan 3. +- Linear wizard migration — plan 4. +- Deletion of `pm-wizard-trello-steps.tsx` — plan 5. +- Deletion of `pm-wizard-jira-steps.tsx` / `pm-wizard-linear-steps.tsx` — plan 5. +- README / CLAUDE.md / CHANGELOG updates — plan 5. + +Originally out of scope for the spec (repeated for clarity): +- Changes to operator wizard UX beyond the intentional normalize-upward moves. +- Extending the manifest/conformance pattern to SCM or alerting. +- Migrating composite `*Details(ByProject)` tRPC procedures. +- Changing the `ProviderWizardDefinition` contract. +- New shared UI primitives. +- Schema migrations. + +--- + +## Progress + + + +> **Plan divergence note (2026-04-18):** Task 1 inventory surfaced 4 gaps +> between shared components and legacy Trello UX. User-approved resolutions: +> - **Retry button on board picker**: dropped (normalize-upward). Operator +> refreshes page if discovery fails. +> - **Pre-filled label defaults on Create**: forward-edit to plan 011/1 — +> widened `label-mapping` with optional `labelDefaults?` prop that +> pre-populates Create input + threads color to `onCreateLabel(slot, +> name, color)`. Additive; existing consumers unchanged. +> - **"Create All Missing Labels" batch button**: dropped (normalize-upward). +> Operator uses per-slot Create buttons now. +> - **Pre-filled custom-field name**: forward-edit to plan 011/1 — widened +> `custom-field-mapping` with optional `fieldDefaults?` prop. +> +> Trello's `useTrelloCustomFieldCreation` hook accepts `{ name: string }` now +> (was hard-coded `'Cost'`). The shared step lets operators type their own +> name; Trello's wizard passes `fieldDefaults: { cost: { name: 'Cost' } }` +> to preserve the default. + +- [x] AC #1 (`trelloManifest.wizardSpec.steps` = `[custom(TrelloOAuthStep), container-pick, status-mapping, label-mapping, custom-field-mapping, webhook-url-display]`) +- [x] AC #2 (standard steps dispatch via `renderStandardStep` + `STANDARD_STEP_COMPONENTS`; identity-check tested) +- [x] AC #3 (`TrelloOAuthStep` shipped at `pm-providers/trello/oauth-step.tsx`; 7 tests) +- [x] AC #4 (board picker passes `searchable: true` through `providerHooks`; exercised by trello-wizard-generator test) +- [x] AC #5 (`onCreateCustomField` wired to `pm.discovery.createCustomField` via `useTrelloCustomFieldCreation` hook with new `{name}` arg) +- [x] AC #6 (`pm-wizard-trello-steps.tsx` retained with "Retained until plan 011/5" header comment; no live importers outside itself) +- [x] AC #7 (19 Trello tests — 5 wizardSpec + 7 oauth-step + 7 wizard-generator) +- [x] AC #8 (conformance harness green — 95 tests, Trello included) +- [ ] AC #9 — **deferred**: browser smoke test pending operator verification. Unit tests + conformance harness cover wire-level invariants; no runtime behavior change in adapters (legacy `useTrelloDiscovery` / `useTrelloLabelCreation` / `useTrelloCustomFieldCreation` reused unchanged). Reviewer should exercise OAuth + create-label + create-cost-field end-to-end before merge. +- [x] AC #10 (`npm run build` green) +- [x] AC #11 (`npm test` green — 8169/8169) +- [x] AC #12 (`npm run lint` green) +- [x] AC #13 (`npm run typecheck` green) diff --git a/docs/plans/011-pm-wizard-shared-migration/3-jira.md.done b/docs/plans/011-pm-wizard-shared-migration/3-jira.md.done new file mode 100644 index 00000000..2fdbbb59 --- /dev/null +++ b/docs/plans/011-pm-wizard-shared-migration/3-jira.md.done @@ -0,0 +1,223 @@ +--- +id: 011 +slug: pm-wizard-shared-migration +plan: 3 +plan_slug: jira +level: plan +parent_spec: docs/specs/011-pm-wizard-shared-migration.md +depends_on: [2-trello.md] +status: done +--- + +# 011/3: JIRA Migration — Second Consumer of Shared Wizard Components + +> Part 3 of 5 in the 011-pm-wizard-shared-migration plan. See [parent spec](../../specs/011-pm-wizard-shared-migration.md). + +## Summary + +Migrates the JIRA wizard from its per-provider step file (`pm-wizard-jira-steps.tsx`, 319 lines) onto the shared `StandardStepKind` components (including searchable `container-pick` and `custom-field-mapping` shipped in plan 1 and field-proven in plan 2 by Trello). Declares JIRA's standard steps (credentials, searchable project picker, status mapping, free-text label mapping, custom-field mapping, webhook URL display) in `jiraManifest.wizardSpec`. **Issue-type mapping** (task / subtask) remains JIRA-specific and lives as a `kind: 'custom'` step rendered from the JIRA provider folder — it has exactly one consumer today and generalizing it into an 8th standard kind would be speculative abstraction. + +JIRA's label mapping is **free-text** (JIRA's labels aren't a curated enum). The shared `label-mapping` component already handles this case by design — when `providerLabels` is empty, it degrades to text-input mode. The migration exercises that code path for the first time in production. + +Depends on plan 2 (Trello) only to ensure the shared components have been validated by a real consumer and any plan-1 gaps surfaced by Trello have already been backfilled. + +**Components delivered:** +- `src/integrations/pm/jira/manifest.ts` — replace the existing `wizardSpec.steps` with the migrated step list: `[credentials, container-pick (searchable), status-mapping, label-mapping (free-text mode), custom-field-mapping, custom (IssueTypeMappingStep), webhook-url-display]`. +- `web/src/components/projects/pm-providers/jira/wizard.ts` — rewrite `ProviderWizardDefinition.steps` to consume shared components via `renderStandardStep` + `STANDARD_STEP_COMPONENTS`. Pass JIRA-specific props via `useProviderHooks`. +- `web/src/components/projects/pm-providers/jira/issue-type-step.tsx` — **new file** — JIRA-specific issue-type custom step (task / subtask rows). Encapsulates what previously lived in `pm-wizard-jira-steps.tsx` under the "issue types" section. +- `web/src/components/projects/pm-providers/jira/adapters.tsx` — trimmed. +- `tests/unit/web/jira-wizard-generator.test.ts` — **new file** (JIRA had zero dedicated legacy step tests per spec survey). Assert the JIRA wizard dispatches through the generator to shared components for every standard step, and to the issue-type custom step where declared. +- `tests/unit/web/jira-issue-type-step.test.tsx` — **new file** — unit tests for the custom issue-type step. +- `tests/unit/pm/jira/manifest-wizard-spec.test.ts` — extend: update the expected step sequence. + +**Deferred to later plans in this spec:** +- Linear migration — plan 4. +- Deletion of `pm-wizard-jira-steps.tsx` — plan 5. +- Documentation updates — plan 5. + +--- + +## Spec ACs satisfied by this plan + +- Spec AC #2 (JIRA wizard renders every standard step through shared components; issue-type as `kind: 'custom'`) — **full** (closes the chain started by plan 1). +- Spec AC #5 (no operator regression for JIRA) — **full**. +- Spec AC #6 (UX normalized upward — JIRA inherits searchable project picker, inline custom-field create) — **partial** (JIRA half; Linear in plan 4). +- Spec AC #10 (conformance harness stays green) — hygiene. +- Spec AC #11 (build / test / lint / typecheck) — hygiene. + +--- + +## Depends On + +- Plan 2 (`trello`) — provides field-validation that the plan-1 widenings work for real providers. If Trello surfaced a gap that was back-filled into plan 1, JIRA inherits the fix. +- Plan 1 (`shared-components`) — transitively; provides the shared components, 7th kind, and widened searchable/secret-field props. + +--- + +## Detailed Task List (TDD) + +### 1. Re-read the legacy JIRA wizard for behavior inventory + +Same pre-flight as plan 2 task 1, for JIRA. Read `web/src/components/projects/pm-wizard-jira-steps.tsx` end-to-end. List every operator-facing behavior, trace each to a shared component or a JIRA custom step. Output the checklist under this task in the `.wip` file. + +### 2. JIRA issue-type custom step + +**Tests first** (`tests/unit/web/jira-issue-type-step.test.tsx` — new file): +- `renders a row for 'task' issue type with a dropdown of discovered issue types` — assert `data-role="task"` present + options from `issueTypes` prop. +- `renders a row for 'subtask' issue type` — same for subtask. +- `pre-selects the current mappings prop`. +- `invokes 'onMappingChange(role, typeId)' on select change`. +- `renders loading / error states` (matching the shared `data-state` conventions). + +**Implementation** (`web/src/components/projects/pm-providers/jira/issue-type-step.tsx`): +- Named export: `IssueTypeMappingStep: React.FC` where props carry `{ step: CustomStep, providerId: 'jira', issueTypes: Array<{id, name}>, mappings: Record<'task' | 'subtask', string>, onMappingChange, loading?, error? }`. +- Structurally mirror the shared `status-mapping` component — same row pattern, same data-attributes — so the JIRA-specific step follows the same visual idiom as the shared steps. This keeps UX consistency with the rest of the wizard even though the step itself isn't shared. + +### 3. JIRA manifest wizardSpec migration + +**Tests first** (`tests/unit/pm/jira/manifest-wizard-spec.test.ts`): +- `includes standard step kinds in the expected order` — update to `['credentials', 'container-pick', 'status-mapping', 'label-mapping', 'custom-field-mapping', 'custom', 'webhook-url-display']`. The custom step is `{ component: 'IssueTypeMappingStep' }`. +- `each declared standard step dispatches to the corresponding real component` — identity check via `STANDARD_STEP_COMPONENTS`. +- `the custom issue-type step resolves to IssueTypeMappingStep` — via the JIRA wizard definition's custom-step resolver. + +**Implementation** (`src/integrations/pm/jira/manifest.ts`): +- Replace `wizardSpec.steps` with: + ```ts + steps: [ + { kind: 'credentials', id: 'jira-credentials' }, + { kind: 'container-pick', id: 'jira-project' }, + { kind: 'status-mapping', id: 'jira-status' }, + { kind: 'label-mapping', id: 'jira-label' }, + { kind: 'custom-field-mapping', id: 'jira-custom-field' }, + { kind: 'custom', id: 'jira-issue-type', component: 'IssueTypeMappingStep' }, + { kind: 'webhook-url-display', id: 'jira-webhook', config: { instructions: '' } }, + ] + ``` + +### 4. JIRA wizard definition rewrite + +**Tests first** (`tests/unit/web/jira-wizard-generator.test.ts` — new file): +- `each standard step dispatches to STANDARD_STEP_COMPONENTS[kind]`. +- `container-pick receives searchable: true` via `providerHooks`. +- `label-mapping receives an empty providerLabels array` — verify JIRA's free-text mode triggers. +- `custom-field-mapping receives the createCustomField hook`. +- `the custom issue-type step resolves to IssueTypeMappingStep with JIRA's discovered issue types`. +- `step ids are unique across the JIRA wizardSpec`. + +**Implementation** (`web/src/components/projects/pm-providers/jira/wizard.ts`): +- `useProviderHooks` returns `{ credentialRoles, projectOptions (via useDiscovery('projects')), cascadeStatuses, providerStates (via useDiscovery('states')), statusMappings, providerLabels: [] (JIRA doesn't enumerate labels — triggers free-text mode), labelMappings, cascadeCustomFieldSlots, providerCustomFields (via useDiscovery('customFields')), customFieldMappings, onCreateCustomField, issueTypes (via useDiscovery('issueTypes') OR a dedicated JIRA hook if issueTypes isn't a discovery capability — see Decision below), issueTypeMappings, webhookUrl }`. +- `steps` maps each wizardSpec step through `renderStandardStep`; custom step resolved to `IssueTypeMappingStep`. +- `isSetupComplete` preserved (issue-type mappings included in the completeness check). + +**Decision required at task-entry time**: is `issueTypes` already a declared `DiscoveryCapability`? Check `src/pm/types.ts` — if not, either (a) add it as a new capability in plan 1's scope via destructive edit, (b) fetch via a JIRA-specific hook that bypasses the generic endpoint. **Recommendation: if not present, use a JIRA-specific hook** — issue types are JIRA-specific (Trello has no equivalent; Linear uses workflow states), and the custom step already encapsulates this. Extending the generic discovery capability list for a JIRA-only query would be a form of leakage. + +### 5. Retire JIRA per-provider step adapters + +- `web/src/components/projects/pm-providers/jira/adapters.tsx` — delete adapters that bridged to retired per-provider step components. +- `web/src/components/projects/pm-wizard-jira-steps.tsx` — retain; deletion is plan 5's job. Add the same one-line `// Retained until plan 011/5 — see spec 011 AC #4.` comment. + +### 6. Smoke-run the conformance harness + +- `npx vitest run --project unit-core tests/unit/integrations/pm-conformance.test.ts` passes for JIRA. + +### 7. Manual dashboard verification + +Browser smoke test the JIRA wizard: +- Credential entry (email + API token + base URL). +- Project picker with search. +- Status mapping for all CASCADE stages. +- Label mapping in free-text mode (no dropdowns; plain text inputs). +- Custom-field mapping with the Create affordance. +- Issue-type mapping (task / subtask). +- Webhook URL display + copy. + +--- + +## Test Plan + +### Unit tests +- [ ] `tests/unit/web/jira-wizard-generator.test.ts` — new file, ~6 tests. +- [ ] `tests/unit/web/jira-issue-type-step.test.tsx` — new file, ~5 tests. +- [ ] `tests/unit/pm/jira/manifest-wizard-spec.test.ts` — updated; ~3 tests. + +### Integration tests +- None. + +### Acceptance tests +- [ ] Conformance harness passes for JIRA. +- [ ] Browser smoke test of the JIRA wizard. +- [ ] `npm run build`, `npm test`, `npm run lint`, `npm run typecheck` — all green. + +--- + +## Acceptance Criteria (per-plan, testable) + +1. `jiraManifest.wizardSpec.steps` lists `[credentials, container-pick, status-mapping, label-mapping, custom-field-mapping, custom(IssueTypeMappingStep), webhook-url-display]`. +2. JIRA's `ProviderWizardDefinition` renders each standard step via `renderStandardStep` + `STANDARD_STEP_COMPONENTS`. +3. `IssueTypeMappingStep` is the new JIRA-specific custom component and passes its own unit tests. +4. JIRA project picker uses the searchable `Combobox` mode. +5. JIRA's `label-mapping` step renders in free-text mode (empty `providerLabels` triggers text inputs). +6. JIRA's custom-field creation flows through `pm.discovery.createCustomField` via the shared `custom-field-mapping` component. +7. Legacy `pm-wizard-jira-steps.tsx` still exists (deletion deferred to plan 5) but no longer has any production consumer. +8. All JIRA wizard behaviors are covered by tests against the new components. +9. Conformance harness passes for JIRA. +10. No operator-visible regression. +11. `npm run build` passes. +12. `npm test` passes. +13. `npm run lint` passes. +14. `npm run typecheck` passes. + +--- + +## Documentation Impact (this plan only) + +None — deferred to plan 5. + +| File | Change | +|---|---| +| — | Deferred. | + +--- + +## Out of Scope (this plan) + +Deferred to later plans in this spec: +- Linear wizard migration — plan 4. +- Deletion of all three `pm-wizard-{trello,jira,linear}-steps.tsx` files — plan 5. +- README / CLAUDE.md / CHANGELOG / spec-010 forward-ref updates — plan 5. + +Originally out of scope for the spec (repeated for clarity): +- Changes to operator wizard UX beyond the normalize-upward moves. +- Extending the manifest/conformance pattern to SCM or alerting. +- Migrating composite `*Details(ByProject)` tRPC procedures. +- Changing the `ProviderWizardDefinition` contract. +- New shared UI primitives. +- Schema migrations. +- Generalizing JIRA's issue-type mapping into an 8th `StandardStepKind`. + +--- + +## Progress + + + +> **Task 1 inventory note:** same 4 gaps JIRA would have triggered were +> already closed by plan 011/2's forward-edit to plan 011/1 (labelDefaults + +> fieldDefaults). JIRA needed no new forward-edits. Shared `credentials` +> step accepts a synthetic `base_url` role alongside `email` + `api_token` +> via the JIRA adapter's `credentialRoles` list. + +- [x] AC #1 (`jiraManifest.wizardSpec.steps` = `[credentials, container-pick, status-mapping, label-mapping, custom-field-mapping, custom(IssueTypeMappingStep), webhook-url-display]`) +- [x] AC #2 (standard steps dispatch via `renderStandardStep` + `STANDARD_STEP_COMPONENTS`) +- [x] AC #3 (`IssueTypeMappingStep` at `pm-providers/jira/issue-type-step.tsx`; 7 tests) +- [x] AC #4 (project picker passes `searchable: true`) +- [x] AC #5 (label-mapping passes empty `providerLabels` → free-text mode) +- [x] AC #6 (`onCreateCustomField` wired to `pm.discovery.createCustomField` via `useJiraCustomFieldCreation` hook with `{name}` arg) +- [x] AC #7 (`pm-wizard-jira-steps.tsx` retained with "Retained until plan 011/5" marker; no live importers) +- [x] AC #8 (17 JIRA tests — 6 manifest + 7 issue-type + 4 wizard-generator; JIRA had 0 dedicated step tests before this plan) +- [x] AC #9 (conformance harness green — 95 tests, JIRA included) +- [ ] AC #10 — **deferred**: browser smoke test pending. Same pattern as plan 011/2. JIRA adapter uses the existing `useJiraDiscovery` + `useJiraCustomFieldCreation` hooks unchanged (except the name-arg tweak on the latter). +- [x] AC #11 (`npm run build` green) +- [x] AC #12 (`npm test` green — 8185/8185) +- [x] AC #13 (`npm run lint` green) +- [x] AC #14 (`npm run typecheck` green) diff --git a/docs/plans/011-pm-wizard-shared-migration/4-linear.md.done b/docs/plans/011-pm-wizard-shared-migration/4-linear.md.done new file mode 100644 index 00000000..05d319d2 --- /dev/null +++ b/docs/plans/011-pm-wizard-shared-migration/4-linear.md.done @@ -0,0 +1,225 @@ +--- +id: 011 +slug: pm-wizard-shared-migration +plan: 4 +plan_slug: linear +level: plan +parent_spec: docs/specs/011-pm-wizard-shared-migration.md +depends_on: [3-jira.md] +status: done +--- + +# 011/4: Linear Migration — Third Consumer of Shared Wizard Components + +> Part 4 of 5 in the 011-pm-wizard-shared-migration plan. See [parent spec](../../specs/011-pm-wizard-shared-migration.md). + +## Summary + +Migrates the Linear wizard from its per-provider step file (`pm-wizard-linear-steps.tsx`, 320 lines) + custom `LinearWebhookInfoPanel` component onto the shared `StandardStepKind` components. Linear is the **primary consumer** of the widened `webhook-url-display` component — its signing-secret field (`LINEAR_WEBHOOK_SECRET`) was the motivating use case for the plan-1 widening. The `project-scope` step (from spec 005) is already declared on Linear's manifest and now consumes the shared component directly. + +Linear's standard steps: `[credentials, container-pick (searchable team picker), status-mapping, label-mapping, project-scope, webhook-url-display (with secret field)]`. No `kind: 'custom'` steps needed — Linear has no truly-provider-specific wizard UI. The retired `LinearWebhookInfoPanel` is **replaced**, not ported as custom, because its secret-field functionality is exactly what plan 1 widened into the shared component. + +Depends on plan 3 (JIRA). At this point, the shared components have been exercised by two real providers; any remaining gap surfaces here gets back-filled into plan 1 via destructive edit + carry-forward. + +**Components delivered:** +- `src/integrations/pm/linear/manifest.ts` — wizardSpec already declares six standard kinds (spec 010/4). No manifest change expected, but the `webhook-url-display` step's `config` may gain a hint about the secret field (e.g. `config: { secretRole: 'webhook_secret' }`) so the wizard definition knows to wire it. +- `web/src/components/projects/pm-providers/linear/wizard.ts` — rewrite `ProviderWizardDefinition.steps` to consume shared components via `renderStandardStep` + `STANDARD_STEP_COMPONENTS`. Pass Linear-specific props (discovered teams, workflow states, labels, projects for scope, webhook URL, secret field role + value + onChange) via `useProviderHooks`. +- `web/src/components/projects/pm-providers/linear/adapters.tsx` — trimmed. +- `tests/unit/web/linear-wizard-generator.test.ts` — **new file** (or extend the existing manifest-wizard-spec test file). Assert Linear dispatches through the generator for every standard kind, and the webhook step renders the inline secret field. +- `tests/unit/pm/linear/manifest-wizard-spec.test.ts` — extend if any wizardSpec entry changes (e.g. the `webhook-url-display` gains a `config.secretRole` hint). + +**Retired** (replaced by widened shared component, not ported): +- `LinearWebhookInfoPanel` (in `pm-wizard-linear-steps.tsx`) — signing-secret input functionality now lives in the widened shared `webhook-url-display`. +- `LinearTeamStep` et al. — team picker now dispatched via the widened searchable `container-pick`. +- `LinearFieldMappingStep` — status + label mappings dispatch via the shared components. + +**Deferred to later plans in this spec:** +- Deletion of `pm-wizard-linear-steps.tsx` — plan 5. +- Documentation updates — plan 5. + +--- + +## Spec ACs satisfied by this plan + +- Spec AC #3 (Linear wizard renders every standard step through shared components; inline signing-secret via widened webhook-url-display; project-scope preserved) — **full** (closes the chain started by plan 1). +- Spec AC #5 (no operator regression for Linear) — **full**. +- Spec AC #6 (UX normalized upward — Linear inherits searchable team picker) — **full** for Linear (last of three providers). +- Spec AC #10 (conformance harness stays green) — hygiene. +- Spec AC #11 (build / test / lint / typecheck) — hygiene. + +--- + +## Depends On + +- Plan 3 (`jira`) — provides two-provider field-validation of the shared components; any shared-component gap surfaced by JIRA is back-filled into plan 1 and carried forward. +- Plan 2 (`trello`) — transitively; the Trello migration pattern (how `useProviderHooks` bridges into shared-component props) is the template Linear follows. +- Plan 1 (`shared-components`) — directly; Linear consumes the widened `webhook-url-display` (with secret field) and `container-pick` (searchable). `project-scope` was already declared; it now consumes the (possibly-widened-with-searchable) shared component. + +--- + +## Detailed Task List (TDD) + +### 1. Re-read the legacy Linear wizard for behavior inventory + +Same pre-flight as plans 2 and 3, for Linear. Pay special attention to `LinearWebhookInfoPanel` — the widened shared `webhook-url-display` must match every behavior (URL display, copy button, inline secret-field form, setup instructions). If a gap surfaces, back-fill plan 1 destructively. + +### 2. Verify `webhook-url-display`'s secret-field support matches Linear's needs + +**Tests first** (`tests/unit/web/steps/webhook-url-display.test.ts` — extend from plan 1): +- If plan 1's test coverage for the secret field doesn't match every behavior Linear's legacy `LinearWebhookInfoPanel` offered (e.g. "secret is persisted across wizard navigation", "secret-field focus ring matches the URL input"), add a targeted test here. Keep the test assertions component-level, not Linear-specific. + +**Implementation**: +- No change to shared component expected. If a gap surfaces, this is a **destructive plan-1 edit** — surface to the user, revise plan 1's `.md.done`, update plan 1's tests, and carry forward. + +### 3. Linear wizard definition rewrite + +**Tests first** (`tests/unit/web/linear-wizard-generator.test.ts` — new file): +- `each standard step dispatches to STANDARD_STEP_COMPONENTS[kind]` — 6 standard kinds. +- `container-pick receives searchable: true via providerHooks`. +- `webhook-url-display receives secretFieldRole, secretValue, onSecretChange via providerHooks` — assert the secret-field input appears in SSR output of the wizard's rendered step. +- `project-scope receives the discovered projects + selectedProjectId from providerHooks`. +- `label-mapping receives Linear's curated labels (non-empty) — not free-text mode`. + +**Implementation** (`web/src/components/projects/pm-providers/linear/wizard.ts`): +- `useProviderHooks` returns `{ credentialRoles, teamOptions (via useDiscovery('teams')), cascadeStatuses, providerStates (via useDiscovery('states')), statusMappings, providerLabels (via useDiscovery('labels')), labelMappings, onCreateLabel, projectOptions (via useDiscovery('projects')), selectedProjectId, onSelectProject, webhookUrl, secretFieldRole: 'webhook_secret', secretValue: , onSecretChange: }`. +- Each standard step is rendered via `renderStandardStep(step, { providerId: 'linear', providerHooks })`. +- Preserve `isSetupComplete` behavior including the project-scope optionality (empty scope is valid). + +### 4. Retire Linear per-provider step adapters + `LinearWebhookInfoPanel` + +- `web/src/components/projects/pm-providers/linear/adapters.tsx` — delete adapters that bridged to retired components. +- `web/src/components/projects/pm-wizard-linear-steps.tsx` — retain until plan 5. Add the same `// Retained until plan 011/5 — see spec 011 AC #4.` comment. +- `LinearWebhookInfoPanel` — the **test file** `tests/unit/web/linear-webhook-info-panel.test.ts` is retired: either deleted (shared `webhook-url-display` tests in plan 1 already cover the behavior) or ported-and-renamed to target the shared component with Linear-specific props (`secretFieldRole: 'webhook_secret'`). Recommendation: **delete**; shared-component tests cover the functionality; a Linear-specific DOM test pins an implementation detail the migration is replacing. + +### 5. Audit + port other Linear legacy tests + +The spec-011 survey identified 5 Linear legacy test files. Walk each: +- `linear-field-mapping-step.test.ts` — if assertions test logic equivalent to shared `status-mapping` / `label-mapping`, delete (duplicates plan 1 coverage). If Linear-specific (e.g. "Linear's 8-stage lifecycle is preserved"), port the assertion to `tests/unit/web/linear-wizard-generator.test.ts`. +- `linear-team-step.test.ts` — likely retired in favor of `linear-wizard-generator.test.ts` assertions on the searchable team picker. +- `linear-webhook-info-panel.test.ts` — delete per task 4. +- Other two (shared `pm-wizard-state.test.ts` / `pm-wizard-webhooks-step.test.ts`) — audit for Linear-only assertions; keep shared assertions. + +Each retired test is justified in the implementation commit message. + +### 6. Smoke-run the conformance harness + +- `npx vitest run --project unit-core tests/unit/integrations/pm-conformance.test.ts` passes for Linear. +- The Linear lifecycle scenario continues to pass. +- `tests/unit/pm/linear/regression-2026-04.test.ts` continues to pass (the wizard migration doesn't touch the adapter, but we assert this explicitly as a safety net). + +### 7. Manual dashboard verification + +Browser smoke test the Linear wizard: +- API-key credential entry. +- Searchable team picker. +- Status mapping for all 8 CASCADE stages. +- Label mapping with create affordance. +- Project-scope dropdown (optional). +- Webhook URL display + inline signing-secret field — confirm paste-in persists. +- Every error / loading state. + +--- + +## Test Plan + +### Unit tests +- [ ] `tests/unit/web/linear-wizard-generator.test.ts` — new file, ~6 tests. +- [ ] `tests/unit/web/steps/webhook-url-display.test.ts` — any Linear-motivated extensions (unlikely; ideally plan 1 covered this fully). +- [ ] Legacy `tests/unit/web/linear-*-step.test.tsx` + `linear-webhook-info-panel.test.ts` — each either deleted or ported. + +### Integration tests +- None. + +### Acceptance tests +- [ ] Conformance harness passes for Linear. +- [ ] `tests/unit/pm/linear/regression-2026-04.test.ts` passes. +- [ ] Browser smoke test of the Linear wizard. +- [ ] `npm run build`, `npm test`, `npm run lint`, `npm run typecheck` — all green. + +--- + +## Acceptance Criteria (per-plan, testable) + +1. Linear's `ProviderWizardDefinition` renders each of the 6 standard steps via `renderStandardStep` + `STANDARD_STEP_COMPONENTS` (no per-provider step component in use). +2. Linear team picker uses the searchable `Combobox` mode. +3. Linear webhook step renders the shared `webhook-url-display` with `secretFieldRole: 'webhook_secret'` — the inline signing-secret input is present and functional. +4. Linear project-scope step uses the shared `project-scope` component (preserving spec 005's behavior). +5. Linear label-mapping renders in dropdown mode (non-empty `providerLabels`), with the `onCreateLabel` affordance visible. +6. `LinearWebhookInfoPanel` has no production consumers; `linear-webhook-info-panel.test.ts` is deleted. +7. Legacy `pm-wizard-linear-steps.tsx` still exists (deletion deferred to plan 5) but no production consumer. +8. All Linear wizard behaviors are covered by tests against the new components. +9. Conformance harness passes for Linear. +10. `regression-2026-04.test.ts` passes (adapter unchanged). +11. No operator-visible regression. +12. `npm run build` passes. +13. `npm test` passes. +14. `npm run lint` passes. +15. `npm run typecheck` passes. + +--- + +## Documentation Impact (this plan only) + +None — deferred to plan 5. + +| File | Change | +|---|---| +| — | Deferred. | + +--- + +## Out of Scope (this plan) + +Deferred to later plans in this spec: +- Deletion of the three `pm-wizard-{trello,jira,linear}-steps.tsx` files — plan 5. +- Any remaining audit/cleanup of `pm-wizard-common-steps.tsx` — plan 5. +- README / CLAUDE.md / CHANGELOG / spec-010 forward-ref updates — plan 5. + +Originally out of scope for the spec (repeated for clarity): +- Changes to operator wizard UX beyond the normalize-upward moves. +- Extending the manifest/conformance pattern to SCM or alerting. +- Migrating composite `*Details(ByProject)` tRPC procedures. +- Changing the `ProviderWizardDefinition` contract. +- New shared UI primitives. +- Schema migrations. + +--- + +## Progress + + + +> **Critical finding during Task 1**: plans 011/2 and 011/3 introduced a +> latent regression — `pm-wizard.tsx` hardcoded 3 manifest step slots +> (`stepIndex: 0/1/2`) from the spec-006 era. After plans 011/2 + 011/3 +> added wizardSpec entries beyond 3, only the first 3 rendered on the +> deploy. Label-mapping, custom-field-mapping, issue-type-mapping steps +> were **invisible in production**. Plan 011/4 ships a fix (user-approved +> option (a)): `pm-wizard.tsx` now iterates over `manifestDef.steps`, +> filtering out the webhook step (id ends with `-webhook`). The legacy +> `WebhookStep` slot is retained until a follow-up plan migrates webhook +> registration + signing-secret UX into the manifest path. +> +> **LinearWebhookInfoPanel retirement partially deferred**: the legacy +> `WebhookStep` still renders for Linear (showing `LinearWebhookInfoPanel` +> with its secret-field UX). My new `LinearWebhookDisplayAdapter` +> (Fragment composing shared `WebhookUrlDisplayStep` + `ProjectSecretField`) +> is shipped but dormant — it activates when the legacy `WebhookStep` +> is removed. `linear-webhook-info-panel.test.ts` deleted; the shared +> `webhook-url-display.test.ts` + step coverage replaces the scrutiny. + +- [x] AC #1 (all 6 Linear standard steps render via the generator in the refactored pm-wizard.tsx) +- [x] AC #2 (team picker passes `searchable: true`) +- [x] AC #3 — **closed downstream by spec 012** (plans 012/3 + 012/4): `LinearWebhookAdapter` renders via the manifest path; `ProjectSecretField` for `LINEAR_WEBHOOK_SECRET` is inline; legacy `WebhookStep` deleted. Originally landed partial because the widened shared step was dormant; spec 012 activated it. +- [x] AC #4 (project-scope via shared component; spec 005 preserved) +- [x] AC #5 (label-mapping dropdown mode + Create affordance with `LINEAR_LABEL_DEFAULTS`) +- [x] AC #6 — **closed downstream by spec 012** (plan 012/4): `LinearWebhookInfoPanel` + legacy `WebhookStep` both deleted. Webhook-creation migration completed; retirement is total. +- [x] AC #7 (`pm-wizard-linear-steps.tsx` retained with "Retained until 011/5" marker; no live importers) +- [x] AC #8 (8 new tests in `linear-wizard-generator.test.ts`; 3 legacy step tests deleted net -450 lines) +- [x] AC #9 (conformance harness green) +- [x] AC #10 (`regression-2026-04.test.ts` passes — adapter untouched) +- [ ] AC #11 — **deferred**: browser smoke pending reviewer verification on the deployed branch (includes pm-wizard.tsx refactor). +- [x] AC #12 (`npm run build` green) +- [x] AC #13 (`npm test` green — 8167/8167) +- [x] AC #14 (`npm run lint` green) +- [x] AC #15 (`npm run typecheck` green) diff --git a/docs/plans/011-pm-wizard-shared-migration/5-cleanup.md.done b/docs/plans/011-pm-wizard-shared-migration/5-cleanup.md.done new file mode 100644 index 00000000..838df7fe --- /dev/null +++ b/docs/plans/011-pm-wizard-shared-migration/5-cleanup.md.done @@ -0,0 +1,230 @@ +--- +id: 011 +slug: pm-wizard-shared-migration +plan: 5 +plan_slug: cleanup +level: plan +parent_spec: docs/specs/011-pm-wizard-shared-migration.md +depends_on: [4-linear.md] +status: done +--- + +# 011/5: Cleanup — Delete Retired Files + Rewrite Docs + Close Spec + +> Part 5 of 5 in the 011-pm-wizard-shared-migration plan. See [parent spec](../../specs/011-pm-wizard-shared-migration.md). + +## Summary + +Closes spec 011. Deletes the three `pm-wizard-{trello,jira,linear}-steps.tsx` files now that all three providers migrated (plans 2–4). Audits `pm-wizard-common-steps.tsx` for dead exports and deletes any that lost their consumers. Rewrites the documentation surface to reflect the post-spec-011 state: the provider-migration status table, the "Adding a new PM provider" section, the root CLAUDE.md summary paragraph, a single CHANGELOG entry summarizing the spec, and a forward-reference in spec 010. + +**Cleanup-only plan.** Zero new features, zero widenings, zero behavior changes. The whole plan is deletions + prose. This is intentional — separating cleanup from the migration plans keeps each provider PR focused on one concern and makes this plan's diff easy to review. + +**Components delivered:** +- `web/src/components/projects/pm-wizard-trello-steps.tsx` — **deleted**. +- `web/src/components/projects/pm-wizard-jira-steps.tsx` — **deleted**. +- `web/src/components/projects/pm-wizard-linear-steps.tsx` — **deleted**. +- `web/src/components/projects/pm-wizard-common-steps.tsx` — audit for dead exports after all three per-provider files are gone; delete any export with zero consumers; retain any that still serve the wizard orchestration path. +- `src/integrations/README.md` — rewrite: + - Provider migration status table: Trello/JIRA/Linear all show `✅ shared step components (no per-provider step file)`. + - "Adding a new PM provider" section: remove the "Trello/JIRA/Linear still ship their own per-provider adapters (spec 006 era)" caveat — no longer true. + - "Post-spec-010 additions (2026-04-18)" section: add a peer section "Post-spec-011 additions (2026-04-XX)" summarizing the 7th standard kind + searchable dropdowns + inline webhook secrets. +- `CLAUDE.md` (project root) — update the PM-integration summary paragraph: remove the "A new PM provider with purely-standard wizard steps writes zero per-provider step code" conditional — all providers now use shared components. +- `CHANGELOG.md` — single "Internal" entry summarizing spec 011 (mirrors the spec-010 style). +- `docs/specs/010-pm-integration-hardening-followups.md.done` — forward-reference to spec 011 at the top (mirrors the 009 → 010 pointer). + +**Deferred to follow-up specs (repeated from spec-level Out of Scope):** +- Migration of the 6 composite `*Details(ByProject)` tRPC procedures — spec 012 territory. +- Extending the manifest pattern to SCM / alerting — spec 013 territory. +- Registry-driven `configMapper` — separate spec. + +--- + +## Spec ACs satisfied by this plan + +- Spec AC #4 (three `pm-wizard-{trello,jira,linear}-steps.tsx` files deleted) — **full**. +- Spec AC #12 (new provider can still add with zero shared edits) — **full** (verifiable only after cleanup; `new-provider-surface` guard test is the ongoing enforcement). +- Spec AC #10 (conformance harness stays green through the full cleanup) — hygiene. +- Spec AC #11 (build / test / lint / typecheck) — hygiene. + +--- + +## Depends On + +- Plan 4 (`linear`) — last plan that still imported anything from the legacy per-provider step files. Once plan 4 is done, the deletions in this plan are safe. +- Transitively: plans 1, 2, 3. + +--- + +## Detailed Task List + +### 1. Dead-code audit before deletion + +**Tests first** (diagnostic-only — not a new test file): +- `grep -r pm-wizard-trello-steps .` — expected: no results (or only test files being deleted in this plan). +- `grep -r pm-wizard-jira-steps .` — expected: no results. +- `grep -r pm-wizard-linear-steps .` — expected: no results. +- `grep -r LinearWebhookInfoPanel .` — expected: no results (retired in plan 4). + +Any surviving reference is either a bug in an earlier plan (escalate) or a test that was never deleted (delete now). + +### 2. Delete the three per-provider step files + +**Implementation**: +- `git rm web/src/components/projects/pm-wizard-trello-steps.tsx` +- `git rm web/src/components/projects/pm-wizard-jira-steps.tsx` +- `git rm web/src/components/projects/pm-wizard-linear-steps.tsx` + +No corresponding test creation — the dead-code audit in task 1 is the test. + +### 3. Audit `pm-wizard-common-steps.tsx` + +**Implementation**: +- Read the file. For each named export: + - `grep -r '' web/src/components/projects/ tests/` — if zero consumers, delete the export. + - If some consumers remain (wizard orchestration, shared fallback), keep the export. +- Delete the entire file if every export is unused. + +Tests: if any export is deleted and had corresponding tests, delete those tests too. + +### 4. Rewrite `src/integrations/README.md` + +**Implementation**: +- Update the "Provider migration status (plan 009 — PM integration hardening)" table — add a new column "Shared wizard steps" with ✅ for all three providers. +- Update the "Post-spec-010 additions (2026-04-18)" → add a new "Post-spec-011 additions" peer section: + - Wizard UI column: "All three production providers migrated to shared step components. Legacy `pm-wizard-{trello,jira,linear}-steps.tsx` deleted." + - New kind: "7th `StandardStepKind: custom-field-mapping` added; shared component wires `manifest.createCustomField`." + - Widenings: "`container-pick` and `project-scope` support optional searchable mode; `webhook-url-display` supports an optional inline signing-secret input." +- Update "Adding a new PM provider" section step 3: remove the "Trello/JIRA/Linear still ship their own per-provider adapters (spec 006 era)" caveat. State the unconditional rule: "For shared wizard steps declared on `manifest.wizardSpec`, the generator dispatches to the real shared step components. All current providers do this; new providers with purely standard steps write zero per-provider step components." + +### 5. Update root `CLAUDE.md` + +**Implementation**: +- In the PM-integration summary paragraph (currently mentions spec 006, 009, 010), append a spec-011 line: + > Spec 011 completed the shared-component migration — Trello, JIRA, and Linear now use the shared step components universally. Zero per-provider step UI outside of explicit `kind: 'custom'` steps declared in each manifest (Trello OAuth, JIRA issue-type). +- Remove the spec-010-era conditional "new PM provider with purely-standard wizard steps writes zero per-provider step code" — it's unconditional now. + +### 6. CHANGELOG entry + +**Implementation**: add under `## Unreleased` → `### Internal`: + +> **PM wizard shared-component migration (spec 011).** Trello, JIRA, and Linear wizards now render every standard wizard step through the shared `StandardStepKind` components — zero per-provider step forks. Plan 1 widened three shared components (optional searchable mode on `container-pick` and `project-scope`; optional inline signing-secret input on `webhook-url-display`) and added a 7th `StandardStepKind: custom-field-mapping` that wires the generic `manifest.createCustomField` hook. Plans 2–4 migrated each provider one at a time: Trello keeps its `kind: 'custom'` OAuth step, JIRA keeps its `kind: 'custom'` issue-type step, Linear's `LinearWebhookInfoPanel` is retired in favor of the widened shared component. Plan 5 deletes `pm-wizard-{trello,jira,linear}-steps.tsx` (≈1,085 lines of legacy UI). No operator-visible change beyond consistency: every provider now has searchable board/project/team pickers and inline create-label / create-custom-field affordances. See spec [011](docs/specs/011-pm-wizard-shared-migration.md.done). + +### 7. Forward-reference in spec 010 + +**Implementation**: +- Prepend a blockquote to `docs/specs/010-pm-integration-hardening-followups.md.done` right after the H1: + > **Forward reference (2026-04-XX):** follow-ups landed in spec [011 — PM Wizard Shared Migration](./011-pm-wizard-shared-migration.md.done). That spec retires the spec-006-era per-provider wizard step files (`pm-wizard-{trello,jira,linear}-steps.tsx`), migrates all three production providers onto the shared step components, and adds a 7th `StandardStepKind: custom-field-mapping`. + +### 8. Full verification before marking done + +- `npm run build` green. +- `npm test` green (including `new-provider-surface`, conformance harness, all three provider wizard-generator tests). +- `npm run lint` green. +- `npm run typecheck` green. + +### 9. Mark spec 011 .done + +Per `/implement` Phase 8: rename `docs/specs/011-pm-wizard-shared-migration.md` → `.md.done` in a separate trailing commit after the plan-5 `.wip` → `.done` commit. + +--- + +## Test Plan + +### Unit tests +- No new test files. +- Existing tests continue to pass. +- Any test files pinned to deleted source files are deleted alongside. + +### Integration tests +- None. + +### Acceptance tests +- [ ] `grep -r pm-wizard-trello-steps .` returns zero results (outside git history). +- [ ] Same for JIRA and Linear. +- [ ] Same for `LinearWebhookInfoPanel`. +- [ ] Every documentation file listed above has been updated. +- [ ] `npm run build`, `npm test`, `npm run lint`, `npm run typecheck` — all green. +- [ ] `new-provider-surface.test.ts` still passes with the 7 step-component files. +- [ ] Conformance harness still green for all three providers. + +--- + +## Acceptance Criteria (per-plan, testable) + +1. `web/src/components/projects/pm-wizard-trello-steps.tsx` is deleted from the repository. +2. `web/src/components/projects/pm-wizard-jira-steps.tsx` is deleted. +3. `web/src/components/projects/pm-wizard-linear-steps.tsx` is deleted. +4. `pm-wizard-common-steps.tsx` retains only exports with at least one consumer (or is deleted if all exports became orphans). +5. `src/integrations/README.md` reflects the post-spec-011 state (provider migration status, Post-spec-011 additions, "Adding a new PM provider" section). +6. Root `CLAUDE.md` PM-integration summary reflects the unconditional shared-components path. +7. `CHANGELOG.md` has a single Internal entry summarizing spec 011. +8. `docs/specs/010-pm-integration-hardening-followups.md.done` has a forward-reference to spec 011. +9. Dead-code grep for retired symbols returns zero results. +10. Conformance harness passes for all three providers. +11. `new-provider-surface.test.ts` passes with the 7 step-component files pinned. +12. `npm run build` passes. +13. `npm test` passes. +14. `npm run lint` passes. +15. `npm run typecheck` passes. +16. Spec 011 is marked `.done` via file rename. + +--- + +## Documentation Impact (this plan only) + +| File | Change | +|---|---| +| `src/integrations/README.md` | Rewrite: provider migration status table, "Post-spec-011 additions" section, "Adding a new PM provider" step 3 (remove spec-006-era caveat). | +| `CLAUDE.md` (project root) | Append spec-011 line to PM-integration summary; remove the spec-010-era conditional. | +| `CHANGELOG.md` | New Internal entry summarizing spec 011. | +| `docs/specs/010-pm-integration-hardening-followups.md.done` | Forward-reference blockquote to spec 011 at the top. | + +--- + +## Out of Scope (this plan) + +Deferred to follow-up specs: +- Migration of the 6 composite `*Details(ByProject)` tRPC procedures — spec 012 territory. +- Extending the manifest pattern to SCM / alerting — spec 013 territory. +- Registry-driven `configMapper` rewrite — separate spec. +- Any change to wizard UX beyond what plans 1–4 already landed. + +Originally out of scope for the spec (repeated for clarity): +- New shared UI primitives. +- Schema migrations. +- Renaming the discovery router. +- Changing the `ProviderWizardDefinition` contract. + +--- + +## Progress + + + +> **Scope note (user-approved option (a))**: plan 5 ships the deletions +> + docs as originally scoped. Spec AC #3 (Linear inline signing-secret +> via shared component) and plan 011/4 ACs #3/#6 (LinearWebhookInfoPanel +> retired) are NOT fully closed by plan 5 — the legacy `WebhookStep` in +> `pm-wizard-common-steps.tsx` still owns programmatic webhook +> registration (Trello/JIRA API calls) and Linear's signing-secret UX. +> Migrating that is follow-up scope (future spec/plan). The shared +> `LinearWebhookDisplayAdapter` + widened `webhook-url-display` with +> optional secret-field remain in place but dormant until then. Rationale: +> webhook-creation UX is its own coherent migration, not a cleanup item. + +- [x] AC #1 (`pm-wizard-trello-steps.tsx` deleted) +- [x] AC #2 (`pm-wizard-jira-steps.tsx` deleted) +- [x] AC #3 (`pm-wizard-linear-steps.tsx` deleted) +- [x] AC #4 (`pm-wizard-common-steps.tsx` audited — all 3 exports have live consumers via `pm-wizard.tsx`; file retained) +- [x] AC #5 (`src/integrations/README.md` rewritten — four-specs preamble; "seven kinds" in step 3; Post-spec-011 additions table) +- [x] AC #6 (root `CLAUDE.md` PM-integration summary references spec 011) +- [x] AC #7 (`CHANGELOG.md` Internal entry summarizing spec 011) +- [x] AC #8 (spec 010's `.md.done` has forward-reference blockquote to spec 011) +- [x] AC #9 (dead-code grep: only doc-comment references to the deleted files remain; no live imports) +- [x] AC #10 (conformance harness green — all three providers) +- [x] AC #11 (`new-provider-surface.test.ts` passes with 7 step files pinned) +- [x] AC #12 (`npm run build` green) +- [x] AC #13 (`npm test` green — 8167/8167) +- [x] AC #14 (`npm run lint` green) +- [x] AC #15 (`npm run typecheck` green) +- [x] AC #16 (spec 011 marked `.done` — handled by Phase 8 in the following trailing commit) diff --git a/docs/plans/011-pm-wizard-shared-migration/_coverage.md b/docs/plans/011-pm-wizard-shared-migration/_coverage.md new file mode 100644 index 00000000..ebb562d5 --- /dev/null +++ b/docs/plans/011-pm-wizard-shared-migration/_coverage.md @@ -0,0 +1,56 @@ +# Coverage map for spec 011-pm-wizard-shared-migration + +Auto-generated by /plan. Tracks which plans satisfy which spec ACs. + +## Spec ACs + +| # | Spec AC (short) | Satisfied by | Status | +|---|---|---|---| +| 1 | Trello wizard on shared components + OAuth custom step | plan 1 (shared-components) + plan 2 (trello) | partial chain | +| 2 | JIRA wizard on shared components + issue-type custom step | plan 1 + plan 3 (jira) | partial chain | +| 3 | Linear wizard on shared components + inline secret + project-scope | plan 1 + plan 4 (linear); ✅ fully closed by spec 012 downstream (legacy `WebhookStep`/`LinearWebhookInfoPanel` deleted; inline secret renders via manifest path) | partial chain → **closed by spec 012** | +| 4 | Three `pm-wizard-{trello,jira,linear}-steps.tsx` files deleted | plan 5 (cleanup) | full | +| 5 | No operator regression per provider | plan 2 (Trello) + plan 3 (JIRA) + plan 4 (Linear) | full per provider | +| 6 | UX normalized upward (searchable pickers, inline create) | plan 1 (capability) + plans 2/3/4 (per-provider activation) | partial chain per provider | +| 7 | 7th `StandardStepKind: custom-field-mapping` declared + wired | plan 1 | full | +| 8 | 31 spec-010 step tests pass without modification | plan 1 | full | +| 9 | `new-provider-surface` snapshot includes new step file | plan 1 | full | +| 10 | Conformance harness stays green every step | plan 1, 2, 3, 4, 5 | hygiene every plan | +| 11 | Build/test/lint/typecheck green | plan 1, 2, 3, 4, 5 | hygiene every plan | +| 12 | New provider still writes zero shared edits | plan 5 (verified after all migrations) | full | + +## Coverage summary + +- **12 spec ACs** mapped to **5 plans** +- **6 plans-worth** of full-coverage ACs (standalone — AC #4, #7, #8, #9, #10, #11, #12) +- **6 plans-worth** of partial-chain ACs (AC #1, #2, #3, #6 each require plan 1 + a per-provider plan; AC #5 requires three per-provider plans) +- **2 plans-worth** of hygiene-only coverage (AC #10, #11 — every plan asserts them) + +## Plan dependency graph + +``` +1-shared-components ──→ 2-trello ──→ 3-jira ──→ 4-linear ──→ 5-cleanup +``` + +Serial DAG per spec Strategic Decision #7 — each provider migration may reveal a shared-component gap that gets back-filled destructively into plan 1 and carried forward. Parallelization would force three independent discovery streams for the same latent gaps. + +## Per-plan AC count (for reference) + +| Plan | Per-plan ACs | Spec ACs cited | +|---|---|---| +| 1 (shared-components) | 15 | 7, 8, 9, 10, 11 (hygiene); partial contribution to 1, 2, 3, 6 | +| 2 (trello) | 13 | 1 (full), 5 (full for Trello), 6 (partial for Trello), 10, 11 | +| 3 (jira) | 14 | 2 (full), 5 (full for JIRA), 6 (partial for JIRA), 10, 11 | +| 4 (linear) | 15 | 3 (full), 5 (full for Linear), 6 (full for Linear — last provider), 10, 11 | +| 5 (cleanup) | 16 | 4 (full), 12 (full), 10, 11 | + +## Doc impact distribution + +Every doc update lives in **plan 5 (cleanup)**. Rationale: docs should reflect the final state; updating them per-plan would mean rewriting the "Adding a new PM provider" section three times. Plan 5 also writes a single CHANGELOG entry covering the whole spec (mirrors spec-010's pattern). + +| Top-level doc | Owner plan | +|---|---| +| `src/integrations/README.md` | 5 | +| `CLAUDE.md` (project root) | 5 | +| `CHANGELOG.md` | 5 | +| `docs/specs/010-pm-integration-hardening-followups.md.done` (forward-ref) | 5 | diff --git a/docs/plans/012-pm-webhook-manifest-migration/1-trello-webhook.md.done b/docs/plans/012-pm-webhook-manifest-migration/1-trello-webhook.md.done new file mode 100644 index 00000000..c4e0a5be --- /dev/null +++ b/docs/plans/012-pm-webhook-manifest-migration/1-trello-webhook.md.done @@ -0,0 +1,190 @@ +--- +id: 012 +slug: pm-webhook-manifest-migration +plan: 1 +plan_slug: trello-webhook +level: plan +parent_spec: docs/specs/012-pm-webhook-manifest-migration.md +depends_on: [] +status: done +--- + +# 012/1: Trello Webhook — Migrate Creation UX Into Manifest Path + +> Part 1 of 4 in the 012-pm-webhook-manifest-migration plan. See [parent spec](../../specs/012-pm-webhook-manifest-migration.md). + +## Summary + +First of three per-provider migrations. Moves Trello's webhook-creation UX from the legacy `WebhookStep` (in `pm-wizard-common-steps.tsx`) into a new Trello webhook adapter rendered from the manifest path. Operator-visible: Trello's webhook step now lives as a peer step in the wizard's dynamic slot list, rendered from the Trello provider folder. Trello's programmatic Create button, active-webhooks list, delete buttons, and curl fallback all continue to work — same tRPC endpoints, same backend behavior, different render path. + +Mechanism: extend `pm-wizard.tsx`'s `-webhook` id-skip filter (introduced by plan 011/4) to exclude Trello specifically — `filter(entry => !(entry.step.id === 'jira-webhook' || entry.step.id === 'linear-webhook'))`. The legacy `WebhookStep` keeps rendering for JIRA + Linear until plans 2 + 3 land; plan 4 then deletes the filter entirely. + +**Components delivered:** +- `web/src/components/projects/pm-providers/trello/webhook-step.tsx` — new file. `TrelloWebhookAdapter` React component. Renders the shared `WebhookUrlDisplayStep` inside a Fragment, composed with Trello-specific UI: active-webhooks list, "Create Webhook" button, delete buttons per active webhook, curl-fallback `
` block. All tRPC calls (`webhooks.list`, `webhooks.create`, `webhooks.delete` with `trelloOnly: true`) happen inside the adapter's `useProviderHooks` slice or inline. +- `web/src/components/projects/pm-providers/trello/wizard.ts` — replace `TrelloWebhookDisplayAdapter` Component (currently renders just the shared step) with the new `TrelloWebhookAdapter`. Extend `useProviderHooks` return shape with Trello-webhook-specific values + callbacks (`activeTrelloWebhooks`, `createTrelloWebhook`, `deleteTrelloWebhook`, `webhookCreateLoading`, etc.). +- `web/src/components/projects/pm-wizard.tsx` — update the filter from `id.endsWith('-webhook')` to `id === 'jira-webhook' || id === 'linear-webhook'`. One-line change. +- `tests/unit/web/trello-webhook-step.test.ts` — new file. Assertions on the rendered adapter: active-list shape, Create button presence + disabled-state, curl command interpolation with `trelloBoardId`, delete-button data-action presence. + +**Deferred to later plans in this spec:** +- JIRA webhook migration — plan 2. +- Linear webhook migration — plan 3. +- Legacy `WebhookStep` + `LinearWebhookInfoPanel` deletion — plan 4. +- `useWebhookManagement` + `useLinearWebhookInfo` hook deletion — plan 4. +- `-webhook` filter entire-removal from `pm-wizard.tsx` — plan 4. +- README / CLAUDE.md / CHANGELOG / spec-011 forward-reference / coverage-map updates — plan 4. + +--- + +## Spec ACs satisfied by this plan + +- Spec AC #2 (Trello programmatic create/list/delete/curl via manifest path) — **full**. +- Spec AC #1 (every wizard renders webhook via manifest) — **partial** (Trello done; JIRA + Linear in plans 2, 3). +- Spec AC #8 (no operator regression) — **full for Trello**. +- Spec AC #10 (conformance harness green) — hygiene. +- Spec AC #11 (build / test / lint / typecheck) — hygiene. + +--- + +## Depends On + +No plan dependencies. Builds on the existing shared `WebhookUrlDisplayStep` (spec 011) and the existing `webhooks.*` tRPC endpoints. + +--- + +## Detailed Task List (TDD) + +### 1. Re-read the legacy `WebhookStep` Trello branch to enumerate behaviors to preserve + +Read `web/src/components/projects/pm-wizard-common-steps.tsx` (the `WebhookStep` function) and `web/src/components/projects/pm-wizard-hooks.ts` (the `useWebhookManagement` hook). Catalog every Trello-specific rendering + mutation — Create button state machine, curl template variables, delete button extraction logic, error display. Output the inventory as a checklist under this task in the `.wip` file. Any behavior that can't map to the Fragment-composition pattern forces a spec divergence note + user sign-off. + +### 2. Trello webhook adapter component + +**Tests first** (`tests/unit/web/trello-webhook-step.test.ts` — new file): +- `renders the shared WebhookUrlDisplayStep (URL + copy button)` — assert `data-step-component="webhook-url-display"` in SSR. +- `renders the active-webhooks list when provided` — assert each active webhook appears with its URL. +- `renders "No Trello webhooks configured" when activeTrelloWebhooks is empty` — assert fallback text. +- `renders the "Create Webhook" button with data-action="create-webhook"` — assert element + attribute. +- `disables the Create button when callbackBaseUrl is empty` — assert `disabled="disabled"` via attribute-agnostic regex. +- `renders the curl fallback with trelloBoardId interpolated into the curl template` — render with `trelloBoardId: 'board-xyz'`, assert body contains `"idModel": "board-xyz"`. +- `falls back to placeholder when trelloBoardId is empty` — render with empty boardId, assert placeholder appears. +- `renders delete buttons (data-action="delete-webhook") per active webhook` — count matches. +- `does not render LinearWebhookInfoPanel or Linear signing-secret field` — regression guard. + +**Implementation** (`web/src/components/projects/pm-providers/trello/webhook-step.tsx`): +- Named export: `TrelloWebhookAdapter: React.FC`. Signature matches `ProviderWizardStepProps`. +- Reads `providerHooks` via the existing `asTrelloHooks` adapter (extend the `TrelloProviderHooks` interface). +- Renders Fragment of: `WebhookUrlDisplayStep` (with `webhookUrl` from providerHooks), then a `
` with active-webhooks list rendered from `providerHooks.activeTrelloWebhooks`, then the Create button wired to `providerHooks.createTrelloWebhook`, then delete buttons wired to `providerHooks.deleteTrelloWebhook(id)`, then a `
` with the curl command constructed inline from `state.trelloBoardId` + `providerHooks.callbackBaseUrl`. +- All DOM elements carry provider-agnostic data attributes (`data-action`, `data-step-component`, …) so tests don't pin class names. + +### 3. Extend Trello wizard's `useProviderHooks` + +**Tests first**: covered by task 2's SSR tests — they exercise the adapter + providerHooks plumbing end-to-end. + +**Implementation** (`web/src/components/projects/pm-providers/trello/wizard.ts`): +- Extend `TrelloProviderHooks` interface: add `activeTrelloWebhooks`, `webhooksLoading`, `createTrelloWebhook`, `createLoading`, `createError`, `deleteTrelloWebhook`, `deleteLoading`, `callbackBaseUrl`. +- Inside `useProviderHooks`, call `useQuery(trpc.webhooks.list.queryOptions({ projectId }))` to fetch active webhooks. Normalize via `deriveActiveWebhooks(state.provider, ...)`. +- Add `createTrelloWebhook = () => createMutation.mutate({ projectId, callbackBaseUrl, trelloOnly: true })` and `deleteTrelloWebhook = (baseUrl: string) => deleteMutation.mutate({ projectId, callbackBaseUrl: baseUrl, trelloOnly: true })` using `useMutation`. +- Compute `callbackBaseUrl` from `API_URL || window.location.origin.replace(':5173', ':3000')` — same inline formula as `useWebhookManagement`. (Don't call `useWebhookManagement` directly; it gets deleted in plan 4.) +- Replace the existing `TrelloWebhookDisplayAdapter` step Component in `trelloProviderWizard.steps` with the new `TrelloWebhookAdapter`. + +### 4. Flip the `-webhook` id-skip filter to exclude Trello + +**Tests first** (`tests/unit/web/pm-wizard-webhook-filter.test.ts` — new file; can reuse an existing test file if one fits): +- `pm-wizard.tsx filter excludes trello-webhook from skip list` — import the filter predicate (extract as a named export if needed) and assert it returns `true` for `{id: 'trello-webhook'}`. +- Alternative if extracting the predicate is too invasive: assert via snapshot of the render pipeline (mount `PMWizard` with a Trello project; assert the Trello webhook step renders). + +**Implementation** (`web/src/components/projects/pm-wizard.tsx`): +- Update the filter from `.filter((entry) => !entry.step.id.endsWith('-webhook'))` to `.filter((entry) => entry.step.id !== 'jira-webhook' && entry.step.id !== 'linear-webhook')`. +- Update the surrounding comment block to note the migration is in-flight (plan 012/1 migrated Trello; plans 012/2-3 migrate JIRA + Linear; plan 012/4 deletes the filter). + +### 5. Conformance harness smoke + +No code change; run `npx vitest run --project unit-core tests/unit/integrations/pm-conformance.test.ts` to confirm Trello's behavioral contract (unchanged in this plan — pure frontend wiring) still passes. + +### 6. Manual dashboard verification + +Per CLAUDE.md: start the dev server and exercise the Trello wizard in a browser. Verify: +- Trello's webhook step now appears as a peer step (its own `WizardStep` slot), not inside the legacy WebhookStep slot. +- Legacy WebhookStep still renders for JIRA + Linear (not for Trello). +- Create Webhook button registers a webhook; active list updates; delete removes it. +- Curl fallback shows with the current board ID interpolated. + +AC #8 (no regression) is browser-verifiable here; flag as deferred if unavailable. + +--- + +## Test Plan + +### Unit tests +- [ ] `tests/unit/web/trello-webhook-step.test.ts` — new file, ~9 tests. +- [ ] `tests/unit/web/pm-wizard-webhook-filter.test.ts` (or integrated into existing wizard test) — 1 test pinning the new filter predicate. + +### Integration tests +- None. + +### Acceptance tests +- [ ] Conformance harness passes for Trello. +- [ ] Browser smoke test — Trello wizard renders with peer webhook step, JIRA + Linear retain legacy WebhookStep. +- [ ] `npm run build`, `npm test`, `npm run lint`, `npm run typecheck` — all green. + +--- + +## Acceptance Criteria (per-plan, testable) + +1. `trelloProviderWizard.steps`'s `trello-webhook` entry has `Component = TrelloWebhookAdapter` (identity-asserted). +2. `TrelloWebhookAdapter` renders the shared `WebhookUrlDisplayStep` + Trello-specific UI (active list, Create button, curl fallback, delete buttons). +3. The `pm-wizard.tsx` filter now excludes Trello — `trello-webhook` passes through the manifest iteration (asserted via predicate test or render snapshot). +4. Legacy `WebhookStep` continues to render for JIRA + Linear (not for Trello) — no regression for the un-migrated providers. +5. Every previously-available Trello webhook action (Create / active list / delete / curl) is exercised in tests against the new adapter. +6. Conformance harness (`pm-conformance.test.ts`) passes for Trello. +7. `npm run build` passes. +8. `npm test` passes. +9. `npm run lint` passes. +10. `npm run typecheck` passes. + +--- + +## Documentation Impact (this plan only) + +None — all doc updates deferred to plan 4 (cleanup) to reflect the final state in one pass. + +| File | Change | +|---|---| +| — | Deferred to plan 4. | + +--- + +## Out of Scope (this plan) + +Deferred to later plans in this spec: +- JIRA webhook migration — plan 2. +- Linear webhook migration — plan 3. +- Legacy `WebhookStep`, `LinearWebhookInfoPanel`, `useWebhookManagement`, `useLinearWebhookInfo` deletion — plan 4. +- `-webhook` filter full removal + legacy test file deletion — plan 4. +- README / CLAUDE.md / CHANGELOG / spec-011 forward-reference / `_coverage.md` updates — plan 4. + +Originally out of scope for the spec (repeated for clarity): +- Generalizing `webhooks.create/list/delete` tRPC endpoints beyond their `{trelloOnly, jiraOnly}` flags. +- Backend webhook API changes. +- Adding programmatic webhook registration for Linear. +- Extending the manifest pattern to SCM or alerting. +- New shared UI primitives. +- Schema migrations. +- Rewriting the form-state model or the `ProviderWizardDefinition` contract. +- Further widening of `WebhookUrlDisplayStep`. + +--- + +## Progress + + +- [x] AC #1 (`trelloProviderWizard.steps[trello-webhook].Component = TrelloWebhookAdapter`) +- [x] AC #2 (Fragment composes shared `WebhookUrlDisplayStep` + active list + Create + curl + delete) +- [x] AC #3 (`pm-wizard.tsx` filter: `id !== 'jira-webhook' && id !== 'linear-webhook'`) +- [x] AC #4 (legacy `WebhookStep` still renders for JIRA + Linear; no code change to the legacy slot) +- [x] AC #5 (10 Trello-webhook tests covering URL / active list / Create / curl / delete / regression guard) +- [x] AC #6 (conformance harness green — full suite 8177/8177 passes) +- [x] AC #7 (`npm run build` green) +- [x] AC #8 (`npm test` green — 8177/8177) +- [x] AC #9 (`npm run lint` green) +- [x] AC #10 (`npm run typecheck` green) diff --git a/docs/plans/012-pm-webhook-manifest-migration/2-jira-webhook.md.done b/docs/plans/012-pm-webhook-manifest-migration/2-jira-webhook.md.done new file mode 100644 index 00000000..20ada755 --- /dev/null +++ b/docs/plans/012-pm-webhook-manifest-migration/2-jira-webhook.md.done @@ -0,0 +1,191 @@ +--- +id: 012 +slug: pm-webhook-manifest-migration +plan: 2 +plan_slug: jira-webhook +level: plan +parent_spec: docs/specs/012-pm-webhook-manifest-migration.md +depends_on: [1-trello-webhook.md] +status: done +--- + +# 012/2: JIRA Webhook — Migrate Creation UX Into Manifest Path + +> Part 2 of 4 in the 012-pm-webhook-manifest-migration plan. See [parent spec](../../specs/012-pm-webhook-manifest-migration.md). + +## Summary + +Second of three per-provider migrations. Mirrors plan 012/1's Trello migration for JIRA. Moves JIRA's webhook-creation UX (programmatic Create button + active list + delete + curl fallback + `jiraEnsureLabels` side-effect) from the legacy `WebhookStep` into a new JIRA webhook adapter rendered from the manifest path. + +Mechanism mirrors plan 1: adjust `pm-wizard.tsx`'s filter to exclude JIRA from the skip list, leaving only Linear still routed through the legacy WebhookStep. Plan 3 completes the Linear migration; plan 4 deletes the filter. + +**Components delivered:** +- `web/src/components/projects/pm-providers/jira/webhook-step.tsx` — new file. `JiraWebhookAdapter` React component. Same Fragment-composition shape as `TrelloWebhookAdapter`: shared `WebhookUrlDisplayStep` + active-webhooks list + "Create Webhook" button + curl fallback. Curl template uses JIRA's REST API v3 shape with `jiraBaseUrl` interpolated. Delete button per active webhook. `jiraEnsureLabels` side-effect preserved via the existing `webhooks.create` mutation call path (no change to backend). +- `web/src/components/projects/pm-providers/jira/wizard.ts` — extend `JiraProviderHooks` interface with JIRA-webhook-specific fields; in `useProviderHooks`, add `useQuery(trpc.webhooks.list…)` + create/delete `useMutation` calls with `jiraOnly: true`; replace `JiraWebhookDisplayAdapter` step Component with the new `JiraWebhookAdapter`. +- `web/src/components/projects/pm-wizard.tsx` — update filter to `entry.step.id !== 'linear-webhook'`. +- `tests/unit/web/jira-webhook-step.test.ts` — new file. Assertions mirroring `trello-webhook-step.test.ts` but scoped to JIRA. + +**Deferred to later plans in this spec:** +- Linear webhook migration — plan 3. +- Legacy `WebhookStep` deletion + full filter removal — plan 4. +- Docs + coverage-map updates — plan 4. + +--- + +## Spec ACs satisfied by this plan + +- Spec AC #3 (JIRA programmatic create/list/delete/curl + `jiraEnsureLabels`) — **full**. +- Spec AC #1 (every wizard renders webhook via manifest) — **partial** (Trello + JIRA done; Linear in plan 3). +- Spec AC #8 (no operator regression) — **full for JIRA**. +- Spec AC #10 (conformance harness green) — hygiene. +- Spec AC #11 (build / test / lint / typecheck) — hygiene. + +--- + +## Depends On + +- Plan 1 (`trello-webhook`) — establishes the Fragment-composition pattern JIRA mirrors. Also flushes out any shared-component gap Trello would have revealed; JIRA inherits the fix. + +--- + +## Detailed Task List (TDD) + +### 1. Re-read the legacy `WebhookStep` JIRA branch + +Same pre-flight as plan 1, for JIRA. Catalog every JIRA-specific rendering + mutation path in the legacy `WebhookStep`. Identify the `jiraEnsureLabels` side-effect call path. Output the inventory as a checklist under this task in the `.wip` file. + +### 2. JIRA webhook adapter component + +**Tests first** (`tests/unit/web/jira-webhook-step.test.ts` — new file): +- `renders the shared WebhookUrlDisplayStep` — asserts presence. +- `renders active-webhooks list when provided`. +- `renders "No JIRA webhooks configured" fallback when empty`. +- `renders the "Create Webhook" button with data-action="create-webhook"`. +- `disables Create button when callbackBaseUrl is empty`. +- `renders the curl fallback with jiraBaseUrl interpolated` — asserts body contains `"url": "{jiraBaseUrl}/rest/api/3/webhook"` shape. +- `falls back to placeholder when jiraBaseUrl is empty`. +- `renders delete buttons per active webhook`. +- `does not render LinearWebhookInfoPanel or Linear signing-secret field`. + +**Implementation** (`web/src/components/projects/pm-providers/jira/webhook-step.tsx`): +- Named export: `JiraWebhookAdapter: React.FC`. +- Fragment composing shared `WebhookUrlDisplayStep` + active-webhooks list + Create button + curl fallback. +- Curl template uses the JIRA REST API v3 POST shape, with events `['jira:issue_created', 'jira:issue_updated', 'comment_created', 'comment_updated']` and Basic-auth placeholder for email + API token. +- The Create button wiring calls `providerHooks.createJiraWebhook` which runs `webhooks.create` with `jiraOnly: true` — this triggers the backend-side `jiraEnsureLabels` side-effect unchanged. + +### 3. Extend JIRA wizard's `useProviderHooks` + +**Tests first**: covered by task 2's SSR tests. + +**Implementation** (`web/src/components/projects/pm-providers/jira/wizard.ts`): +- Extend `JiraProviderHooks` with `activeJiraWebhooks`, `webhooksLoading`, `createJiraWebhook`, `createLoading`, `createError`, `deleteJiraWebhook`, `deleteLoading`, `callbackBaseUrl`. +- Inside `useProviderHooks`, add `useQuery(trpc.webhooks.list.queryOptions({ projectId }))` and normalize via `deriveActiveWebhooks`. Add create/delete `useMutation` calls with `jiraOnly: true`. +- Replace the existing `JiraWebhookDisplayAdapter` Component in `jiraProviderWizard.steps` with the new `JiraWebhookAdapter`. +- Compute `callbackBaseUrl` inline (same formula as plan 1's Trello adapter) — do not consume `useWebhookManagement` (which gets deleted in plan 4). + +### 4. Update the `-webhook` id-skip filter to exclude JIRA + +**Tests first**: extend the existing filter test from plan 1 with a row for JIRA. + +**Implementation** (`web/src/components/projects/pm-wizard.tsx`): +- Update filter to `entry.step.id !== 'linear-webhook'`. +- Update the comment block. + +### 5. Conformance harness smoke + +No code change; verify harness passes. + +### 6. Manual dashboard verification + +Per CLAUDE.md: browser smoke test. Verify: +- JIRA wizard's webhook step now renders as a peer step (not inside legacy WebhookStep). +- Legacy WebhookStep still renders for Linear only (not Trello, not JIRA). +- Create / active list / delete / curl all work. +- `jiraEnsureLabels` fires (check Atlassian side-effect — first issue gets + loses CASCADE labels on successful Create). + +--- + +## Test Plan + +### Unit tests +- [ ] `tests/unit/web/jira-webhook-step.test.ts` — new file, ~9 tests. +- [ ] Filter-predicate test extended with JIRA row. + +### Integration tests +- None. + +### Acceptance tests +- [ ] Conformance harness passes for JIRA. +- [ ] Browser smoke test — JIRA wizard renders with peer webhook step. +- [ ] `npm run build`, `npm test`, `npm run lint`, `npm run typecheck` — all green. + +--- + +## Acceptance Criteria (per-plan, testable) + +1. `jiraProviderWizard.steps`'s `jira-webhook` entry has `Component = JiraWebhookAdapter`. +2. `JiraWebhookAdapter` renders the shared `WebhookUrlDisplayStep` + JIRA-specific UI (active list, Create button, curl fallback, delete buttons). +3. `jiraEnsureLabels` side-effect preserved — verified by confirming the Create button calls the existing `webhooks.create({ jiraOnly: true })` path (backend unchanged). +4. `pm-wizard.tsx` filter now excludes JIRA; `jira-webhook` passes through the manifest iteration. +5. Legacy `WebhookStep` continues to render for Linear only. +6. Every previously-available JIRA webhook action is exercised in tests against the new adapter. +7. Conformance harness green for JIRA. +8. `npm run build` passes. +9. `npm test` passes. +10. `npm run lint` passes. +11. `npm run typecheck` passes. + +--- + +## Documentation Impact (this plan only) + +None — deferred to plan 4. + +| File | Change | +|---|---| +| — | Deferred to plan 4. | + +--- + +## Out of Scope (this plan) + +Deferred to later plans in this spec: +- Linear webhook migration — plan 3. +- Legacy `WebhookStep`, `LinearWebhookInfoPanel`, `useWebhookManagement`, `useLinearWebhookInfo` deletion — plan 4. +- `-webhook` filter full removal + legacy test file deletion — plan 4. +- Docs / coverage-map updates — plan 4. + +Originally out of scope for the spec (repeated for clarity): +- Generalizing `webhooks.*` tRPC endpoints. +- Backend webhook API changes. +- Adding programmatic webhook registration for Linear. +- Extending the manifest pattern to SCM / alerting. +- New shared UI primitives. +- Schema migrations. +- Rewriting the form-state model or `ProviderWizardDefinition`. +- Further widening of `WebhookUrlDisplayStep`. + +--- + +## Progress + + + +> **Note on curl template**: spec and plan text mentioned JIRA REST API v3 +> shape (`/rest/api/3/webhook`). The legacy `WebhookStep` actually used +> the older v1 endpoint (`/rest/webhooks/1.0/webhook`). Preserved v1 +> verbatim to satisfy zero-regression; migrating to v3 would require a +> coordinated payload change (different event names + JQL filter shape) +> and is out of scope. Documented in the adapter source. + +- [x] AC #1 (`jiraProviderWizard.steps[jira-webhook].Component = JiraWebhookAdapter`) +- [x] AC #2 (Fragment composes shared `WebhookUrlDisplayStep` + active list + Create + curl + delete) +- [x] AC #3 (`jiraEnsureLabels` side-effect preserved — server-side; adapter calls `webhooks.create({jiraOnly:true})` unchanged) +- [x] AC #4 (`pm-wizard.tsx` filter: `id !== 'linear-webhook'`) +- [x] AC #5 (legacy `WebhookStep` still renders for Linear only; no code change to legacy slot) +- [x] AC #6 (10 JIRA-webhook tests covering URL / active list / Create / curl / delete / regression guard) +- [x] AC #7 (conformance harness green — full suite 8187/8187) +- [x] AC #8 (`npm run build` green) +- [x] AC #9 (`npm test` green — 8187/8187) +- [x] AC #10 (`npm run lint` green) +- [x] AC #11 (`npm run typecheck` green) diff --git a/docs/plans/012-pm-webhook-manifest-migration/3-linear-webhook.md.done b/docs/plans/012-pm-webhook-manifest-migration/3-linear-webhook.md.done new file mode 100644 index 00000000..78bfba06 --- /dev/null +++ b/docs/plans/012-pm-webhook-manifest-migration/3-linear-webhook.md.done @@ -0,0 +1,186 @@ +--- +id: 012 +slug: pm-webhook-manifest-migration +plan: 3 +plan_slug: linear-webhook +level: plan +parent_spec: docs/specs/012-pm-webhook-manifest-migration.md +depends_on: [2-jira-webhook.md] +status: done +--- + +# 012/3: Linear Webhook — Activate Manifest Path With Signing-Secret + Instructions + +> Part 3 of 4 in the 012-pm-webhook-manifest-migration plan. See [parent spec](../../specs/012-pm-webhook-manifest-migration.md). + +## Summary + +Third and final per-provider migration. Activates the `LinearWebhookDisplayAdapter` that plan 011/4 already shipped (Fragment composing shared `WebhookUrlDisplayStep` + `ProjectSecretField` for `LINEAR_WEBHOOK_SECRET`) and extends it with the 5-step manual setup-instructions list currently owned by the legacy `LinearWebhookInfoPanel`. Removes the last remaining id in the `-webhook` filter so Linear's webhook step passes through the manifest iteration. + +Smaller than plans 1 + 2 — Linear has no programmatic registration (no Create button, no active list, no delete, no curl fallback — Linear's API forbids it). The secret-field + instructions list are the only UI to migrate. The adapter skeleton already exists from plan 011/4; this plan completes it and wires it up. + +**Components delivered:** +- `web/src/components/projects/pm-providers/linear/webhook-step.tsx` — new file (or extend the dormant `LinearWebhookDisplayAdapter` currently inlined in `linear/wizard.ts`). Named export: `LinearWebhookAdapter: React.FC`. Fragment composing shared `WebhookUrlDisplayStep` + a new setup-instructions block (ordered list, 5 items matching the current `LinearWebhookInfoPanel` copy) + `ProjectSecretField` bound to `LINEAR_WEBHOOK_SECRET`. +- `web/src/components/projects/pm-providers/linear/wizard.ts` — replace the inlined `LinearWebhookDisplayAdapter` with the new extracted component. Extend `LinearProviderHooks` only if new fields are needed beyond what plan 011/4 declared (`projectIdForSecret`, `webhookSecretCredential`). Existing plumbing likely covers it. +- `web/src/components/projects/pm-wizard.tsx` — remove the conditional filter entirely, replacing `filter((entry) => entry.step.id !== 'linear-webhook')` with **no filter** (i.e. remove the `.filter(...)` call from `renderedManifestSteps` construction). The manifest iteration now includes every step. **Note**: plan 4 owns the deletion of the legacy `WebhookStep` slot and the `useWebhookManagement`/`useLinearWebhookInfo` hook imports. This plan leaves those in place but with no provider routed to them; the legacy slot becomes a no-op (renders but no provider's state matches the branch conditions inside `WebhookStep`). Plan 4 deletes it. +- `tests/unit/web/linear-webhook-step.test.ts` — new file. Assertions on the rendered adapter: `ProjectSecretField` rendered with `envVarKey="LINEAR_WEBHOOK_SECRET"`, the 5-step instructions list present, webhook URL + copy button preserved. + +**Deferred to later plans in this spec:** +- Legacy `WebhookStep`, `LinearWebhookInfoPanel`, `useWebhookManagement`, `useLinearWebhookInfo` deletion — plan 4. +- Legacy test file deletion — plan 4. +- Docs / coverage-map updates — plan 4. + +--- + +## Spec ACs satisfied by this plan + +- Spec AC #4 (Linear signing-secret + instructions inline via manifest) — **full**. +- Spec AC #1 (every wizard renders webhook via manifest) — **partial** (Linear closes the chain; plan 4 removes the filter stopgap). +- Spec AC #7 (plan 011/4 ACs #3 + #6 close) — **partial** (manifest path now renders Linear's webhook inline; the deletion of `LinearWebhookInfoPanel` + `_coverage.md` update land in plan 4). +- Spec AC #8 (no operator regression) — **full for Linear**. +- Spec AC #10 (conformance harness green) — hygiene. +- Spec AC #11 (build / test / lint / typecheck) — hygiene. + +--- + +## Depends On + +- Plan 2 (`jira-webhook`) — establishes two-provider track record of the Fragment-composition pattern; any carry-forward widening from plans 1/2 is already in place. +- Plan 1 (`trello-webhook`) — transitively; the Fragment pattern originated there. + +--- + +## Detailed Task List (TDD) + +### 1. Re-read the legacy `LinearWebhookInfoPanel` + `useLinearWebhookInfo` hook + +Catalog every piece of UI the legacy panel renders: the blue-info banner, the 5-step instructions list, the `ProjectSecretField`, the webhook URL display. Confirm the URL formula (`${callbackBaseUrl}/linear/webhook`) matches what `webhookUrl` carries in the Linear wizard's `useProviderHooks`. Output checklist under this task in the `.wip` file. + +### 2. Extract the Linear webhook adapter component + +**Tests first** (`tests/unit/web/linear-webhook-step.test.ts` — new file): +- `renders the shared WebhookUrlDisplayStep with webhookUrl prop`. +- `renders ProjectSecretField with envVarKey="LINEAR_WEBHOOK_SECRET"`. +- `renders ProjectSecretField with the threaded credential metadata` (when `webhookSecretCredential` is populated). +- `renders a 5-item ordered setup-instructions list`. +- `includes the linear.app/settings/api link in the instructions`. +- `does not render Trello/JIRA UI elements (Create button, active-webhooks list)` — regression guard. + +**Implementation** (`web/src/components/projects/pm-providers/linear/webhook-step.tsx`): +- Named export: `LinearWebhookAdapter: React.FC`. +- Fragment composing: shared `WebhookUrlDisplayStep` + a `
` with the info banner + an ordered list of 5 setup steps (copy lifted verbatim from `LinearWebhookInfoPanel`) + `ProjectSecretField` bound to `LINEAR_WEBHOOK_SECRET` with the threaded credential. +- The 5-step instructions are hardcoded in this component (Linear-specific). + +### 3. Wire the new adapter into `linearProviderWizard.steps` + +**Implementation** (`web/src/components/projects/pm-providers/linear/wizard.ts`): +- Replace the inlined `LinearWebhookDisplayAdapter` (currently a Fragment of `WebhookUrlDisplayStep` + `ProjectSecretField`) with an import of the new `LinearWebhookAdapter` from `./webhook-step.js`. +- Update the `linear-webhook` step's `Component` reference. +- Confirm `providerHooks` already returns `projectIdForSecret` + `webhookSecretCredential` (plan 011/4 added these) — no new fields expected. + +### 4. Remove the `-webhook` filter from `pm-wizard.tsx` + +**Tests first**: +- Filter-predicate test from plans 1 + 2: update to confirm no step is filtered out now. +- Render-snapshot test (if present) for Linear — confirm the Linear webhook step renders as a peer step. + +**Implementation** (`web/src/components/projects/pm-wizard.tsx`): +- Remove the `.filter(...)` call from `renderedManifestSteps` construction. `renderedManifestSteps` becomes `manifestDef ? manifestDef.steps.map((step, index) => ({ step, index })) : []`. +- Update the surrounding comment to note the migration is complete and the legacy `WebhookStep` slot is now a no-op that plan 4 will delete. + +### 5. Legacy `WebhookStep` slot behavior post-plan-3 + +No code change in this plan beyond removing the filter. The legacy `WebhookStep` still renders in its hardcoded slot; with no provider entering the Trello / JIRA / Linear conditional branches (all three now own their webhook UX in the manifest path), the slot renders a narrow artifact — likely the "No webhooks configured" fallback or similar. Document this in the commit message + PR description as transient visual noise; plan 4 deletes the slot. + +### 6. Conformance harness smoke + manual dashboard verification + +Per CLAUDE.md: browser smoke test. +- Linear wizard renders the webhook step via the manifest path with signing-secret + instructions inline. +- Trello + JIRA + Linear all use manifest-path webhooks; the legacy `WebhookStep` slot renders but is effectively empty (transient; plan 4 deletes). +- Secret-field save / load / clear cycle via `ProjectSecretField` works unchanged. + +--- + +## Test Plan + +### Unit tests +- [ ] `tests/unit/web/linear-webhook-step.test.ts` — new file, ~6 tests. +- [ ] Filter-predicate test: update to assert no step is filtered out. + +### Integration tests +- None. + +### Acceptance tests +- [ ] Conformance harness passes for Linear. +- [ ] `tests/unit/pm/linear/regression-2026-04.test.ts` passes (adapter untouched). +- [ ] Browser smoke test — Linear webhook step renders inline; secret-field save / load works. +- [ ] `npm run build`, `npm test`, `npm run lint`, `npm run typecheck` — all green. + +--- + +## Acceptance Criteria (per-plan, testable) + +1. `linearProviderWizard.steps`'s `linear-webhook` entry has `Component = LinearWebhookAdapter` (identity-asserted). +2. `LinearWebhookAdapter` renders the shared `WebhookUrlDisplayStep` + `ProjectSecretField(envVarKey=LINEAR_WEBHOOK_SECRET)` + 5-step instructions list. +3. The `-webhook` filter in `pm-wizard.tsx` is removed; `manifestDef.steps` iterates in full. +4. Legacy `WebhookStep` slot still renders but no provider is routed to its Trello / JIRA / Linear branches — deletion deferred to plan 4. +5. Every Linear webhook behavior (URL display + copy, signing-secret save/load/clear, 5-step instructions) is exercised in tests against the new adapter. +6. Conformance harness passes for Linear. +7. `regression-2026-04.test.ts` passes (adapter unchanged). +8. `npm run build` passes. +9. `npm test` passes. +10. `npm run lint` passes. +11. `npm run typecheck` passes. + +--- + +## Documentation Impact (this plan only) + +None — deferred to plan 4. + +| File | Change | +|---|---| +| — | Deferred to plan 4. | + +--- + +## Out of Scope (this plan) + +Deferred to later plans in this spec: +- Legacy `WebhookStep`, `LinearWebhookInfoPanel`, `useWebhookManagement`, `useLinearWebhookInfo` deletion — plan 4. +- Legacy test file deletion — plan 4. +- Docs / coverage-map updates — plan 4. + +Originally out of scope for the spec (repeated for clarity): +- Generalizing `webhooks.*` tRPC endpoints. +- Backend webhook API changes. +- Adding programmatic webhook registration for Linear. +- Extending the manifest pattern to SCM / alerting. +- New shared UI primitives. +- Schema migrations. +- Rewriting the form-state model or `ProviderWizardDefinition`. +- Further widening of `WebhookUrlDisplayStep`. + +--- + +## Progress + + + +> **Note on test mock**: `ProjectSecretField` uses `useQueryClient` +> internally which pulls React from `web/node_modules` (different +> instance from the root-aliased React the test env uses). SSR crashes on +> the null Context hook. Same pattern as plan 011/1's Combobox — mocked +> `ProjectSecretField` to a deterministic stub preserving props. + +- [x] AC #1 (`linearProviderWizard.steps[linear-webhook].Component = LinearWebhookAdapter`) +- [x] AC #2 (Fragment composes shared `WebhookUrlDisplayStep` + info banner + 5-step instructions + `ProjectSecretField(envVarKey=LINEAR_WEBHOOK_SECRET)`) +- [x] AC #3 (`-webhook` filter removed from `pm-wizard.tsx`; `manifestDef.steps` iterates in full) +- [x] AC #4 (legacy `WebhookStep` slot still renders but no provider is routed to its branches; plan 4 deletes) +- [x] AC #5 (7 Linear-webhook tests covering URL display, secret-field presence + absence, 5-step instructions, linear.app link, info banner, Trello/JIRA-UI regression guard) +- [x] AC #6 (conformance harness green — full suite 8194/8194) +- [x] AC #7 (`regression-2026-04.test.ts` passes — adapter untouched) +- [x] AC #8 (`npm run build` green) +- [x] AC #9 (`npm test` green — 8194/8194) +- [x] AC #10 (`npm run lint` green) +- [x] AC #11 (`npm run typecheck` green) diff --git a/docs/plans/012-pm-webhook-manifest-migration/4-cleanup.md.done b/docs/plans/012-pm-webhook-manifest-migration/4-cleanup.md.done new file mode 100644 index 00000000..82d26ecf --- /dev/null +++ b/docs/plans/012-pm-webhook-manifest-migration/4-cleanup.md.done @@ -0,0 +1,240 @@ +--- +id: 012 +slug: pm-webhook-manifest-migration +plan: 4 +plan_slug: cleanup +level: plan +parent_spec: docs/specs/012-pm-webhook-manifest-migration.md +depends_on: [3-linear-webhook.md] +status: done +--- + +# 012/4: Cleanup — Delete Legacy Webhook UX + Rewrite Docs + Close Spec + +> Part 4 of 4 in the 012-pm-webhook-manifest-migration plan. See [parent spec](../../specs/012-pm-webhook-manifest-migration.md). + +## Summary + +Closes spec 012. Deletes the legacy webhook surface now that all three providers own their webhook steps via the manifest path: + +- `WebhookStep` + `LinearWebhookInfoPanel` from `pm-wizard-common-steps.tsx` (the remaining two exports; `SaveStep` stays as the only live export). +- `useWebhookManagement` + `useLinearWebhookInfo` from `pm-wizard-hooks.ts`. +- `tests/unit/web/pm-wizard-webhooks-step.test.ts` (181 lines, 8 tests — all assertions now live in the three per-provider adapter tests from plans 012/1-3). +- The legacy-webhook slot rendering in `pm-wizard.tsx` (the entire `` block + `useWebhookManagement` / `useLinearWebhookInfo` imports + related state). + +Also rewrites the docs to reflect the fully-migrated state (same plan-5-style cleanup pattern from spec 011) and updates `docs/plans/011-pm-wizard-shared-migration/_coverage.md` to mark plan 011/4 ACs #3 + #6 as fully closed by this spec downstream. + +Cleanup-only plan. Zero new features, zero widenings, zero behavior changes. Deletions + prose. + +**Components delivered:** +- `web/src/components/projects/pm-wizard-common-steps.tsx` — `WebhookStep` + `LinearWebhookInfoPanel` deleted. File retained as `SaveStep` is still a live export. +- `web/src/components/projects/pm-wizard-hooks.ts` — `useWebhookManagement` + `useLinearWebhookInfo` deleted. +- `web/src/components/projects/pm-wizard.tsx` — legacy webhook `WizardStep` slot deleted; associated imports + call-sites removed. `webhookStepNumber` / `saveStepNumber` simplified: the save step becomes `renderedManifestSteps.length + 2`. +- `tests/unit/web/pm-wizard-webhooks-step.test.ts` — **deleted**. +- `src/integrations/README.md` — rewrite: + - Four-specs → five-specs paragraph (add spec 012). + - Update the provider migration status table: Trello/JIRA/Linear all show `✅ manifest-driven webhook step`. + - Add a Post-spec-012 additions peer section. + - Update the "Adding a new PM provider" section's webhook guidance: compose the shared step with provider-specific UX via Fragment; no shared-orchestration edits needed. +- `CLAUDE.md` (project root) — PM-integration summary references spec 012 alongside 009/010/011; removes any phrasing about the legacy `WebhookStep` still owning webhook UX. +- `CHANGELOG.md` — single Internal-change entry summarizing spec 012. +- `docs/specs/011-pm-wizard-shared-migration.md.done` — forward-reference blockquote to spec 012 at the top (mirrors 009→010→011 pointer). +- `docs/plans/011-pm-wizard-shared-migration/_coverage.md` — update the entries for plan 011/4 ACs #3 + #6 to `✅ fully closed by spec 012`. + +**Deferred to follow-up specs**: nothing. Closes spec 012. + +--- + +## Spec ACs satisfied by this plan + +- Spec AC #5 (legacy `WebhookStep`, `LinearWebhookInfoPanel`, 2 hooks, legacy test file deleted) — **full**. +- Spec AC #6 (`-webhook` id-skip filter removed; `pm-wizard.tsx` iterates every manifest step) — **full** (plan 012/3 removed the filter itself; this plan removes the now-empty legacy slot that sat alongside it). +- Spec AC #7 (plan 011/4 ACs #3 + #6 close; `_coverage.md` updated) — **full** (closes the chain started by plan 012/3). +- Spec AC #9 (new provider requires zero shared-orchestration edits) — **full** (verifiable only after legacy slot + hooks deleted). +- Spec AC #10 (conformance harness green) — hygiene. +- Spec AC #11 (build / test / lint / typecheck) — hygiene. + +--- + +## Depends On + +- Plan 3 (`linear-webhook`) — last plan that routed a provider through the legacy `WebhookStep`. Once it merges, the legacy slot is a no-op safe to delete. +- Transitively: plans 1 + 2. + +--- + +## Detailed Task List + +### 1. Dead-code audit before deletion + +Grep for every reference to the legacy symbols: +- `grep -r "WebhookStep\|LinearWebhookInfoPanel" web/src tests` — expected: only the file being deleted + the test file being deleted. +- `grep -r "useWebhookManagement\|useLinearWebhookInfo" web/src tests` — expected: only the hook definitions. +- `grep -r "webhookStepNumber" web/src tests` — expected: only the `pm-wizard.tsx` block being deleted. + +Any surviving live reference is a plan-3 gap (escalate). + +### 2. Delete legacy exports + imports + +**Implementation**: +- `web/src/components/projects/pm-wizard-common-steps.tsx`: + - Delete the `LinearWebhookInfoPanel` function export. + - Delete the `WebhookStep` function export. + - Leave `SaveStep` in place. + - Clean up any now-unused imports at the top of the file. +- `web/src/components/projects/pm-wizard-hooks.ts`: + - Delete `useWebhookManagement` export. + - Delete `useLinearWebhookInfo` export. + - Clean up any now-unused imports. +- `web/src/components/projects/pm-wizard.tsx`: + - Delete the entire legacy-webhook `` block. + - Delete imports of `WebhookStep`, `useWebhookManagement`, `useLinearWebhookInfo`. + - Delete the `webhookManagement = useWebhookManagement(...)` call. + - Delete the `linearWebhookUrl` + `linearWebhookSecretCredential` derivations (providers now compute these inside their own `useProviderHooks`). + - Rename `webhookStepNumber` → `saveStepNumber`; saveStep becomes `renderedManifestSteps.length + 2`. + +### 3. Delete the legacy test file + +**Implementation**: +- `git rm tests/unit/web/pm-wizard-webhooks-step.test.ts`. +- Verify the three per-provider adapter tests cover the assertions that file made (they should after plans 1-3). + +### 4. Rewrite `src/integrations/README.md` + +**Implementation**: +- Update the top-of-file spec list: "Four specs shape it" → "Five specs shape it"; add spec 012 bullet. +- Update the "Adding a new PM provider" section's step 3 webhook guidance: "the webhook step composes the shared `WebhookUrlDisplayStep` with whatever provider-specific UI the provider needs (programmatic registration, secret fields, setup instructions) via Fragment in the provider's wizard definition." +- Add a new "Post-spec-012 additions (2026-04-18+)" peer section: + - Every PM wizard step, without exception, renders via the manifest path. + - Legacy `WebhookStep` + `LinearWebhookInfoPanel` deleted. `pm-wizard-common-steps.tsx` now only exports `SaveStep`. + - `useWebhookManagement` + `useLinearWebhookInfo` deleted. + - Provider-specific webhook UX (Trello/JIRA programmatic create, Linear secret + instructions) all live in each provider's folder. + - Adding a new PM provider's webhook step: declare `webhook-url-display` in `wizardSpec`, compose the shared step with any provider-specific UX via Fragment in the provider's `ProviderWizardDefinition.steps` Component. + +### 5. Update root `CLAUDE.md` + +**Implementation**: +- Append a spec-012 line to the PM-integration summary: "Spec 012 completed the webhook-UX manifest migration — Trello/JIRA/Linear all render their webhook steps via the manifest path. Legacy `WebhookStep` + `LinearWebhookInfoPanel` deleted." +- Remove any phrasing about the legacy `WebhookStep` retaining webhook creation UX (from spec-011 era). + +### 6. CHANGELOG entry + +**Implementation**: add under `## Unreleased` → `### Internal`: + +> **PM webhook-UX manifest migration complete (spec 012).** Closes the final gap from spec 011 — every PM wizard step, without exception, now renders via the manifest path. Plans 012/1-3 migrated Trello, JIRA, and Linear webhook steps into per-provider adapters composed of the shared `WebhookUrlDisplayStep` + provider-specific UX (Trello/JIRA programmatic Create + active list + delete + curl fallback via existing `webhooks.*` tRPC endpoints; Linear signing-secret via `ProjectSecretField` + 5-step manual-setup instructions). Plan 012/4 deleted the legacy `WebhookStep`, `LinearWebhookInfoPanel`, `useWebhookManagement`, `useLinearWebhookInfo`, the legacy test file, and the `-webhook` id-skip filter introduced by plan 011/4. Plan 011/4 ACs #3 + #6 fully close; spec 011 `_coverage.md` updated. No operator-visible regression. See spec [012](docs/specs/012-pm-webhook-manifest-migration.md.done). + +### 7. Forward-reference in spec 011 + +**Implementation**: prepend a blockquote to `docs/specs/011-pm-wizard-shared-migration.md.done` right after the H1: + +> **Forward reference (2026-04-18+):** the remaining webhook-UX migration (deferred at close of 011/4) landed in spec [012 — PM Webhook Manifest Migration](./012-pm-webhook-manifest-migration.md.done). That spec migrated Trello/JIRA/Linear webhook steps into the manifest path, deleted the legacy `WebhookStep` + `LinearWebhookInfoPanel` + supporting hooks, and fully closed plan 011/4 ACs #3 + #6. + +### 8. Update spec 011 `_coverage.md` + +**Implementation**: in `docs/plans/011-pm-wizard-shared-migration/_coverage.md`, update the rows for plan 011/4 ACs #3 + #6 (currently marked "partial — deferred to follow-up spec" or similar): +- `AC #3 (Linear inline secret via shared)` → `✅ fully closed by spec 012 downstream`. +- `AC #6 (LinearWebhookInfoPanel retired)` → `✅ fully closed by spec 012 downstream`. + +### 9. Full verification before marking done + +- `npm run build` green. +- `npm test` green. +- `npm run lint` green. +- `npm run typecheck` green. +- `new-provider-surface.test.ts` passes — the guard doesn't care about the deletions, but verify for safety. +- Conformance harness green for all three providers. + +### 10. Mark spec 012 `.done` + +Per `/implement` Phase 8: rename `docs/specs/012-pm-webhook-manifest-migration.md` → `.md.done` in a separate trailing commit after the plan-4 `.wip` → `.done` commit. + +--- + +## Test Plan + +### Unit tests +- No new test files. +- `tests/unit/web/pm-wizard-webhooks-step.test.ts` — **deleted**. +- Existing tests (the three per-provider adapter tests from plans 1-3) continue to pass. + +### Integration tests +- None. + +### Acceptance tests +- [ ] `grep -r "WebhookStep\|LinearWebhookInfoPanel\|useWebhookManagement\|useLinearWebhookInfo" web/src tests` returns zero results (outside git history). +- [ ] Every documentation file listed above has been updated. +- [ ] `npm run build`, `npm test`, `npm run lint`, `npm run typecheck` — all green. +- [ ] Conformance harness green for all three providers. + +--- + +## Acceptance Criteria (per-plan, testable) + +1. `WebhookStep` export removed from `pm-wizard-common-steps.tsx`. +2. `LinearWebhookInfoPanel` export removed from `pm-wizard-common-steps.tsx`. +3. `useWebhookManagement` + `useLinearWebhookInfo` removed from `pm-wizard-hooks.ts`. +4. `pm-wizard.tsx` no longer imports the above; the legacy webhook `` slot is gone. +5. `tests/unit/web/pm-wizard-webhooks-step.test.ts` is deleted. +6. Dead-code grep returns zero live references to the retired symbols. +7. `src/integrations/README.md` reflects the post-spec-012 state (five specs, updated "Adding a new PM provider" webhook guidance, Post-spec-012 additions section). +8. Root `CLAUDE.md` PM-integration summary references spec 012. +9. `CHANGELOG.md` has a single Internal entry summarizing spec 012. +10. `docs/specs/011-pm-wizard-shared-migration.md.done` has a forward-reference to spec 012. +11. `docs/plans/011-pm-wizard-shared-migration/_coverage.md` rows for plan 011/4 ACs #3 + #6 updated to closed. +12. Conformance harness passes for all three providers. +13. `npm run build` passes. +14. `npm test` passes. +15. `npm run lint` passes. +16. `npm run typecheck` passes. +17. Spec 012 is marked `.done` via file rename. + +--- + +## Documentation Impact (this plan only) + +| File | Change | +|---|---| +| `src/integrations/README.md` | Rewrite: five-specs preamble; updated "Adding a new PM provider" webhook guidance; Post-spec-012 additions section. | +| `CLAUDE.md` (project root) | Append spec-012 line to PM-integration summary; remove any "legacy WebhookStep still owns..." phrasing. | +| `CHANGELOG.md` | New Internal entry summarizing spec 012. | +| `docs/specs/011-pm-wizard-shared-migration.md.done` | Forward-reference blockquote to spec 012 at the top. | +| `docs/plans/011-pm-wizard-shared-migration/_coverage.md` | Plan 011/4 ACs #3 + #6 rows updated to reflect closure via spec 012. | + +--- + +## Out of Scope (this plan) + +Deferred to follow-up specs: nothing — this plan closes the spec. + +Originally out of scope for the spec (repeated for clarity): +- Generalizing `webhooks.*` tRPC endpoints. +- Backend webhook API changes. +- Adding programmatic webhook registration for Linear. +- Extending the manifest pattern to SCM / alerting. +- New shared UI primitives. +- Schema migrations. +- Rewriting the form-state model or `ProviderWizardDefinition`. +- Further widening of `WebhookUrlDisplayStep`. + +--- + +## Progress + + +- [x] AC #1 (`WebhookStep` export removed from `pm-wizard-common-steps.tsx`) +- [x] AC #2 (`LinearWebhookInfoPanel` export removed) +- [x] AC #3 (`useWebhookManagement` + `useLinearWebhookInfo` removed from `pm-wizard-hooks.ts`) +- [x] AC #4 (`pm-wizard.tsx` legacy webhook slot + imports deleted; `webhookStepNumber` removed) +- [x] AC #5 (`tests/unit/web/pm-wizard-webhooks-step.test.ts` deleted) +- [x] AC #6 (dead-code grep: only doc-comment references remain; no live imports/usages) +- [x] AC #7 (`src/integrations/README.md` five-specs preamble + Post-spec-012 additions + updated step-3 webhook guidance) +- [x] AC #8 (root `CLAUDE.md` PM-integration summary references spec 012) +- [x] AC #9 (`CHANGELOG.md` Internal entry for spec 012) +- [x] AC #10 (spec 011 forward-reference blockquote to spec 012 added) +- [x] AC #11 (spec 011 `_coverage.md` AC #3 marked "closed by spec 012"; plan 011/4 ACs #3 + #6 marked closed downstream) +- [x] AC #12 (conformance harness green — full suite 8186/8186) +- [x] AC #13 (`npm run build` green) +- [x] AC #14 (`npm test` green — 8186/8186) +- [x] AC #15 (`npm run lint` green) +- [x] AC #16 (`npm run typecheck` green) +- [x] AC #17 (spec 012 `.done` rename — handled by Phase 8 in the trailing commit) diff --git a/docs/plans/012-pm-webhook-manifest-migration/_coverage.md b/docs/plans/012-pm-webhook-manifest-migration/_coverage.md new file mode 100644 index 00000000..f8af58f6 --- /dev/null +++ b/docs/plans/012-pm-webhook-manifest-migration/_coverage.md @@ -0,0 +1,55 @@ +# Coverage map for spec 012-pm-webhook-manifest-migration + +Auto-generated by /plan. Tracks which plans satisfy which spec ACs. + +## Spec ACs + +| # | Spec AC (short) | Satisfied by | Status | +|---|---|---|---| +| 1 | Every wizard renders webhook step via manifest path | plan 1 (trello-webhook) + plan 2 (jira-webhook) + plan 3 (linear-webhook) | partial chain (plan 4 removes the legacy slot) | +| 2 | Trello programmatic create / active list / delete / curl fallback via manifest | plan 1 | full | +| 3 | JIRA equivalent + `jiraEnsureLabels` preserved | plan 2 | full | +| 4 | Linear signing-secret + 5-step instructions inline | plan 3 | full | +| 5 | Legacy `WebhookStep` + `LinearWebhookInfoPanel` + 2 hooks + legacy test file deleted | plan 4 (cleanup) | full | +| 6 | `-webhook` id-skip filter removed; `pm-wizard.tsx` iterates every manifest step | plan 3 (filter itself) + plan 4 (legacy slot removal) | partial chain | +| 7 | Plan 011/4 ACs #3 + #6 fully close; spec 011 `_coverage.md` updated | plan 3 (manifest-path rendering) + plan 4 (deletion + coverage-map edit) | partial chain | +| 8 | No operator regression per provider | plan 1 (Trello) + plan 2 (JIRA) + plan 3 (Linear) | full per provider | +| 9 | New provider requires zero shared-orchestration edits | plan 4 (verified after legacy slot + hooks deleted) | full | +| 10 | Conformance harness stays green every step | plan 1, 2, 3, 4 | hygiene every plan | +| 11 | Build/test/lint/typecheck green | plan 1, 2, 3, 4 | hygiene every plan | + +## Coverage summary + +- **11 spec ACs** mapped to **4 plans** +- **5 plans-worth** of full-coverage ACs (standalone: AC #2, #3, #4, #5, #9) +- **6 plans-worth** of partial-chain ACs (AC #1, #6, #7 require multiple plans; AC #8 full per-provider across three plans) +- **2 plans-worth** of hygiene-only coverage (AC #10, #11 — every plan asserts them) + +## Plan dependency graph + +``` +1-trello-webhook ──→ 2-jira-webhook ──→ 3-linear-webhook ──→ 4-cleanup +``` + +Serial DAG — same pattern as spec 011. Each provider plan may reveal a composition gap that carries forward; Trello first concentrates risk (most behavior: programmatic create + list + delete + curl). + +## Per-plan AC count (for reference) + +| Plan | Per-plan ACs | Spec ACs cited | +|---|---|---| +| 1 (trello-webhook) | 10 | 2 (full), 1 (partial), 8 (full for Trello), 10, 11 | +| 2 (jira-webhook) | 11 | 3 (full), 1 (partial), 8 (full for JIRA), 10, 11 | +| 3 (linear-webhook) | 11 | 4 (full), 1 (partial), 7 (partial), 8 (full for Linear), 10, 11 | +| 4 (cleanup) | 17 | 5 (full), 6 (closes), 7 (closes), 9 (full), 10, 11 | + +## Doc impact distribution + +Every doc update lives in **plan 4 (cleanup)**. Rationale: docs reflect the final state; updating per-plan = 3 rewrites of the same section. Plan 4 also writes a single CHANGELOG entry covering the whole spec (mirrors spec 010 + 011 cadence). + +| Top-level doc | Owner plan | +|---|---| +| `src/integrations/README.md` | 4 | +| `CLAUDE.md` (project root) | 4 | +| `CHANGELOG.md` | 4 | +| `docs/specs/011-pm-wizard-shared-migration.md.done` (forward-ref) | 4 | +| `docs/plans/011-pm-wizard-shared-migration/_coverage.md` (update) | 4 | diff --git a/docs/specs/009-pm-integration-hardening.md.done b/docs/specs/009-pm-integration-hardening.md.done index 94d4f051..32362381 100644 --- a/docs/specs/009-pm-integration-hardening.md.done +++ b/docs/specs/009-pm-integration-hardening.md.done @@ -9,6 +9,8 @@ status: done # 009: PM Integration Hardening — Make the Next Provider Boring +> **Forward reference (2026-04-18):** follow-ups landed in spec [010 — PM Integration Hardening Follow-ups](./010-pm-integration-hardening-followups.md.done). That spec generalizes `createLabel` / `createCustomField` into `pm.discovery.*` mutation endpoints + manifest hooks, adds the `currentUser` discovery capability, and upgrades the wizard generator to dispatch to real shared React components for every `StandardStepKind`. + ## Problem & Motivation Over the last ten days CASCADE shipped its third PM provider (Linear) and the workstream exposed that the plug-and-play manifest pattern delivered by spec 006 is **structurally correct but contractually thin**. The initial Linear integration landed in ~15 PRs of expected work (#1094–#1108). Another **18 PRs of corrective work** followed (#1112–#1142), each a real production defect or papercut. Those 18 PRs cluster into six repeating shapes — every one a case where the manifest *allowed* drift rather than *preventing* it: diff --git a/docs/specs/010-pm-integration-hardening-followups.md.done b/docs/specs/010-pm-integration-hardening-followups.md.done new file mode 100644 index 00000000..9c043ec0 --- /dev/null +++ b/docs/specs/010-pm-integration-hardening-followups.md.done @@ -0,0 +1,133 @@ +--- +id: 010 +slug: pm-integration-hardening-followups +level: spec +title: PM Integration Hardening — Followups +created: 2026-04-18 +status: done +--- + +# 010: PM Integration Hardening — Followups + +> **Forward reference (2026-04-18):** spec [011 — PM Wizard Shared Migration](./011-pm-wizard-shared-migration.md.done) consumes the shared step components landed here (plan 010/3) and migrates all three production providers (Trello, JIRA, Linear) onto them. Spec 011 also adds a 7th `StandardStepKind: custom-field-mapping`, widens `container-pick` / `project-scope` / `webhook-url-display` with optional props, and deletes the three legacy `pm-wizard-{trello,jira,linear}-steps.tsx` files. + +## Problem & Motivation + +Spec 009 landed the hardened PM provider contract and migrated Trello, JIRA, and Linear onto it — but closed with three conscious deferrals, each called out in the `.done` plan files: + +- **Mutations were left on the legacy tRPC router.** Spec 009/5 deleted the three `verify*` procedures because they had a clean `pm.discover`-based replacement, but kept `createTrelloLabel`, `createTrelloLabels`, `createJiraCustomField`, `createLinearLabel`, and `createLinearLabels` in place with a TODO comment. Five wizard-hook call sites still route through those legacy procedures. +- **Shared wizard step components are still placeholders.** Spec 009/1 introduced a `renderStandardStep` generator that returns typed placeholder divs. The real UI for each standard step kind (`credentials`, `container-pick`, `status-mapping`, `label-mapping`, `webhook-url-display`, `project-scope`) still lives in per-provider `pm-wizard--steps.tsx` files. Three working wizards, three duplicated sets of UI that do semantically identical work. +- **Read-side legacy procedures were only partially cleaned up.** `integrationsDiscovery.ts` still carries roughly ten per-provider read procedures (Trello board discovery, JIRA project discovery, Linear team discovery, and their "by project" variants) that were out of plan 009/5's narrowed scope. +- **Wizard verification UX regressed cosmetically.** Plan 009/5 migrated the three verify-credentials buttons to `pm.discover` with a generic "Credentials verified — found N boards/teams/projects" message. The old "Verified as @username (Full Name)" identity display is gone. Users lose identity context — minor, but worth restoring. + +This spec finishes the job. It does not introduce new capabilities; it completes the migrations spec 009 started, on the same hardened contracts. The win: after this lands, `integrationsDiscovery.ts` contains only SCM (GitHub) and alerting (Sentry) procedures; every PM mutation and every PM read flow through the generic `pm.*` endpoints; every standard wizard step renders through a single canonical component; and the wizard verification UX is back to the user-friendly identity display. + +--- + +## Goals + +- Every PM mutation the wizard performs (create label, create custom field) goes through a single generic `pm.create*` endpoint dispatched by the provider manifest. +- Every PM read the wizard or CLI performs (boards, projects, teams, project details, current user) goes through `pm.discover` — no per-provider duplicate remains in `integrationsDiscovery.ts`. +- The standard wizard step kinds render from a single canonical component per kind; per-provider wizard folders contain only genuinely provider-specific custom UI. +- The wizard's "verify credentials" step displays the authenticated user's handle (restoring the pre-009/5 UX) via the same generic dispatch mechanism. +- A new PM provider added after this spec lands needs to touch **zero** files outside its provider + wizard folders + the single-line entrypoint registration — the same AC #10 invariant spec 009 established, now tight across read + write + wizard-UI surfaces. + +--- + +## Non-goals + +- Removing `trello` / `jira` / `linear` as first-class keys on the central project config schema (the registry-driven `configMapper` rewrite). That is substantial enough to earn its own spec. +- Shipping the in-memory fake PM provider as a user-facing demo mode. +- Extending the manifest pattern to SCM (GitHub) or alerting (Sentry). Those stay on the legacy `IntegrationModule` pattern for now. +- Widening TypeScript typecheck coverage to include the `tests/` tree. Real issue; orthogonal; separate spec. +- Changing the agent-facing PM interface method names or trigger categories. + +--- + +## Constraints + +- Must preserve behavioral parity with the current wizard, CLI, and agent runs. No user-visible regression beyond the verification-UX restoration (which is itself a regression fix, not a new behavior). +- Must not break existing projects: every existing Trello / JIRA / Linear project must continue working through the wizard + CLI + agent pipeline without re-setup. +- The manifest contract extension must stay backward-compatible with the fake PM provider fixture and with any future PM provider that chooses not to implement every mutation. +- Shared wizard components must not introduce new runtime dependencies; they render through the existing React + tRPC + react-query stack already in the dashboard. +- The legacy-removed tests from spec 009/5 continue to hold — all PM procedures previously removed stay removed. + +--- + +## User stories / Requirements + +1. **As a CASCADE contributor adding a new PM provider**, I declare a manifest with optional `createLabel` / `createCustomField` hooks. The generic `pm.createLabel` / `pm.createCustomField` endpoints automatically route through them. No edits to shared tRPC files are needed. +2. **As a CASCADE contributor adding a new PM provider**, I implement `discover('currentUser', {})` alongside the other capabilities. The wizard's verification step automatically displays the authenticated user's handle — no per-provider code required in `pm-wizard-hooks.ts`. +3. **As a CASCADE contributor adding a new PM provider**, my wizard folder contains only `index.ts`, `wizard.ts`, `adapters.tsx`, and any genuinely provider-specific custom step components. The six standard step kinds render from canonical shared components. +4. **As a dashboard operator setting up a project**, the wizard's "verify credentials" step shows me the authenticated identity (username / email / handle), not just "found N items". +5. **As a dashboard operator setting up a project**, the wizard's label-creation and custom-field-creation buttons behave exactly as they do today, even though they now route through a different endpoint. +6. **As a CASCADE maintainer reviewing a new-provider PR**, the conformance harness + `new-provider-surface` snapshot guard report specific failures if the contributor misses a shared-surface rule introduced by this spec. + +--- + +## Research Notes + +No external research needed. This spec is strictly finishing work on patterns already established in specs 006 + 009: + +- **`pm.discover` as a generic dispatch pattern** — proven across read operations in spec 009. Adding a `currentUser` capability follows the same shape. +- **Per-manifest factory hooks** — `createLabel?` is already on the `PMProviderManifest` interface (spec 006); `createCustomField?` follows the same pattern. `createDiscoveryProvider` (spec 009/1) is the reference shape for a full-adapter factory. +- **Standard step components under the wizard shell** — the `ProviderWizardStepProps` contract already defined in spec 009/1 (`{state, dispatch, providerHooks}`) is what the shared components consume. Components are plain React; no new frameworks needed. +- **Wizard verification UX via `discover('currentUser')`** — symmetric with `discover('teams')` / `discover('boards')` etc. Semantically "look up the current identity" is a discovery operation, not a mutation. + +--- + +## Open Source Decisions + +No new OSS dependencies. This spec consumes what's already in the stack (tRPC, React, react-query, Zod, Biome) and extends the in-repo patterns from specs 006 + 009. + +--- + +## Strategic decisions + +1. **Two named endpoints, not one generic `pm.create`** — chose `pm.createLabel` + `pm.createCustomField` as separate endpoints. Reason: only two mutations, the shapes differ meaningfully (`color?` for labels, `type` for custom fields), and naming gives type-safety per shape. If future mutations arrive, a unified dispatcher can be added then. +2. **Extend existing top-level manifest hooks** — `createCustomField?` ships as a sibling of the already-present `createLabel?` on `PMProviderManifest`, not nested under a new `mutations` namespace. Keeps the additive shape spec 006 established. +3. **`currentUser` as a new discovery capability**, not a new endpoint — restores the verification UX with zero new surface. Symmetric with `teams` / `boards` / `labels` / `states` / `projects`. +4. **Shared wizard step components live in a dedicated shared folder**, one component per `StandardStepKind`. Reason: discoverable, mirrors the manifest's declarative shape. +5. **Per-provider wizard files shrink, don't vanish.** The `pm-wizard--steps.tsx` files keep only truly provider-specific custom steps; standard-kind step UI gets deleted. Providers without any custom steps end up with an empty file that can be deleted later. +6. **Migration as three sequential plans** — Theme A (mutations + verification UX), Theme B (read-side legacy cleanup), Theme C (real shared wizard components). They touch overlapping infrastructure (the wizard-hooks file, tests/unit/api), so sequential ordering avoids merge churn. +7. **`integrationsDiscovery.ts` filename preserved.** Post-spec-010 it contains only GitHub SCM + Sentry alerting procedures. A future spec can rename or fold into a unified SCM/alerting registry; premature renaming would be churn. +8. **No behavioral change to the central config schema or to `configMapper`.** Those surfaces stay exactly as spec 009/5 left them. The registry-driven `configMapper` rewrite is an explicit non-goal of this spec. + +--- + +## Acceptance Criteria (outcome-level) + +1. The wizard can create labels on Trello and Linear, and create custom fields on Trello and JIRA, via new generic endpoints. The user-visible behavior is identical to today's legacy-per-provider flow. +2. After this spec lands, no file in the repository calls the legacy `createTrelloLabel` / `createTrelloLabels` / `createJiraCustomField` / `createLinearLabel` / `createLinearLabels` procedures, and those procedures are deleted from the integrations-discovery router. +3. The wizard's read-side credential verification and discovery dropdowns (boards, projects, teams) fetch their data through a single generic discovery endpoint. Per-provider read procedures in the legacy router are deleted. +4. After this spec lands, the integrations-discovery router contains only SCM (GitHub) and alerting (Sentry) procedures — no PM-specific procedures remain. +5. The wizard's "verify credentials" step displays the authenticated user's identity (username / email / handle format identical to the pre-009/5 UX), restored via the generic discovery endpoint. +6. Every standard wizard step kind renders from a single canonical shared component. The three existing providers (Trello, JIRA, Linear) use those components directly; provider-folder step files retain only genuinely provider-specific custom steps. +7. The conformance harness exercises `createLabel` and `createCustomField` through the manifest for every provider that declares the hook, and exercises `discover('currentUser')` for every provider that declares the capability. Regression tests guard each. +8. A new PM provider added after this spec lands needs to touch only its provider folder, its wizard folder, and the single entrypoint line — the `new-provider-surface` snapshot guard from spec 009/5 is tightened to include any files this spec adds. +9. All three providers' existing adapter, integration, wizard, and agent-run tests continue to pass unchanged. +10. Build, lint, typecheck, and the full unit test suite pass with no regressions. + +--- + +## Documentation Impact (high-level) + +- `src/integrations/README.md` — extend the "Adding a new PM provider" section with `createCustomField?`, `discover('currentUser')`, and the shared wizard components. Update the provider migration status table to reflect post-spec-010 state (all PM surfaces now generic). +- `tests/README.md` — document the `createLabel` / `createCustomField` conformance assertions and the `currentUser` capability check. +- `CLAUDE.md` (project root) — brief update to reference spec 010 alongside 009 in the PM-integration summary. +- `CHANGELOG.md` — entry per plan as it lands. +- `docs/specs/009-pm-integration-hardening.md.done` — add a forward-reference to spec 010 mirroring how 006 points to 009 (so readers of 009 discover the follow-up). + +--- + +## Out of Scope + +- The registry-driven `configMapper` rewrite (removing `trello` / `jira` / `linear` as first-class keys on the central project schema). +- Extending the manifest pattern to SCM (GitHub) or alerting (Sentry). +- Changing the agent-facing PM interface method names or trigger categories. +- Credential storage / encryption / resolution changes. +- Replacing Zod, tRPC, React, or Biome. +- Widening TypeScript typecheck coverage to the `tests/` tree. +- Shipping the fake PM provider as a user-facing demo mode. +- Introducing a new generic mutation beyond `createLabel` / `createCustomField` (e.g., `deleteLabel`, `renameLabel`). If a future mutation is needed, this spec's pattern applies — but this spec does not anticipate them. +- Rearranging the file layout of `integrationsDiscovery.ts` (e.g., renaming, folding into a new file, splitting by category). The filename stays even though the file becomes SCM+alerting-only. diff --git a/docs/specs/011-pm-wizard-shared-migration.md.done b/docs/specs/011-pm-wizard-shared-migration.md.done new file mode 100644 index 00000000..717838dc --- /dev/null +++ b/docs/specs/011-pm-wizard-shared-migration.md.done @@ -0,0 +1,153 @@ +--- +id: 011 +slug: pm-wizard-shared-migration +level: spec +title: PM Wizard Migration — Existing Providers Onto Shared Components +created: 2026-04-18 +status: done +--- + +# 011: PM Wizard Migration — Existing Providers Onto Shared Components + +> **Forward reference (2026-04-18+):** the remaining webhook-UX migration (deferred at close of plan 011/4 — legacy `WebhookStep` + `LinearWebhookInfoPanel` still owned webhook registration + signing-secret UX) landed in spec [012 — PM Webhook Manifest Migration](./012-pm-webhook-manifest-migration.md.done). That spec migrated Trello/JIRA/Linear webhook steps into the manifest path, deleted the legacy surface, and fully closed plan 011/4 ACs #3 + #6. + +## Problem & Motivation + +Spec 010 shipped six real shared React step components — one per `StandardStepKind` — so a new PM provider could configure its wizard with zero per-provider step UI. The shared path works: the wizard generator dispatches to the shared registry, every component has tests, the `new-provider-surface` guard pins them. But the three **existing** production providers (Trello, JIRA, Linear) never migrated. They still fork their own step components in per-provider wizard-step files — roughly 1,085 lines of UI that duplicates what the shared components already do. + +The "zero per-provider step code" promise is half-delivered. A fourth PM provider can claim it; Trello, JIRA, and Linear cannot. More practically: the shared components have exactly zero production consumers today. Without a consumer, they silently rot — the 31 tests pin the components in isolation but don't catch drift between what a real wizard needs (searchable pickers, custom-field creation, webhook signing secrets) and what the shared components provide. Spec 010's feature parity with the legacy per-provider steps was assumed, not verified. + +The migration forces the shared components to meet real-provider requirements. Where the gap is generic (searchable dropdowns via the already-adopted cmdk combobox; custom-field creation via the already-shipped `manifest.createCustomField` hook; webhook signing-secret fields alongside URL display) the shared components widen to close it. Where the gap is genuinely provider-specific (Trello OAuth popup, JIRA issue-type mapping) the provider declares a `kind: 'custom'` step and keeps the UI in its provider folder. When all three real providers migrate and the legacy files are deleted, the shared components become the single source of truth for PM wizards — new and existing alike. + +--- + +## Goals + +- All three PM provider wizards (Trello, JIRA, Linear) render their standard wizard steps through the shared `StandardStepKind` components. +- The three `pm-wizard-{trello,jira,linear}-steps.tsx` files are deleted. +- Every wizard feature available today continues to work — operators see no regression in the Trello/JIRA/Linear wizard UX. +- True provider-specific UI (Trello OAuth popup; JIRA issue-type mapping) is explicit as `kind: 'custom'` in the manifest, rendered from the provider folder, so the standard path stays clean. +- The shared components are exercised by three real providers, catching any drift between the abstraction and real-world requirements. +- Adding a fourth PM provider tomorrow still writes zero per-provider standard-step code — the promise holds, now verified. + +--- + +## Non-goals + +- Changing operator-visible wizard UX. The migration is internal — same steps in the same order, same inputs, same feedback. The only permitted UX improvements are where a provider was inconsistent with others (e.g. one provider had searchable picker, one didn't — normalize upward). +- Extending the manifest pattern to SCM (GitHub) or alerting (Sentry). Spec 013 territory. +- Migrating the six composite `*Details(ByProject)` tRPC procedures off `integrationsDiscovery.ts`. Spec 012 territory — tracked separately as it's a backend concern, not a wizard concern. +- Rebuilding the wizard step orchestration, form state, or validation model. The per-provider `ProviderWizardDefinition` contract stays. Only the step Components change. +- Introducing a new UI framework, design system, or form library. Everything uses primitives already in the repo. + +--- + +## Constraints + +- **Zero operator-visible regression.** The 3 production wizards must render identically in every step to the legacy implementation, modulo the decided upgrades (searchable dropdowns everywhere, unified custom-field UI). +- **Additive shared-component API only.** Widening a shared component adds optional props; the 31 spec-010 step tests continue to pass without modification. +- **One reviewable PR per provider plus a cleanup PR.** No single-PR big-bang migration of all three. +- **Test surface net-positive.** The 6 legacy step tests (5 Linear + 1 Trello) are either rewritten against shared components where the assertion still makes sense, or deleted where they pin retired DOM shapes. Shared-component coverage must not shrink. +- **No breaking of `new-provider-surface` invariant.** Adding a 7th `StandardStepKind` for custom-field mapping widens `SHARED_SURFACE_FILES` with the new component file; the guard still refuses cross-provider edits. +- **Conformance harness stays green.** `tests/unit/integrations/pm-conformance.test.ts` must continue to pass for every provider through every step of the migration. + +--- + +## User stories / Requirements + +As an **operator setting up a new Trello project**: +- I can paste my API key and token, or complete OAuth via popup, the same way I do today. +- I can search and filter boards by name in a dropdown — the same way I search and filter in Linear or JIRA today. +- I can create a missing label or custom field from the wizard, the same way I do today. + +As an **operator configuring JIRA**: +- Free-text label input works unchanged (JIRA is free-form). +- I can select issue types for task/subtask creation — unchanged from today. +- The webhook step shows me the URL and signing-secret input in one place. + +As an **operator configuring Linear**: +- The webhook step includes the signing-secret field inline with the URL, the way it does today. +- The optional project-scope selector narrows the integration to one Linear Project — unchanged from spec 005. +- I can create a missing label from the label-mapping step, the same way I do today. + +As a **CASCADE contributor adding a fourth PM provider (Asana, GitLab, ClickUp, …)**: +- I declare `wizardSpec.steps` on my manifest and everything renders from the shared components — no per-provider step UI, just like spec 010 promised. Now verifiable because three real providers already do the same. + +As a **CASCADE reviewer inspecting a wizard PR**: +- The diff is focused: widen component X, migrate provider Y to consume it, delete the retired per-provider file. No "also changed Z" surprises. + +--- + +## Research Notes + +- **cmdk + radix-ui already adopted** via a shared Combobox component. Nine components consume it, including all three legacy PM wizards' board/project/team pickers. Widening the shared `container-pick` to consume the shared Combobox is drop-in. +- **`manifest.createCustomField` hook already exists** (spec 010/1). `pm.discovery.createCustomField` tRPC endpoint already serves every provider. Missing piece is a shared step component that consumes it. +- **Strangler-fig migration is the canonical pattern** when replacing a forked UI with a shared one: one provider at a time, each migration reversible via `git revert`, retired files deleted in a closing commit. No prior art needs researching — it's what every long-lived codebase does when consolidating duplicated UI. +- **Trello OAuth** uses a `window.open(authorizeUrl, 'trello_oauth', ...)` popup with Trello-specific return handling. Not generalizable without leaking Trello semantics. +- **JIRA issue-type mapping** (task / subtask) has one consumer today and would likely have at most one more (ClickUp). Staying as `kind: 'custom'` avoids a speculative 8th standard kind. + +--- + +## Open Source Decisions + +| Tool | Solves | Decision | Reason | +|---|---|---|---| +| [cmdk](https://cmdk.paco.me/) | Searchable dropdowns | **Use** | Already adopted (9 consumers, 3 PM wizards); shared Combobox wraps it. | +| [radix-ui](https://www.radix-ui.com/) | Popover / Dialog primitives | **Use** | Already adopted; needed for custom-field create modal. | +| [lucide-react](https://lucide.dev/) | Icons | **Use** | Already adopted; no new icon additions expected. | + +No new OSS adoption. The spec stays internal. + +--- + +## Strategic decisions + +1. **Close searchable-dropdown gap by widening shared components** — chose extending shared `container-pick` / `project-scope` to consume the existing cmdk Combobox over leaving the plain ` { - const html = render({ - linearStatusMappings: { - splitting: 'st-sp', - planning: 'st-pl', - }, - }); - // The persisted values should appear as selected option values. - expect(html).toContain('value="st-sp"'); - expect(html).toContain('value="st-pl"'); - }); - - // Regression: Linear webhooks deliver workflow-state UUIDs in `data.stateId`, - // not display names. Storing names in the mapping makes the trigger handler's - // strict equality check (src/triggers/linear/status-changed.ts) silently no-op. - it('uses state IDs (not names) as dropdown option values', () => { - const html = render(); - // Each Linear workflow state's ID must appear as an option value. - for (const id of ['st-bl', 'st-sp', 'st-pl', 'st-td', 'st-ip', 'st-ir', 'st-dn', 'st-mg']) { - expect(html, `option value="${id}" missing`).toContain(`value="${id}"`); - } - // State names must NOT appear as option values (they may still be option labels). - for (const name of [ - 'Backlog', - 'Splitting', - 'Planning', - 'Todo', - 'In Progress', - 'In Review', - 'Done', - 'Merged', - ]) { - expect(html, `state name "${name}" must not be a value`).not.toContain(`value="${name}"`); - } - }); -}); - -describe('LinearFieldMappingStep — label slots', () => { - function renderWithLabels( - labels: Array<{ id: string; name: string; color: string }>, - persisted: Record = {}, - onCreateLabel?: (slot: string) => void, - onCreateAllMissingLabels?: () => void, - ): string { - const state = makeState({ - linearTeamDetails: { - states: [], - labels, - }, - linearLabels: persisted, - }); - return renderToStaticMarkup( - createElement(LinearFieldMappingStep, { - state, - dispatch: () => {}, - onCreateLabel, - onCreateAllMissingLabels, - }), - ); - } - - it('renders label dropdowns sourced from linearTeamDetails.labels (ID-backed options)', () => { - const html = renderWithLabels([ - { id: 'lbl-proc-uuid', name: 'cascade-processing', color: '#2563EB' }, - { id: 'lbl-done-uuid', name: 'cascade-processed', color: '#16A34A' }, - ]); - // The label dropdown must expose each Linear label's UUID as an option value. - expect(html).toContain('value="lbl-proc-uuid"'); - expect(html).toContain('value="lbl-done-uuid"'); - // Display names should NOT appear as option values (they can still be in the label text). - expect(html).not.toContain('value="cascade-processing"'); - }); - - it('shows the "Create" affordance for slots with no mapping and no existing matching label', () => { - const html = renderWithLabels( - [], - {}, - () => {}, - () => {}, - ); - // A dedicated create button per slot — look for the batch button text too. - expect(html).toMatch(/Create All Missing/); - }); - - it('hides the per-slot Create button when the default label already exists on the team', () => { - const html = renderWithLabels( - [ - { id: 'lbl-ready', name: 'cascade-ready', color: '#0284C7' }, - { id: 'lbl-proc', name: 'cascade-processing', color: '#2563EB' }, - { id: 'lbl-procd', name: 'cascade-processed', color: '#16A34A' }, - { id: 'lbl-err', name: 'cascade-error', color: '#DC2626' }, - { id: 'lbl-auto', name: 'cascade-auto', color: '#9333EA' }, - ], - {}, - () => {}, - () => {}, - ); - // With every default present, there's nothing left to create → batch button hidden. - expect(html).not.toMatch(/Create All Missing/); - }); - - it('reflects persisted label mappings as selected dropdown values', () => { - const html = renderWithLabels( - [{ id: 'lbl-proc-uuid', name: 'cascade-processing', color: '#2563EB' }], - { processing: 'lbl-proc-uuid' }, - ); - expect(html).toContain('value="lbl-proc-uuid"'); - }); -}); diff --git a/tests/unit/web/linear-team-step.test.ts b/tests/unit/web/linear-team-step.test.ts deleted file mode 100644 index c48fa547..00000000 --- a/tests/unit/web/linear-team-step.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * SSR tests for LinearTeamStep — verify team + project selector rendering - * and the new optional project-scope selector behavior. - */ - -import { createElement } from 'react'; -import { renderToStaticMarkup } from 'react-dom/server'; -import { describe, expect, it, vi } from 'vitest'; -import { LinearTeamStep } from '../../../web/src/components/projects/pm-wizard-linear-steps.js'; -import type { WizardState } from '../../../web/src/components/projects/pm-wizard-state.js'; - -function makeState(overrides: Partial = {}): WizardState { - return { - provider: 'linear', - linearApiKey: 'lin_api_test', - linearTeamId: '', - linearTeams: [ - { id: 'team-1', name: 'Engineering', key: 'ENG' }, - { id: 'team-2', name: 'Design', key: 'DES' }, - ], - linearTeamDetails: null, - linearStatusMappings: {}, - linearLabels: {}, - linearProjectId: '', - linearProjects: [], - isEditing: false, - hasStoredCredentials: false, - ...overrides, - } as unknown as WizardState; -} - -function pendingMutation(): { - isPending: boolean; - isError: boolean; - error: null; - mutate: () => void; -} { - return { isPending: false, isError: false, error: null, mutate: vi.fn() }; -} - -function render(extra: Partial = {}): string { - const state = makeState(extra); - return renderToStaticMarkup( - createElement(LinearTeamStep, { - state, - onTeamSelect: () => {}, - dispatch: () => {}, - // biome-ignore lint/suspicious/noExplicitAny: test stub for tanstack mutation object - linearTeamsMutation: pendingMutation() as any, - // biome-ignore lint/suspicious/noExplicitAny: test stub for tanstack mutation object - linearDetailsMutation: pendingMutation() as any, - // biome-ignore lint/suspicious/noExplicitAny: test stub for tanstack mutation object - linearProjectsMutation: pendingMutation() as any, - }), - ); -} - -describe('LinearTeamStep — project selector', () => { - it('does not render the Linear Project selector when no team is selected', () => { - const html = render({ linearTeamId: '' }); - expect(html).not.toContain('Linear Project'); - }); - - it('renders the Linear Project selector when a team is selected', () => { - const html = render({ - linearTeamId: 'team-1', - linearProjects: [ - { id: 'P1', name: 'Alpha', icon: null, color: null }, - { id: 'P2', name: 'Beta', icon: null, color: null }, - ], - }); - expect(html).toContain('Linear Project'); - }); - - it('populates the selector options from state.linearProjects', () => { - const html = render({ - linearTeamId: 'team-1', - linearProjects: [ - { id: 'P1', name: 'Alpha', icon: null, color: null }, - { id: 'P2', name: 'Beta', icon: null, color: null }, - ], - }); - expect(html).toContain('Alpha'); - expect(html).toContain('Beta'); - expect(html).toContain('value="P1"'); - expect(html).toContain('value="P2"'); - }); - - it('pre-selects the stored projectId when set', () => { - const html = render({ - linearTeamId: 'team-1', - linearProjectId: 'P2', - linearProjects: [ - { id: 'P1', name: 'Alpha', icon: null, color: null }, - { id: 'P2', name: 'Beta', icon: null, color: null }, - ], - }); - // Native , +
+ ) : ( + + )} +

+ {state.trelloApiKey + ? 'Click to open Trello authorization in a popup.' + : 'Enter your API key above to enable authorization.'} +

+
+
setManualOpen(e.currentTarget.open)}> + + Enter token manually + +
+ + dispatch({ type: 'SET_TRELLO_TOKEN', value: e.target.value })} + placeholder="Trello token" + autoComplete="off" + /> +

+ {isWaitingForAuth + ? 'After clicking "Allow" in the Trello popup, copy the token shown and paste it above.' + : 'Generate a token from the API key page linked above.'} +

+
+
+ + ); +} diff --git a/web/src/components/projects/pm-providers/trello/webhook-step.tsx b/web/src/components/projects/pm-providers/trello/webhook-step.tsx new file mode 100644 index 00000000..1d295346 --- /dev/null +++ b/web/src/components/projects/pm-providers/trello/webhook-step.tsx @@ -0,0 +1,213 @@ +/** + * Trello webhook step adapter (plan 012/1; styling restored post-spec-012 + * follow-up). + * + * Fragment composing the shared `WebhookUrlDisplayStep` (URL + copy) with + * Trello-specific UX: active-webhooks list, programmatic "Create Webhook" + * button, delete buttons, curl fallback. + * + * Rendered as the Trello wizard's `trello-webhook` step Component via the + * manifest path. All tRPC wiring (webhooks.list/create/delete with + * trelloOnly:true) lives in the Trello wizard's `useProviderHooks`; this + * component just renders what it receives. + */ + +import { Loader2, Trash2 } from 'lucide-react'; +import { createElement, Fragment, type ReactElement } from 'react'; +import { Button } from '@/components/ui/button.js'; +import { CopyButton } from '@/components/ui/copy-button.js'; +import type { DataProps } from '@/lib/data-props.js'; +import { WebhookUrlDisplayStep } from '../steps/webhook-url-display.js'; +import type { ProviderWizardStepProps } from '../types.js'; + +interface ActiveWebhook { + readonly id: string; + readonly url: string; + readonly active: boolean; +} + +interface TrelloWebhookProviderHooks { + readonly webhookUrl: string; + readonly callbackBaseUrl: string; + readonly activeTrelloWebhooks: ReadonlyArray; + readonly webhooksLoading: boolean; + readonly createTrelloWebhook: () => void; + readonly createLoading: boolean; + readonly createError: string | undefined; + readonly deleteTrelloWebhook: (callbackBaseUrl: string) => void; + readonly deleteLoading: boolean; +} + +function asTrelloWebhookHooks( + providerHooks: Record | undefined, +): TrelloWebhookProviderHooks { + return (providerHooks ?? {}) as unknown as TrelloWebhookProviderHooks; +} + +function buildTrelloCurl(boardId: string, callbackBaseUrl: string): string { + const effectiveBoardId = boardId || ''; + const callbackUrl = callbackBaseUrl + ? `${callbackBaseUrl}/trello/webhook` + : '/trello/webhook'; + return `curl -X POST "https://api.trello.com/1/webhooks" \\ + -H "Content-Type: application/json" \\ + -d '{ + "key": "", + "token": "", + "callbackURL": "${callbackUrl}", + "idModel": "${effectiveBoardId}", + "description": "CASCADE webhook" + }'`; +} + +export function TrelloWebhookAdapter({ + state, + providerHooks, +}: ProviderWizardStepProps): ReactElement { + const h = asTrelloWebhookHooks(providerHooks); + const createDisabled = !h.callbackBaseUrl || h.createLoading; + const curlCommand = buildTrelloCurl(state.trelloBoardId ?? '', h.callbackBaseUrl); + + return createElement( + Fragment, + null, + // Shared URL display + copy button. + WebhookUrlDisplayStep({ + step: { + kind: 'webhook-url-display', + id: 'trello-webhook', + config: { + instructions: + 'Click "Create Webhook" below to register automatically, or use the curl command for manual setup.', + }, + }, + providerId: 'trello', + webhookUrl: h.webhookUrl, + }), + + // Active-webhooks list. + createElement( + 'div', + { + className: 'space-y-2', + 'data-section': 'active-webhooks', + }, + h.webhooksLoading + ? createElement( + 'p', + { + 'data-state': 'loading', + className: 'flex items-center gap-2 text-sm text-muted-foreground', + }, + createElement(Loader2, { className: 'h-4 w-4 animate-spin' }), + 'Loading webhooks…', + ) + : h.activeTrelloWebhooks.length === 0 + ? createElement( + 'p', + { className: 'text-sm text-amber-600 dark:text-amber-400' }, + 'No Trello webhooks configured for this project.', + ) + : createElement( + 'ul', + { className: 'space-y-1' }, + ...h.activeTrelloWebhooks.map((wh) => + createElement( + 'li', + { + key: wh.id, + className: 'flex items-center justify-between rounded-md border px-3 py-2', + 'data-webhook-id': wh.id, + }, + createElement( + 'div', + { className: 'flex items-center gap-2 text-sm' }, + createElement('span', { + className: `inline-block h-2 w-2 rounded-full ${wh.active ? 'bg-green-500 dark:bg-green-400' : 'bg-amber-500 dark:bg-amber-400'}`, + 'data-active': wh.active ? 'true' : 'false', + }), + createElement('code', { className: 'font-mono text-xs break-all' }, wh.url), + ), + createElement( + Button, + { + type: 'button', + variant: 'ghost', + size: 'icon-sm', + 'data-action': 'delete-webhook', + 'data-webhook-id': wh.id, + disabled: h.deleteLoading, + onClick: () => { + // Extract base URL by stripping the trailing /trello/webhook + // path (matches the legacy WebhookStep delete behavior). + const base = wh.url.replace(/\/trello\/webhook$/, ''); + h.deleteTrelloWebhook(base); + }, + title: 'Delete webhook', + } as React.ComponentProps & DataProps, + createElement(Trash2, { className: 'h-4 w-4' }), + ), + ), + ), + ), + ), + + // Create button. + createElement( + 'div', + { className: 'space-y-2' }, + createElement( + Button, + { + type: 'button', + variant: 'default', + 'data-action': 'create-webhook', + disabled: createDisabled, + onClick: () => h.createTrelloWebhook(), + } as React.ComponentProps & DataProps, + h.createLoading ? createElement(Loader2, { className: 'h-4 w-4 animate-spin' }) : null, + h.createLoading ? 'Creating…' : 'Create Webhook', + ), + h.createError + ? createElement( + 'p', + { className: 'text-sm text-destructive', 'data-state': 'error' }, + h.createError, + ) + : null, + ), + + // Curl fallback. + createElement( + 'details', + { + className: + 'rounded-md border border-blue-200 bg-blue-50 px-3 py-2 dark:border-blue-900/50 dark:bg-blue-900/20', + }, + createElement( + 'summary', + { + className: + 'cursor-pointer select-none text-xs text-blue-700 dark:text-blue-300 font-medium', + }, + "Manual webhook creation (alternative: if the button above doesn't work)", + ), + createElement( + 'div', + { className: 'mt-2 relative rounded-md bg-muted border' }, + createElement( + 'div', + { className: 'absolute top-2 right-2' }, + createElement(CopyButton, { text: curlCommand }), + ), + createElement( + 'pre', + { + className: 'text-xs font-mono whitespace-pre-wrap break-all p-3 pr-16 overflow-x-auto', + }, + curlCommand, + ), + ), + ), + ); +} diff --git a/web/src/components/projects/pm-providers/trello/wizard.ts b/web/src/components/projects/pm-providers/trello/wizard.ts index 69392fef..7c34f795 100644 --- a/web/src/components/projects/pm-providers/trello/wizard.ts +++ b/web/src/components/projects/pm-providers/trello/wizard.ts @@ -1,29 +1,77 @@ /** - * Trello ProviderWizardDefinition — the frontend half of the manifest - * pattern. Registered via `./index.ts` at module load. + * Trello ProviderWizardDefinition (plan 011/2). * - * `useProviderHooks` composes the existing Trello hooks - * (`useTrelloDiscovery`, `useTrelloLabelCreation`, - * `useTrelloCustomFieldCreation`) and exposes the mutations + handlers - * the step adapters consume. This is where the per-provider React - * wiring lives; `pm-wizard.tsx` no longer needs to call - * `useTrelloDiscovery` directly (task 3 of this plan removes those - * calls from the parent wizard). + * Migrated from per-provider step components to the shared step + * components shipped by spec 010/3 + widened by plan 011/1. Every step + * except the custom OAuth credentials step now renders through + * `renderStandardStep` + `STANDARD_STEP_COMPONENTS`. + * + * Step sequence mirrors `trelloManifest.wizardSpec.steps`: + * 1. custom(TrelloOAuthStep) — OAuth popup + manual token fallback + * 2. container-pick (searchable) — board picker with type-ahead + * 3. status-mapping — CASCADE stages → Trello lists + * 4. label-mapping (w/defaults) — CASCADE labels → Trello labels + create + * 5. custom-field-mapping — cost custom field + create + * 6. webhook-url-display — router URL + copy button */ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { Loader2 } from 'lucide-react'; +import type { ReactElement } from 'react'; import { useState } from 'react'; +import { API_URL } from '@/lib/api.js'; +import { trpc, trpcClient } from '@/lib/trpc.js'; import { useTrelloCustomFieldCreation, useTrelloDiscovery, useTrelloLabelCreation, } from '../../pm-wizard-hooks.js'; -import { TRELLO_LABEL_DEFAULTS } from '../../pm-wizard-trello-steps.js'; -import type { ProviderWizardDefinition } from '../types.js'; -import { - TrelloBoardStepAdapter, - TrelloCredentialsStepAdapter, - TrelloFieldMappingStepAdapter, -} from './adapters.js'; +import { deriveActiveWebhooks } from '../../pm-wizard-state.js'; +import { ContainerPickStep } from '../steps/container-pick.js'; +import { CustomFieldMappingStep } from '../steps/custom-field-mapping.js'; +import { LabelMappingStep } from '../steps/label-mapping.js'; +import { StatusMappingStep } from '../steps/status-mapping.js'; +import type { ProviderWizardDefinition, ProviderWizardStepProps } from '../types.js'; +import { TrelloOAuthStep } from './oauth-step.js'; +import { TrelloWebhookAdapter } from './webhook-step.js'; + +// CASCADE stage keys that map to Trello lists (one list per stage). +const TRELLO_LIST_SLOTS = [ + { key: 'backlog', label: 'Backlog' }, + { key: 'splitting', label: 'Splitting' }, + { key: 'planning', label: 'Planning' }, + { key: 'todo', label: 'Todo' }, + { key: 'inProgress', label: 'In Progress' }, + { key: 'inReview', label: 'In Review' }, + { key: 'done', label: 'Done' }, + { key: 'merged', label: 'Merged' }, + { key: 'debug', label: 'Debug' }, +] as const; + +// CASCADE labels that map to Trello labels. Defaults (name + color) +// pre-populate the shared `label-mapping` Create affordance and thread +// the color to `onCreateLabel(slot, name, color)`. Lives here after +// plan 011/5 deleted the legacy `pm-wizard-trello-steps.tsx` file. +const TRELLO_LABEL_DEFAULTS: Readonly< + Record +> = { + readyToProcess: { name: 'cascade-ready', color: 'sky' }, + processing: { name: 'cascade-processing', color: 'blue' }, + processed: { name: 'cascade-processed', color: 'green' }, + error: { name: 'cascade-error', color: 'red' }, + auto: { name: 'cascade-auto', color: 'purple' }, +}; + +const TRELLO_LABEL_SLOTS = [ + { key: 'readyToProcess', label: 'Ready to Process' }, + { key: 'processing', label: 'Processing' }, + { key: 'processed', label: 'Processed' }, + { key: 'error', label: 'Error' }, + { key: 'auto', label: 'Auto' }, +] as const; + +// Trello has one known custom-field slot: the cost estimate. +const TRELLO_CUSTOM_FIELD_SLOTS = [{ key: 'cost', label: 'Cost (number)' }] as const; function isCredentialsComplete(state: { trelloApiKey: string; @@ -36,36 +84,187 @@ function isCredentialsComplete(state: { return Boolean(state.trelloApiKey && state.trelloToken && state.verificationResult); } +/** + * The shape returned by `useProviderHooks`. Each step adapter pulls the + * slice it needs from this record. Ports all the mutations + memoized + * callbacks that the legacy adapters consumed. + */ +interface TrelloProviderHooks { + readonly boardOptions: ReadonlyArray<{ + readonly id: string; + readonly name: string; + readonly url?: string; + }>; + readonly boardsLoading: boolean; + readonly boardsError: string | undefined; + readonly onBoardSelect: (boardId: string) => void; + readonly boardDetailsLoading: boolean; + readonly providerStates: ReadonlyArray<{ readonly id: string; readonly name: string }>; + readonly providerLabels: ReadonlyArray<{ + readonly id: string; + readonly name: string; + readonly color?: string; + }>; + readonly providerCustomFields: ReadonlyArray<{ + readonly id: string; + readonly name: string; + readonly type: string; + }>; + readonly onCreateLabel: (slotKey: string, name: string, color?: string) => void; + readonly onCreateMissingLabels: ( + slots: ReadonlyArray<{ slot: string; name: string; color?: string }>, + ) => void; + readonly creatingMissingLabels: boolean; + readonly onCreateCustomField: (slotKey: string, name: string) => void; + readonly webhookUrl: string; + readonly creatingSlot: string | null; + // Plan 012/1 — webhook-step plumbing: programmatic Create + active list + delete. + readonly callbackBaseUrl: string; + readonly activeTrelloWebhooks: ReadonlyArray<{ + readonly id: string; + readonly url: string; + readonly active: boolean; + }>; + readonly webhooksLoading: boolean; + readonly createTrelloWebhook: () => void; + readonly createLoading: boolean; + readonly createError: string | undefined; + readonly deleteTrelloWebhook: (callbackBaseUrl: string) => void; + readonly deleteLoading: boolean; +} + +function asTrelloHooks(providerHooks: Record | undefined): TrelloProviderHooks { + return (providerHooks ?? {}) as unknown as TrelloProviderHooks; +} + +// ── Per-step adapters ──────────────────────────────────────────────── +// +// Each adapter bridges `ProviderWizardStepProps` → the shared step's prop +// contract, pulling Trello-specific state off `providerHooks`. + +function TrelloBoardPickAdapter({ state, providerHooks }: ProviderWizardStepProps): ReactElement { + const h = asTrelloHooks(providerHooks); + return ContainerPickStep({ + step: { kind: 'container-pick', id: 'trello-board' }, + providerId: 'trello', + label: 'Select Board', + options: h.boardOptions, + selectedId: state.trelloBoardId || null, + onSelect: h.onBoardSelect, + loading: h.boardsLoading, + error: h.boardsError, + searchable: true, + }); +} + +function TrelloStatusMappingAdapter({ + state, + dispatch, + providerHooks, +}: ProviderWizardStepProps): ReactElement { + const h = asTrelloHooks(providerHooks); + return StatusMappingStep({ + step: { kind: 'status-mapping', id: 'trello-statuses' }, + providerId: 'trello', + cascadeStatuses: TRELLO_LIST_SLOTS, + providerStates: h.providerStates, + mappings: state.trelloListMappings, + onMappingChange: (key, value) => dispatch({ type: 'SET_TRELLO_LIST_MAPPING', key, value }), + loading: h.boardDetailsLoading, + }); +} + +function TrelloLabelMappingAdapter({ + state, + dispatch, + providerHooks, +}: ProviderWizardStepProps): ReactElement { + const h = asTrelloHooks(providerHooks); + return LabelMappingStep({ + step: { kind: 'label-mapping', id: 'trello-labels' }, + providerId: 'trello', + labelSlots: TRELLO_LABEL_SLOTS, + providerLabels: h.providerLabels, + mappings: state.trelloLabelMappings, + onMappingChange: (key, value) => dispatch({ type: 'SET_TRELLO_LABEL_MAPPING', key, value }), + onCreateLabel: h.onCreateLabel, + onCreateMissingLabels: h.onCreateMissingLabels, + creatingMissing: h.creatingMissingLabels, + labelDefaults: TRELLO_LABEL_DEFAULTS, + loading: h.boardDetailsLoading, + }); +} + +function TrelloCustomFieldMappingAdapter({ + state, + dispatch, + providerHooks, +}: ProviderWizardStepProps): ReactElement { + const h = asTrelloHooks(providerHooks); + return CustomFieldMappingStep({ + step: { kind: 'custom-field-mapping', id: 'trello-custom-fields' }, + providerId: 'trello', + cascadeSlots: TRELLO_CUSTOM_FIELD_SLOTS, + providerCustomFields: h.providerCustomFields, + mappings: { cost: state.trelloCostFieldId || undefined }, + onMappingChange: (key, value) => { + if (key === 'cost') dispatch({ type: 'SET_TRELLO_COST_FIELD', id: value }); + }, + onCreateCustomField: h.onCreateCustomField, + fieldDefaults: { cost: { name: 'Cost' } }, + loading: h.boardDetailsLoading, + }); +} + +// Plan 012/1: the trello-webhook step's Component is now `TrelloWebhookAdapter` +// (imported from `./webhook-step.js`), a Fragment composing the shared +// WebhookUrlDisplayStep with Trello-specific UX: active-webhooks list, +// programmatic Create button, delete buttons, curl fallback. + export const trelloProviderWizard: ProviderWizardDefinition = { id: 'trello', label: 'Trello', + // Each step mirrors `trelloManifest.wizardSpec.steps` by id. steps: [ { - id: 'credentials', + id: 'trello-credentials-oauth', title: 'Trello credentials', - Component: TrelloCredentialsStepAdapter, + Component: TrelloOAuthStep, isComplete: isCredentialsComplete, }, { - id: 'board', + id: 'trello-board', title: 'Board', - Component: TrelloBoardStepAdapter, + Component: TrelloBoardPickAdapter, isComplete: (state) => Boolean(state.trelloBoardId), }, { - id: 'fields', - title: 'Field mappings', - Component: TrelloFieldMappingStepAdapter, + id: 'trello-statuses', + title: 'Status mapping', + Component: TrelloStatusMappingAdapter, isComplete: (state) => Object.keys(state.trelloListMappings).length > 0, }, + { + id: 'trello-labels', + title: 'Label mapping', + Component: TrelloLabelMappingAdapter, + isComplete: () => true, // labels are optional + }, + { + id: 'trello-custom-fields', + title: 'Custom fields', + Component: TrelloCustomFieldMappingAdapter, + isComplete: () => true, // cost field is optional + }, + { + id: 'trello-webhook', + title: 'Webhook', + Component: TrelloWebhookAdapter, + isComplete: () => true, + }, ], - // Shape mirrors the existing inline save body in `useSaveMutation` - // (pm-wizard-hooks.ts). `saveMutation` still constructs the same shape - // directly while the parent wizard owns the save flow; plan 006/5 will - // consolidate save onto `def.buildIntegrationConfig` and remove the - // per-provider if/else in `saveMutation`. buildIntegrationConfig: (state) => ({ boardId: state.trelloBoardId, lists: state.trelloListMappings, @@ -80,62 +279,117 @@ export const trelloProviderWizard: ProviderWizardDefinition = { }, useProviderHooks: ({ state, dispatch, projectId, advanceToStep }) => { - // Parent wizard previously called these at the top level; moved here so - // pm-wizard.tsx no longer contains Trello-specific hook wiring. const discovery = useTrelloDiscovery(state, dispatch, advanceToStep, projectId ?? ''); - const labels = useTrelloLabelCreation(state, dispatch); - const customField = useTrelloCustomFieldCreation(state, dispatch); + const labels = useTrelloLabelCreation(state, dispatch, projectId ?? ''); + const customField = useTrelloCustomFieldCreation(state, dispatch, projectId ?? ''); + const queryClient = useQueryClient(); - // creatingSlot + creatingCostField are shared setter state between parent - // components. For the manifest path we recreate them here; the Trello - // wizard UI only renders while the manifest shell is mounted. const [creatingSlot, setCreatingSlot] = useState(null); - const [creatingCostField, setCreatingCostField] = useState(false); - const onCreateLabel = (slot: string) => { - const defaults = TRELLO_LABEL_DEFAULTS[slot]; - if (!defaults) return; + const onCreateLabel = (slot: string, name: string, color?: string) => { + // If caller didn't supply a color, fall back to the canonical default. + const resolvedColor = color ?? TRELLO_LABEL_DEFAULTS[slot]?.color ?? 'sky'; setCreatingSlot(slot); labels.createLabelMutation.mutate( - { name: defaults.name, color: defaults.color, slot }, + { name, color: resolvedColor, slot }, { onSettled: () => setCreatingSlot(null) }, ); }; - const onCreateAllMissingLabels = () => { - const existingLabelNames = new Set( - (state.trelloBoardDetails?.labels ?? []).map((l) => l.name.toLowerCase()), - ); - const labelsToCreate = Object.entries(TRELLO_LABEL_DEFAULTS) - .filter(([slot, { name }]) => { - if (state.trelloLabelMappings[slot]) return false; - return !existingLabelNames.has(name.toLowerCase()); - }) - .map(([slot, { name, color }]) => ({ slot, name, color })); - if (labelsToCreate.length > 0) { - setCreatingSlot('__batch__'); - labels.createMissingLabelsMutation.mutate(labelsToCreate, { - onSettled: () => setCreatingSlot(null), - }); - } + const onCreateMissingLabels = ( + slots: ReadonlyArray<{ slot: string; name: string; color?: string }>, + ) => { + const resolved = slots.map((s) => ({ + slot: s.slot, + name: s.name, + color: s.color ?? TRELLO_LABEL_DEFAULTS[s.slot]?.color ?? 'sky', + })); + labels.createMissingLabelsMutation.mutate(resolved); }; - const onCreateCostField = () => { - setCreatingCostField(true); - customField.createCustomFieldMutation.mutate(undefined, { - onSettled: () => setCreatingCostField(false), - }); + const onCreateCustomField = (_slotKey: string, name: string) => { + customField.createCustomFieldMutation.mutate({ name }); }; + const webhookUrl = projectId ? `${window.location.origin}/webhooks/${projectId}/trello` : ''; + + // Plan 012/1 — webhook plumbing. Mirrors the legacy `useWebhookManagement` + // formula (plan 012/4 deletes that hook). Computes the public router URL + // from the Vite env (dev) or current origin (prod), fetches active + // webhooks via `trpc.webhooks.list`, wraps create/delete mutations with + // `trelloOnly: true`. + const callbackBaseUrl = + API_URL || + (typeof window !== 'undefined' ? window.location.origin.replace(':5173', ':3000') : ''); + + const webhooksQuery = useQuery(trpc.webhooks.list.queryOptions({ projectId: projectId ?? '' })); + const activeTrelloWebhooks = deriveActiveWebhooks('trello', webhooksQuery.data) as Array<{ + id: string; + url: string; + active: boolean; + }>; + + const createWebhookMutation = useMutation({ + mutationFn: () => + trpcClient.webhooks.create.mutate({ + projectId: projectId ?? '', + callbackBaseUrl, + trelloOnly: true, + }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: trpc.webhooks.list.queryOptions({ projectId: projectId ?? '' }).queryKey, + }); + }, + }); + + const deleteWebhookMutation = useMutation({ + mutationFn: (deleteBaseUrl: string) => + trpcClient.webhooks.delete.mutate({ + projectId: projectId ?? '', + callbackBaseUrl: deleteBaseUrl, + trelloOnly: true, + }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: trpc.webhooks.list.queryOptions({ projectId: projectId ?? '' }).queryKey, + }); + }, + }); + + const boardDetails = state.trelloBoardDetails; + return { + boardOptions: state.trelloBoards, + boardsLoading: discovery.boardsMutation.isPending, + boardsError: discovery.boardsMutation.isError + ? (discovery.boardsMutation.error as Error).message + : undefined, onBoardSelect: discovery.handleBoardSelect, - boardsMutation: discovery.boardsMutation, - boardDetailsMutation: discovery.boardDetailsMutation, + boardDetailsLoading: discovery.boardDetailsMutation.isPending, + providerStates: boardDetails?.lists ?? [], + providerLabels: boardDetails?.labels ?? [], + providerCustomFields: boardDetails?.customFields.filter((f) => f.type === 'number') ?? [], onCreateLabel, - onCreateAllMissingLabels, - onCreateCostField, + onCreateMissingLabels, + creatingMissingLabels: labels.createMissingLabelsMutation.isPending, + onCreateCustomField, + webhookUrl, creatingSlot, - creatingCostField, - }; + // Exposed for any caller that wants to render a secondary + // loading indicator near the board-picker step. + boardDetailsLoadingIcon: Loader2, + // Plan 012/1 — webhook plumbing consumed by `TrelloWebhookAdapter`. + callbackBaseUrl, + activeTrelloWebhooks, + webhooksLoading: webhooksQuery.isLoading, + createTrelloWebhook: () => createWebhookMutation.mutate(), + createLoading: createWebhookMutation.isPending, + createError: createWebhookMutation.isError + ? (createWebhookMutation.error as Error).message + : undefined, + deleteTrelloWebhook: (baseUrl: string) => deleteWebhookMutation.mutate(baseUrl), + deleteLoading: deleteWebhookMutation.isPending, + } satisfies TrelloProviderHooks & Record; }, }; diff --git a/web/src/components/projects/pm-wizard-common-steps.tsx b/web/src/components/projects/pm-wizard-common-steps.tsx index 428df778..ce83b140 100644 --- a/web/src/components/projects/pm-wizard-common-steps.tsx +++ b/web/src/components/projects/pm-wizard-common-steps.tsx @@ -1,369 +1,16 @@ /** - * Provider-agnostic step renderer components for PMWizard: - * WebhookStep and SaveStep. + * Provider-agnostic step renderer components for PMWizard. + * + * **Plan 012/4 (2026-04-18+):** `WebhookStep` + `LinearWebhookInfoPanel` + * deleted. Every PM provider now owns its webhook step via the manifest + * path (see `./pm-providers//webhook-step.tsx`). Only + * `SaveStep` remains in this file — it's the one cross-provider step + * that doesn't fit the per-provider-step model (operates on the + * `saveMutation` from the parent wizard). */ import type { UseMutationResult } from '@tanstack/react-query'; -import { - AlertCircle, - AlertTriangle, - Check, - Clipboard, - ExternalLink, - Info, - Loader2, - RefreshCw, - Trash2, -} from 'lucide-react'; -import { useState } from 'react'; -import { Label } from '@/components/ui/label.js'; import type { WizardState } from './pm-wizard-state.js'; -import { type ProjectCredentialMeta, ProjectSecretField } from './project-secret-field.js'; - -// ============================================================================ -// WebhookStep -// ============================================================================ - -interface ActiveWebhook { - id: string; - url: string; - active: boolean; -} - -interface WebhooksQueryProps { - isLoading: boolean; - data?: { - errors?: Record; - }; - refetch: () => void; -} - -function CopyButton({ text }: { text: string }) { - const [copied, setCopied] = useState(false); - const handleCopy = async () => { - await navigator.clipboard.writeText(text); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - return ( - - ); -} - -// ============================================================================ -// LinearWebhookInfoPanel -// ============================================================================ - -export function LinearWebhookInfoPanel({ - webhookUrl, - projectId, - webhookSecretCredential, -}: { - webhookUrl: string; - projectId: string; - webhookSecretCredential?: ProjectCredentialMeta; -}) { - return ( -
-
-
- -
-

- Manual Webhook Setup Required -

-

- Linear webhooks must be configured manually in your Linear team settings. CASCADE - cannot create them programmatically. -

-
-
-
- -
- -
- {webhookUrl} - -
-
- - - -
-

Setup instructions:

-
    -
  1. - Go to{' '} - - linear.app/settings/api - {' '} - and navigate to Webhooks -
  2. -
  3. Click "New webhook" and enter the URL above
  4. -
  5. - Enable these events (each maps to a CASCADE trigger handler): -
      -
    • - Issues — status transitions drive CASCADE's splitting, - planning, and implementation agents -
    • -
    • - Comments — @mentions of the CASCADE bot trigger a response agent -
    • -
    • - Issue Labels — adding the "Ready to Process" label starts - an agent on the issue -
    • -
    -
  6. -
  7. Select your team and save — webhooks are team-scoped in Linear
  8. -
  9. - If you set a signing secret in Linear, paste it into the field above so CASCADE can - verify webhook authenticity -
  10. -
-
- -

- If you also set a Linear project scope in the Board / Project Selection - step, CASCADE applies that filter on its side after receiving each webhook — your Linear - webhook configuration stays team-scoped and unchanged. -

-
- ); -} - -// ============================================================================ -// WebhookStep -// ============================================================================ - -export function WebhookStep({ - state, - webhooksQuery, - activeWebhooks, - callbackBaseUrl, - createWebhookMutation, - deleteWebhookMutation, - linearWebhookUrl, - projectId, - linearWebhookSecretCredential, -}: { - state: WizardState; - webhooksQuery: WebhooksQueryProps; - activeWebhooks: ActiveWebhook[]; - callbackBaseUrl: string; - createWebhookMutation: UseMutationResult; - deleteWebhookMutation: UseMutationResult; - linearWebhookUrl?: string; - projectId: string; - linearWebhookSecretCredential?: ProjectCredentialMeta; -}) { - // Linear uses a display-only panel — no create/delete buttons - if (state.provider === 'linear') { - return ( - - ); - } - - const isTrello = state.provider === 'trello'; - const providerName = isTrello ? 'Trello' : 'JIRA'; - - // Build curl commands for manual webhook creation - const buildTrelloCurl = () => { - const boardId = state.trelloBoardId || ''; - const callbackUrl = callbackBaseUrl - ? `${callbackBaseUrl}/trello/webhook` - : '/trello/webhook'; - return `curl -X POST "https://api.trello.com/1/webhooks" \\ - -H "Content-Type: application/json" \\ - -d '{ - "key": "", - "token": "", - "callbackURL": "${callbackUrl}", - "idModel": "${boardId}", - "description": "CASCADE webhook" - }'`; - }; - - const buildJiraCurl = () => { - const baseUrl = state.jiraBaseUrl || ''; - const callbackUrl = callbackBaseUrl - ? `${callbackBaseUrl}/jira/webhook` - : '/jira/webhook'; - return `curl -X POST "${baseUrl}/rest/webhooks/1.0/webhook" \\ - -H "Content-Type: application/json" \\ - -u ":" \\ - -d '{ - "name": "CASCADE webhook", - "url": "${callbackUrl}", - "events": ["jira:issue_updated", "jira:issue_created"], - "filters": {}, - "excludeBody": false - }'`; - }; - - const curlCommand = isTrello ? buildTrelloCurl() : buildJiraCurl(); - - return ( -
- {/* Per-provider errors */} - {webhooksQuery.data?.errors && - Object.entries(webhooksQuery.data.errors) - .filter(([provider, err]) => err != null && provider !== 'github') - .map(([provider, err]) => ( -
- -
- - {provider} - - : {String(err)} -
- -
- ))} - - {webhooksQuery.isLoading ? ( -
- Loading webhooks... -
- ) : activeWebhooks.length > 0 ? ( -
- - {activeWebhooks.map((w) => ( -
-
- - {w.url} -
- -
- ))} -
- ) : ( -
- - No {providerName} webhooks configured for this project. -
- )} - - {/* curl instructions for manual webhook creation (collapsible) */} -
- - -

- Manual webhook creation (alternative: if the button below doesn't work) -

-
-
-

- Use the following curl command to create the {providerName} webhook manually with your - own credentials: -

-
-
- -
-
-							{curlCommand}
-						
-
-
-
- -
-
- -
- {createWebhookMutation.isError && ( -

- {(createWebhookMutation.error as Error).message} -

- )} - {createWebhookMutation.isSuccess && ( -

- {webhooksQuery.data?.errors && - Object.entries(webhooksQuery.data.errors) - .filter(([provider]) => provider !== 'github') - .some(([, e]) => e != null) - ? 'Webhook created, but some providers failed to load — see warnings above.' - : 'Webhook created successfully.'} -

- )} -
-
- ); -} - -// ============================================================================ -// SaveStep -// ============================================================================ export function SaveStep({ state, diff --git a/web/src/components/projects/pm-wizard-hooks.ts b/web/src/components/projects/pm-wizard-hooks.ts index e5ab510f..00a5c061 100644 --- a/web/src/components/projects/pm-wizard-hooks.ts +++ b/web/src/components/projects/pm-wizard-hooks.ts @@ -5,7 +5,6 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useEffect } from 'react'; -import { API_URL } from '@/lib/api.js'; import { trpc, trpcClient } from '@/lib/trpc.js'; import { getCredentialRoles } from '../../../../src/config/integrationRoles.js'; import type { @@ -15,7 +14,7 @@ import type { WizardAction, WizardState, } from './pm-wizard-state.js'; -import { buildLinearIntegrationConfig } from './pm-wizard-state.js'; +import { buildLinearIntegrationConfig, shouldUseStoredCredentials } from './pm-wizard-state.js'; // ============================================================================ // Trello Discovery @@ -28,19 +27,34 @@ export function useTrelloDiscovery( projectId: string, ) { const boardsMutation = useMutation({ - mutationFn: () => { + mutationFn: async () => { + // Plan 010/2: routes through generic pm.discovery.discover. + // In edit mode with stored credentials, pass projectId — the + // endpoint resolves credentials from project_credentials. + // Otherwise pass raw credentials from wizard state. if (state.isEditing && state.hasStoredCredentials && !state.trelloApiKey) { - return trpcClient.integrationsDiscovery.trelloBoardsByProject.mutate({ projectId }); + return (await trpcClient.pm.discovery.discover.mutate({ + providerId: 'trello', + capability: 'boards', + args: {}, + projectId, + })) as Array<{ id: string; name: string; url?: string }>; } if (!state.trelloApiKey || !state.trelloToken) { throw new Error('Enter both credentials before fetching boards'); } - return trpcClient.integrationsDiscovery.trelloBoards.mutate({ - apiKey: state.trelloApiKey, - token: state.trelloToken, - }); + return (await trpcClient.pm.discovery.discover.mutate({ + providerId: 'trello', + capability: 'boards', + args: {}, + credentials: { api_key: state.trelloApiKey, token: state.trelloToken }, + })) as Array<{ id: string; name: string; url?: string }>; }, - onSuccess: (boards) => dispatch({ type: 'SET_TRELLO_BOARDS', boards }), + onSuccess: (boards) => + dispatch({ + type: 'SET_TRELLO_BOARDS', + boards: boards.map((b) => ({ ...b, url: b.url ?? '' })), + }), }); const boardDetailsMutation = useMutation({ @@ -116,18 +130,33 @@ export function useJiraDiscovery( projectId: string, ) { const jiraProjectsMutation = useMutation({ - mutationFn: () => { + mutationFn: async () => { + // Plan 010/2: routes through generic pm.discovery.discover. if (state.isEditing && state.hasStoredCredentials && !state.jiraEmail) { - return trpcClient.integrationsDiscovery.jiraProjectsByProject.mutate({ projectId }); + const projects = (await trpcClient.pm.discovery.discover.mutate({ + providerId: 'jira', + capability: 'projects', + args: {}, + projectId, + })) as Array<{ id: string; name: string }>; + // Legacy shape has `key` — pm.discover returns `id` containing + // the JIRA key. Normalize for downstream consumers. + return projects.map((p) => ({ key: p.id, name: p.name })); } if (!state.jiraEmail || !state.jiraApiToken) { throw new Error('Enter both credentials before fetching projects'); } - return trpcClient.integrationsDiscovery.jiraProjects.mutate({ - email: state.jiraEmail, - apiToken: state.jiraApiToken, - baseUrl: state.jiraBaseUrl, - }); + const projects = (await trpcClient.pm.discovery.discover.mutate({ + providerId: 'jira', + capability: 'projects', + args: {}, + credentials: { + email: state.jiraEmail, + api_token: state.jiraApiToken, + base_url: state.jiraBaseUrl, + }, + })) as Array<{ id: string; name: string }>; + return projects.map((p) => ({ key: p.id, name: p.name })); }, onSuccess: (projects) => dispatch({ type: 'SET_JIRA_PROJECTS', projects }), }); @@ -206,16 +235,25 @@ export function useLinearDiscovery( projectId: string, ) { const linearTeamsMutation = useMutation({ - mutationFn: () => { + mutationFn: async () => { + // Plan 010/2: routes through generic pm.discovery.discover. if (state.isEditing && state.hasStoredCredentials && !state.linearApiKey) { - return trpcClient.integrationsDiscovery.linearTeamsByProject.mutate({ projectId }); + return (await trpcClient.pm.discovery.discover.mutate({ + providerId: 'linear', + capability: 'teams', + args: {}, + projectId, + })) as Array<{ id: string; name: string }>; } if (!state.linearApiKey) { throw new Error('Enter your API key before fetching teams'); } - return trpcClient.integrationsDiscovery.linearTeams.mutate({ - apiKey: state.linearApiKey, - }); + return (await trpcClient.pm.discovery.discover.mutate({ + providerId: 'linear', + capability: 'teams', + args: {}, + credentials: { api_key: state.linearApiKey }, + })) as Array<{ id: string; name: string }>; }, onSuccess: (teams) => dispatch({ @@ -250,20 +288,25 @@ export function useLinearDiscovery( }); const linearProjectsMutation = useMutation({ - mutationFn: (teamId: string) => { + mutationFn: async (teamId: string) => { + // Plan 010/2: routes through generic pm.discovery.discover. if (state.isEditing && state.hasStoredCredentials && !state.linearApiKey) { - return trpcClient.integrationsDiscovery.linearProjectsByProject.mutate({ + return (await trpcClient.pm.discovery.discover.mutate({ + providerId: 'linear', + capability: 'projects', + args: { containerId: teamId }, projectId, - teamId, - }); + })) as Array<{ id: string; name: string }>; } if (!state.linearApiKey) { throw new Error('Enter your API key before fetching projects'); } - return trpcClient.integrationsDiscovery.linearProjects.mutate({ - apiKey: state.linearApiKey, - teamId, - }); + return (await trpcClient.pm.discovery.discover.mutate({ + providerId: 'linear', + capability: 'projects', + args: { containerId: teamId }, + credentials: { api_key: state.linearApiKey }, + })) as Array<{ id: string; name: string }>; }, onSuccess: (projects) => dispatch({ @@ -324,71 +367,84 @@ export function useLinearDiscovery( // Verification // ============================================================================ +/** + * Build the `{ projectId }` or `{ credentials: ... }` portion of a tRPC + * request, picking the stored-creds path when the user is editing an + * existing integration and hasn't re-typed the key. Extracted so the + * `verifyMutation` body stays below the cognitive-complexity threshold. + */ +function buildVerifyAuthArg( + state: WizardState, + projectId: string, +): { projectId: string } | { credentials: Record } { + if (shouldUseStoredCredentials(state)) { + return { projectId }; + } + if (state.provider === 'trello') { + if (!state.trelloApiKey || !state.trelloToken) { + throw new Error('Enter both credentials before verifying'); + } + return { credentials: { api_key: state.trelloApiKey, token: state.trelloToken } }; + } + if (state.provider === 'linear') { + if (!state.linearApiKey) { + throw new Error('Enter your API key before verifying'); + } + return { credentials: { api_key: state.linearApiKey } }; + } + if (!state.jiraEmail || !state.jiraApiToken) { + throw new Error('Enter both credentials before verifying'); + } + return { + credentials: { + email: state.jiraEmail, + api_token: state.jiraApiToken, + base_url: state.jiraBaseUrl, + }, + }; +} + export function useVerification( state: WizardState, dispatch: React.Dispatch, advanceToStep: (step: number) => void, + projectId: string, ) { const verifyMutation = useMutation({ mutationFn: async () => { - // Plan 009/5 migrated verification from provider-specific - // verifyTrello / verifyJira / verifyLinear procedures to the - // generic pm.discover endpoint. The side effect of a successful - // discover call is that credentials are authenticated by the - // provider — we use the discovered-container count as the - // user-facing "verified" signal (simpler than the former - // username display, but unambiguous). + // Plan 010/2: restore the pre-009/5 "Verified as @username" UX. + // Calls the `currentUser` discovery capability; every provider + // maps its native `getMe()` response to `{ id, name, displayName? }`. + // + // Edit-mode fallback: `buildVerifyAuthArg` returns `{ projectId }` + // when the user is editing with stored credentials but an empty + // API-key field, so the backend resolves the stored secret via + // `resolvePMCredentials` instead of requiring re-entry. const provider = state.provider; - if (provider === 'trello') { - if (!state.trelloApiKey || !state.trelloToken) { - throw new Error('Enter both credentials before verifying'); - } - const boards = (await trpcClient.pm.discovery.discover.mutate({ - providerId: 'trello', - capability: 'boards', - args: {}, - credentials: { - api_key: state.trelloApiKey, - token: state.trelloToken, - }, - })) as Array<{ id: string; name: string }>; - return { provider: 'trello' as const, count: boards.length }; - } - if (provider === 'linear') { - if (!state.linearApiKey) { - throw new Error('Enter your API key before verifying'); - } - const teams = (await trpcClient.pm.discovery.discover.mutate({ - providerId: 'linear', - capability: 'teams', - args: {}, - credentials: { api_key: state.linearApiKey }, - })) as Array<{ id: string; name: string }>; - return { provider: 'linear' as const, count: teams.length }; - } - if (!state.jiraEmail || !state.jiraApiToken) { - throw new Error('Enter both credentials before verifying'); - } - const projects = (await trpcClient.pm.discovery.discover.mutate({ - providerId: 'jira', - capability: 'projects', + const authArg = buildVerifyAuthArg(state, projectId); + const me = (await trpcClient.pm.discovery.discover.mutate({ + providerId: provider, + capability: 'currentUser', args: {}, - credentials: { - email: state.jiraEmail, - api_token: state.jiraApiToken, - base_url: state.jiraBaseUrl, - }, - })) as Array<{ id: string; name: string }>; - return { provider: 'jira' as const, count: projects.length }; + ...authArg, + })) as { id: string; name: string; displayName?: string }; + return { provider, me }; }, - onSuccess: ({ provider, count }) => { + onSuccess: ({ provider, me }) => { // Ignore if provider changed while we were verifying if (provider !== state.provider) return; - const containerLabel = - provider === 'trello' ? 'board' : provider === 'linear' ? 'team' : 'project'; - const display = `Credentials verified — found ${count} ${containerLabel}${ - count === 1 ? '' : 's' - }`; + // Per-provider display formatting mirrors the pre-009/5 UX: + // Trello: "@{username} ({fullName})" — displayName is username + // JIRA: "{displayName} ({email})" — displayName is email + // Linear: "{displayName || name}" — displayName is the preferred handle + let display: string; + if (provider === 'trello') { + display = me.displayName ? `@${me.displayName} (${me.name})` : me.name; + } else if (provider === 'jira') { + display = me.displayName ? `${me.name} (${me.displayName})` : me.name; + } else { + display = me.displayName || me.name; + } dispatch({ type: 'SET_VERIFICATION', result: { provider, display }, @@ -407,85 +463,74 @@ export function useVerification( return { verifyMutation }; } -// ============================================================================ -// Webhook Management -// ============================================================================ - -export function useWebhookManagement(projectId: string, state: WizardState) { - const queryClient = useQueryClient(); - const callbackBaseUrl = - API_URL || - (typeof window !== 'undefined' ? window.location.origin.replace(':5173', ':3000') : ''); +// Plan 012/4: `useWebhookManagement` + `useLinearWebhookInfo` deleted. +// Each provider's `useProviderHooks` now inlines the webhook plumbing +// (`webhooks.list/create/delete` + `callbackBaseUrl` formula) — +// see `./pm-providers/{trello,jira,linear}/wizard.ts`. - const createWebhookMutation = useMutation({ - mutationFn: () => - trpcClient.webhooks.create.mutate({ - projectId, - callbackBaseUrl, - trelloOnly: state.provider === 'trello' ? true : undefined, - jiraOnly: state.provider === 'jira' ? true : undefined, - }), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: trpc.webhooks.list.queryOptions({ projectId }).queryKey, - }); - }, - }); - - const deleteWebhookMutation = useMutation({ - mutationFn: (deleteCallbackBaseUrl: string) => - trpcClient.webhooks.delete.mutate({ - projectId, - callbackBaseUrl: deleteCallbackBaseUrl, - trelloOnly: state.provider === 'trello' ? true : undefined, - jiraOnly: state.provider === 'jira' ? true : undefined, - }), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: trpc.webhooks.list.queryOptions({ projectId }).queryKey, +/** + * Iterate `labelsToCreate` through `pm.discovery.createLabel`, collecting + * successes + per-name errors. Factored out so the two + * `createMissingLabelsMutation` bodies (Trello + Linear) stay below the + * biome cognitive-complexity threshold. + */ +async function runPerLabelCreations(opts: { + labelsToCreate: Array<{ slot: string; name: string; color?: string }>; + providerId: 'trello' | 'linear'; + containerId: string; + authArg: { projectId: string } | { credentials: Record }; +}): Promise<{ + successes: Array<{ id: string; name: string; color: string }>; + errors: Array<{ name: string; error: string }>; +}> { + const successes: Array<{ id: string; name: string; color: string }> = []; + const errors: Array<{ name: string; error: string }> = []; + for (const { name, color } of opts.labelsToCreate) { + try { + const label = await trpcClient.pm.discovery.createLabel.mutate({ + providerId: opts.providerId, + containerId: opts.containerId, + name, + color, + ...opts.authArg, }); - }, - }); - - return { - callbackBaseUrl, - createWebhookMutation, - deleteWebhookMutation, - }; -} - -// ============================================================================ -// Linear Webhook Info (display-only) -// ============================================================================ - -export function useLinearWebhookInfo() { - const callbackBaseUrl = - API_URL || - (typeof window !== 'undefined' ? window.location.origin.replace(':5173', ':3000') : ''); - - const webhookUrl = callbackBaseUrl - ? `${callbackBaseUrl}/linear/webhook` - : '/linear/webhook'; - - return { webhookUrl }; + successes.push(label); + } catch (err) { + errors.push({ name, error: err instanceof Error ? err.message : String(err) }); + } + } + return { successes, errors }; } // ============================================================================ // Trello Label Creation // ============================================================================ -export function useTrelloLabelCreation(state: WizardState, dispatch: React.Dispatch) { +export function useTrelloLabelCreation( + state: WizardState, + dispatch: React.Dispatch, + projectId: string, +) { const createLabelMutation = useMutation({ mutationFn: (vars: { name: string; color?: string; slot: string }) => { - if (!state.trelloApiKey || !state.trelloToken || !state.trelloBoardId) { - throw new Error('Missing credentials or board selection'); - } - return trpcClient.integrationsDiscovery.createTrelloLabel.mutate({ - apiKey: state.trelloApiKey, - token: state.trelloToken, - boardId: state.trelloBoardId, + if (!state.trelloBoardId) { + throw new Error('Board must be selected before creating a label'); + } + const useStored = shouldUseStoredCredentials(state); + if (!useStored && (!state.trelloApiKey || !state.trelloToken)) { + throw new Error('Missing credentials — enter them on the credentials step'); + } + // Plan 010/1: routes through generic pm.discovery.createLabel. + // Edit mode with stored creds → projectId path (see + // `shouldUseStoredCredentials` in pm-wizard-state.ts). + return trpcClient.pm.discovery.createLabel.mutate({ + providerId: 'trello', + containerId: state.trelloBoardId, name: vars.name, color: vars.color, + ...(useStored + ? { projectId } + : { credentials: { api_key: state.trelloApiKey, token: state.trelloToken } }), }); }, onSuccess: (label, vars) => { @@ -499,15 +544,22 @@ export function useTrelloLabelCreation(state: WizardState, dispatch: React.Dispa }); const createMissingLabelsMutation = useMutation({ - mutationFn: (labelsToCreate: Array<{ slot: string; name: string; color?: string }>) => { - if (!state.trelloApiKey || !state.trelloToken || !state.trelloBoardId) { - throw new Error('Missing credentials or board selection'); - } - return trpcClient.integrationsDiscovery.createTrelloLabels.mutate({ - apiKey: state.trelloApiKey, - token: state.trelloToken, - boardId: state.trelloBoardId, - labels: labelsToCreate.map(({ name, color }) => ({ name, color })), + mutationFn: async (labelsToCreate: Array<{ slot: string; name: string; color?: string }>) => { + if (!state.trelloBoardId) { + throw new Error('Board must be selected before creating labels'); + } + const useStored = shouldUseStoredCredentials(state); + if (!useStored && (!state.trelloApiKey || !state.trelloToken)) { + throw new Error('Missing credentials — enter them on the credentials step'); + } + const authArg = useStored + ? { projectId } + : { credentials: { api_key: state.trelloApiKey, token: state.trelloToken } }; + return runPerLabelCreations({ + labelsToCreate, + providerId: 'trello', + containerId: state.trelloBoardId, + authArg, }); }, onSuccess: (result, labelsToCreate) => { @@ -546,18 +598,33 @@ export function useTrelloLabelCreation(state: WizardState, dispatch: React.Dispa export function useTrelloCustomFieldCreation( state: WizardState, dispatch: React.Dispatch, + projectId: string, ) { const createCustomFieldMutation = useMutation({ - mutationFn: () => { - if (!state.trelloApiKey || !state.trelloToken || !state.trelloBoardId) { - throw new Error('Missing credentials or board selection'); - } - return trpcClient.integrationsDiscovery.createTrelloCustomField.mutate({ - apiKey: state.trelloApiKey, - token: state.trelloToken, - boardId: state.trelloBoardId, - name: 'Cost', - type: 'number', + // Plan 011/2: the shared custom-field-mapping step lets operators type + // a name. `mutate({ name })` — callers without a preference pass + // `{ name: 'Cost' }` to preserve the legacy default. + mutationFn: ({ name }: { name: string }) => { + if (!state.trelloBoardId) { + throw new Error('Board must be selected before creating a custom field'); + } + const useStored = shouldUseStoredCredentials(state); + if (!useStored && (!state.trelloApiKey || !state.trelloToken)) { + throw new Error('Missing credentials — enter them on the credentials step'); + } + // Plan 010/1 (leftover caller): routes through pm.discovery.createCustomField. + return trpcClient.pm.discovery.createCustomField.mutate({ + providerId: 'trello', + containerId: state.trelloBoardId, + name, + ...(useStored + ? { projectId } + : { + credentials: { + api_key: state.trelloApiKey, + token: state.trelloToken, + }, + }), }); }, onSuccess: (customField) => { @@ -587,17 +654,32 @@ export function useTrelloCustomFieldCreation( export function useJiraCustomFieldCreation( state: WizardState, dispatch: React.Dispatch, + projectId: string, ) { const createJiraCustomFieldMutation = useMutation({ - mutationFn: () => { - if (!state.jiraEmail || !state.jiraApiToken || !state.jiraBaseUrl) { + // Plan 011/3: the shared custom-field-mapping step lets operators type + // a name; callers without a preference pass `{ name: 'Cost' }`. + mutationFn: ({ name }: { name: string }) => { + const useStored = shouldUseStoredCredentials(state); + if (!useStored && (!state.jiraEmail || !state.jiraApiToken || !state.jiraBaseUrl)) { throw new Error('Missing JIRA credentials or base URL'); } - return trpcClient.integrationsDiscovery.createJiraCustomField.mutate({ - email: state.jiraEmail, - apiToken: state.jiraApiToken, - baseUrl: state.jiraBaseUrl, - name: 'Cost', + // Plan 010/1: routes through generic pm.discovery.createCustomField. + // JIRA's project key isn't needed for the mutation (fields are global) + // but we pass the configured projectKey as containerId for uniform shape. + return trpcClient.pm.discovery.createCustomField.mutate({ + providerId: 'jira', + containerId: state.jiraProjectKey || 'global', + name, + ...(useStored + ? { projectId } + : { + credentials: { + email: state.jiraEmail, + api_token: state.jiraApiToken, + base_url: state.jiraBaseUrl, + }, + }), }); }, onSuccess: (field) => { @@ -750,17 +832,27 @@ export function useSaveMutation(projectId: string, state: WizardState) { // Linear Label Creation // ============================================================================ -export function useLinearLabelCreation(state: WizardState, dispatch: React.Dispatch) { +export function useLinearLabelCreation( + state: WizardState, + dispatch: React.Dispatch, + projectId: string, +) { const createLabelMutation = useMutation({ mutationFn: (vars: { name: string; color?: string; slot: string }) => { - if (!state.linearApiKey || !state.linearTeamId) { - throw new Error('Missing credentials or team selection'); + if (!state.linearTeamId) { + throw new Error('Team must be selected before creating a label'); } - return trpcClient.integrationsDiscovery.createLinearLabel.mutate({ - apiKey: state.linearApiKey, - teamId: state.linearTeamId, + const useStored = shouldUseStoredCredentials(state); + if (!useStored && !state.linearApiKey) { + throw new Error('Missing credentials — enter them on the credentials step'); + } + // Plan 010/1: routes through generic pm.discovery.createLabel. + return trpcClient.pm.discovery.createLabel.mutate({ + providerId: 'linear', + containerId: state.linearTeamId, name: vars.name, color: vars.color, + ...(useStored ? { projectId } : { credentials: { api_key: state.linearApiKey } }), }); }, onSuccess: (label, vars) => { @@ -774,14 +866,20 @@ export function useLinearLabelCreation(state: WizardState, dispatch: React.Dispa }); const createMissingLabelsMutation = useMutation({ - mutationFn: (labelsToCreate: Array<{ slot: string; name: string; color?: string }>) => { - if (!state.linearApiKey || !state.linearTeamId) { - throw new Error('Missing credentials or team selection'); - } - return trpcClient.integrationsDiscovery.createLinearLabels.mutate({ - apiKey: state.linearApiKey, - teamId: state.linearTeamId, - labels: labelsToCreate.map(({ name, color }) => ({ name, color })), + mutationFn: async (labelsToCreate: Array<{ slot: string; name: string; color?: string }>) => { + if (!state.linearTeamId) { + throw new Error('Team must be selected before creating labels'); + } + const useStored = shouldUseStoredCredentials(state); + if (!useStored && !state.linearApiKey) { + throw new Error('Missing credentials — enter them on the credentials step'); + } + const authArg = useStored ? { projectId } : { credentials: { api_key: state.linearApiKey } }; + return runPerLabelCreations({ + labelsToCreate, + providerId: 'linear', + containerId: state.linearTeamId, + authArg, }); }, onSuccess: (result, labelsToCreate) => { diff --git a/web/src/components/projects/pm-wizard-jira-steps.tsx b/web/src/components/projects/pm-wizard-jira-steps.tsx deleted file mode 100644 index 66205219..00000000 --- a/web/src/components/projects/pm-wizard-jira-steps.tsx +++ /dev/null @@ -1,319 +0,0 @@ -/** - * JIRA-specific step renderer components for PMWizard. - */ - -import type { UseMutationResult } from '@tanstack/react-query'; -import { Loader2, Plus } from 'lucide-react'; -import { Button } from '@/components/ui/button.js'; -import { Input } from '@/components/ui/input.js'; -import { Label } from '@/components/ui/label.js'; -import type { WizardAction, WizardState } from './pm-wizard-state.js'; -import { FieldMappingRow, SearchableSelect } from './wizard-shared.js'; - -// ============================================================================ -// Slot definitions -// ============================================================================ - -const JIRA_STATUS_SLOTS = [ - 'backlog', - 'splitting', - 'planning', - 'todo', - 'inProgress', - 'inReview', - 'done', - 'merged', -]; - -const JIRA_LABEL_SLOTS = ['processing', 'processed', 'error', 'readyToProcess', 'auto']; - -// ============================================================================ -// JiraCredentialsStep -// ============================================================================ - -export function JiraCredentialsStep({ - state, - dispatch, -}: { - state: WizardState; - dispatch: React.Dispatch; -}) { - return ( -
-

- Enter your JIRA credentials. These will be saved securely to the project. -

-
- - dispatch({ type: 'SET_JIRA_BASE_URL', url: e.target.value })} - placeholder="https://your-instance.atlassian.net" - /> -
-
- - dispatch({ type: 'SET_JIRA_EMAIL', value: e.target.value })} - placeholder="your@email.com" - autoComplete="off" - /> -
-
- - dispatch({ type: 'SET_JIRA_API_TOKEN', value: e.target.value })} - placeholder="JIRA API token" - autoComplete="off" - /> -

- Generate a token at{' '} - - Atlassian account settings - -

-
-
- ); -} - -// ============================================================================ -// JiraProjectStep -// ============================================================================ - -export function JiraProjectStep({ - state, - onProjectSelect, - jiraProjectsMutation, - jiraDetailsMutation, -}: { - state: WizardState; - onProjectSelect: (key: string) => void; - jiraProjectsMutation: UseMutationResult; - jiraDetailsMutation: UseMutationResult; -}) { - return ( -
- - ({ - label: p.name, - value: p.key, - detail: p.key, - }))} - value={state.jiraProjectKey} - onChange={onProjectSelect} - placeholder="Select a JIRA project..." - isLoading={jiraProjectsMutation.isPending} - error={jiraProjectsMutation.isError ? (jiraProjectsMutation.error as Error).message : null} - onRetry={() => - (jiraProjectsMutation as UseMutationResult).mutate() - } - /> - {state.jiraProjectKey && jiraDetailsMutation.isPending && ( -
- Loading project details... -
- )} -
- ); -} - -// ============================================================================ -// JiraFieldMappingStep -// ============================================================================ - -export function JiraFieldMappingStep({ - state, - dispatch, - onCreateCostField, - creatingCostField, -}: { - state: WizardState; - dispatch: React.Dispatch; - onCreateCostField?: () => void; - creatingCostField?: boolean; -}) { - const existingCostField = state.jiraProjectDetails?.fields.some( - (f) => f.name.toLowerCase() === 'cost', - ); - const showCreateCostButton = - state.jiraProjectDetails && onCreateCostField && !state.jiraCostFieldId && !existingCostField; - - return ( -
- {/* Status mappings */} -
- -

- Map each CASCADE status to a JIRA status in the project. -

- {state.jiraProjectDetails ? ( - JIRA_STATUS_SLOTS.map((slot) => ( - ({ - label: s.name, - value: s.name, - })) ?? [] - } - value={state.jiraStatusMappings[slot] ?? ''} - onChange={(v) => - dispatch({ - type: 'SET_JIRA_STATUS_MAPPING', - key: slot, - value: v, - }) - } - manualFallback - /> - )) - ) : ( -

- Select a project first to populate status options. -

- )} -
- - {/* Issue types */} -
- -

- Map CASCADE issue types. Typically "task" for the main type and - "subtask" for sub-tasks. -

- {state.jiraProjectDetails ? ( - <> - !t.subtask) - .map((t) => ({ - label: t.name, - value: t.name, - }))} - value={state.jiraIssueTypes.task ?? ''} - onChange={(v) => - dispatch({ - type: 'SET_JIRA_ISSUE_TYPE', - key: 'task', - value: v, - }) - } - manualFallback - /> - t.subtask) - .map((t) => ({ - label: t.name, - value: t.name, - }))} - value={state.jiraIssueTypes.subtask ?? ''} - onChange={(v) => - dispatch({ - type: 'SET_JIRA_ISSUE_TYPE', - key: 'subtask', - value: v, - }) - } - manualFallback - /> - - ) : ( -

Select a project first.

- )} -
- - {/* Labels */} -
- -

- CASCADE label names used in JIRA. These are created automatically by CASCADE. -

- {JIRA_LABEL_SLOTS.map((slot) => ( -
- {slot} - - dispatch({ - type: 'SET_JIRA_LABEL', - key: slot, - value: e.target.value, - }) - } - placeholder={`JIRA label for ${slot}`} - className="flex-1" - /> -
- ))} -
- - {/* Cost custom field */} -
-
- - {showCreateCostButton && ( - - )} -
-

- JIRA custom fields are global and require admin permissions to create. -

- {state.jiraProjectDetails ? ( - ({ - label: `${f.name} (${f.id})`, - value: f.id, - }))} - value={state.jiraCostFieldId} - onChange={(v) => dispatch({ type: 'SET_JIRA_COST_FIELD', id: v })} - manualFallback - /> - ) : ( - - dispatch({ - type: 'SET_JIRA_COST_FIELD', - id: e.target.value, - }) - } - placeholder="e.g., customfield_10042" - /> - )} -
-
- ); -} diff --git a/web/src/components/projects/pm-wizard-linear-steps.tsx b/web/src/components/projects/pm-wizard-linear-steps.tsx deleted file mode 100644 index fc96e6c3..00000000 --- a/web/src/components/projects/pm-wizard-linear-steps.tsx +++ /dev/null @@ -1,320 +0,0 @@ -/** - * Linear-specific step renderer components for PMWizard. - */ - -import type { UseMutationResult } from '@tanstack/react-query'; -import { CheckCircle2, Loader2, Plus } from 'lucide-react'; -import { Button } from '@/components/ui/button.js'; -import { Input } from '@/components/ui/input.js'; -import { Label } from '@/components/ui/label.js'; -import type { WizardAction, WizardState } from './pm-wizard-state.js'; -import { FieldMappingRow, SearchableSelect } from './wizard-shared.js'; - -// ============================================================================ -// Slot definitions -// ============================================================================ - -const LINEAR_STATUS_SLOTS = [ - 'backlog', - 'splitting', - 'planning', - 'todo', - 'inProgress', - 'inReview', - 'done', - 'merged', -] as const; - -const LINEAR_LABEL_SLOTS = ['processing', 'processed', 'error', 'readyToProcess', 'auto']; - -/** - * Default CASCADE label names + hex colors used when the operator clicks - * "Create" on an unmapped slot. Linear expects hex color strings on - * issueLabelCreate; picked to roughly match the Trello named-color palette. - */ -export const LINEAR_LABEL_DEFAULTS: Record = { - readyToProcess: { name: 'cascade-ready', color: '#0284C7' }, - processing: { name: 'cascade-processing', color: '#2563EB' }, - processed: { name: 'cascade-processed', color: '#16A34A' }, - error: { name: 'cascade-error', color: '#DC2626' }, - auto: { name: 'cascade-auto', color: '#9333EA' }, -}; - -// ============================================================================ -// LinearCredentialsStep -// ============================================================================ - -export function LinearCredentialsStep({ - state, - dispatch, -}: { - state: WizardState; - dispatch: React.Dispatch; -}) { - return ( -
- {state.isEditing && state.hasStoredCredentials && !state.linearApiKey && ( -
- - Credentials stored — enter new values below to replace them. -
- )} -

- Enter your Linear API key. This will be saved securely to the project. -

-
- - dispatch({ type: 'SET_LINEAR_API_KEY', value: e.target.value })} - placeholder="lin_api_..." - autoComplete="off" - /> -

- Generate a Personal API key at{' '} - - linear.app/settings/api - -

-
-
- ); -} - -// ============================================================================ -// LinearTeamStep -// ============================================================================ - -export function LinearTeamStep({ - state, - onTeamSelect, - dispatch, - linearTeamsMutation, - linearDetailsMutation, - linearProjectsMutation, -}: { - state: WizardState; - onTeamSelect: (id: string) => void; - dispatch: React.Dispatch; - linearTeamsMutation: UseMutationResult; - linearDetailsMutation: UseMutationResult; - linearProjectsMutation: UseMutationResult; -}) { - return ( -
-
- - ({ - label: t.name, - value: t.id, - detail: t.key, - }))} - value={state.linearTeamId} - onChange={onTeamSelect} - placeholder="Select a Linear team..." - isLoading={linearTeamsMutation.isPending} - error={linearTeamsMutation.isError ? (linearTeamsMutation.error as Error).message : null} - onRetry={() => - (linearTeamsMutation as UseMutationResult).mutate() - } - /> - {state.linearTeamId && linearDetailsMutation.isPending && ( -
- Loading team details... -
- )} -
- - {state.linearTeamId && ( -
- - ({ - label: p.name, - value: p.id, - }))} - value={state.linearProjectId} - onChange={(v) => dispatch({ type: 'SET_LINEAR_PROJECT_ID', value: v })} - placeholder="No project scope — all team issues" - isLoading={linearProjectsMutation.isPending} - error={ - linearProjectsMutation.isError - ? (linearProjectsMutation.error as Error).message - : null - } - onRetry={() => linearProjectsMutation.mutate(state.linearTeamId)} - /> -

- Optional — leave empty to process all issues in this team. When set, CASCADE only - responds to issues that belong to this Linear Project. -

-
- )} -
- ); -} - -// ============================================================================ -// LinearFieldMappingStep -// ============================================================================ - -export function LinearFieldMappingStep({ - state, - dispatch, - onCreateLabel, - onCreateAllMissingLabels, - creatingSlot, -}: { - state: WizardState; - dispatch: React.Dispatch; - onCreateLabel?: (slot: string) => void; - onCreateAllMissingLabels?: () => void; - creatingSlot?: string | null; -}) { - const existingLabelNames = new Set( - (state.linearTeamDetails?.labels ?? []).map((l) => l.name.toLowerCase()), - ); - - const missingSlots = LINEAR_LABEL_SLOTS.filter((slot) => { - if (state.linearLabels[slot]) return false; - const defaultName = LINEAR_LABEL_DEFAULTS[slot]?.name ?? ''; - return !existingLabelNames.has(defaultName.toLowerCase()); - }); - - return ( -
- {/* Status mappings */} -
- -

- Map each CASCADE status to a Linear workflow state in the team. -

- {state.linearTeamDetails ? ( - LINEAR_STATUS_SLOTS.map((slot) => ( - ({ - label: s.name, - value: s.id, - })) ?? [] - } - value={state.linearStatusMappings[slot] ?? ''} - onChange={(v) => - dispatch({ - type: 'SET_LINEAR_STATUS_MAPPING', - key: slot, - value: v, - }) - } - manualFallback - /> - )) - ) : ( -

- Select a team first to populate status options. -

- )} -
- - {/* Label mappings */} -
-
- - {state.linearTeamDetails && missingSlots.length > 0 && onCreateAllMissingLabels && ( - - )} -
-

- Map each CASCADE label to a Linear label on the team. Click "Create" to add missing ones. -

- {state.linearTeamDetails ? ( - LINEAR_LABEL_SLOTS.map((slot) => { - const isMapped = !!state.linearLabels[slot]; - const defaultInfo = LINEAR_LABEL_DEFAULTS[slot]; - const alreadyExists = - defaultInfo && existingLabelNames.has(defaultInfo.name.toLowerCase()); - const showCreateButton = !isMapped && !alreadyExists && onCreateLabel && defaultInfo; - - return ( -
-
- l.name) - .map((l) => ({ - label: `${l.name} (${l.color})`, - value: l.id, - })) ?? [] - } - value={state.linearLabels[slot] ?? ''} - onChange={(v) => - dispatch({ - type: 'SET_LINEAR_LABEL', - key: slot, - value: v, - }) - } - manualFallback - /> -
- {showCreateButton && ( - - )} -
- ); - }) - ) : ( -

- Select a team first to populate label options. -

- )} -
-
- ); -} diff --git a/web/src/components/projects/pm-wizard-state.ts b/web/src/components/projects/pm-wizard-state.ts index 99685951..0b8e931f 100644 --- a/web/src/components/projects/pm-wizard-state.ts +++ b/web/src/components/projects/pm-wizard-state.ts @@ -489,6 +489,27 @@ export function areCredentialsReady(state: WizardState): boolean { return !!state.linearApiKey; } +/** + * Returns `true` when a wizard mutation (verify, createLabel, createCustomField) + * should pass `projectId` to the backend — meaning: edit mode is active, the + * provider has stored credentials in `project_credentials`, and the user has + * NOT re-typed the primary API key in the form (because `buildEditState` + * intentionally leaves raw credentials blank for security). + * + * `resolvePMCredentials` on the backend (`src/api/routers/pm-discovery.ts`) + * resolves stored credentials when `projectId` is supplied, so this check + * lets edit-mode mutations work without the user re-typing their key. + * + * Fresh setup (no `isEditing`) → false → mutation passes `credentials` from + * form state (current behavior). + */ +export function shouldUseStoredCredentials(state: WizardState): boolean { + if (!state.isEditing || !state.hasStoredCredentials) return false; + if (state.provider === 'trello') return !state.trelloApiKey; + if (state.provider === 'jira') return !state.jiraApiToken; + return !state.linearApiKey; +} + /** * Build the Linear integration config payload from wizard state. * Pure function so it can be unit-tested without the React runtime. diff --git a/web/src/components/projects/pm-wizard-trello-steps.tsx b/web/src/components/projects/pm-wizard-trello-steps.tsx deleted file mode 100644 index 94ab9496..00000000 --- a/web/src/components/projects/pm-wizard-trello-steps.tsx +++ /dev/null @@ -1,446 +0,0 @@ -/** - * Trello-specific step renderer components for PMWizard. - */ - -import type { UseMutationResult } from '@tanstack/react-query'; -import { CheckCircle2, Loader2, Plus } from 'lucide-react'; -import { useEffect, useRef, useState } from 'react'; -import { toast } from 'sonner'; -import { Button } from '@/components/ui/button.js'; -import { Input } from '@/components/ui/input.js'; -import { Label } from '@/components/ui/label.js'; -import type { WizardAction, WizardState } from './pm-wizard-state.js'; -import { FieldMappingRow, SearchableSelect } from './wizard-shared.js'; - -// ============================================================================ -// Slot definitions -// ============================================================================ - -const TRELLO_LIST_SLOTS = [ - 'backlog', - 'splitting', - 'planning', - 'todo', - 'inProgress', - 'inReview', - 'done', - 'merged', - 'debug', -]; - -const TRELLO_LABEL_SLOTS = ['readyToProcess', 'processing', 'processed', 'error', 'auto']; - -export const TRELLO_LABEL_DEFAULTS: Record = { - readyToProcess: { name: 'cascade-ready', color: 'sky' }, - processing: { name: 'cascade-processing', color: 'blue' }, - processed: { name: 'cascade-processed', color: 'green' }, - error: { name: 'cascade-error', color: 'red' }, - auto: { name: 'cascade-auto', color: 'purple' }, -}; - -// ============================================================================ -// TrelloCredentialsStep -// ============================================================================ - -export function TrelloCredentialsStep({ - state, - dispatch, -}: { - state: WizardState; - dispatch: React.Dispatch; -}) { - const popupRef = useRef(null); - const [isWaitingForAuth, setIsWaitingForAuth] = useState(false); - // Start open if a token is already present (e.g. edit mode) so the user can see and change it. - const [manualOpen, setManualOpen] = useState(!!state.trelloToken); - - function openAuthPopup() { - // Omitting return_url: Trello enforces an origin allowlist that users haven't configured, - // so redirecting back is not reliable. Without it, Trello displays the token on-screen - // after "Allow" and the user pastes it in below. - const url = `https://trello.com/1/authorize?key=${encodeURIComponent(state.trelloApiKey)}&name=CASCADE&expiration=never&scope=read,write&response_type=token`; - const popup = window.open(url, 'trello_oauth', 'width=600,height=700'); - if (!popup) { - toast.error('Popup blocked', { - description: 'Allow popups for this site, then try again.', - }); - return; - } - popupRef.current = popup; - setIsWaitingForAuth(true); - setManualOpen(true); - } - - // Detect the user closing the popup without completing authorization. - useEffect(() => { - if (!isWaitingForAuth) return; - const interval = setInterval(() => { - if (popupRef.current?.closed) { - popupRef.current = null; - setIsWaitingForAuth(false); - } - }, 500); - return () => clearInterval(interval); - }, [isWaitingForAuth]); - - return ( -
- {state.isEditing && state.hasStoredCredentials && !state.trelloApiKey && ( -
- - Credentials stored — enter new values below to replace them. -
- )} -

- Enter your Trello API credentials. These will be saved securely to the project. -

-
- - dispatch({ type: 'SET_TRELLO_API_KEY', value: e.target.value })} - placeholder="Trello API key" - autoComplete="off" - /> -

- Find your API key at{' '} - - trello.com/app-key - -

-
-
- - {state.trelloToken ? ( -
- - Token set - -
- ) : ( - - )} -

- {state.trelloApiKey - ? 'Click to open Trello authorization in a popup.' - : 'Enter your API key above to enable authorization.'} -

-
-
setManualOpen(e.currentTarget.open)}> - - Enter token manually - -
- - dispatch({ type: 'SET_TRELLO_TOKEN', value: e.target.value })} - placeholder="Trello token" - autoComplete="off" - /> -

- {isWaitingForAuth - ? 'After clicking "Allow" in the Trello popup, copy the token shown and paste it above.' - : 'Generate a token from the API key page linked above.'} -

-
-
-
- ); -} - -// ============================================================================ -// TrelloBoardStep -// ============================================================================ - -export function TrelloBoardStep({ - state, - onBoardSelect, - boardsMutation, - boardDetailsMutation, -}: { - state: WizardState; - onBoardSelect: (boardId: string) => void; - boardsMutation: UseMutationResult; - boardDetailsMutation: UseMutationResult; -}) { - return ( -
- - ({ - label: b.name, - value: b.id, - detail: b.url.split('/').pop(), - }))} - value={state.trelloBoardId} - onChange={onBoardSelect} - placeholder="Select a Trello board..." - isLoading={boardsMutation.isPending} - error={boardsMutation.isError ? (boardsMutation.error as Error).message : null} - onRetry={() => - (boardsMutation as UseMutationResult).mutate() - } - /> - {state.trelloBoardId && boardDetailsMutation.isPending && ( -
- Loading board details... -
- )} -
- ); -} - -// ============================================================================ -// TrelloFieldMappingStep -// ============================================================================ - -export function TrelloFieldMappingStep({ - state, - dispatch, - onCreateLabel, - onCreateAllMissingLabels, - onCreateCostField, - creatingSlot, - creatingCostField, -}: { - state: WizardState; - dispatch: React.Dispatch; - onCreateLabel?: (slot: string) => void; - onCreateAllMissingLabels?: () => void; - onCreateCostField?: () => void; - creatingSlot?: string | null; - creatingCostField?: boolean; -}) { - const existingLabelNames = new Set( - (state.trelloBoardDetails?.labels ?? []).map((l) => l.name.toLowerCase()), - ); - - const missingSlots = TRELLO_LABEL_SLOTS.filter((slot) => { - if (state.trelloLabelMappings[slot]) return false; - const defaultName = TRELLO_LABEL_DEFAULTS[slot]?.name ?? ''; - return !existingLabelNames.has(defaultName.toLowerCase()); - }); - - return ( -
- {/* List mappings */} -
- -

- Map each CASCADE stage to a Trello list on the board. -

- {state.trelloBoardDetails ? ( - TRELLO_LIST_SLOTS.map((slot) => ( - ({ - label: l.name, - value: l.id, - })) ?? [] - } - value={state.trelloListMappings[slot] ?? ''} - onChange={(v) => - dispatch({ - type: 'SET_TRELLO_LIST_MAPPING', - key: slot, - value: v, - }) - } - manualFallback - /> - )) - ) : ( -

- Select a board first to populate list options. -

- )} -
- - {/* Label mappings */} -
-
- - {state.trelloBoardDetails && missingSlots.length > 0 && onCreateAllMissingLabels && ( - - )} -
-

- Map each CASCADE label to a Trello label on the board. -

- {state.trelloBoardDetails ? ( - TRELLO_LABEL_SLOTS.map((slot) => { - const isMapped = !!state.trelloLabelMappings[slot]; - const defaultInfo = TRELLO_LABEL_DEFAULTS[slot]; - const alreadyExists = - defaultInfo && existingLabelNames.has(defaultInfo.name.toLowerCase()); - const showCreateButton = !isMapped && !alreadyExists && onCreateLabel && defaultInfo; - - return ( -
-
- l.name) - .map((l) => ({ - label: `${l.name} (${l.color})`, - value: l.id, - })) ?? [] - } - value={state.trelloLabelMappings[slot] ?? ''} - onChange={(v) => - dispatch({ - type: 'SET_TRELLO_LABEL_MAPPING', - key: slot, - value: v, - }) - } - manualFallback - /> -
- {showCreateButton && ( - - )} -
- ); - }) - ) : ( -

- Select a board first to populate label options. -

- )} -
- - {/* Cost custom field */} -
-
- - {(() => { - const existingCostField = state.trelloBoardDetails?.customFields.some( - (f) => f.type === 'number' && f.name.toLowerCase() === 'cost', - ); - const showCreateCostButton = - state.trelloBoardDetails && - onCreateCostField && - !state.trelloCostFieldId && - !existingCostField; - return showCreateCostButton ? ( - - ) : null; - })()} -
- {state.trelloBoardDetails ? ( - f.type === 'number') - .map((f) => ({ - label: f.name, - value: f.id, - }))} - value={state.trelloCostFieldId} - onChange={(v) => dispatch({ type: 'SET_TRELLO_COST_FIELD', id: v })} - manualFallback - /> - ) : ( - - dispatch({ - type: 'SET_TRELLO_COST_FIELD', - id: e.target.value, - }) - } - placeholder="Custom field ID for cost tracking" - /> - )} -
-
- ); -} diff --git a/web/src/components/projects/pm-wizard.tsx b/web/src/components/projects/pm-wizard.tsx index 192b5dbe..78d4bbc1 100644 --- a/web/src/components/projects/pm-wizard.tsx +++ b/web/src/components/projects/pm-wizard.tsx @@ -11,49 +11,29 @@ import './pm-providers/jira/index.js'; import './pm-providers/linear/index.js'; import { ManifestProviderWizardSection } from './pm-providers/manifest-section.js'; import { getProviderWizard } from './pm-providers/registry.js'; -import { SaveStep, WebhookStep } from './pm-wizard-common-steps.js'; -import { - useLinearWebhookInfo, - useSaveMutation, - useVerification, - useWebhookManagement, -} from './pm-wizard-hooks.js'; -// JIRA legacy step imports removed — all JIRA wizard rendering flows -// through the manifest path (see ./pm-providers/jira/). The -// `pm-wizard-jira-steps` module is still imported transitively by the -// adapters in `./pm-providers/jira/adapters.tsx`. -// Linear legacy step imports removed — all Linear wizard rendering flows -// through the manifest path (see ./pm-providers/linear/). +import { SaveStep } from './pm-wizard-common-steps.js'; +import { useSaveMutation, useVerification } from './pm-wizard-hooks.js'; +// Plan 011/5: the three legacy `pm-wizard-{trello,jira,linear}-steps.tsx` +// files were deleted; all three providers now render exclusively through +// the manifest path (see `./pm-providers//wizard.ts`). +// Plan 012/4: `WebhookStep` + `LinearWebhookInfoPanel` + supporting hooks +// deleted; every provider owns its webhook UX via the manifest path. import { areCredentialsReady, buildEditState, createInitialState, - deriveActiveWebhooks, isStep1Complete, - isStep2Complete, - isStep3Complete, - isStep4Complete, wizardReducer, } from './pm-wizard-state.js'; -// Trello legacy step imports removed — all Trello wizard rendering flows -// through the manifest path (see ./pm-providers/trello/). The -// `pm-wizard-trello-steps` module is still imported transitively by the -// adapters in `./pm-providers/trello/adapters.tsx`, so its behavior is -// unchanged — only the per-provider branching in this file is gone. import { WizardStep } from './wizard-shared.js'; // ============================================================================ // Constants // ============================================================================ -const STEP_TITLES = [ - 'Provider', - 'Credentials & Verification', - 'Board / Project Selection', - 'Field Mapping', - 'Webhooks', - 'Save', -] as const; +// Plan 011/4: step titles now come from each provider's wizard definition +// (manifestDef.steps[i].title). Only step 1 (provider picker) and the +// legacy Webhook + Save slots have fixed titles; rendered inline. const PROVIDER_LABELS: Record<'trello' | 'jira' | 'linear', string> = { trello: 'Trello', @@ -83,7 +63,6 @@ export function PMWizard({ initialProvider: string; initialConfig?: Record; }) { - const webhooksQuery = useQuery(trpc.webhooks.list.queryOptions({ projectId })); const credentialsQuery = useQuery(trpc.projects.credentials.list.queryOptions({ projectId })); const [state, dispatch] = useReducer(wizardReducer, undefined, createInitialState); @@ -124,7 +103,9 @@ export function PMWizard({ const configuredKeys = new Set(credentialsQuery.data.map((c) => c.envVarKey)); const editState = buildEditState(initialProvider, initialConfig, configuredKeys); dispatch({ type: 'INIT_EDIT', state: editState }); - setOpenSteps(new Set([1, 2, 3, 4, 5, 6])); + // Plan 011/4: open all steps up to a comfortable ceiling; actual + // step count is provider-dependent (Trello 7, JIRA 8, Linear 7). + setOpenSteps(new Set([1, 2, 3, 4, 5, 6, 7, 8, 9])); }, [initialConfig, initialProvider, credentialsQuery.data]); // ---- Custom hooks ---- @@ -135,21 +116,13 @@ export function PMWizard({ // through to the legacy per-provider branches. const manifestDef = getProviderWizard(state.provider); - const { verifyMutation } = useVerification(state, dispatch, advanceToStep); + const { verifyMutation } = useVerification(state, dispatch, advanceToStep, projectId); // Every PM provider (Trello 006/2, JIRA 006/3, Linear 006/4) composes its - // discovery / label / custom-field hooks inside its own useProviderHooks. - // The parent wizard no longer calls any provider-specific React hook. - const webhookManagement = useWebhookManagement(projectId, state); - const { webhookUrl: linearWebhookUrl } = useLinearWebhookInfo(); + // discovery / label / custom-field / webhook hooks inside its own + // useProviderHooks. The parent wizard no longer calls any provider- + // specific React hook. const { saveMutation } = useSaveMutation(projectId, state); - const linearWebhookSecretCredential = credentialsQuery.data?.find( - (c) => c.envVarKey === 'LINEAR_WEBHOOK_SECRET', - ); - - // Label creation + discovery handlers now live inside each provider's - // useProviderHooks (Trello 006/2, JIRA 006/3, Linear 006/4). - // ---- Step status ---- const credsReady = areCredentialsReady(state); @@ -163,8 +136,15 @@ export function PMWizard({ return 'pending'; } - // ---- Active webhooks for this provider ---- - const activeWebhooks = deriveActiveWebhooks(state.provider, webhooksQuery.data); + // ---- Manifest step layout (plans 011/4 + 012/1-4) ---- + // Iterate over `manifestDef.steps`. Every PM provider owns every + // wizard step via the manifest path — credentials, container-pick, + // mappings, webhook, everything. Parent wizard only owns the provider + // picker (step 1) and the final Save step. + const renderedManifestSteps = manifestDef + ? manifestDef.steps.map((step, index) => ({ step, index })) + : []; + const saveStepNumber = renderedManifestSteps.length + 2; // +1 for provider picker, +1 for 1-indexed // ---- Render ---- @@ -173,7 +153,7 @@ export function PMWizard({ {/* Step 1: Provider */} toggleStep(1)} @@ -204,122 +184,86 @@ export function PMWizard({ - {/* Step 2: Credentials & Verification */} - toggleStep(2)} - > - {manifestDef && ( - - )} - -
- {(!state.isEditing || !state.hasStoredCredentials || credsReady) && ( - - )} - {state.verificationResult && ( -
- - Connected as {state.verificationResult.display} -
- )} - {state.verifyError && ( -
- - {state.verifyError} -
- )} -
-
- - {/* Step 3: Board / Project Selection */} - toggleStep(3)} - > - {manifestDef && ( - - )} - + - {/* Step 4: Field Mapping */} - toggleStep(4)} - > - {manifestDef && ( - - )} - - - {/* Step 5: Webhooks */} - toggleStep(5)} - > - - + {/* Verify Connection button belongs on the first manifest + step (credentials). Always render — edit mode with stored + credentials uses the `projectId` path on the backend, so + users can verify without re-typing the key. */} + {isCredentials && ( +
+ + {!credsReady && state.isEditing && state.hasStoredCredentials && ( + Using stored credentials + )} + {state.verificationResult && ( +
+ + Connected as{' '} + {state.verificationResult.display} +
+ )} + {state.verifyError && ( +
+ + {state.verifyError} +
+ )} +
+ )} + + ); + })} - {/* Step 6: Save */} + {/* Save slot. */} toggleStep(6)} + stepNumber={saveStepNumber} + title="Save" + status={getStatus(saveStepNumber, saveMutation.isSuccess)} + isOpen={openSteps.has(saveStepNumber)} + onToggle={() => toggleStep(saveStepNumber)} > diff --git a/web/src/components/ui/combobox.tsx b/web/src/components/ui/combobox.tsx index 787fccb6..8883a9d2 100644 --- a/web/src/components/ui/combobox.tsx +++ b/web/src/components/ui/combobox.tsx @@ -12,6 +12,12 @@ export interface ComboboxOption { detail?: string; /** Optional group for organizing options */ group?: string; + /** + * Optional color swatch shown as an 8x8 rounded dot before the label. + * Any CSS color (hex, rgb(), named) works. Used by the PM-wizard label + * pickers so operators can tell `cascade-*` labels apart at a glance. + */ + swatch?: string; } interface ComboboxProps { @@ -85,8 +91,17 @@ export function Combobox({ disabled={disabled} className={cn('w-full justify-between font-normal', className)} > - - {displayValue || {emptyLabel}} + + {selectedOption?.swatch && ( + @@ -137,6 +152,13 @@ export function Combobox({ value === option.value ? 'opacity-100' : 'opacity-0', )} /> + {option.swatch && ( +