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
2 changes: 1 addition & 1 deletion src/agents/definitions/backlog-manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ triggers:
- event: internal:auto-chain
label: Auto-chain after Splitting
description: When splitting completes on a card with the auto label, immediately chain to backlog manager
defaultEnabled: true
defaultEnabled: false
contextPipeline: [pipelineSnapshot]

strategies: {}
Expand Down
4 changes: 2 additions & 2 deletions src/agents/definitions/implementation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ triggers:
- event: pm:status-changed
label: Status Changed to Todo
description: Trigger when work item status changes to Todo
defaultEnabled: true
defaultEnabled: false
parameters:
- name: targetStatus
type: select
Expand All @@ -39,7 +39,7 @@ triggers:
- event: pm:label-added
label: Ready to Process Label
description: Trigger when Ready to Process label added to a card in the Todo list
defaultEnabled: true
defaultEnabled: false
parameters:
- name: listKey
type: select
Expand Down
6 changes: 3 additions & 3 deletions src/agents/definitions/planning.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ triggers:
- event: pm:status-changed
label: Status Changed to Planning
description: Trigger when work item status changes to Planning
defaultEnabled: true
defaultEnabled: false
parameters:
- name: targetStatus
type: select
Expand All @@ -36,7 +36,7 @@ triggers:
- event: pm:label-added
label: Ready to Process Label
description: Trigger when Ready to Process label added to a card in Planning list
defaultEnabled: true
defaultEnabled: false
parameters:
- name: listKey
type: select
Expand All @@ -47,7 +47,7 @@ triggers:
- event: pm:comment-mention
label: Comment @mention
description: Trigger when bot is @mentioned in a card/issue comment
defaultEnabled: true
defaultEnabled: false
contextPipeline: [directoryListing, contextFiles, squint, workItem]

strategies: {}
Expand Down
2 changes: 1 addition & 1 deletion src/agents/definitions/resolve-conflicts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ triggers:
- event: scm:pr-conflict-detected
label: PR Conflict Detected
description: Trigger when a PR has merge conflicts with the base branch
defaultEnabled: true
defaultEnabled: false
providers: [github]
contextPipeline: [prContext, directoryListing, contextFiles, squint, workItem]

Expand Down
2 changes: 1 addition & 1 deletion src/agents/definitions/respond-to-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ triggers:
- event: scm:check-suite-failure
label: Check Suite Failure
description: Trigger when CI checks fail
defaultEnabled: true
defaultEnabled: false
providers: [github]
contextPipeline: [prContext, directoryListing, contextFiles, squint, workItem]

Expand Down
2 changes: 1 addition & 1 deletion src/agents/definitions/respond-to-planning-comment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ triggers:
- event: pm:comment-mention
label: Comment @mention
description: Trigger when bot is @mentioned in a card/issue comment
defaultEnabled: true
defaultEnabled: false
contextPipeline: [directoryListing, contextFiles, squint, workItem]

strategies: {}
Expand Down
2 changes: 1 addition & 1 deletion src/agents/definitions/respond-to-pr-comment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ triggers:
- event: scm:pr-comment-mention
label: PR Comment @mention
description: Trigger when the implementer bot is @mentioned in a PR comment
defaultEnabled: true
defaultEnabled: false
providers: [github]
contextPipeline: [prContext, prConversation, directoryListing, contextFiles, squint]

Expand Down
2 changes: 1 addition & 1 deletion src/agents/definitions/respond-to-review.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ triggers:
- event: scm:pr-review-submitted
label: PR Review Submitted
description: Trigger when a review with changes requested or comments is submitted
defaultEnabled: true
defaultEnabled: false
providers: [github]
contextPipeline: [prContext, prConversation, directoryListing, contextFiles, squint]

Expand Down
4 changes: 2 additions & 2 deletions src/agents/definitions/review.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ triggers:
- event: scm:pr-ready-to-merge
label: PR Ready to Merge
description: Move work item to DONE when PR is approved and all checks pass
defaultEnabled: true
defaultEnabled: false
providers: [github]
contextPipeline: []
- event: scm:pr-merged
label: PR Merged
description: Move work item to MERGED status when PR is merged
defaultEnabled: true
defaultEnabled: false
providers: [github]
contextPipeline: []

