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
18 changes: 17 additions & 1 deletion src/agents/capabilities/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type { IntegrationCategory } from '../definitions/schema.js';
*
* Format: {source}:{action}
* - Built-in sources: fs (filesystem), shell, session
* - Integration sources: pm, scm, email
* - Integration sources: pm, scm, alerting
*/
export const CAPABILITIES = [
// Built-in capabilities (always available, no integration required)
Expand All @@ -40,6 +40,9 @@ export const CAPABILITIES = [
'scm:comment',
'scm:review',
'scm:pr',

// Alerting integration capabilities
'alerting:read',
] as const;

export type Capability = (typeof CAPABILITIES)[number];
Expand Down Expand Up @@ -183,6 +186,18 @@ export const CAPABILITY_REGISTRY: Record<Capability, CapabilityDefinition> = {
sdkToolNames: [],
cliToolNames: [],
},

// -------------------------------------------------------------------------
// Alerting integration capabilities
// -------------------------------------------------------------------------

'alerting:read': {
integration: 'alerting',
description: 'Read issue and event data from alerting tools',
gadgetNames: ['GetAlertingIssue', 'GetAlertingEventDetail', 'ListAlertingEvents'],
sdkToolNames: [],
cliToolNames: [],
},
};

// ============================================================================
Expand All @@ -200,6 +215,7 @@ export function getCapabilitiesByIntegration(): Record<
builtin: [],
pm: [],
scm: [],
alerting: [],
};

