Skip to content
Closed
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
6 changes: 1 addition & 5 deletions src/agents/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { type Todo, formatTodoList, initTodoSession, saveTodos } from '../gadget
import { getPMProvider } from '../pm/index.js';
import type { AgentInput, AgentResult, CascadeConfig, ProjectConfig } from '../types/index.js';
import { logger } from '../utils/logging.js';
import { extractPRUrl } from '../utils/prUrl.js';
import type { PromptContext } from './prompts/index.js';
import { type BuilderType, createConfiguredBuilder } from './shared/builderFactory.js';
import { type FileLogger, executeAgentLifecycle } from './shared/lifecycle.js';
Expand Down Expand Up @@ -511,11 +512,6 @@ async function setupWorkingDirectory(
return setupRepository(project, log, agentType, prBranch);
}

function extractPRUrl(output: string): string | undefined {
const match = output.match(/https:\/\/github\.com\/[^\s]+\/pull\/\d+/);
return match ? match[0] : undefined;
}

export async function executeAgent(
agentType: string,
input: AgentInput & { project: ProjectConfig; config: CascadeConfig },
Expand Down
9 changes: 1 addition & 8 deletions src/backends/claude-code/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
SDKSystemMessage,
} from '@anthropic-ai/claude-agent-sdk';
import { logger } from '../../utils/logging.js';
import { extractPRUrl } from '../../utils/prUrl.js';
import type {
AgentBackend,
AgentBackendInput,
Expand Down Expand Up @@ -140,14 +141,6 @@ export function buildEnv(projectSecrets?: Record<string, string>): {
return { env };
}

/**
* Extract a GitHub PR URL from text.
*/
function extractPRUrl(text: string): string | undefined {
const match = text.match(/https:\/\/github\.com\/[^\s"')\]]+\/pull\/\d+/);
return match ? match[0] : undefined;
}

/**
* Extract a GitHub PR URL from assistant messages (tool results containing create-pr output).
*/
Expand Down
120 changes: 28 additions & 92 deletions src/triggers/github/webhook-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { runAgent } from '../../agents/registry.js';
import {
findProjectByRepo,
getAgentCredential,
Expand All @@ -24,115 +23,52 @@ import {
setProcessing,
startWatchdog,
} from '../../utils/index.js';
import { injectLlmApiKeys } from '../../utils/llmEnv.js';
import { safeOperation } from '../../utils/safeOperation.js';
import type { TriggerRegistry } from '../registry.js';
import { handleAgentResultArtifacts } from '../shared/agent-result-handler.js';
import { checkBudgetExceeded } from '../shared/budget.js';
import { triggerDebugAnalysis } from '../shared/debug-runner.js';
import { shouldTriggerDebug } from '../shared/debug-trigger.js';
import { executeAgentPipeline } from '../shared/agent-pipeline.js';
import { withProjectCredentials } from '../shared/credential-scope.js';
import type { TriggerResult } from '../types.js';

async function executeGitHubAgent(
result: TriggerResult,
project: ProjectConfig,
config: CascadeConfig,
): Promise<void> {
const trelloApiKey = await getProjectSecret(project.id, 'TRELLO_API_KEY').catch(() => '');
const trelloToken = await getProjectSecret(project.id, 'TRELLO_TOKEN').catch(() => '');
const githubToken = await getProjectSecret(project.id, 'GITHUB_TOKEN');

const restoreLlmEnv = await injectLlmApiKeys(project.id);

try {
const pmProvider = createPMProvider(project);
await withTrelloCredentials({ apiKey: trelloApiKey, token: trelloToken }, () =>
withPMProvider(pmProvider, () =>
withGitHubToken(githubToken, () => executeGitHubAgentWithCreds(result, project, config)),
),
);
} finally {
restoreLlmEnv();
}
}

async function executeGitHubAgentWithCreds(
result: TriggerResult,
project: ProjectConfig,
config: CascadeConfig,
): Promise<void> {
const cardId = result.cardId ?? result.workItemId;
const pmProvider = createPMProvider(project);
const pmConfig = resolveProjectPMConfig(project);
const lifecycle = new PMLifecycleManager(pmProvider, pmConfig);

let remainingBudgetUsd: number | undefined;
if (cardId) {
const budgetCheck = await checkBudgetExceeded(cardId, project, config);
if (budgetCheck?.exceeded) {
logger.warn('Budget exceeded, GitHub agent not started', {
cardId,
currentCost: budgetCheck.currentCost,
budget: budgetCheck.budget,
});
await lifecycle.handleBudgetExceeded(cardId, budgetCheck.currentCost, budgetCheck.budget);
return;
}
remainingBudgetUsd = budgetCheck?.remaining;
}

const agentResult = await runAgent(result.agentType, {
...result.agentInput,
remainingBudgetUsd,
project,
config,
});

if (cardId) {
await handleAgentResultArtifacts(cardId, result.agentType, agentResult, project);
await withProjectCredentials(project, result.agentType, async () => {
const agentResult = await executeAgentPipeline({
agentType: result.agentType,
agentInput: result.agentInput,
workItemId: cardId,
project,
config,
lifecycle,
prepareLifecycle: false, // GitHub agents don't call prepareForAgent
cleanupLifecycle: false, // GitHub agents don't call cleanupProcessing
onAgentFailure: async (agentResult) => {
if (result.prNumber) {
await updateInitialCommentWithError(result, agentResult);
}
},
});

const postBudgetCheck = await checkBudgetExceeded(cardId, project, config);
if (postBudgetCheck?.exceeded) {
await lifecycle.handleBudgetWarning(
cardId,
postBudgetCheck.currentCost,
postBudgetCheck.budget,
);
// GitHub-specific: Move to in-review if implementation completed successfully
if (cardId && result.agentType === 'implementation' && agentResult.success) {
await lifecycle.handleSuccess(cardId, result.agentType, agentResult.prUrl);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKING] Double handleSuccess call: The pipeline (agent-pipeline.ts:172) already calls lifecycle.handleSuccess for all successful agents with a workItemId. This line calls it again for implementation agents. The original code only called it here (once), because the pipeline equivalent didn't exist.

Fix: Either (1) add a handleLifecycleResult: false option to the pipeline so the GitHub handler manages its own success/failure, or (2) remove this line and ensure the pipeline's unconditional handleSuccess is acceptable for all GitHub agent types (but note the original code intentionally skipped handleSuccess for non-implementation agents).

}
}

// Move to in-review if implementation completed successfully
if (cardId && result.agentType === 'implementation' && agentResult.success) {
await lifecycle.handleSuccess(cardId, result.agentType, agentResult.prUrl);
}

if (!agentResult.success && result.prNumber) {
await updateInitialCommentWithError(result, agentResult);
}

logger.info('GitHub agent completed', {
agentType: result.agentType,
prNumber: result.prNumber,
success: agentResult.success,
cost: agentResult.cost,
runId: agentResult.runId,
logger.info('GitHub agent completed', {
agentType: result.agentType,
prNumber: result.prNumber,
success: agentResult.success,
cost: agentResult.cost,
runId: agentResult.runId,
});
});

await tryGitHubAutoDebug(agentResult, project, config);
}

async function tryGitHubAutoDebug(
agentResult: { runId?: string },
project: ProjectConfig,
config: CascadeConfig,
): Promise<void> {
if (!agentResult.runId) return;
const debugTarget = await shouldTriggerDebug(agentResult.runId);
if (debugTarget) {
triggerDebugAnalysis(debugTarget.runId, project, config, debugTarget.cardId).catch((err) =>
logger.error('Auto-debug failed', { error: String(err) }),
);
}
}

async function updateInitialCommentWithError(
Expand Down
115 changes: 12 additions & 103 deletions src/triggers/jira/webhook-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,11 @@
* and dispatches to the trigger registry.
*/

import { runAgent } from '../../agents/registry.js';
import {
findProjectByJiraProjectKey,
getAgentCredential,
getProjectSecret,
loadConfig,
} from '../../config/provider.js';
import { withGitHubToken } from '../../github/client.js';
import { withJiraCredentials } from '../../jira/client.js';
import {
PMLifecycleManager,
Expand All @@ -31,12 +28,9 @@ import {
setProcessing,
startWatchdog,
} from '../../utils/index.js';
import { injectLlmApiKeys } from '../../utils/llmEnv.js';
import type { TriggerRegistry } from '../registry.js';
import { handleAgentResultArtifacts } from '../shared/agent-result-handler.js';
import { checkBudgetExceeded } from '../shared/budget.js';
import { triggerDebugAnalysis } from '../shared/debug-runner.js';
import { shouldTriggerDebug } from '../shared/debug-trigger.js';
import { executeAgentPipeline } from '../shared/agent-pipeline.js';
import { withProjectCredentials } from '../shared/credential-scope.js';
import type { TriggerResult } from '../types.js';

interface JiraWebhookPayload {
Expand Down Expand Up @@ -76,107 +70,22 @@ async function executeJiraAgent(
result: TriggerResult,
project: ProjectConfig,
config: CascadeConfig,
): Promise<void> {
const jiraEmail = await getProjectSecret(project.id, 'JIRA_EMAIL');
const jiraApiToken = await getProjectSecret(project.id, 'JIRA_API_TOKEN');
const jiraBaseUrl =
project.jira?.baseUrl ?? (await getProjectSecret(project.id, 'JIRA_BASE_URL'));
const githubToken = await getProjectSecret(project.id, 'GITHUB_TOKEN');

const agentGitHubToken = await getAgentCredential(project.id, result.agentType, 'GITHUB_TOKEN');
const effectiveGithubToken = agentGitHubToken || githubToken;

const restoreLlmEnv = await injectLlmApiKeys(project.id);

try {
const pmProvider = createPMProvider(project);
await withJiraCredentials(
{ email: jiraEmail, apiToken: jiraApiToken, baseUrl: jiraBaseUrl },
() =>
withPMProvider(pmProvider, () =>
withGitHubToken(effectiveGithubToken, () =>
executeJiraAgentWithCreds(result, project, config),
),
),
);
} finally {
restoreLlmEnv();
}
}

async function executeJiraAgentWithCreds(
result: TriggerResult,
project: ProjectConfig,
config: CascadeConfig,
): Promise<void> {
const workItemId = result.workItemId ?? result.cardId;
const pmProvider = createPMProvider(project);
const pmConfig = resolveProjectPMConfig(project);
const lifecycle = new PMLifecycleManager(pmProvider, pmConfig);

let remainingBudgetUsd: number | undefined;
if (workItemId) {
const budgetCheck = await checkBudgetExceeded(workItemId, project, config);
if (budgetCheck?.exceeded) {
logger.warn('Budget exceeded, JIRA agent not started', {
workItemId,
currentCost: budgetCheck.currentCost,
budget: budgetCheck.budget,
});
await lifecycle.handleBudgetExceeded(workItemId, budgetCheck.currentCost, budgetCheck.budget);
return;
}
remainingBudgetUsd = budgetCheck?.remaining;
}

if (workItemId) {
await lifecycle.prepareForAgent(workItemId, result.agentType);
}

const agentResult = await runAgent(result.agentType, {
...result.agentInput,
cardId: workItemId,
remainingBudgetUsd,
project,
config,
});

if (workItemId) {
await handleAgentResultArtifacts(workItemId, result.agentType, agentResult, project);

const postBudgetCheck = await checkBudgetExceeded(workItemId, project, config);
if (postBudgetCheck?.exceeded) {
await lifecycle.handleBudgetWarning(
workItemId,
postBudgetCheck.currentCost,
postBudgetCheck.budget,
);
}

await lifecycle.cleanupProcessing(workItemId);

if (agentResult.success) {
await lifecycle.handleSuccess(workItemId, result.agentType, agentResult.prUrl);
} else {
await lifecycle.handleFailure(workItemId, agentResult.error);
}
}

logger.info('JIRA agent completed', {
agentType: result.agentType,
success: agentResult.success,
runId: agentResult.runId,
});

// Auto-debug
if (agentResult.runId) {
const debugTarget = await shouldTriggerDebug(agentResult.runId);
if (debugTarget) {
triggerDebugAnalysis(debugTarget.runId, project, config, debugTarget.cardId).catch((err) =>
logger.error('Auto-debug failed', { error: String(err) }),
);
}
}
await withProjectCredentials(project, result.agentType, () =>
executeAgentPipeline({
agentType: result.agentType,
agentInput: { ...result.agentInput, cardId: workItemId },
workItemId,
project,
config,
lifecycle,
}),
);
}

function processNextQueuedJiraWebhook(registry: TriggerRegistry): void {
Expand Down
Loading