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 docs/architecture/04-agent-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ interface CapabilityDefinition {
```mermaid
flowchart TD
A["Agent definition<br/>(capabilities.required + optional)"] --> B[Create integration checker]
B --> C["Check hasPmIntegration(),<br/>hasScmIntegration(),<br/>hasAlertingIntegration()"]
B --> C["integrationRegistry.getByCategory(cat)<br/>.hasIntegration(projectId)<br/>for pm, scm, alerting"]
C --> D[resolveEffectiveCapabilities]
D --> E["Built-in caps: always included"]
D --> F["Integration caps: only if provider configured"]
Expand Down
37 changes: 20 additions & 17 deletions src/agents/capabilities/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { Tmux } from '../../gadgets/tmux.js';
import { TodoDelete, TodoUpdateStatus, TodoUpsert } from '../../gadgets/todo/index.js';
import { VerifyChanges } from '../../gadgets/VerifyChanges.js';
import { WriteFile } from '../../gadgets/WriteFile.js';
import { integrationRegistry } from '../../integrations/registry.js';
import type { ToolManifest } from '../contracts/index.js';
import type { IntegrationCategory } from '../definitions/schema.js';
import {
Expand Down Expand Up @@ -378,28 +379,30 @@ export function generateUnavailableCapabilitiesNote(unavailableCaps: Capability[
*
* This function pre-fetches integration availability for all categories
* and returns a synchronous checker callback.
*
* Uses integrationRegistry.getByCategory() to check all registered integrations
* for each category — returns true if any integration in that category is configured.
*/
export async function createIntegrationChecker(projectId: string): Promise<IntegrationChecker> {
// Import integration checking functions dynamically to avoid circular deps
const [{ hasPmIntegration }, { hasScmIntegration }, { hasAlertingIntegration }] =
await Promise.all([
import('../../pm/integration.js'),
import('../../github/integration.js'),
import('../../sentry/integration.js'),
]);

// Pre-fetch all integration statuses in parallel
const [hasPm, hasScm, hasAlerting] = await Promise.all([
hasPmIntegration(projectId),
hasScmIntegration(projectId),
hasAlertingIntegration(projectId),
]);
const categories: IntegrationCategory[] = ['pm', 'scm', 'alerting'];

// Pre-fetch all integration statuses in parallel across all categories
const results = await Promise.all(
categories.map(async (cat) => {
const integrations = integrationRegistry.getByCategory(cat);
// Category is available if ANY registered integration for it is configured
const statuses = await Promise.all(
integrations.map((integration) => integration.hasIntegration(projectId)),
);
return statuses.some(Boolean);
}),
);

// Return synchronous checker
const availableIntegrations: Record<IntegrationCategory, boolean> = {
pm: hasPm,
scm: hasScm,
alerting: hasAlerting,
pm: results[0],
scm: results[1],
alerting: results[2],
};

return (category: IntegrationCategory) => availableIntegrations[category] ?? false;
Expand Down
37 changes: 0 additions & 37 deletions src/github/integration.ts

This file was deleted.

11 changes: 4 additions & 7 deletions src/github/scm-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@
* Encapsulates GitHub SCM credential resolution and validation
* into a unified integration class following the IntegrationModule pattern.
*
* Consolidates:
* - `hasScmIntegration()` logic from src/github/integration.ts
* - `hasScmPersonaToken()` logic from src/github/integration.ts
* - `withGitHubToken()` usage from src/github/client.ts
*
* Backward compatibility: the standalone functions in src/github/integration.ts
* remain exported and continue to work identically.
* Provides:
* - `hasIntegration()` — checks if at least one token (implementer or reviewer) is configured
* - `hasPersonaToken()` — checks if a specific persona token is configured
* - `withCredentials()` — runs a function within the implementer token credential scope
*/

import { getIntegrationCredential, getIntegrationCredentialOrNull } from '../config/provider.js';
Expand Down
1 change: 0 additions & 1 deletion src/pm/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export { getPMProvider, getPMProviderOrNull, withPMProvider } from './context.js';
// PMIntegration interface + registry
export type { PMIntegration, PMWebhookEvent } from './integration.js';
export { hasPmIntegration } from './integration.js';
export { JiraPMProvider } from './jira/adapter.js';
export type { ProjectPMConfig } from './lifecycle.js';
export { hasAutoLabel, PMLifecycleManager, resolveProjectPMConfig } from './lifecycle.js';
Expand Down
28 changes: 0 additions & 28 deletions src/pm/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@
* Extends IntegrationModule so PM providers participate in the unified registry.
*/

import { PROVIDER_CREDENTIAL_ROLES } from '../config/integrationRoles.js';
import { getIntegrationCredentialOrNull } from '../config/provider.js';
import { getIntegrationProvider } from '../db/repositories/credentialsRepository.js';
import type { IntegrationModule } from '../integrations/types.js';
import type { AgentExecutionConfig } from '../triggers/shared/agent-execution.js';
import type { CascadeConfig, ProjectConfig } from '../types/index.js';
Expand Down Expand Up @@ -92,28 +89,3 @@ export interface PMIntegration extends IntegrationModule {
/** 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 exists with all required credentials present.
*
* Uses the data-driven PROVIDER_CREDENTIAL_ROLES table so this function
* does not need to be updated when a new PM provider is added.
*/
export async function hasPmIntegration(projectId: string): Promise<boolean> {
const provider = await getIntegrationProvider(projectId, 'pm');
if (!provider) return false;

const roles = PROVIDER_CREDENTIAL_ROLES[provider as keyof typeof PROVIDER_CREDENTIAL_ROLES];
if (!roles || roles.length === 0) return false;

const requiredRoles = roles.filter((r) => !r.optional);
const values = await Promise.all(
requiredRoles.map((roleDef) => getIntegrationCredentialOrNull(projectId, 'pm', roleDef.role)),
);
return values.every((v) => v !== null);
}
20 changes: 6 additions & 14 deletions src/sentry/alerting-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,29 @@
* Encapsulates Sentry alerting credential resolution and validation
* into a unified integration class following the IntegrationModule pattern.
*
* Consolidates:
* - `getSentryIntegrationConfig()` logic from src/sentry/integration.ts
* - `hasAlertingIntegration()` logic from src/sentry/integration.ts
*
* Backward compatibility: the standalone functions in src/sentry/integration.ts
* remain exported and continue to work identically.
* Inlines the hasIntegration logic directly (calls getSentryIntegrationConfig)
* rather than delegating to the now-deleted hasAlertingIntegration() standalone function.
*/

import { getIntegrationCredential } from '../config/provider.js';
import type { AlertingIntegration } from '../integrations/alerting.js';
import {
getSentryIntegrationConfig,
hasAlertingIntegration,
type SentryIntegrationConfig,
} from './integration.js';
import { getSentryIntegrationConfig, type SentryIntegrationConfig } from './integration.js';

export class SentryAlertingIntegration implements AlertingIntegration {
readonly type = 'sentry';
readonly category = 'alerting' as const;

/**
* Check if Sentry alerting integration is configured for a project.
* Delegates to existing hasAlertingIntegration() logic.
* Returns true if getSentryIntegrationConfig returns a valid config.
*/
async hasIntegration(projectId: string): Promise<boolean> {
return hasAlertingIntegration(projectId);
const config = await getSentryIntegrationConfig(projectId);
return config !== null;
}

/**
* Get the Sentry integration config for a project.
* Delegates to existing getSentryIntegrationConfig() logic.
*/
async getConfig(projectId: string): Promise<SentryIntegrationConfig | null> {
return getSentryIntegrationConfig(projectId);
Expand Down
9 changes: 0 additions & 9 deletions src/sentry/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,3 @@ export async function getSentryIntegrationConfig(
organizationSlug: config.organizationSlug,
};
}

/**
* Returns true if a Sentry alerting integration is configured for the project.
* Used by createIntegrationChecker() in the capability resolver.
*/
export async function hasAlertingIntegration(projectId: string): Promise<boolean> {
const config = await getSentryIntegrationConfig(projectId);
return config !== null;
}
30 changes: 28 additions & 2 deletions tests/integration/integration-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
*/

import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { hasScmIntegration, hasScmPersonaToken } from '../../src/github/integration.js';
// Bootstrap the integration registry so validateIntegrations() can find registered modules.
// The new registry-driven implementation requires integrations to be registered before
// calling getByCategory() — without this import the registry is empty and all validations
// report "none is registered" instead of checking actual project credentials.
import '../../src/integrations/bootstrap.js';
import { hasPmIntegration } from '../../src/pm/integration.js';
import { integrationRegistry } from '../../src/integrations/registry.js';
import type { SCMIntegration } from '../../src/integrations/scm.js';
import {
formatValidationErrors,
getIntegrationRequirements,
Expand Down Expand Up @@ -50,6 +50,32 @@ beforeAll(async () => {
await truncateAll();
});

// Helper functions using the integration registry
async function hasPmIntegration(projectId: string): Promise<boolean> {
const integrations = integrationRegistry.getByCategory('pm');
const statuses = await Promise.all(integrations.map((i) => i.hasIntegration(projectId)));
return statuses.some(Boolean);
}

async function hasScmIntegration(projectId: string): Promise<boolean> {
const integrations = integrationRegistry.getByCategory('scm');
const statuses = await Promise.all(integrations.map((i) => i.hasIntegration(projectId)));
return statuses.some(Boolean);
}

async function hasScmPersonaToken(
projectId: string,
persona: 'implementer' | 'reviewer',
): Promise<boolean> {
const integrations = integrationRegistry.getByCategory('scm');
const statuses = await Promise.all(
integrations
.filter((i): i is SCMIntegration => 'hasPersonaToken' in i)
.map((i) => i.hasPersonaToken(projectId, persona)),
);
return statuses.some(Boolean);
}

describe('Integration Validation (integration)', () => {
beforeEach(async () => {
await truncateAll();
Expand Down
Loading
Loading