for (const cap of CAPABILITIES) {
Expand Down
25 changes: 20 additions & 5 deletions src/agents/capabilities/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ import {
ReadWorkItem,
UpdateWorkItem,
} from '../../gadgets/pm/index.js';
import {
GetAlertingEventDetail,
GetAlertingIssue,
ListAlertingEvents,
} from '../../gadgets/sentry/index.js';
import { Tmux } from '../../gadgets/tmux.js';
import { TodoDelete, TodoUpdateStatus, TodoUpsert } from '../../gadgets/todo/index.js';
import type { ToolManifest } from '../contracts/index.js';
Expand Down Expand Up @@ -124,6 +129,11 @@ const GADGET_CONSTRUCTORS: Record<string, new () => any> = {

// scm:pr
CreatePR,

// alerting:read
GetAlertingIssue,
GetAlertingEventDetail,
ListAlertingEvents,
};

// ============================================================================
Expand Down Expand Up @@ -345,6 +355,7 @@ export function generateUnavailableCapabilitiesNote(unavailableCaps: Capability[
const integrationLabels: Record<IntegrationCategory, string> = {
pm: 'PM integration (Trello/JIRA)',
scm: 'SCM integration (GitHub)',
alerting: 'Alerting integration',
};

for (const [integration, gadgetNames] of byIntegration) {
Expand All @@ -370,21 +381,25 @@ export function generateUnavailableCapabilitiesNote(unavailableCaps: Capability[
*/
export async function createIntegrationChecker(projectId: string): Promise<IntegrationChecker> {
// Import integration checking functions dynamically to avoid circular deps
const [{ hasPmIntegration }, { hasScmIntegration }] = await Promise.all([
import('../../pm/integration.js'),
import('../../github/integration.js'),
]);
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] = await Promise.all([
const [hasPm, hasScm, hasAlerting] = await Promise.all([
hasPmIntegration(projectId),
hasScmIntegration(projectId),
hasAlertingIntegration(projectId),
]);

// Return synchronous checker
const availableIntegrations: Record<IntegrationCategory, boolean> = {
pm: hasPm,
scm: hasScm,
alerting: hasAlerting,
};

return (category: IntegrationCategory) => availableIntegrations[category] ?? false;
Expand Down
71 changes: 71 additions & 0 deletions src/agents/definitions/alerting.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
identity:
emoji: "\U0001F6A8"
label: Alert Investigator
roleHint: Investigates alerts from configured alerting providers, identifies root cause, and creates bug fix work items
initialMessage: "**\U0001F6A8 Investigating alert** — Analyzing stacktrace and root cause..."

# Alerting integration required; PM is optional for creating backlog work items.
integrations:
required: [alerting]
optional: [pm]

# Read source code and shell for log analysis. Alerting tools for event data.
# Optional PM write to create backlog bug work item if PM integration is configured.
capabilities:
required:
- fs:read
- shell:exec
- session:ctrl
- alerting:read
optional:
- pm:read
- pm:write

# Supported triggers for this agent
triggers:
- event: alerting:issue-alert
label: New Issue Alert
description: Trigger when an issue alert fires (exception, error rate, etc.)
defaultEnabled: true
providers: [sentry]
contextPipeline: [alertingIssue, directoryListing, contextFiles]
- event: alerting:metric-alert
label: Metric Alert (Critical/Warning)
description: Trigger when a metric alert enters critical or warning state
defaultEnabled: false
providers: [sentry]
parameters:
- name: severity
type: select
label: Minimum Severity
description: Only trigger for alerts at or above this severity
options: [critical, warning]
defaultValue: critical
contextPipeline: [directoryListing, contextFiles]

strategies: {}

prompts:
taskPrompt: |
An alert has been triggered: "<%= it.alertTitle %>"

<% if (it.alertIssueUrl) { %>Issue: <%= it.alertIssueUrl %><% } %>

The full event details (stacktrace, breadcrumbs, tags) have been pre-loaded above.

Your task:
1. Review the pre-loaded event data — identify the error type, failing function, and line number
2. Read the relevant source files to understand context and confirm the root cause
3. Summarize: what failed, why, and which code path is responsible
<% if (it.backlogListId) { %>
4. Create a bug fix work item in the backlog (list/status ID: <%= it.backlogListId %>) with:
- Title: short, actionable description (e.g. "Fix: NullPointerException in PaymentService.charge")
- Description: root cause, error details, affected file/function, link to the alert issue
<% } %>
5. Call Finish when done

hint: |
Start with the stacktrace in the pre-loaded event and work backwards through the call chain.
Focus on frames from application code (not third-party library frames).
Check the event timeline/breadcrumbs for the action or request that preceded the failure.
Keep the bug work item description concise and actionable.
57 changes: 57 additions & 0 deletions src/agents/definitions/contextSteps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { execFileSync } from 'node:child_process';
import { ListDirectory } from '../../gadgets/ListDirectory.js';
import { formatCheckStatus } from '../../gadgets/github/core/getPRChecks.js';
import { readWorkItem, readWorkItemWithMedia } from '../../gadgets/pm/core/readWorkItem.js';
import { formatSentryEvent } from '../../gadgets/sentry/core/format.js';
import {
formatTodoList,
getNextId,
Expand All @@ -20,6 +21,7 @@ import type { Todo } from '../../gadgets/todo/storage.js';
import { githubClient } from '../../github/client.js';
import { getJiraConfig, getTrelloConfig } from '../../pm/config.js';
import { MAX_IMAGES_PER_WORK_ITEM, getPMProviderOrNull } from '../../pm/index.js';
import { getSentryClient } from '../../sentry/client.js';
import type { AgentInput, ProjectConfig } from '../../types/index.js';
import { parseRepoFullName } from '../../utils/repo.js';
import { resolveSquintDbPath } from '../../utils/squintDb.js';
Expand Down Expand Up @@ -554,3 +556,58 @@ export async function fetchPipelineSnapshotStep(
},
];
}

// ============================================================================
// Sentry Issue Step
// ============================================================================

/**
* Pre-fetch the latest alerting event (with full stacktrace and breadcrumbs)
* so the agent starts with the error context already loaded.
*
* Reads alertIssueId and alertOrgId from AgentInput.
* Silently skips if credentials or config are missing.
*/
export async function fetchAlertingIssueStep(
params: FetchContextParams,
): Promise<ContextInjection[]> {
const { alertIssueId, alertOrgId } = params.input;
if (!alertIssueId || typeof alertIssueId !== 'string') return [];
if (!alertOrgId || typeof alertOrgId !== 'string') return [];

try {
params.logWriter('INFO', 'fetchAlertingIssueStep: fetching latest alerting event', {
issueId: alertIssueId,
orgId: alertOrgId,
});

const client = getSentryClient();
const event = await client.getIssueEvent(alertOrgId, alertIssueId, 'latest');
const result = formatSentryEvent(event);

params.logWriter('INFO', 'fetchAlertingIssueStep: fetched alerting event successfully', {
issueId: alertIssueId,
eventId: event.event_id,
});

return [
{
toolName: 'GetAlertingEventDetail',
params: {
organizationId: alertOrgId,
issueId: alertIssueId,
eventId: 'latest',
},
result,
description: 'Pre-fetched alerting event with stacktrace and breadcrumbs',
},
];
} catch (error) {
params.logWriter('WARN', 'fetchAlertingIssueStep: failed to fetch alerting event', {
issueId: alertIssueId,
orgId: alertOrgId,
error: error instanceof Error ? error.message : String(error),
});
return [];
}
}
11 changes: 6 additions & 5 deletions src/agents/definitions/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ import { CAPABILITIES } from '../capabilities/registry.js';
// ============================================================================

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

// Known providers for validation
export const KnownProviderSchema = z.enum(['trello', 'jira', 'github']);
export const KnownProviderSchema = z.enum(['trello', 'jira', 'github', 'sentry']);

// Trigger event format validation: {category}:{event-name}
// Categories: pm, scm (integration-bound), internal (orchestration chaining)
// Categories: pm, scm (integration-bound), alerting (monitoring), internal (orchestration chaining)
const TriggerEventSchema = z
.string()
.regex(
/^(pm|scm|internal):[a-z][a-z0-9-]*$/,
'Event must be in format {category}:{event-name} (e.g., pm:status-changed, scm:check-suite-success)',
/^(pm|scm|alerting|internal):[a-z][a-z0-9-]*$/,
'Event must be in format {category}:{event-name} (e.g., pm:status-changed, scm:check-suite-success, alerting:issue-alert)',
);

// ============================================================================
Expand Down Expand Up @@ -81,6 +81,7 @@ export const CONTEXT_STEP_NAMES = [
'prContext',
'prConversation',
'pipelineSnapshot',
'alertingIssue',
] as const;

/** Context step name schema for use in triggers */
Expand Down
2 changes: 2 additions & 0 deletions src/agents/definitions/strategies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import type { ContextInjection } from '../contracts/index.js';
import {
type FetchContextParams,
fetchAlertingIssueStep,
fetchContextFilesStep,
fetchDirectoryListingStep,
fetchPRContextStep,
Expand All @@ -37,4 +38,5 @@ export const CONTEXT_STEP_REGISTRY: Record<
prContext: fetchPRContextStep,
prConversation: fetchPRConversationStep,
pipelineSnapshot: fetchPipelineSnapshotStep,
alertingIssue: fetchAlertingIssueStep,
};
11 changes: 11 additions & 0 deletions src/cli/alerting/get-alerting-event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { getSentryEventDetail } from '../../gadgets/sentry/core/getSentryEventDetail.js';
import { getAlertingEventDetailDef } from '../../gadgets/sentry/definitions.js';
import { createCLICommand } from '../../gadgets/shared/cliCommandFactory.js';

export default createCLICommand(getAlertingEventDetailDef, async (params) => {
return getSentryEventDetail(
params.organizationId as string,
params.issueId as string,
params.eventId as string | undefined,
);
});
7 changes: 7 additions & 0 deletions src/cli/alerting/get-alerting-issue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { getSentryIssue } from '../../gadgets/sentry/core/getSentryIssue.js';
import { getAlertingIssueDef } from '../../gadgets/sentry/definitions.js';
import { createCLICommand } from '../../gadgets/shared/cliCommandFactory.js';

export default createCLICommand(getAlertingIssueDef, async (params) => {
return getSentryIssue(params.organizationId as string, params.issueId as string);
});
11 changes: 11 additions & 0 deletions src/cli/alerting/list-alerting-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { listSentryEvents } from '../../gadgets/sentry/core/listSentryEvents.js';
import { listAlertingEventsDef } from '../../gadgets/sentry/definitions.js';
import { createCLICommand } from '../../gadgets/shared/cliCommandFactory.js';

export default createCLICommand(listAlertingEventsDef, async (params) => {
return listSentryEvents(
params.organizationId as string,
params.issueId as string,
params.limit as number | undefined,
);
});
14 changes: 12 additions & 2 deletions src/config/integrationRoles.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
export type IntegrationCategory = 'pm' | 'scm';
export type IntegrationProvider = 'trello' | 'jira' | 'github';
export type IntegrationCategory = 'pm' | 'scm' | 'alerting';
export type IntegrationProvider = 'trello' | 'jira' | 'github' | 'sentry';

export const PROVIDER_CATEGORY: Record<IntegrationProvider, IntegrationCategory> = {
trello: 'pm',
jira: 'pm',
github: 'scm',
sentry: 'alerting',
};

export interface CredentialRoleDef {
Expand Down Expand Up @@ -45,4 +46,13 @@ export const PROVIDER_CREDENTIAL_ROLES: Record<IntegrationProvider, CredentialRo
optional: true,
},
],
sentry: [
{ role: 'api_token', label: 'API Token', envVarKey: 'SENTRY_API_TOKEN' },
{
role: 'webhook_secret',
label: 'Webhook Secret',
envVarKey: 'SENTRY_WEBHOOK_SECRET',
optional: true,
},
],
};
13 changes: 3 additions & 10 deletions src/config/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from '../db/repositories/credentialsRepository.js';
import type { CascadeConfig, ProjectConfig } from '../types/index.js';
import { configCache } from './configCache.js';
import { PROVIDER_CREDENTIAL_ROLES } from './integrationRoles.js';
import { PROVIDER_CATEGORY, PROVIDER_CREDENTIAL_ROLES } from './integrationRoles.js';

export async function loadConfig(): Promise<CascadeConfig> {
const cached = configCache.getConfig();
Expand Down Expand Up @@ -210,15 +210,8 @@ export function invalidateConfigCache(): void {
function roleToEnvVarKey(category: string, role: string): string | undefined {
// Look through all providers in the category to find the role
for (const [provider, roles] of Object.entries(PROVIDER_CREDENTIAL_ROLES)) {
let providerCategory: string;
if (provider === 'trello' || provider === 'jira') {
providerCategory = 'pm';
} else if (provider === 'github') {
providerCategory = 'scm';
} else {
continue;
}
if (providerCategory !== category) continue;
const providerCategory = PROVIDER_CATEGORY[provider as keyof typeof PROVIDER_CATEGORY];
if (!providerCategory || providerCategory !== category) continue;
const roleDef = roles.find((r) => r.role === role);
if (roleDef) return roleDef.envVarKey;
}
Expand Down
Loading
Loading