Expand Down
2 changes: 1 addition & 1 deletion src/agents/definitions/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export const SupportedTriggerSchema = z.object({
/** Optional description for help text */
description: z.string().optional(),
/** Whether the trigger is enabled by default */
defaultEnabled: z.boolean().default(true),
defaultEnabled: z.boolean().default(false),
/** Configurable parameters for this trigger */
parameters: z.array(TriggerParameterSchema).default([]),
/** Provider filter - only applies to these providers (e.g., ['trello']) */
Expand Down
4 changes: 2 additions & 2 deletions src/agents/definitions/splitting.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ triggers:
- event: pm:status-changed
label: Status Changed to Splitting
description: Trigger when work item status changes to Splitting
defaultEnabled: true
defaultEnabled: false
parameters:
- name: targetStatus
type: select
Expand All @@ -37,7 +37,7 @@ triggers:
- event: pm:label-added
label: Ready to Process Label
description: Trigger when Ready to Process label added to a card in Splitting list
defaultEnabled: true
defaultEnabled: false
parameters:
- name: listKey
type: select
Expand Down
10 changes: 10 additions & 0 deletions src/api/routers/_shared/triggerTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,19 @@ export interface ProjectIntegrationsMap {
/**
* Complete triggers view for a project.
* Response type for getProjectTriggersView.
*
* `enabledAgents` — agents that have an explicit agent_configs row (opt-in enabled).
* `availableAgents` — agents that exist in definitions but are NOT yet configured.
*
* The legacy `agents` field equals `enabledAgents` for backwards compatibility.
*/
export interface ProjectTriggersView {
/** @deprecated Use enabledAgents instead */
agents: AgentTriggersView[];
/** Agents with an explicit agent_configs row — actively configured for this project */
enabledAgents: AgentTriggersView[];
/** Agent types defined in YAML/DB but not yet configured for this project */
availableAgents: string[];
integrations: ProjectIntegrationsMap;
}

Expand Down
37 changes: 25 additions & 12 deletions src/api/routers/agentTriggerConfigs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
SupportedTrigger,
TriggerParameter,
} from '../../agents/definitions/schema.js';
import { listAgentConfigs } from '../../db/repositories/agentConfigsRepository.js';
import { listAgentDefinitions } from '../../db/repositories/agentDefinitionsRepository.js';
import {
deleteTriggerConfig,
Expand Down Expand Up @@ -204,16 +205,20 @@ export const agentTriggerConfigsRouter = router({
.query(async ({ ctx, input }): Promise<ProjectTriggersView> => {
await verifyProjectOrgAccess(input.projectId, ctx.effectiveOrgId);

// Fetch DB definitions and configs in parallel
const [dbDefinitions, configs, integrations] = await Promise.all([
// Fetch DB definitions, trigger configs, agent configs (for enabled check), and integrations
const [dbDefinitions, configs, projectAgentConfigs, integrations] = await Promise.all([
listAgentDefinitions().catch((err) => {
logger.warn('Failed to fetch agent definitions from DB', { error: err });
return [];
}),
getTriggerConfigsByProject(input.projectId),
listAgentConfigs({ projectId: input.projectId }),
listProjectIntegrations(input.projectId),
]);

// Build set of explicitly enabled agent types for this project
const enabledAgentTypes = new Set(projectAgentConfigs.map((c) => c.agentType));

// Build a combined list of definitions (DB + YAML)
const yamlTypes = getKnownAgentTypes();
const definitions: Array<{ agentType: string; definition: AgentDefinition }> = [];
Expand Down Expand Up @@ -280,12 +285,12 @@ export const agentTriggerConfigsRouter = router({
};
}

// Build the agents array with merged trigger data
const agents = definitions.map((def) => {
const agentConfigs = configMap.get(def.agentType);
// Build merged trigger data for a definition
function buildAgentTriggersView(def: { agentType: string; definition: AgentDefinition }) {
const agentTriggerConfigs = configMap.get(def.agentType);
const triggers: ResolvedTrigger[] = (def.definition.triggers ?? []).map(
(trigger: SupportedTrigger) => {
const config = agentConfigs?.get(trigger.event);
const config = agentTriggerConfigs?.get(trigger.event);
return {
event: trigger.event,
label: trigger.label,
Expand All @@ -301,12 +306,18 @@ export const agentTriggerConfigsRouter = router({
};
},
);
return { agentType: def.agentType, triggers };
}

return {
agentType: def.agentType,
triggers,
};
});
// Split definitions into enabled (have agent_configs row) and available (no row)
// The debug agent is always shown as enabled (internal infrastructure)
const enabledAgents = definitions
.filter((def) => enabledAgentTypes.has(def.agentType) || def.agentType === 'debug')
.map(buildAgentTriggersView);

const availableAgents = definitions
.filter((def) => !enabledAgentTypes.has(def.agentType) && def.agentType !== 'debug')
.map((def) => def.agentType);

// Build integrations map with single pass
const integrationsMap = {
Expand All @@ -321,7 +332,9 @@ export const agentTriggerConfigsRouter = router({
}

return {
agents,
agents: enabledAgents, // backwards compat: same as enabledAgents
enabledAgents,
availableAgents,
integrations: integrationsMap,
};
}),
Expand Down
10 changes: 10 additions & 0 deletions src/api/routers/runs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { loadProjectConfigById } from '../../config/provider.js';
import { isAgentEnabledForProject } from '../../db/repositories/agentConfigsRepository.js';
import {
DEFAULT_STALE_RUN_THRESHOLD_MS,
cancelRunById,
Expand Down Expand Up @@ -274,6 +275,15 @@ export const runsRouter = router({
});
}

// Check agent is explicitly enabled for this project
const agentEnabled = await isAgentEnabledForProject(input.projectId, input.agentType);
if (!agentEnabled) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: `Agent '${input.agentType}' is not enabled for this project. Add an agent config in Project Settings > Agent Configs to enable it.`,
});
}

if (useQueue) {
const { submitDashboardJob } = await import('../../queue/client.js');
await submitDashboardJob({
Expand Down
8 changes: 7 additions & 1 deletion src/cli/dashboard/agents/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Flags } from '@oclif/core';
import { DashboardCommand } from '../_shared/base.js';

export default class AgentsList extends DashboardCommand {
static override description = 'List agent configurations for a project.';
static override description =
'List enabled agent configurations for a project. Only agents with an explicit config row are shown (opt-in required).';

static override flags = {
...DashboardCommand.baseFlags,
Expand All @@ -22,6 +23,11 @@ export default class AgentsList extends DashboardCommand {
return;
}

if (configs.length === 0) {
this.log('No agents enabled for this project. Use `cascade agents create` to enable one.');
return;
}

this.outputTable(configs as unknown as Record<string, unknown>[], [
{ key: 'id', header: 'ID' },
{ key: 'agentType', header: 'Agent Type' },
Expand Down
50 changes: 50 additions & 0 deletions src/db/repositories/agentConfigsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,56 @@ export async function getAgentConfigPrompts(
return result;
}

/**
* Check whether an agent is explicitly enabled for a project.
* An agent is enabled if and only if it has a row in `agent_configs` for that project.
* The `debug` agent is always considered enabled (internal infrastructure).
*
* Results are cached for 5 seconds to avoid repeated DB queries on
* sequential webhook batches.
*/
const AGENT_ENABLED_TTL_MS = 5_000;
const agentEnabledCache = new Map<string, { value: boolean; expiresAt: number }>();

export async function isAgentEnabledForProject(
projectId: string,
agentType: string,
): Promise<boolean> {
// Debug agent is always enabled — internal infrastructure agent
if (agentType === 'debug') {
return true;
}

const cacheKey = `${projectId}:${agentType}`;
const cached = agentEnabledCache.get(cacheKey);
if (cached && Date.now() < cached.expiresAt) {
return cached.value;
}

const db = getDb();

const [row] = await db
.select({ id: agentConfigs.id })
.from(agentConfigs)
.where(and(eq(agentConfigs.projectId, projectId), eq(agentConfigs.agentType, agentType)))
.limit(1);

const result = row !== undefined;
agentEnabledCache.set(cacheKey, {
value: result,
expiresAt: Date.now() + AGENT_ENABLED_TTL_MS,
});
return result;
}

/**
* Clear the agent-enabled cache (for testing only).
* This allows integration tests to seed agent configs and see them without waiting for TTL expiry.
*/
export function clearAgentEnabledCache(): void {
agentEnabledCache.clear();
}

/**
* Resolve max_concurrency for a (projectId, agentType) pair.
* Returns null if no project-scoped config with max_concurrency is found (= no limit).
Expand Down
Loading
Loading