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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <project-id> --no-card-moved-to-briefing

# Disable JIRA issue-transitioned for implementation agent only
cascade projects pm-trigger-set <project-id> --no-issue-transitioned-implementation

# Enable JIRA triggers for briefing and planning, disable for implementation
cascade projects pm-trigger-set <project-id> \
--issue-transitioned-briefing \
--issue-transitioned-planning \
--no-issue-transitioned-implementation

# Disable all Trello card-moved triggers
cascade projects pm-trigger-set <project-id> \
--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 <project-id> \
--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:
Expand Down
196 changes: 196 additions & 0 deletions src/cli/dashboard/projects/pm-trigger-set.ts
Original file line number Diff line number Diff line change
@@ -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 <project-id> [--card-moved-to-briefing] [--issue-transitioned-briefing] ...
*
* At least one flag must be provided. Pass `--no-<flag>` 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<string, boolean | Record<string, boolean>> {
const {
cardMovedToBriefing,
cardMovedToPlanning,
cardMovedToTodo,
issueTransitionedBriefing,
issueTransitionedPlanning,
issueTransitionedImplementation,
} = parsedFlags;

const triggers: Record<string, boolean | Record<string, boolean>> = {};

if (cardMovedToBriefing !== undefined) triggers.cardMovedToBriefing = cardMovedToBriefing;
if (cardMovedToPlanning !== undefined) triggers.cardMovedToPlanning = cardMovedToPlanning;
if (cardMovedToTodo !== undefined) triggers.cardMovedToTodo = cardMovedToTodo;

const issueTransitioned: Record<string, boolean> = {};
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<void> {
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-<flag> 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);
}
}
}
50 changes: 49 additions & 1 deletion src/config/triggerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof IssueTransitionedSchema>;

/**
* 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),
});
Expand Down Expand Up @@ -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<JiraTriggerConfig> | 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).
Expand All @@ -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);
}

Expand Down
14 changes: 13 additions & 1 deletion src/triggers/jira/issue-transitioned.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
Loading