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
4 changes: 4 additions & 0 deletions src/agents/definitions/debug.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ backend:
compaction: default

hint: Analyze the current issue fully before moving to the next.

integrations:
required: [pm]
optional: []
4 changes: 4 additions & 0 deletions src/agents/definitions/email-joke.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ trailingMessage: {}
hint: >-
Search for emails, read them, and send funny responses. Mark processed emails
as seen to prevent re-processing.

integrations:
required: [email]
optional: []
4 changes: 4 additions & 0 deletions src/agents/definitions/implementation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ trailingMessage:
includeGitStatus: true
includePRStatus: true
includeReminder: true

integrations:
required: [scm, pm]
optional: []
4 changes: 4 additions & 0 deletions src/agents/definitions/planning.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ backend:
compaction: default

hint: Complete the current planning step efficiently before moving to the next.

integrations:
required: [scm, pm]
optional: []
4 changes: 4 additions & 0 deletions src/agents/definitions/respond-to-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ hint: Fix CI failures with minimal, focused changes. Batch related file edits to

trailingMessage:
includeDiagnostics: true

integrations:
required: [scm]
optional: [pm]
4 changes: 4 additions & 0 deletions src/agents/definitions/respond-to-planning-comment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ backend:
compaction: default

hint: Complete the current task efficiently before moving to the next.

integrations:
required: [scm, pm]
optional: []
4 changes: 4 additions & 0 deletions src/agents/definitions/respond-to-pr-comment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ backend:
compaction: default

hint: Complete the current task efficiently before moving to the next.

integrations:
required: [scm]
optional: [pm]
4 changes: 4 additions & 0 deletions src/agents/definitions/respond-to-review.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,7 @@ hint: Address the current review comment fully before moving to the next. Batch

trailingMessage:
includeDiagnostics: true

integrations:
required: [scm]
optional: [pm]
4 changes: 4 additions & 0 deletions src/agents/definitions/review.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ backend:
compaction: default

hint: Focus on the current aspect of review before moving to the next. Read related files together.

integrations:
required: [scm]
optional: [pm]
28 changes: 28 additions & 0 deletions src/agents/definitions/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,29 @@ import { z } from 'zod';
// Agent Definition Schema
// ============================================================================

// Integration categories (aligned with integrationRoles.ts)
export const IntegrationCategorySchema = z.enum(['pm', 'scm', 'email']);

// Integration requirements schema (REQUIRED field)
const IntegrationsSchema = z
.object({
/** Integrations that MUST be configured for the agent to run */
required: z.array(IntegrationCategorySchema),
/**
* Integrations the agent CAN use if available (for future use).
* Currently not validated - reserved for dashboard filtering and
* conditional agent behavior based on available integrations.
*/
optional: z.array(IntegrationCategorySchema),
})
.refine(
(data) => {
const requiredSet = new Set(data.required);
return !data.optional.some((cat) => requiredSet.has(cat));
},
{ message: 'A category cannot be both required and optional' },
);

