From d3023b7770302f076cb62a32224d0c2ff44b7dc0 Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Mon, 23 Feb 2026 10:50:35 +0000 Subject: [PATCH 1/2] feat(triggers): add per-agent JIRA issue-transitioned toggles and pm-trigger-set CLI --- CLAUDE.md | 64 ++++++ src/cli/dashboard/projects/pm-trigger-set.ts | 196 ++++++++++++++++++ src/config/triggerConfig.ts | 50 ++++- src/triggers/jira/issue-transitioned.ts | 14 +- tests/unit/config/triggerConfig.test.ts | 87 +++++++- .../triggers/jira-issue-transitioned.test.ts | 84 +++++++- web/src/lib/trigger-agent-mapping.ts | 36 +++- 7 files changed, 513 insertions(+), 18 deletions(-) create mode 100644 src/cli/dashboard/projects/pm-trigger-set.ts diff --git a/CLAUDE.md b/CLAUDE.md index 6c08a6bd..3e8e8ed0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -240,6 +240,70 @@ When `reviewTrigger` is absent, the system falls back to legacy booleans: - `reviewRequested` → `onReviewRequested` (default `false`) - `externalPrs` always `false` in legacy mode (no legacy equivalent) +### PM Agent Trigger Modes + +Briefing, planning, and implementation agents each have independent toggles for their PM triggers. **All modes default to `true`** for backward compatibility. + +#### Trello card-moved triggers + +| Flag | Description | +|------|-------------| +| `cardMovedToBriefing` | Trigger briefing agent when a card is moved to the Briefing list | +| `cardMovedToPlanning` | Trigger planning agent when a card is moved to the Planning list | +| `cardMovedToTodo` | Trigger implementation agent when a card is moved to the Todo list | + +#### JIRA issue-transitioned triggers (per-agent) + +The `issueTransitioned` field supports both a legacy boolean (applies to all agents) and a nested per-agent object: + +| Agent | Field | Description | +|-------|-------|-------------| +| briefing | `issueTransitioned.briefing` | Trigger briefing when issue transitions to Briefing status | +| planning | `issueTransitioned.planning` | Trigger planning when issue transitions to Planning status | +| implementation | `issueTransitioned.implementation` | Trigger implementation when issue transitions to Todo status | + +#### Setting via CLI + +```bash +# Disable Trello card-moved trigger for briefing agent +cascade projects pm-trigger-set --no-card-moved-to-briefing + +# Disable JIRA issue-transitioned for implementation agent only +cascade projects pm-trigger-set --no-issue-transitioned-implementation + +# Enable JIRA triggers for briefing and planning, disable for implementation +cascade projects pm-trigger-set \ + --issue-transitioned-briefing \ + --issue-transitioned-planning \ + --no-issue-transitioned-implementation + +# Disable all Trello card-moved triggers +cascade projects pm-trigger-set \ + --no-card-moved-to-briefing \ + --no-card-moved-to-planning \ + --no-card-moved-to-todo +``` + +#### Setting via Dashboard + +In the **Agent Configs** tab, the briefing, planning, and implementation agent sections each show: +- **Card moved to [list]** — Trello card-moved toggle (Trello projects only) +- **Issue Transitioned** — JIRA per-agent transition toggle (JIRA projects only) +- **Ready to Process label** — label-based trigger toggle + +#### Direct JSON Config + +```bash +# Disable JIRA issue-transitioned for implementation only +cascade projects integration-set \ + --category pm --provider jira --config '{"projectKey":"PROJ","statuses":{...}}' \ + --triggers '{"issueTransitioned":{"briefing":true,"planning":true,"implementation":false}}' +``` + +#### Backward Compatibility + +The legacy `issueTransitioned: true/false` boolean is still supported — it applies to all agents uniformly. + ## Claude Code Backend CASCADE supports using Claude Code SDK as an alternative agent backend. Configure per-project: diff --git a/src/cli/dashboard/projects/pm-trigger-set.ts b/src/cli/dashboard/projects/pm-trigger-set.ts new file mode 100644 index 00000000..b56709ce --- /dev/null +++ b/src/cli/dashboard/projects/pm-trigger-set.ts @@ -0,0 +1,196 @@ +import { Args, Flags } from '@oclif/core'; +import { DashboardCommand } from '../_shared/base.js'; + +/** + * CLI command for configuring PM trigger modes per agent type. + * + * Usage: + * cascade projects pm-trigger-set [--card-moved-to-briefing] [--issue-transitioned-briefing] ... + * + * At least one flag must be provided. Pass `--no-` to disable a mode. + * Uses the `projects.integrations.updateTriggers` tRPC endpoint, updating the + * PM integration triggers config for the project. + * + * Trello flags update the top-level boolean keys (cardMovedToBriefing, etc.). + * JIRA flags update the nested `issueTransitioned` object per agent type. + */ +export default class ProjectsPmTriggerSet extends DashboardCommand { + static override description = + 'Configure PM trigger modes per agent type (card-moved for Trello, issue-transitioned for JIRA).'; + + static override aliases = ['projects:pm-trigger-set']; + + static override args = { + id: Args.string({ description: 'Project ID', required: true }), + }; + + static override flags = { + ...DashboardCommand.baseFlags, + // Trello card-moved triggers + 'card-moved-to-briefing': Flags.boolean({ + description: 'Enable briefing agent when a card is moved to the Briefing list (Trello).', + allowNo: true, + default: undefined, + }), + 'card-moved-to-planning': Flags.boolean({ + description: 'Enable planning agent when a card is moved to the Planning list (Trello).', + allowNo: true, + default: undefined, + }), + 'card-moved-to-todo': Flags.boolean({ + description: 'Enable implementation agent when a card is moved to the Todo list (Trello).', + allowNo: true, + default: undefined, + }), + // JIRA issue-transitioned triggers (per-agent) + 'issue-transitioned-briefing': Flags.boolean({ + description: + 'Enable briefing agent when a JIRA issue transitions to the configured Briefing status.', + allowNo: true, + default: undefined, + }), + 'issue-transitioned-planning': Flags.boolean({ + description: + 'Enable planning agent when a JIRA issue transitions to the configured Planning status.', + allowNo: true, + default: undefined, + }), + 'issue-transitioned-implementation': Flags.boolean({ + description: + 'Enable implementation agent when a JIRA issue transitions to the configured Todo status.', + allowNo: true, + default: undefined, + }), + }; + + /** Build the triggers patch object from parsed flag values. */ + private buildTriggers(parsedFlags: { + cardMovedToBriefing: boolean | undefined; + cardMovedToPlanning: boolean | undefined; + cardMovedToTodo: boolean | undefined; + issueTransitionedBriefing: boolean | undefined; + issueTransitionedPlanning: boolean | undefined; + issueTransitionedImplementation: boolean | undefined; + }): Record> { + const { + cardMovedToBriefing, + cardMovedToPlanning, + cardMovedToTodo, + issueTransitionedBriefing, + issueTransitionedPlanning, + issueTransitionedImplementation, + } = parsedFlags; + + const triggers: Record> = {}; + + if (cardMovedToBriefing !== undefined) triggers.cardMovedToBriefing = cardMovedToBriefing; + if (cardMovedToPlanning !== undefined) triggers.cardMovedToPlanning = cardMovedToPlanning; + if (cardMovedToTodo !== undefined) triggers.cardMovedToTodo = cardMovedToTodo; + + const issueTransitioned: Record = {}; + if (issueTransitionedBriefing !== undefined) + issueTransitioned.briefing = issueTransitionedBriefing; + if (issueTransitionedPlanning !== undefined) + issueTransitioned.planning = issueTransitionedPlanning; + if (issueTransitionedImplementation !== undefined) + issueTransitioned.implementation = issueTransitionedImplementation; + + if (Object.keys(issueTransitioned).length > 0) { + triggers.issueTransitioned = issueTransitioned; + } + + return triggers; + } + + /** Format a human-readable summary of changed triggers. */ + private formatOutput( + projectId: string, + parsedFlags: { + cardMovedToBriefing: boolean | undefined; + cardMovedToPlanning: boolean | undefined; + cardMovedToTodo: boolean | undefined; + issueTransitionedBriefing: boolean | undefined; + issueTransitionedPlanning: boolean | undefined; + issueTransitionedImplementation: boolean | undefined; + }, + ): string { + const { + cardMovedToBriefing, + cardMovedToPlanning, + cardMovedToTodo, + issueTransitionedBriefing, + issueTransitionedPlanning, + issueTransitionedImplementation, + } = parsedFlags; + + const lines: string[] = [`PM trigger modes updated for project: ${projectId}`]; + if (cardMovedToBriefing !== undefined) + lines.push(` cardMovedToBriefing: ${cardMovedToBriefing}`); + if (cardMovedToPlanning !== undefined) + lines.push(` cardMovedToPlanning: ${cardMovedToPlanning}`); + if (cardMovedToTodo !== undefined) lines.push(` cardMovedToTodo: ${cardMovedToTodo}`); + if (issueTransitionedBriefing !== undefined) + lines.push(` issueTransitioned.briefing: ${issueTransitionedBriefing}`); + if (issueTransitionedPlanning !== undefined) + lines.push(` issueTransitioned.planning: ${issueTransitionedPlanning}`); + if (issueTransitionedImplementation !== undefined) + lines.push(` issueTransitioned.implementation: ${issueTransitionedImplementation}`); + return lines.join('\n'); + } + + async run(): Promise { + const { args, flags } = await this.parse(ProjectsPmTriggerSet); + + const cardMovedToBriefing = flags['card-moved-to-briefing']; + const cardMovedToPlanning = flags['card-moved-to-planning']; + const cardMovedToTodo = flags['card-moved-to-todo']; + const issueTransitionedBriefing = flags['issue-transitioned-briefing']; + const issueTransitionedPlanning = flags['issue-transitioned-planning']; + const issueTransitionedImplementation = flags['issue-transitioned-implementation']; + + const hasAnyFlag = + cardMovedToBriefing !== undefined || + cardMovedToPlanning !== undefined || + cardMovedToTodo !== undefined || + issueTransitionedBriefing !== undefined || + issueTransitionedPlanning !== undefined || + issueTransitionedImplementation !== undefined; + + if (!hasAnyFlag) { + this.error( + 'At least one flag must be provided: ' + + '--card-moved-to-briefing, --card-moved-to-planning, --card-moved-to-todo, ' + + '--issue-transitioned-briefing, --issue-transitioned-planning, --issue-transitioned-implementation ' + + '(use --no- to disable).', + ); + } + + const parsedFlags = { + cardMovedToBriefing, + cardMovedToPlanning, + cardMovedToTodo, + issueTransitionedBriefing, + issueTransitionedPlanning, + issueTransitionedImplementation, + }; + + const triggers = this.buildTriggers(parsedFlags); + + try { + await this.client.projects.integrations.updateTriggers.mutate({ + projectId: args.id, + category: 'pm', + triggers, + }); + + if (flags.json) { + this.outputJson({ ok: true, triggers }); + return; + } + + this.log(this.formatOutput(args.id, parsedFlags)); + } catch (err) { + this.handleError(err); + } + } +} diff --git a/src/config/triggerConfig.ts b/src/config/triggerConfig.ts index e9a1f0a8..a95e320f 100644 --- a/src/config/triggerConfig.ts +++ b/src/config/triggerConfig.ts @@ -33,12 +33,29 @@ export const TrelloTriggerConfigSchema = z.object({ commentMention: z.boolean().default(true), }); +/** + * Per-agent issue-transitioned configuration for JIRA. + * Each agent type can independently toggle whether the issue-transitioned trigger fires for it. + */ +export const IssueTransitionedSchema = z + .union([ + z.boolean(), + z.object({ + briefing: z.boolean().default(true), + planning: z.boolean().default(true), + implementation: z.boolean().default(true), + }), + ]) + .optional(); + +export type IssueTransitionedConfig = z.infer; + /** * Trigger configuration for JIRA integrations. * All triggers default to `true` for backward compatibility. */ export const JiraTriggerConfigSchema = z.object({ - issueTransitioned: z.boolean().default(true), + issueTransitioned: IssueTransitionedSchema, readyToProcessLabel: ReadyToProcessLabelSchema, commentMention: z.boolean().default(true), }); @@ -170,6 +187,30 @@ export function resolveReadyToProcessEnabled( return true; } +/** + * Resolve whether the issue-transitioned trigger is enabled for a specific agent type. + * Supports both the new nested object format and the legacy boolean format. + * Returns `true` when no config is present (backward compatible). + */ +export function resolveIssueTransitionedEnabled( + config: Partial | undefined, + agentType: string, +): boolean { + if (!config) return true; + const it = config.issueTransitioned as IssueTransitionedConfig; + if (it === undefined) return true; + if (typeof it === 'boolean') { + // Legacy: boolean applies to all agents + return it; + } + // Nested object: check per-agent toggle + if (agentType === 'briefing') return it.briefing ?? true; + if (agentType === 'planning') return it.planning ?? true; + if (agentType === 'implementation') return it.implementation ?? true; + // Unknown agent type — default to enabled + return true; +} + /** * Resolve whether a JIRA trigger is enabled based on project trigger config. * Returns `true` (enabled) when no config is present (backward compatible). @@ -186,6 +227,13 @@ export function resolveJiraTriggerEnabled( if (typeof rtp === 'boolean') return rtp; return rtp.briefing || rtp.planning || rtp.implementation; } + if (key === 'issueTransitioned') { + const it = value as IssueTransitionedConfig; + if (it === undefined) return true; + if (typeof it === 'boolean') return it; + // Object form: enabled if any agent is enabled + return it.briefing || it.planning || it.implementation; + } return value === undefined ? true : (value as boolean); } diff --git a/src/triggers/jira/issue-transitioned.ts b/src/triggers/jira/issue-transitioned.ts index 1c3d647c..7d5fea27 100644 --- a/src/triggers/jira/issue-transitioned.ts +++ b/src/triggers/jira/issue-transitioned.ts @@ -5,7 +5,10 @@ * a CASCADE agent type (briefing, planning, implementation). */ -import { resolveJiraTriggerEnabled } from '../../config/triggerConfig.js'; +import { + resolveIssueTransitionedEnabled, + resolveJiraTriggerEnabled, +} from '../../config/triggerConfig.js'; import { getJiraConfig } from '../../pm/config.js'; import type { TriggerContext, TriggerHandler, TriggerResult } from '../../types/index.js'; import { logger } from '../../utils/logging.js'; @@ -121,6 +124,15 @@ export class JiraIssueTransitionedTrigger implements TriggerHandler { return null; } + // Check per-agent toggle for issueTransitioned + if (!resolveIssueTransitionedEnabled(jiraConfig?.triggers, agentType)) { + logger.debug('JIRA issue-transitioned trigger disabled for agent', { + issueKey, + agentType, + }); + return null; + } + logger.info('JIRA issue transitioned to agent-triggering status', { issueKey, fromStatus: statusChange.fromString, diff --git a/tests/unit/config/triggerConfig.test.ts b/tests/unit/config/triggerConfig.test.ts index 0b912414..78993637 100644 --- a/tests/unit/config/triggerConfig.test.ts +++ b/tests/unit/config/triggerConfig.test.ts @@ -4,6 +4,7 @@ import { JiraTriggerConfigSchema, TrelloTriggerConfigSchema, resolveGitHubTriggerEnabled, + resolveIssueTransitionedEnabled, resolveJiraTriggerEnabled, resolveReadyToProcessEnabled, resolveReviewTriggerConfig, @@ -46,14 +47,28 @@ describe('TrelloTriggerConfigSchema', () => { }); describe('JiraTriggerConfigSchema', () => { - it('defaults boolean fields to true, readyToProcessLabel optional', () => { + it('defaults commentMention to true, issueTransitioned and readyToProcessLabel optional', () => { const result = JiraTriggerConfigSchema.parse({}); - expect(result).toEqual({ - issueTransitioned: true, - commentMention: true, - }); + expect(result.commentMention).toBe(true); + expect(result.issueTransitioned).toBeUndefined(); expect(result.readyToProcessLabel).toBeUndefined(); }); + + it('accepts legacy boolean issueTransitioned', () => { + const result = JiraTriggerConfigSchema.parse({ issueTransitioned: false }); + expect(result.issueTransitioned).toBe(false); + }); + + it('accepts per-agent issueTransitioned object', () => { + const result = JiraTriggerConfigSchema.parse({ + issueTransitioned: { briefing: true, planning: false, implementation: true }, + }); + expect(result.issueTransitioned).toEqual({ + briefing: true, + planning: false, + implementation: true, + }); + }); }); describe('GitHubTriggerConfigSchema', () => { @@ -145,7 +160,7 @@ describe('resolveJiraTriggerEnabled', () => { expect(resolveJiraTriggerEnabled(undefined, 'commentMention')).toBe(true); }); - it('returns false when key is explicitly disabled', () => { + it('returns false when issueTransitioned is explicitly false (legacy boolean)', () => { expect(resolveJiraTriggerEnabled({ issueTransitioned: false }, 'issueTransitioned')).toBe( false, ); @@ -154,6 +169,24 @@ describe('resolveJiraTriggerEnabled', () => { it('returns true when config is empty (no explicit settings)', () => { expect(resolveJiraTriggerEnabled({}, 'issueTransitioned')).toBe(true); }); + + it('returns true for issueTransitioned object when any agent is enabled', () => { + expect( + resolveJiraTriggerEnabled( + { issueTransitioned: { briefing: false, planning: true, implementation: false } }, + 'issueTransitioned', + ), + ).toBe(true); + }); + + it('returns false for issueTransitioned object when all agents disabled', () => { + expect( + resolveJiraTriggerEnabled( + { issueTransitioned: { briefing: false, planning: false, implementation: false } }, + 'issueTransitioned', + ), + ).toBe(false); + }); }); describe('resolveGitHubTriggerEnabled', () => { @@ -230,6 +263,48 @@ describe('resolveReadyToProcessEnabled', () => { }); }); +describe('resolveIssueTransitionedEnabled', () => { + it('returns true when config is undefined (backward compatible)', () => { + expect(resolveIssueTransitionedEnabled(undefined, 'briefing')).toBe(true); + expect(resolveIssueTransitionedEnabled(undefined, 'planning')).toBe(true); + expect(resolveIssueTransitionedEnabled(undefined, 'implementation')).toBe(true); + }); + + it('returns true when issueTransitioned is not set', () => { + expect(resolveIssueTransitionedEnabled({}, 'briefing')).toBe(true); + }); + + it('applies legacy boolean true to all agents', () => { + const config = { issueTransitioned: true as const }; + expect(resolveIssueTransitionedEnabled(config, 'briefing')).toBe(true); + expect(resolveIssueTransitionedEnabled(config, 'planning')).toBe(true); + expect(resolveIssueTransitionedEnabled(config, 'implementation')).toBe(true); + }); + + it('applies legacy boolean false to all agents', () => { + const config = { issueTransitioned: false as const }; + expect(resolveIssueTransitionedEnabled(config, 'briefing')).toBe(false); + expect(resolveIssueTransitionedEnabled(config, 'planning')).toBe(false); + expect(resolveIssueTransitionedEnabled(config, 'implementation')).toBe(false); + }); + + it('returns per-agent value from nested object', () => { + const config = { + issueTransitioned: { briefing: true, planning: false, implementation: true }, + }; + expect(resolveIssueTransitionedEnabled(config, 'briefing')).toBe(true); + expect(resolveIssueTransitionedEnabled(config, 'planning')).toBe(false); + expect(resolveIssueTransitionedEnabled(config, 'implementation')).toBe(true); + }); + + it('defaults to true for unknown agent types', () => { + const config = { + issueTransitioned: { briefing: false, planning: false, implementation: false }, + }; + expect(resolveIssueTransitionedEnabled(config, 'unknown-agent')).toBe(true); + }); +}); + describe('resolveReviewTriggerConfig', () => { it('maps legacy defaults when config is undefined (backward compatible)', () => { // No config → legacy fallback: checkSuiteSuccess defaults to true → ownPrsOnly=true diff --git a/tests/unit/triggers/jira-issue-transitioned.test.ts b/tests/unit/triggers/jira-issue-transitioned.test.ts index d9a2efe0..dc962132 100644 --- a/tests/unit/triggers/jira-issue-transitioned.test.ts +++ b/tests/unit/triggers/jira-issue-transitioned.test.ts @@ -36,9 +36,15 @@ function buildCtx( issueKey?: string; statusChangeItems?: Array<{ field?: string; fromString?: string; toString?: string }>; noJiraConfig?: boolean; + triggers?: Record; } = {}, ): TriggerContext { - const project = overrides.noJiraConfig ? { ...mockProject, jira: undefined } : mockProject; + const baseJira = overrides.triggers + ? { ...mockProject.jira, triggers: overrides.triggers } + : mockProject.jira; + const project = overrides.noJiraConfig + ? { ...mockProject, jira: undefined } + : { ...mockProject, jira: baseJira }; return { project: project as TriggerContext['project'], @@ -238,5 +244,81 @@ describe('JiraIssueTransitionedTrigger', () => { expect(result).toBeNull(); }); + + describe('per-agent issueTransitioned toggle', () => { + it('fires when issueTransitioned toggle is true for agent (legacy boolean)', async () => { + const ctx = buildCtx({ + statusChangeItems: [{ field: 'status', fromString: 'Backlog', toString: 'Briefing' }], + triggers: { issueTransitioned: true }, + }); + + const result = await trigger.handle(ctx); + + expect(result?.agentType).toBe('briefing'); + }); + + it('returns null when issueTransitioned disabled globally (legacy boolean false)', async () => { + const ctx = buildCtx({ + statusChangeItems: [{ field: 'status', fromString: 'Backlog', toString: 'Briefing' }], + triggers: { issueTransitioned: false }, + }); + + const result = await trigger.handle(ctx); + + expect(result).toBeNull(); + }); + + it('fires when per-agent issueTransitioned.briefing is enabled', async () => { + const ctx = buildCtx({ + statusChangeItems: [{ field: 'status', fromString: 'Backlog', toString: 'Briefing' }], + triggers: { + issueTransitioned: { briefing: true, planning: false, implementation: false }, + }, + }); + + const result = await trigger.handle(ctx); + + expect(result?.agentType).toBe('briefing'); + }); + + it('returns null when per-agent issueTransitioned.briefing is disabled', async () => { + const ctx = buildCtx({ + statusChangeItems: [{ field: 'status', fromString: 'Backlog', toString: 'Briefing' }], + triggers: { + issueTransitioned: { briefing: false, planning: true, implementation: true }, + }, + }); + + const result = await trigger.handle(ctx); + + expect(result).toBeNull(); + }); + + it('fires planning agent when issueTransitioned.planning is enabled', async () => { + const ctx = buildCtx({ + statusChangeItems: [{ field: 'status', fromString: 'Briefing', toString: 'Planning' }], + triggers: { + issueTransitioned: { briefing: false, planning: true, implementation: false }, + }, + }); + + const result = await trigger.handle(ctx); + + expect(result?.agentType).toBe('planning'); + }); + + it('returns null when per-agent issueTransitioned.implementation is disabled', async () => { + const ctx = buildCtx({ + statusChangeItems: [{ field: 'status', fromString: 'Planning', toString: 'To Do' }], + triggers: { + issueTransitioned: { briefing: true, planning: true, implementation: false }, + }, + }); + + const result = await trigger.handle(ctx); + + expect(result).toBeNull(); + }); + }); }); }); diff --git a/web/src/lib/trigger-agent-mapping.ts b/web/src/lib/trigger-agent-mapping.ts index 6dd6a810..b7cdda9c 100644 --- a/web/src/lib/trigger-agent-mapping.ts +++ b/web/src/lib/trigger-agent-mapping.ts @@ -44,15 +44,6 @@ export const LIFECYCLE_TRIGGERS: TriggerDef[] = [ * Displayed once in a dedicated section rather than duplicated per-agent. */ export const SHARED_PM_TRIGGERS: TriggerDef[] = [ - { - key: 'issueTransitioned', - label: 'Issue Transitioned', - description: - 'Trigger agent when a JIRA issue transitions to a configured status. Affects briefing, planning, and implementation agents.', - defaultValue: true, - pmProvider: 'jira', - category: 'pm', - }, { key: 'commentMention', label: 'Comment @mention', @@ -76,6 +67,15 @@ export const AGENT_TRIGGER_MAP: Record = { pmProvider: 'trello', category: 'pm', }, + { + key: 'issueTransitioned.briefing', + label: 'Issue Transitioned', + description: + 'Trigger briefing agent when a JIRA issue transitions to the configured Briefing status.', + defaultValue: true, + pmProvider: 'jira', + category: 'pm', + }, { key: 'readyToProcessLabel.briefing', label: 'Ready to Process label', @@ -94,6 +94,15 @@ export const AGENT_TRIGGER_MAP: Record = { pmProvider: 'trello', category: 'pm', }, + { + key: 'issueTransitioned.planning', + label: 'Issue Transitioned', + description: + 'Trigger planning agent when a JIRA issue transitions to the configured Planning status.', + defaultValue: true, + pmProvider: 'jira', + category: 'pm', + }, { key: 'readyToProcessLabel.planning', label: 'Ready to Process label', @@ -112,6 +121,15 @@ export const AGENT_TRIGGER_MAP: Record = { pmProvider: 'trello', category: 'pm', }, + { + key: 'issueTransitioned.implementation', + label: 'Issue Transitioned', + description: + 'Trigger implementation agent when a JIRA issue transitions to the configured Todo status.', + defaultValue: true, + pmProvider: 'jira', + category: 'pm', + }, { key: 'readyToProcessLabel.implementation', label: 'Ready to Process label', From 82b19258a7b0ece4bbc342aae28940ac2c76eb67 Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Mon, 23 Feb 2026 10:53:56 +0000 Subject: [PATCH 2/2] fix(tests): use vi.stubEnv to prevent env vars from shadowing credential mocks getIntegrationCredentialOrNull checks process.env before the DB mock. If GITHUB_TOKEN_IMPLEMENTER is set in the environment, it bypasses the mock and causes the credential resolver tests to fail. Use vi.stubEnv to set the vars to empty string (falsy) so the mock controls the result. Co-Authored-By: Claude Opus 4.6 --- tests/unit/config/projects.test.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/unit/config/projects.test.ts b/tests/unit/config/projects.test.ts index e7375d81..b1f493f7 100644 --- a/tests/unit/config/projects.test.ts +++ b/tests/unit/config/projects.test.ts @@ -170,6 +170,15 @@ describe('config provider', () => { }); describe('getIntegrationCredential', () => { + // These tests go through getIntegrationCredentialOrNull which checks process.env first. + // Use vi.stubEnv to prevent any env vars from shadowing the DB mock. + beforeEach(() => { + vi.stubEnv('TRELLO_API_KEY', ''); + }); + afterEach(() => { + vi.unstubAllEnvs(); + }); + it('resolves credential from DB', async () => { vi.mocked(resolveIntegrationCredential).mockResolvedValue('db-secret-value'); @@ -187,6 +196,14 @@ describe('config provider', () => { }); describe('getIntegrationCredentialOrNull', () => { + // Clear any env vars that might shadow the mock (implementer_token maps to GITHUB_TOKEN_IMPLEMENTER). + beforeEach(() => { + vi.stubEnv('GITHUB_TOKEN_IMPLEMENTER', ''); + }); + afterEach(() => { + vi.unstubAllEnvs(); + }); + it('returns credential value when found', async () => { vi.mocked(resolveIntegrationCredential).mockResolvedValue('secret-value'); @@ -227,6 +244,15 @@ describe('config provider', () => { }); describe('getProjectGitHubToken', () => { + // getProjectGitHubToken calls getIntegrationCredentialOrNull which checks process.env first. + // Use vi.stubEnv to prevent the env var from shadowing the mock. + beforeEach(() => { + vi.stubEnv('GITHUB_TOKEN_IMPLEMENTER', ''); + }); + afterEach(() => { + vi.unstubAllEnvs(); + }); + it('returns implementer token when available', async () => { vi.mocked(resolveIntegrationCredential).mockResolvedValue('implementer-token');