diff --git a/src/agents/definitions/backlog-manager.yaml b/src/agents/definitions/backlog-manager.yaml
index 3ba81f1f..f1d0ada4 100644
--- a/src/agents/definitions/backlog-manager.yaml
+++ b/src/agents/definitions/backlog-manager.yaml
@@ -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: {}
diff --git a/src/agents/definitions/implementation.yaml b/src/agents/definitions/implementation.yaml
index 0e7bee91..912f99d6 100644
--- a/src/agents/definitions/implementation.yaml
+++ b/src/agents/definitions/implementation.yaml
@@ -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
@@ -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
diff --git a/src/agents/definitions/planning.yaml b/src/agents/definitions/planning.yaml
index 950a510a..7ea28d07 100644
--- a/src/agents/definitions/planning.yaml
+++ b/src/agents/definitions/planning.yaml
@@ -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
@@ -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
@@ -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: {}
diff --git a/src/agents/definitions/resolve-conflicts.yaml b/src/agents/definitions/resolve-conflicts.yaml
index 10e38bff..c7a7591f 100644
--- a/src/agents/definitions/resolve-conflicts.yaml
+++ b/src/agents/definitions/resolve-conflicts.yaml
@@ -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]
diff --git a/src/agents/definitions/respond-to-ci.yaml b/src/agents/definitions/respond-to-ci.yaml
index 80068950..23d64ae9 100644
--- a/src/agents/definitions/respond-to-ci.yaml
+++ b/src/agents/definitions/respond-to-ci.yaml
@@ -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]
diff --git a/src/agents/definitions/respond-to-planning-comment.yaml b/src/agents/definitions/respond-to-planning-comment.yaml
index db223d54..b2f1db7a 100644
--- a/src/agents/definitions/respond-to-planning-comment.yaml
+++ b/src/agents/definitions/respond-to-planning-comment.yaml
@@ -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: {}
diff --git a/src/agents/definitions/respond-to-pr-comment.yaml b/src/agents/definitions/respond-to-pr-comment.yaml
index 7408d28e..b962b80c 100644
--- a/src/agents/definitions/respond-to-pr-comment.yaml
+++ b/src/agents/definitions/respond-to-pr-comment.yaml
@@ -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]
diff --git a/src/agents/definitions/respond-to-review.yaml b/src/agents/definitions/respond-to-review.yaml
index 061187f8..5a768c83 100644
--- a/src/agents/definitions/respond-to-review.yaml
+++ b/src/agents/definitions/respond-to-review.yaml
@@ -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]
diff --git a/src/agents/definitions/review.yaml b/src/agents/definitions/review.yaml
index bedb64a5..3bd11888 100644
--- a/src/agents/definitions/review.yaml
+++ b/src/agents/definitions/review.yaml
@@ -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: []
diff --git a/src/agents/definitions/schema.ts b/src/agents/definitions/schema.ts
index 9dafd15e..16492314 100644
--- a/src/agents/definitions/schema.ts
+++ b/src/agents/definitions/schema.ts
@@ -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']) */
diff --git a/src/agents/definitions/splitting.yaml b/src/agents/definitions/splitting.yaml
index 974918c8..e8dfa87e 100644
--- a/src/agents/definitions/splitting.yaml
+++ b/src/agents/definitions/splitting.yaml
@@ -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
@@ -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
diff --git a/src/api/routers/_shared/triggerTypes.ts b/src/api/routers/_shared/triggerTypes.ts
index 29218544..1b8f1e23 100644
--- a/src/api/routers/_shared/triggerTypes.ts
+++ b/src/api/routers/_shared/triggerTypes.ts
@@ -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;
}
diff --git a/src/api/routers/agentTriggerConfigs.ts b/src/api/routers/agentTriggerConfigs.ts
index 7c94ee5a..65812ccf 100644
--- a/src/api/routers/agentTriggerConfigs.ts
+++ b/src/api/routers/agentTriggerConfigs.ts
@@ -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,
@@ -204,16 +205,20 @@ export const agentTriggerConfigsRouter = router({
.query(async ({ ctx, input }): Promise => {
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 }> = [];
@@ -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,
@@ -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 = {
@@ -321,7 +332,9 @@ export const agentTriggerConfigsRouter = router({
}
return {
- agents,
+ agents: enabledAgents, // backwards compat: same as enabledAgents
+ enabledAgents,
+ availableAgents,
integrations: integrationsMap,
};
}),
diff --git a/src/api/routers/runs.ts b/src/api/routers/runs.ts
index 7f70ca2e..374ca937 100644
--- a/src/api/routers/runs.ts
+++ b/src/api/routers/runs.ts
@@ -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,
@@ -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({
diff --git a/src/cli/dashboard/agents/list.ts b/src/cli/dashboard/agents/list.ts
index 71422313..b8802229 100644
--- a/src/cli/dashboard/agents/list.ts
+++ b/src/cli/dashboard/agents/list.ts
@@ -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,
@@ -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[], [
{ key: 'id', header: 'ID' },
{ key: 'agentType', header: 'Agent Type' },
diff --git a/src/db/repositories/agentConfigsRepository.ts b/src/db/repositories/agentConfigsRepository.ts
index 85448c3d..eaaf7bf7 100644
--- a/src/db/repositories/agentConfigsRepository.ts
+++ b/src/db/repositories/agentConfigsRepository.ts
@@ -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();
+
+export async function isAgentEnabledForProject(
+ projectId: string,
+ agentType: string,
+): Promise {
+ // 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).
diff --git a/src/triggers/config-resolver.ts b/src/triggers/config-resolver.ts
index 94397113..df207f24 100644
--- a/src/triggers/config-resolver.ts
+++ b/src/triggers/config-resolver.ts
@@ -38,6 +38,7 @@
import { resolveAgentDefinition } from '../agents/definitions/index.js';
import type { SupportedTrigger } from '../agents/definitions/schema.js';
+import { isAgentEnabledForProject } from '../db/repositories/agentConfigsRepository.js';
import {
type AgentTriggerConfig,
getTriggerConfig,
@@ -77,6 +78,12 @@ export async function resolveTriggerConfigs(
projectId: string,
agentType: string,
): Promise {
+ // Gate on agent config existence — agent must be explicitly enabled
+ const enabled = await isAgentEnabledForProject(projectId, agentType);
+ if (!enabled) {
+ return [];
+ }
+
// Get definition triggers
const definition = await resolveAgentDefinition(agentType);
if (!definition) {
@@ -111,6 +118,12 @@ export async function isTriggerEnabled(
agentType: string,
triggerEvent: string,
): Promise {
+ // Gate on agent config existence — agent must be explicitly enabled
+ const agentEnabled = await isAgentEnabledForProject(projectId, agentType);
+ if (!agentEnabled) {
+ return false;
+ }
+
// First check DB override
const dbConfig = await getTriggerConfig(projectId, agentType, triggerEvent);
if (dbConfig) {
@@ -140,6 +153,12 @@ export async function getTriggerParameters(
agentType: string,
triggerEvent: string,
): Promise> {
+ // Gate on agent config existence — agent must be explicitly enabled
+ const agentEnabled = await isAgentEnabledForProject(projectId, agentType);
+ if (!agentEnabled) {
+ return {};
+ }
+
const definition = await resolveAgentDefinition(agentType);
if (!definition) {
return {};
@@ -176,6 +195,12 @@ export async function getResolvedTriggerConfig(
agentType: string,
triggerEvent: string,
): Promise {
+ // Gate on agent config existence — agent must be explicitly enabled
+ const agentEnabled = await isAgentEnabledForProject(projectId, agentType);
+ if (!agentEnabled) {
+ return null;
+ }
+
const definition = await resolveAgentDefinition(agentType);
if (!definition) {
return null;
diff --git a/src/triggers/shared/manual-runner.ts b/src/triggers/shared/manual-runner.ts
index e3862763..715507a0 100644
--- a/src/triggers/shared/manual-runner.ts
+++ b/src/triggers/shared/manual-runner.ts
@@ -1,4 +1,5 @@
import { runAgent } from '../../agents/registry.js';
+import { isAgentEnabledForProject } from '../../db/repositories/agentConfigsRepository.js';
import { getRunById } from '../../db/repositories/runsRepository.js';
import { withPMCredentials } from '../../pm/context.js';
import { createPMProvider, pmRegistry, withPMProvider } from '../../pm/index.js';
@@ -79,6 +80,14 @@ export async function triggerManualRun(
);
}
+ // Check agent is explicitly enabled for this project
+ const agentEnabled = await isAgentEnabledForProject(input.projectId, input.agentType);
+ if (!agentEnabled) {
+ throw new Error(
+ `Agent '${input.agentType}' is not enabled for project '${input.projectId}'. Add an agent config in Project Settings > Agent Configs to enable it.`,
+ );
+ }
+
// Pre-flight integration validation
const validation = await validateIntegrations(input.projectId, input.agentType);
if (!validation.valid) {
diff --git a/tests/integration/github-personas.test.ts b/tests/integration/github-personas.test.ts
index 19fe6451..c4cf9639 100644
--- a/tests/integration/github-personas.test.ts
+++ b/tests/integration/github-personas.test.ts
@@ -22,7 +22,13 @@ import { ReviewRequestedTrigger } from '../../src/triggers/github/review-request
import type { TriggerContext } from '../../src/types/index.js';
import { assertFound } from './helpers/assert.js';
import { truncateAll } from './helpers/db.js';
-import { seedIntegration, seedOrg, seedProject, seedTriggerConfig } from './helpers/seed.js';
+import {
+ seedAgentConfig,
+ seedIntegration,
+ seedOrg,
+ seedProject,
+ seedTriggerConfig,
+} from './helpers/seed.js';
// ============================================================================
// Helpers
@@ -229,6 +235,13 @@ describe('GitHub Dual-Persona System (integration)', () => {
config: {},
triggers: { prReviewSubmitted: true },
});
+ // Agent must be explicitly enabled for the trigger to fire
+ await seedAgentConfig({ agentType: 'respond-to-review' });
+ await seedTriggerConfig({
+ agentType: 'respond-to-review',
+ triggerEvent: 'scm:pr-review-submitted',
+ enabled: true,
+ });
const project = await findProjectByRepoFromDb('owner/repo');
expect(project).toBeDefined();
@@ -350,6 +363,8 @@ describe('GitHub Dual-Persona System (integration)', () => {
provider: 'github',
config: {},
});
+ // Agent must be explicitly enabled for the trigger to fire
+ await seedAgentConfig({ agentType: 'review' });
await seedTriggerConfig({
agentType: 'review',
triggerEvent: 'scm:review-requested',
diff --git a/tests/integration/helpers/db.ts b/tests/integration/helpers/db.ts
index df3720ad..f1c605e1 100644
--- a/tests/integration/helpers/db.ts
+++ b/tests/integration/helpers/db.ts
@@ -4,6 +4,7 @@ import net from 'node:net';
import path from 'node:path';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { _setTestDb, closeDb, getDb } from '../../../src/db/client.js';
+import { clearAgentEnabledCache } from '../../../src/db/repositories/agentConfigsRepository.js';
function checkPortReachable(host: string, port: number, timeoutMs = 500): Promise {
return new Promise((resolve) => {
@@ -98,6 +99,7 @@ export async function runMigrations() {
/**
* Truncates all application tables in dependency order.
* Call in `beforeEach` to isolate tests.
+ * Also clears in-memory repository caches so tests see fresh DB state.
*/
export async function truncateAll() {
const db = getDb();
@@ -121,6 +123,8 @@ export async function truncateAll() {
organizations
CASCADE
`);
+ // Clear in-memory caches so subsequent tests see fresh DB state
+ clearAgentEnabledCache();
}
/**
diff --git a/tests/integration/pm-provider-switching.test.ts b/tests/integration/pm-provider-switching.test.ts
index c866a576..3d8f8fcf 100644
--- a/tests/integration/pm-provider-switching.test.ts
+++ b/tests/integration/pm-provider-switching.test.ts
@@ -21,7 +21,13 @@ import { TrelloStatusChangedTodoTrigger } from '../../src/triggers/trello/status
import type { TriggerContext } from '../../src/types/index.js';
import { assertFound } from './helpers/assert.js';
import { truncateAll } from './helpers/db.js';
-import { seedIntegration, seedOrg, seedProject } from './helpers/seed.js';
+import {
+ seedAgentConfig,
+ seedIntegration,
+ seedOrg,
+ seedProject,
+ seedTriggerConfig,
+} from './helpers/seed.js';
// ============================================================================
// Helpers
@@ -226,6 +232,13 @@ describe('PM Provider Switching (integration)', () => {
labels: {},
},
});
+ // Agent must be explicitly enabled for the trigger to fire
+ await seedAgentConfig({ agentType: 'implementation' });
+ await seedTriggerConfig({
+ agentType: 'implementation',
+ triggerEvent: 'pm:status-changed',
+ enabled: true,
+ });
const project = await findProjectByBoardIdFromDb('board-123');
expect(project).toBeDefined();
@@ -252,6 +265,13 @@ describe('PM Provider Switching (integration)', () => {
statuses: { todo: 'To Do', planning: 'In Planning', splitting: 'Splitting' },
},
});
+ // Agent must be explicitly enabled for the trigger to fire
+ await seedAgentConfig({ agentType: 'implementation' });
+ await seedTriggerConfig({
+ agentType: 'implementation',
+ triggerEvent: 'pm:status-changed',
+ enabled: true,
+ });
const project = await findProjectByJiraProjectKeyFromDb('IMPL');
expect(project).toBeDefined();
@@ -279,6 +299,13 @@ describe('PM Provider Switching (integration)', () => {
statuses: { todo: 'To Do', planning: 'In Planning', splitting: 'Splitting' },
},
});
+ // Agent must be explicitly enabled for the trigger to fire
+ await seedAgentConfig({ agentType: 'planning' });
+ await seedTriggerConfig({
+ agentType: 'planning',
+ triggerEvent: 'pm:status-changed',
+ enabled: true,
+ });
const project = await findProjectByJiraProjectKeyFromDb('PLAN');
expect(project).toBeDefined();
diff --git a/tests/integration/trigger-registry.test.ts b/tests/integration/trigger-registry.test.ts
index 5e3405fb..09bc2a56 100644
--- a/tests/integration/trigger-registry.test.ts
+++ b/tests/integration/trigger-registry.test.ts
@@ -20,7 +20,13 @@ import {
import type { TriggerContext } from '../../src/types/index.js';
import { assertFound } from './helpers/assert.js';
import { truncateAll } from './helpers/db.js';
-import { seedIntegration, seedOrg, seedProject, seedTriggerConfig } from './helpers/seed.js';
+import {
+ seedAgentConfig,
+ seedIntegration,
+ seedOrg,
+ seedProject,
+ seedTriggerConfig,
+} from './helpers/seed.js';
// ============================================================================
// Helpers
@@ -291,6 +297,14 @@ describe('Trigger Registry (integration)', () => {
labels: {},
},
});
+ // Agent must be explicitly enabled for the trigger to fire
+ await seedAgentConfig({ agentType: 'implementation' });
+ // Seed trigger config to enable the trigger
+ await seedTriggerConfig({
+ agentType: 'implementation',
+ triggerEvent: 'pm:status-changed',
+ enabled: true,
+ });
const project = await findProjectByBoardIdFromDb('board-123');
@@ -323,6 +337,14 @@ describe('Trigger Registry (integration)', () => {
labels: {},
},
});
+ // Agent must be explicitly enabled for the trigger to fire
+ await seedAgentConfig({ agentType: 'splitting' });
+ // Seed trigger config to enable the trigger
+ await seedTriggerConfig({
+ agentType: 'splitting',
+ triggerEvent: 'pm:status-changed',
+ enabled: true,
+ });
const project = await findProjectByBoardIdFromDb('board-123');
@@ -353,6 +375,14 @@ describe('Trigger Registry (integration)', () => {
labels: {},
},
});
+ // Agent must be explicitly enabled for the trigger to fire
+ await seedAgentConfig({ agentType: 'planning' });
+ // Seed trigger config to enable the trigger
+ await seedTriggerConfig({
+ agentType: 'planning',
+ triggerEvent: 'pm:status-changed',
+ enabled: true,
+ });
const project = await findProjectByBoardIdFromDb('board-123');
@@ -525,6 +555,20 @@ describe('Trigger Registry (integration)', () => {
labels: { readyToProcess: 'Ready to Process' },
},
});
+ // Agents must be explicitly enabled for triggers to fire
+ await seedAgentConfig({ agentType: 'implementation' });
+ await seedAgentConfig({ agentType: 'splitting' });
+ // Seed trigger configs to enable the triggers
+ await seedTriggerConfig({
+ agentType: 'implementation',
+ triggerEvent: 'pm:status-changed',
+ enabled: true,
+ });
+ await seedTriggerConfig({
+ agentType: 'splitting',
+ triggerEvent: 'pm:status-changed',
+ enabled: true,
+ });
const registry = createTriggerRegistry();
registry.register(TrelloStatusChangedSplittingTrigger);
diff --git a/tests/unit/agents/definitions/loader.test.ts b/tests/unit/agents/definitions/loader.test.ts
index 807d1655..f5c747cb 100644
--- a/tests/unit/agents/definitions/loader.test.ts
+++ b/tests/unit/agents/definitions/loader.test.ts
@@ -279,10 +279,10 @@ describe('YAML agent definitions loader', () => {
}
});
- it('backlog-manager internal:auto-chain trigger is defaultEnabled: true', () => {
+ it('backlog-manager internal:auto-chain trigger is defaultEnabled: false (all triggers off by default)', () => {
const def = loadAgentDefinition('backlog-manager');
const autoChainTrigger = def.triggers.find((t) => t.event === 'internal:auto-chain');
- expect(autoChainTrigger?.defaultEnabled).toBe(true);
+ expect(autoChainTrigger?.defaultEnabled).toBe(false);
});
it('backlog-manager requires only pm integration', () => {
diff --git a/tests/unit/api/routers/agentTriggerConfigs.getProjectTriggersView.test.ts b/tests/unit/api/routers/agentTriggerConfigs.getProjectTriggersView.test.ts
index 07a279d8..653414d1 100644
--- a/tests/unit/api/routers/agentTriggerConfigs.getProjectTriggersView.test.ts
+++ b/tests/unit/api/routers/agentTriggerConfigs.getProjectTriggersView.test.ts
@@ -11,6 +11,7 @@ const mockGetTriggerConfigsByProject = vi.fn();
const mockListProjectIntegrations = vi.fn();
const mockGetKnownAgentTypes = vi.fn();
const mockLoadAgentDefinition = vi.fn();
+const mockListAgentConfigs = vi.fn();
vi.mock('../../../../src/db/repositories/agentDefinitionsRepository.js', () => ({
listAgentDefinitions: (...args: unknown[]) => mockListAgentDefinitions(...args),
@@ -30,6 +31,11 @@ vi.mock('../../../../src/db/repositories/settingsRepository.js', () => ({
listProjectIntegrations: (...args: unknown[]) => mockListProjectIntegrations(...args),
}));
+vi.mock('../../../../src/db/repositories/agentConfigsRepository.js', () => ({
+ listAgentConfigs: (...args: unknown[]) => mockListAgentConfigs(...args),
+ isAgentEnabledForProject: vi.fn().mockResolvedValue(true),
+}));
+
vi.mock('../../../../src/agents/definitions/loader.js', () => ({
getKnownAgentTypes: (...args: unknown[]) => mockGetKnownAgentTypes(...args),
loadAgentDefinition: (...args: unknown[]) => mockLoadAgentDefinition(...args),
@@ -86,6 +92,8 @@ describe('agentTriggerConfigsRouter — getProjectTriggersView', () => {
mockListAgentDefinitions.mockResolvedValue([]);
mockGetKnownAgentTypes.mockReturnValue([]);
mockLoadAgentDefinition.mockReturnValue(makeAgentDefinition());
+ // Default: no agent configs (all agents are unconfigured / available)
+ mockListAgentConfigs.mockResolvedValue([]);
});
it('throws UNAUTHORIZED when not authenticated', async () => {
@@ -95,17 +103,32 @@ describe('agentTriggerConfigsRouter — getProjectTriggersView', () => {
).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
});
- it('returns empty agents and null integrations when nothing is configured', async () => {
+ it('returns empty enabledAgents and null integrations when nothing is configured', async () => {
const caller = createCaller(mockCtx);
const result = await caller.getProjectTriggersView({ projectId: 'test-project' });
- expect(result.agents).toEqual([]);
+ expect(result.enabledAgents).toEqual([]);
+ expect(result.agents).toEqual([]); // backwards compat alias
expect(result.integrations).toEqual({ pm: null, scm: null });
});
+ it('returns availableAgents for unconfigured agent types', async () => {
+ const definition = makeAgentDefinition();
+ mockListAgentDefinitions.mockResolvedValue([{ agentType: 'implementation', definition }]);
+ // No agent_configs row → implementation should be in availableAgents, not enabledAgents
+
+ const caller = createCaller(mockCtx);
+ const result = await caller.getProjectTriggersView({ projectId: 'test-project' });
+
+ expect(result.enabledAgents).toHaveLength(0);
+ expect(result.availableAgents).toContain('implementation');
+ });
+
it('merges DB definitions with project trigger configs', async () => {
const definition = makeAgentDefinition();
mockListAgentDefinitions.mockResolvedValue([{ agentType: 'implementation', definition }]);
+ // Agent has an agent_configs row → it is enabled
+ mockListAgentConfigs.mockResolvedValue([{ agentType: 'implementation', id: 1 }]);
mockGetTriggerConfigsByProject.mockResolvedValue([
{
agentType: 'implementation',
@@ -118,16 +141,18 @@ describe('agentTriggerConfigsRouter — getProjectTriggersView', () => {
const caller = createCaller(mockCtx);
const result = await caller.getProjectTriggersView({ projectId: 'test-project' });
- expect(result.agents).toHaveLength(1);
+ expect(result.enabledAgents).toHaveLength(1);
+ expect(result.agents).toHaveLength(1); // backwards compat
expect(result.agents[0].agentType).toBe('implementation');
expect(result.agents[0].triggers[0].event).toBe('pm:status-changed');
expect(result.agents[0].triggers[0].enabled).toBe(true);
expect(result.agents[0].triggers[0].isCustomized).toBe(true);
});
- it('uses defaultEnabled when no config exists (isCustomized=false)', async () => {
+ it('uses defaultEnabled when no trigger config exists (isCustomized=false)', async () => {
const definition = makeAgentDefinition();
mockListAgentDefinitions.mockResolvedValue([{ agentType: 'implementation', definition }]);
+ mockListAgentConfigs.mockResolvedValue([{ agentType: 'implementation', id: 1 }]);
// No trigger configs
const caller = createCaller(mockCtx);
@@ -164,6 +189,7 @@ describe('agentTriggerConfigsRouter — getProjectTriggersView', () => {
mockListAgentDefinitions.mockResolvedValue([
{ agentType: 'review', definition: definitionWithParams },
]);
+ mockListAgentConfigs.mockResolvedValue([{ agentType: 'review', id: 2 }]);
mockGetTriggerConfigsByProject.mockResolvedValue([
{
agentType: 'review',
@@ -209,6 +235,7 @@ describe('agentTriggerConfigsRouter — getProjectTriggersView', () => {
mockListAgentDefinitions.mockResolvedValue([
{ agentType: 'review', definition: definitionWithParams },
]);
+ mockListAgentConfigs.mockResolvedValue([{ agentType: 'review', id: 2 }]);
mockGetTriggerConfigsByProject.mockResolvedValue([
{
agentType: 'review',
@@ -253,12 +280,13 @@ describe('agentTriggerConfigsRouter — getProjectTriggersView', () => {
// Falls back to YAML — need some types for that
mockGetKnownAgentTypes.mockReturnValue(['implementation']);
mockLoadAgentDefinition.mockReturnValue(makeAgentDefinition());
+ mockListAgentConfigs.mockResolvedValue([{ agentType: 'implementation', id: 1 }]);
const caller = createCaller(mockCtx);
const result = await caller.getProjectTriggersView({ projectId: 'test-project' });
- // Should not throw; falls back to YAML
- expect(result.agents).toHaveLength(1);
+ // Should not throw; falls back to YAML, and shows as enabled since it has a config
+ expect(result.enabledAgents).toHaveLength(1);
expect(result.agents[0].agentType).toBe('implementation');
});
@@ -267,6 +295,7 @@ describe('agentTriggerConfigsRouter — getProjectTriggersView', () => {
mockListAgentDefinitions.mockResolvedValue([{ agentType: 'implementation', definition }]);
// YAML also has 'implementation'
mockGetKnownAgentTypes.mockReturnValue(['implementation']);
+ mockListAgentConfigs.mockResolvedValue([{ agentType: 'implementation', id: 1 }]);
const caller = createCaller(mockCtx);
const result = await caller.getProjectTriggersView({ projectId: 'test-project' });
@@ -275,17 +304,20 @@ describe('agentTriggerConfigsRouter — getProjectTriggersView', () => {
expect(result.agents).toHaveLength(1);
});
- it('includes YAML-only agents not in DB', async () => {
+ it('enabled agents appear in enabledAgents; unconfigured appear in availableAgents', async () => {
mockListAgentDefinitions.mockResolvedValue([]); // no DB definitions
mockGetKnownAgentTypes.mockReturnValue(['splitting', 'planning']);
mockLoadAgentDefinition.mockReturnValue(makeAgentDefinition());
+ // Only 'splitting' is enabled
+ mockListAgentConfigs.mockResolvedValue([{ agentType: 'splitting', id: 1 }]);
const caller = createCaller(mockCtx);
const result = await caller.getProjectTriggersView({ projectId: 'test-project' });
- expect(result.agents).toHaveLength(2);
- expect(result.agents.map((a) => a.agentType)).toContain('splitting');
- expect(result.agents.map((a) => a.agentType)).toContain('planning');
+ expect(result.enabledAgents).toHaveLength(1);
+ expect(result.enabledAgents[0].agentType).toBe('splitting');
+ expect(result.availableAgents).toContain('planning');
+ expect(result.availableAgents).not.toContain('splitting');
});
it('handles YAML load failure gracefully (skips that agent)', async () => {
@@ -295,11 +327,12 @@ describe('agentTriggerConfigsRouter — getProjectTriggersView', () => {
.mockImplementationOnce(() => {
throw new Error('YAML parse error');
});
+ mockListAgentConfigs.mockResolvedValue([{ agentType: 'implementation', id: 1 }]);
const caller = createCaller(mockCtx);
const result = await caller.getProjectTriggersView({ projectId: 'test-project' });
- // 'failing-agent' should be skipped; 'implementation' included
+ // 'failing-agent' should be skipped; 'implementation' included in enabled
expect(result.agents).toHaveLength(1);
expect(result.agents[0].agentType).toBe('implementation');
});
@@ -312,7 +345,7 @@ describe('agentTriggerConfigsRouter — getProjectTriggersView', () => {
label: 'Status Changed',
description: 'When status changes',
providers: null,
- defaultEnabled: true,
+ defaultEnabled: false,
parameters: [
{
name: 'myParam',
@@ -328,6 +361,7 @@ describe('agentTriggerConfigsRouter — getProjectTriggersView', () => {
],
};
mockListAgentDefinitions.mockResolvedValue([{ agentType: 'implementation', definition }]);
+ mockListAgentConfigs.mockResolvedValue([{ agentType: 'implementation', id: 1 }]);
const caller = createCaller(mockCtx);
const result = await caller.getProjectTriggersView({ projectId: 'test-project' });
@@ -345,6 +379,7 @@ describe('agentTriggerConfigsRouter — getProjectTriggersView', () => {
it('handles trigger with no parameters (empty parameterDefs and parameters)', async () => {
const definition = makeAgentDefinition();
mockListAgentDefinitions.mockResolvedValue([{ agentType: 'implementation', definition }]);
+ mockListAgentConfigs.mockResolvedValue([{ agentType: 'implementation', id: 1 }]);
const caller = createCaller(mockCtx);
const result = await caller.getProjectTriggersView({ projectId: 'test-project' });
diff --git a/tests/unit/api/routers/runs.test.ts b/tests/unit/api/routers/runs.test.ts
index 7a49b96f..68f39087 100644
--- a/tests/unit/api/routers/runs.test.ts
+++ b/tests/unit/api/routers/runs.test.ts
@@ -79,6 +79,12 @@ vi.mock('../../../../src/queue/cancel.js', () => ({
publishCancelCommand: (...args: unknown[]) => mockPublishCancelCommand(...args),
}));
+// Mock isAgentEnabledForProject — default: agent is enabled
+const mockIsAgentEnabledForProject = vi.fn().mockResolvedValue(true);
+vi.mock('../../../../src/db/repositories/agentConfigsRepository.js', () => ({
+ isAgentEnabledForProject: (...args: unknown[]) => mockIsAgentEnabledForProject(...args),
+}));
+
import { runsRouter } from '../../../../src/api/routers/runs.js';
function createCaller(ctx: TRPCContext) {
@@ -851,6 +857,20 @@ describe('runsRouter', () => {
).rejects.toMatchObject({ code: 'NOT_FOUND' });
});
+ it('throws BAD_REQUEST when agent is not enabled for the project', async () => {
+ mockDbWhere.mockResolvedValue([{ orgId: 'org-1' }]);
+ mockLoadProjectConfigById.mockResolvedValue({
+ project: { id: 'p1', name: 'Test Project' },
+ config: {},
+ });
+ mockIsAgentEnabledForProject.mockResolvedValueOnce(false);
+
+ const caller = createCaller({ user: mockUser, effectiveOrgId: mockUser.orgId });
+ await expect(
+ caller.trigger({ projectId: 'p1', agentType: 'implementation' }),
+ ).rejects.toMatchObject({ code: 'BAD_REQUEST' });
+ });
+
it('throws UNAUTHORIZED when unauthenticated', async () => {
const caller = createCaller({ user: null, effectiveOrgId: null });
await expect(
diff --git a/tests/unit/triggers/config-resolver.test.ts b/tests/unit/triggers/config-resolver.test.ts
index f0a3ec94..3965c4e5 100644
--- a/tests/unit/triggers/config-resolver.test.ts
+++ b/tests/unit/triggers/config-resolver.test.ts
@@ -1,12 +1,18 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
// Hoist mocks before any imports
-const { mockResolveAgentDefinition, mockGetTriggerConfig, mockGetTriggerConfigsByProjectAndAgent } =
- vi.hoisted(() => ({
- mockResolveAgentDefinition: vi.fn(),
- mockGetTriggerConfig: vi.fn(),
- mockGetTriggerConfigsByProjectAndAgent: vi.fn(),
- }));
+const {
+ mockResolveAgentDefinition,
+ mockGetTriggerConfig,
+ mockGetTriggerConfigsByProjectAndAgent,
+ mockIsAgentEnabledForProject,
+} = vi.hoisted(() => ({
+ mockResolveAgentDefinition: vi.fn(),
+ mockGetTriggerConfig: vi.fn(),
+ mockGetTriggerConfigsByProjectAndAgent: vi.fn(),
+ // Default: agent is enabled (has a config row)
+ mockIsAgentEnabledForProject: vi.fn().mockResolvedValue(true),
+}));
vi.mock('../../../src/agents/definitions/index.js', () => ({
resolveAgentDefinition: mockResolveAgentDefinition,
@@ -17,6 +23,10 @@ vi.mock('../../../src/db/repositories/agentTriggerConfigsRepository.js', () => (
getTriggerConfigsByProjectAndAgent: mockGetTriggerConfigsByProjectAndAgent,
}));
+vi.mock('../../../src/db/repositories/agentConfigsRepository.js', () => ({
+ isAgentEnabledForProject: mockIsAgentEnabledForProject,
+}));
+
import {
getResolvedTriggerConfig,
getTriggerParameters,
@@ -68,6 +78,15 @@ function makeDbConfig(overrides: Record = {}) {
describe('resolveTriggerConfigs', () => {
beforeEach(() => {
vi.resetAllMocks();
+ // Default: agent is enabled (has a config row)
+ mockIsAgentEnabledForProject.mockResolvedValue(true);
+ });
+
+ it('returns empty array when agent is not enabled for project (no config row)', async () => {
+ mockIsAgentEnabledForProject.mockResolvedValue(false);
+ const result = await resolveTriggerConfigs(PROJECT_ID, AGENT_TYPE);
+ expect(result).toEqual([]);
+ expect(mockResolveAgentDefinition).not.toHaveBeenCalled();
});
it('returns empty array when agent definition is not found', async () => {
@@ -143,6 +162,19 @@ describe('resolveTriggerConfigs', () => {
describe('isTriggerEnabled', () => {
beforeEach(() => {
vi.resetAllMocks();
+ // Default: agent is enabled (has a config row)
+ mockIsAgentEnabledForProject.mockResolvedValue(true);
+ });
+
+ it('returns false when agent has no config row (not enabled for project)', async () => {
+ mockIsAgentEnabledForProject.mockResolvedValue(false);
+
+ const result = await isTriggerEnabled(PROJECT_ID, AGENT_TYPE, TRIGGER_EVENT);
+
+ expect(result).toBe(false);
+ // Should not check DB trigger config or definition
+ expect(mockGetTriggerConfig).not.toHaveBeenCalled();
+ expect(mockResolveAgentDefinition).not.toHaveBeenCalled();
});
it('returns DB override enabled value when config exists', async () => {
@@ -195,6 +227,17 @@ describe('isTriggerEnabled', () => {
describe('getTriggerParameters', () => {
beforeEach(() => {
vi.resetAllMocks();
+ // Default: agent is enabled (has a config row)
+ mockIsAgentEnabledForProject.mockResolvedValue(true);
+ });
+
+ it('returns empty object when agent is not enabled for project (no config row)', async () => {
+ mockIsAgentEnabledForProject.mockResolvedValue(false);
+
+ const result = await getTriggerParameters(PROJECT_ID, AGENT_TYPE, TRIGGER_EVENT);
+
+ expect(result).toEqual({});
+ expect(mockResolveAgentDefinition).not.toHaveBeenCalled();
});
it('returns empty object when agent definition not found', async () => {
@@ -261,6 +304,17 @@ describe('getTriggerParameters', () => {
describe('getResolvedTriggerConfig', () => {
beforeEach(() => {
vi.resetAllMocks();
+ // Default: agent is enabled (has a config row)
+ mockIsAgentEnabledForProject.mockResolvedValue(true);
+ });
+
+ it('returns null when agent is not enabled for project (no config row)', async () => {
+ mockIsAgentEnabledForProject.mockResolvedValue(false);
+
+ const result = await getResolvedTriggerConfig(PROJECT_ID, AGENT_TYPE, TRIGGER_EVENT);
+
+ expect(result).toBeNull();
+ expect(mockResolveAgentDefinition).not.toHaveBeenCalled();
});
it('returns null when agent definition not found', async () => {
diff --git a/tests/unit/triggers/manual-runner.test.ts b/tests/unit/triggers/manual-runner.test.ts
index 89dac37b..2fd37e21 100644
--- a/tests/unit/triggers/manual-runner.test.ts
+++ b/tests/unit/triggers/manual-runner.test.ts
@@ -8,6 +8,11 @@ vi.mock('../../../src/db/repositories/runsRepository.js', () => ({
getRunById: vi.fn(),
}));
+// Default: agent is enabled (has a config row)
+vi.mock('../../../src/db/repositories/agentConfigsRepository.js', () => ({
+ isAgentEnabledForProject: vi.fn().mockResolvedValue(true),
+}));
+
vi.mock('../../../src/utils/logging.js', () => ({
logger: {
info: vi.fn(),
@@ -40,6 +45,7 @@ vi.mock('../../../src/triggers/shared/integration-validation.js', () => ({
}));
import { runAgent } from '../../../src/agents/registry.js';
+import { isAgentEnabledForProject } from '../../../src/db/repositories/agentConfigsRepository.js';
import { getRunById } from '../../../src/db/repositories/runsRepository.js';
import { withPMCredentials } from '../../../src/pm/context.js';
import { createPMProvider, withPMProvider } from '../../../src/pm/index.js';
@@ -71,6 +77,22 @@ describe('triggerManualRun', () => {
clearTriggerTracking();
});
+ it('throws when agent is not enabled for the project', async () => {
+ vi.mocked(isAgentEnabledForProject).mockResolvedValueOnce(false);
+
+ await expect(
+ triggerManualRun(
+ {
+ projectId: 'test-project',
+ agentType: 'implementation',
+ workItemId: 'card-1',
+ },
+ mockProject,
+ mockConfig,
+ ),
+ ).rejects.toThrow('not enabled for project');
+ });
+
it('throws when trigger is already running for same project+agent+card', async () => {
vi.mocked(runAgent).mockImplementation(() => new Promise(() => {})); // Never resolves
diff --git a/web/src/components/projects/project-agent-configs.tsx b/web/src/components/projects/project-agent-configs.tsx
index a1ce893d..e02a9b4e 100644
--- a/web/src/components/projects/project-agent-configs.tsx
+++ b/web/src/components/projects/project-agent-configs.tsx
@@ -591,78 +591,109 @@ function AgentRow({
}
interface AgentListViewProps {
- agentTypes: string[];
+ enabledAgentTypes: string[];
+ availableAgentTypes: string[];
configByAgent: Map;
triggersByAgent: Map;
integrations: { pm: string | null; scm: string | null };
onSelect: (agentType: string) => void;
onDelete: (id: number) => void;
+ onEnable: (agentType: string) => void;
isDeleting: boolean;
+ isEnabling: boolean;
projectModel: string | null;
projectEngine: string | null;
systemDefaults: SystemDefaults | undefined;
}
function AgentListView({
- agentTypes,
+ enabledAgentTypes,
+ availableAgentTypes,
configByAgent,
triggersByAgent,
integrations,
onSelect,
onDelete,
+ onEnable,
isDeleting,
+ isEnabling,
projectModel,
projectEngine,
systemDefaults,
}: AgentListViewProps) {
const [deleteTarget, setDeleteTarget] = useState<{ id: number; label: string } | null>(null);
- if (agentTypes.length === 0) {
- return (
- No agent definitions found.
- );
- }
-
return (
<>
-
-
-
-
- Agent
- Status
- Model / Engine
- Active Triggers
-
-
-
-
- {agentTypes.map((type) => (
- setDeleteTarget({ id, label })}
- projectModel={projectModel}
- projectEngine={projectEngine}
- systemDefaults={systemDefaults}
- />
- ))}
-
-
-
+ {enabledAgentTypes.length === 0 ? (
+
+ No agents enabled. Enable agents below to start processing.
+
+ ) : (
+
+
+
+
+ Agent
+ Status
+ Model / Engine
+ Active Triggers
+
+
+
+
+ {enabledAgentTypes.map((type) => (
+ setDeleteTarget({ id, label })}
+ projectModel={projectModel}
+ projectEngine={projectEngine}
+ systemDefaults={systemDefaults}
+ />
+ ))}
+
+
+
+ )}
+
+ {availableAgentTypes.length > 0 && (
+
+
Available Agents
+
+ {availableAgentTypes.map((agentType) => {
+ const label =
+ (AGENT_LABELS as Record
)[agentType] ?? agentType;
+ return (
+
+ {label}
+
+
+ );
+ })}
+
+
+ )}
!open && setDeleteTarget(null)}>
Delete Agent Config
- Are you sure you want to delete the custom config for{' '}
- {deleteTarget?.label}? The agent will revert to default settings.
- This action cannot be undone.
+ Are you sure you want to delete the config for {deleteTarget?.label}?
+ The agent will be disabled and no longer process any events. This action cannot be
+ undone.
@@ -886,12 +917,39 @@ export function ProjectAgentConfigs({ projectId }: { projectId: string }) {
const deleteMutation = useMutation({
mutationFn: (id: number) => trpcClient.agentConfigs.delete.mutate({ id }),
- onSuccess: () => queryClient.invalidateQueries({ queryKey: configsQueryKey }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: configsQueryKey });
+ queryClient.invalidateQueries({ queryKey: triggersViewQueryKey });
+ },
onError: (err) => {
toast.error('Failed to delete agent config', { description: err.message });
},
});
+ // Enable an agent by creating a bare agent_configs row (no overrides = project defaults)
+ const enableAgentMutation = useMutation({
+ mutationFn: (agentType: string) =>
+ trpcClient.agentConfigs.create.mutate({
+ projectId,
+ agentType,
+ model: null,
+ maxIterations: null,
+ agentEngine: null,
+ engineSettings: null,
+ maxConcurrency: null,
+ systemPrompt: null,
+ taskPrompt: null,
+ }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: configsQueryKey });
+ queryClient.invalidateQueries({ queryKey: triggersViewQueryKey });
+ toast.success('Agent enabled');
+ },
+ onError: (err) => {
+ toast.error('Failed to enable agent', { description: err.message });
+ },
+ });
+
// New trigger mutation (uses agentTriggerConfigs.upsert)
const upsertTriggerMutation = useMutation({
mutationFn: (input: {
@@ -945,18 +1003,22 @@ export function ProjectAgentConfigs({ projectId }: { projectId: string }) {
configByAgent.set(c.agentType, c);
}
- // Build triggers map from API
+ // Build triggers map from API — use enabledAgents (configured agents only)
const triggersByAgent = new Map();
const triggersViewIntegrations = triggersViewQuery.data?.integrations ?? {
pm: null,
scm: null,
};
if (triggersViewQuery.data) {
- for (const agent of triggersViewQuery.data.agents) {
+ const agentsList = triggersViewQuery.data.enabledAgents ?? triggersViewQuery.data.agents ?? [];
+ for (const agent of agentsList) {
triggersByAgent.set(agent.agentType, agent.triggers as ResolvedTrigger[]);
}
}
+ // Available (unconfigured) agent types
+ const availableAgentTypes = triggersViewQuery.data?.availableAgents ?? [];
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: save handler dispatches create vs update, builds payload from many optional fields, and chains trigger upsert
const handleSaveConfig = (type: string, configId: number | null, values: SaveConfigValues) => {
setSavingAgentType(type);
@@ -1017,8 +1079,8 @@ export function ProjectAgentConfigs({ projectId }: { projectId: string }) {
);
};
- // Get list of agent types to display
- const agentTypes = Array.from(triggersByAgent.keys());
+ // Get list of enabled agent types to display
+ const enabledAgentTypes = Array.from(triggersByAgent.keys());
// Render detail view when an agent is selected
if (selectedAgent !== null) {
@@ -1057,13 +1119,16 @@ export function ProjectAgentConfigs({ projectId }: { projectId: string }) {
deleteMutation.mutate(id)}
+ onEnable={(agentType) => enableAgentMutation.mutate(agentType)}
isDeleting={deleteMutation.isPending}
+ isEnabling={enableAgentMutation.isPending}
projectModel={projectModel}
projectEngine={projectEngine}
systemDefaults={systemDefaults}