const IdentitySchema = z.object({
emoji: z.string(),
label: z.string(),
Expand Down Expand Up @@ -85,6 +108,11 @@ export const AgentDefinitionSchema = z.object({
compaction: z.enum(['implementation', 'default']),
hint: z.string(),
trailingMessage: TrailingMessageSchema,
integrations: IntegrationsSchema,
});

export type AgentDefinition = z.infer<typeof AgentDefinitionSchema>;

export type IntegrationCategory = z.infer<typeof IntegrationCategorySchema>;

export type AgentIntegrations = z.infer<typeof IntegrationsSchema>;
4 changes: 4 additions & 0 deletions src/agents/definitions/splitting.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ backend:
compaction: default

hint: Gather all context needed for the current step before proceeding.

integrations:
required: [scm, pm]
optional: []
38 changes: 38 additions & 0 deletions src/github/integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* SCM (GitHub) integration — credential validation helpers.
*
* Provides hasScmIntegration() for checking if SCM integration is configured,
* consistent with the pattern in email/integration.ts.
*/

import { getIntegrationCredentialOrNull } from '../config/provider.js';
import { getIntegrationProvider } from '../db/repositories/credentialsRepository.js';

/**
* Check if SCM integration is configured for a project.
* Returns true if the integration exists and has at least one token linked.
*/
export async function hasScmIntegration(projectId: string): Promise<boolean> {
const provider = await getIntegrationProvider(projectId, 'scm');
if (!provider) return false;

// Check if either token is available (some agents only need one)
const [impl, rev] = await Promise.all([
getIntegrationCredentialOrNull(projectId, 'scm', 'implementer_token'),
getIntegrationCredentialOrNull(projectId, 'scm', 'reviewer_token'),
]);

return impl !== null || rev !== null;
}

/**
* Check if a specific SCM persona token is configured.
*/
export async function hasScmPersonaToken(
projectId: string,
persona: 'implementer' | 'reviewer',
): Promise<boolean> {
const role = persona === 'implementer' ? 'implementer_token' : 'reviewer_token';
const token = await getIntegrationCredentialOrNull(projectId, 'scm', role);
return token !== null;
}
1 change: 1 addition & 0 deletions src/pm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type { ProjectPMConfig } from './lifecycle.js';

// PMIntegration interface + registry
export type { PMIntegration, PMWebhookEvent } from './integration.js';
export { hasPmIntegration } from './integration.js';
export { pmRegistry } from './registry.js';
export { processPMWebhook } from './webhook-handler.js';

Expand Down
32 changes: 32 additions & 0 deletions src/pm/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
* provider-specific branching.
*/

import { getIntegrationCredentialOrNull } from '../config/provider.js';
import { getIntegrationProvider } from '../db/repositories/credentialsRepository.js';
import type { CascadeConfig, ProjectConfig } from '../types/index.js';
import type { ProjectPMConfig } from './lifecycle.js';
import type { PMProvider } from './types.js';
Expand Down Expand Up @@ -70,3 +72,33 @@ export interface PMIntegration {
/** Extract a work item ID from text (e.g. PR body). Returns null if not found. */
extractWorkItemId(text: string): string | null;
}

// ============================================================================
// Integration check helpers
// ============================================================================

/**
* Check if PM integration is configured for a project.
* Returns true if a PM integration (Trello/JIRA) exists with required credentials.
*/
export async function hasPmIntegration(projectId: string): Promise<boolean> {
const provider = await getIntegrationProvider(projectId, 'pm');
if (!provider) return false;

// Check provider-specific required credentials
if (provider === 'trello') {
const [key, token] = await Promise.all([
getIntegrationCredentialOrNull(projectId, 'pm', 'api_key'),
getIntegrationCredentialOrNull(projectId, 'pm', 'token'),
]);
return key !== null && token !== null;
}
if (provider === 'jira') {
const [email, apiToken] = await Promise.all([
getIntegrationCredentialOrNull(projectId, 'pm', 'email'),
getIntegrationCredentialOrNull(projectId, 'pm', 'api_token'),
]);
return email !== null && apiToken !== null;
}
return false;
}
32 changes: 29 additions & 3 deletions src/triggers/shared/agent-execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { handleAgentResultArtifacts } from './agent-result-handler.js';
import { checkBudgetExceeded } from './budget.js';
import { triggerDebugAnalysis } from './debug-runner.js';
import { shouldTriggerDebug } from './debug-trigger.js';
import { formatValidationErrors, validateIntegrations } from './integration-validation.js';

/**
* Configuration for source-specific behavior in the agent execution pipeline.
Expand Down Expand Up @@ -149,13 +150,38 @@ export async function runAgentExecutionPipeline(
}
const agentType = result.agentType;

const { skipPrepareForAgent = false, onFailure, logLabel = 'Agent' } = executionConfig;

const workItemId = result.workItemId;
// Create lifecycle manager once (reused for validation failure and normal flow)
const pmProvider = createPMProvider(project);
const pmConfig = resolveProjectPMConfig(project);
const lifecycle = new PMLifecycleManager(pmProvider, pmConfig);

// Pre-flight integration validation
const validation = await validateIntegrations(project.id, agentType);
if (!validation.valid) {
const errorMessage = formatValidationErrors(validation);
logger.error('Integration validation failed', {
agentType,
projectId: project.id,
errors: validation.errors,
});

// Only notify via PM if PM validation passed (otherwise PM isn't configured)
const pmFailed = validation.errors.some((e) => e.category === 'pm');
if (result.workItemId && !pmFailed) {
await lifecycle.handleFailure(result.workItemId, errorMessage);
}

// Call onFailure callback (for GitHub PR updates)
if (executionConfig.onFailure) {
await executionConfig.onFailure(result, { success: false, output: '', error: errorMessage });
}
return;
}

const { skipPrepareForAgent = false, onFailure, logLabel = 'Agent' } = executionConfig;

const workItemId = result.workItemId;

let remainingBudgetUsd: number | undefined;
if (workItemId) {
const budgetResult = await checkPreRunBudget(workItemId, project, config, lifecycle);
Expand Down
Loading