From 506d5745ec35a5da6574b6a7fff06a711214a18e Mon Sep 17 00:00:00 2001 From: Zbigniew Sobiecki Date: Tue, 24 Feb 2026 10:56:50 +0100 Subject: [PATCH] fix(triggers): wrap manual-run agents with PM context to prevent crash (#519) Manual and retry runs called runAgent() directly without establishing AsyncLocalStorage-based PM context, causing agents (especially review) to crash with "No PMProvider in scope". Webhook-triggered agents already had this wrapping via withPMCredentials() + withPMProvider(). Wrap the runAgent() call in triggerManualRun() with the same PM context pattern used by the GitHub webhook handler. withPMCredentials gracefully falls through when no PM integration is configured. Closes #518 Co-authored-by: Claude Opus 4.6 --- src/triggers/shared/manual-runner.ts | 10 ++++- tests/unit/triggers/manual-runner.test.ts | 47 +++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/triggers/shared/manual-runner.ts b/src/triggers/shared/manual-runner.ts index 461977c6..0a2efb20 100644 --- a/src/triggers/shared/manual-runner.ts +++ b/src/triggers/shared/manual-runner.ts @@ -1,5 +1,7 @@ import { runAgent } from '../../agents/registry.js'; import { getRunById } from '../../db/repositories/runsRepository.js'; +import { withPMCredentials } from '../../pm/context.js'; +import { createPMProvider, pmRegistry, withPMProvider } from '../../pm/index.js'; import type { AgentInput, CascadeConfig, ProjectConfig } from '../../types/index.js'; import { logger } from '../../utils/logging.js'; @@ -99,7 +101,13 @@ export async function triggerManualRun( }; try { - const result = await runAgent(input.agentType, agentInput); + const pmProvider = createPMProvider(project); + const result = await withPMCredentials( + project.id, + project.pm?.type, + (t) => pmRegistry.getOrNull(t), + () => withPMProvider(pmProvider, () => runAgent(input.agentType, agentInput)), + ); logger.info('Manual agent run completed', { projectId: input.projectId, agentType: input.agentType, diff --git a/tests/unit/triggers/manual-runner.test.ts b/tests/unit/triggers/manual-runner.test.ts index 71ff3203..9f3ae166 100644 --- a/tests/unit/triggers/manual-runner.test.ts +++ b/tests/unit/triggers/manual-runner.test.ts @@ -15,8 +15,27 @@ vi.mock('../../../src/utils/logging.js', () => ({ }, })); +vi.mock('../../../src/pm/index.js', () => ({ + createPMProvider: vi.fn(() => ({ type: 'mock-pm' })), + pmRegistry: { getOrNull: vi.fn(() => null) }, + withPMProvider: vi.fn((_provider: unknown, fn: () => unknown) => fn()), +})); + +vi.mock('../../../src/pm/context.js', () => ({ + withPMCredentials: vi.fn( + ( + _projectId: string, + _pmType: string | undefined, + _getIntegration: unknown, + fn: () => unknown, + ) => fn(), + ), +})); + import { runAgent } from '../../../src/agents/registry.js'; import { getRunById } from '../../../src/db/repositories/runsRepository.js'; +import { withPMCredentials } from '../../../src/pm/context.js'; +import { createPMProvider, withPMProvider } from '../../../src/pm/index.js'; import { clearTriggerTracking, isTriggerRunning, @@ -141,6 +160,34 @@ describe('triggerManualRun', () => { ); }); + it('wraps runAgent with PM credential and provider context', async () => { + vi.mocked(runAgent).mockResolvedValue({ + success: true, + output: 'Done', + runId: 'run-pm', + }); + + await triggerManualRun( + { projectId: 'test-project', agentType: 'review', prNumber: 42 }, + mockProject, + mockConfig, + ); + + // createPMProvider called with the project + expect(createPMProvider).toHaveBeenCalledWith(mockProject); + + // withPMCredentials called with project.id, pm type, registry lookup, and inner fn + expect(withPMCredentials).toHaveBeenCalledWith( + 'test-project', + undefined, // mockProject has no pm.type + expect.any(Function), + expect.any(Function), + ); + + // withPMProvider called with the created provider and inner fn + expect(withPMProvider).toHaveBeenCalledWith({ type: 'mock-pm' }, expect.any(Function)); + }); + it('marks trigger as complete after runAgent finishes', async () => { const projectId = 'test-project'; const agentType = 'implementation';