From e43d5026c3a89638f8a55e00927f6e8f1dbbf0b6 Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Mon, 23 Mar 2026 20:57:12 +0000 Subject: [PATCH] feat(lifecycle): replace hardcoded agentType branching with profile-driven lifecycle hooks --- src/agents/definitions/implementation.yaml | 5 ++ src/agents/definitions/profiles.ts | 15 ++++++ src/agents/definitions/schema.ts | 23 +++++++- src/backends/adapter.ts | 1 + src/backends/progressLifecycle.ts | 2 + src/backends/progressMonitor.ts | 10 +++- src/pm/lifecycle.ts | 52 +++++++++++-------- src/triggers/shared/agent-execution.ts | 20 ++++++- tests/unit/backends/adapter.test.ts | 2 + tests/unit/backends/progress.test.ts | 5 +- tests/unit/backends/progressMonitor.test.ts | 14 +++-- tests/unit/pm/lifecycle.test.ts | 40 +++++++------- tests/unit/triggers/agent-execution.test.ts | 14 +++-- .../triggers/shared/agent-execution.test.ts | 6 +++ 14 files changed, 150 insertions(+), 59 deletions(-) diff --git a/src/agents/definitions/implementation.yaml b/src/agents/definitions/implementation.yaml index 912f99d6..f125c472 100644 --- a/src/agents/definitions/implementation.yaml +++ b/src/agents/definitions/implementation.yaml @@ -66,6 +66,11 @@ hooks: finish: scm: requiresPR: true + lifecycle: + moveOnPrepare: inProgress + moveOnSuccess: inReview + linkPR: true + syncChecklist: true hint: >- Complete the current todo in as few iterations as possible. Batch related diff --git a/src/agents/definitions/profiles.ts b/src/agents/definitions/profiles.ts index a57511a5..e08355ff 100644 --- a/src/agents/definitions/profiles.ts +++ b/src/agents/definitions/profiles.ts @@ -26,6 +26,7 @@ import type { AgentDefinition, ContextStepName, FinishHookFlags, + LifecycleHooks, SupportedTrigger, } from './schema.js'; import { CONTEXT_STEP_REGISTRY } from './strategies.js'; @@ -46,6 +47,8 @@ export interface AgentProfile { needsGitHubToken: boolean; /** Finish hook flags (SCM requirements: requiresPR, requiresReview, etc.) */ finishHooks: FinishHookFlags; + /** Lifecycle hooks — drives PM lifecycle behavior (moveOnPrepare, moveOnSuccess, linkPR, syncChecklist) */ + lifecycleHooks: LifecycleHooks; /** Fetch context injections for this agent type */ fetchContext(params: FetchContextParams): Promise; /** Build the task prompt for this agent type */ @@ -67,6 +70,14 @@ export interface AgentProfile { // Helpers // ============================================================================ +/** + * Resolve lifecycle hooks from an agent definition. + * Returns an empty object (no-op) when no lifecycle block is defined. + */ +function resolveLifecycleHooks(def: AgentDefinition): LifecycleHooks { + return def.hooks?.lifecycle ?? {}; +} + /** * Resolve finish hooks from an agent definition. */ @@ -175,6 +186,9 @@ function buildProfileFromDefinition(def: AgentDefinition, agentType: string): Ag // Resolve finish hooks const finish = resolveFinishHooks(def); + // Resolve lifecycle hooks + const lifecycle = resolveLifecycleHooks(def); + const profile: AgentProfile = { filterTools: (allTools: ToolManifest[]) => { // Filter tools by the gadget names derived from capabilities @@ -184,6 +198,7 @@ function buildProfileFromDefinition(def: AgentDefinition, agentType: string): Ag allCapabilities, needsGitHubToken: requiresScmIntegration(def), finishHooks: finish, + lifecycleHooks: lifecycle, fetchContext: async (params) => { // Resolve context pipeline from the trigger (empty array if no trigger or trigger has no pipeline) const contextPipeline = resolveContextPipeline(triggers, params.input.triggerEvent); diff --git a/src/agents/definitions/schema.ts b/src/agents/definitions/schema.ts index 8fb41579..722c748e 100644 --- a/src/agents/definitions/schema.ts +++ b/src/agents/definitions/schema.ts @@ -243,10 +243,28 @@ const FinishHooksSchema = z.object({ pm: PmFinishSchema.optional(), }); +// --- Lifecycle hook schema --- +/** + * Lifecycle hooks drive PM lifecycle behavior for agents. + * These replace hard-coded agentType === 'implementation' checks. + * + * - `moveOnPrepare`: PM status to move the work item to when prepareForAgent() is called + * - `moveOnSuccess`: PM status to move the work item to when handleSuccess() is called + * - `linkPR`: Whether to link the PR to the work item on success + * - `syncChecklist`: Whether to sync completed todos to the PM checklist on progress ticks + */ +export const LifecycleHooksSchema = z.object({ + moveOnPrepare: z.string().optional(), + moveOnSuccess: z.string().optional(), + linkPR: z.boolean().optional(), + syncChecklist: z.boolean().optional(), +}); + // --- Top-level integration hooks --- export const IntegrationHooksSchema = z.object({ trailing: TrailingHooksSchema.optional(), finish: FinishHooksSchema.optional(), + lifecycle: LifecycleHooksSchema.optional(), }); const PromptsSchema = z.object({ @@ -325,9 +343,12 @@ export type IntegrationRequirements = z.infer; -/** Integration hooks (trailing + finish) */ +/** Integration hooks (trailing + finish + lifecycle) */ export type IntegrationHooks = z.infer; +/** Lifecycle hook configuration for PM lifecycle behavior */ +export type LifecycleHooks = z.infer; + /** Flattened trailing hook flags for consumers */ export type TrailingHookFlags = z.infer & z.infer; diff --git a/src/backends/adapter.ts b/src/backends/adapter.ts index de3442f9..68e7d0bc 100644 --- a/src/backends/adapter.ts +++ b/src/backends/adapter.ts @@ -108,6 +108,7 @@ export async function executeWithEngine( isGitHubAck, engine.definition.id, partialInput.model ?? '', + profile.lifecycleHooks.syncChecklist ?? false, ), ); diff --git a/src/backends/progressLifecycle.ts b/src/backends/progressLifecycle.ts index 9f05567f..5a261d5a 100644 --- a/src/backends/progressLifecycle.ts +++ b/src/backends/progressLifecycle.ts @@ -24,6 +24,7 @@ export function buildProgressMonitorConfig( isGitHubAck: boolean, engineId: string, model: string, + syncChecklist = false, ) { const { workItemId } = input; @@ -49,6 +50,7 @@ export function buildProgressMonitorConfig( trello: workItemId ? { workItemId } : undefined, preSeededCommentId: isGitHubAck ? undefined : (input.ackCommentId as string | undefined), runLink, + syncChecklist, ...(input.prNumber && input.repoFullName ? { github: { diff --git a/src/backends/progressMonitor.ts b/src/backends/progressMonitor.ts index 97e4e4a5..dfd1f247 100644 --- a/src/backends/progressMonitor.ts +++ b/src/backends/progressMonitor.ts @@ -42,6 +42,12 @@ export interface ProgressMonitorConfig { github?: { owner: string; repo: string }; /** Pre-seeded comment ID from router ack — skip initial comment posting */ preSeededCommentId?: string; + /** + * Whether to sync completed todos to the PM checklist on each progress tick. + * Replaces the hard-coded agentType === 'implementation' check. + * Defaults to false (no-op for agents without lifecycle.syncChecklist in their definition). + */ + syncChecklist?: boolean; /** * Progressive schedule of delays (in minutes) before falling back to * `intervalMinutes` for steady-state ticks. @@ -264,8 +270,8 @@ export class ProgressMonitor implements ProgressReporter { } } - // Sync checklist items for implementation agents - if (this.config.agentType === 'implementation' && this.config.trello) { + // Sync checklist items when lifecycle hooks enable it + if (this.config.syncChecklist && this.config.trello) { await syncCompletedTodosToChecklist(this.config.trello.workItemId); } } catch (err) { diff --git a/src/pm/lifecycle.ts b/src/pm/lifecycle.ts index cbeae340..09a6738d 100644 --- a/src/pm/lifecycle.ts +++ b/src/pm/lifecycle.ts @@ -6,6 +6,7 @@ * manipulating labels, statuses, and comments. */ +import type { LifecycleHooks } from '../agents/definitions/schema.js'; import type { ProjectConfig } from '../types/index.js'; import { safeOperation, silentOperation } from '../utils/safeOperation.js'; import { pmRegistry } from './registry.js'; @@ -67,43 +68,48 @@ export class PMLifecycleManager { private pmConfig: ProjectPMConfig, ) {} - async prepareForAgent(workItemId: string, agentType: string): Promise { + async prepareForAgent(workItemId: string, hooks: LifecycleHooks): Promise { await this.safeAddLabel(workItemId, this.pmConfig.labels.processing); await this.safeRemoveLabel(workItemId, this.pmConfig.labels.readyToProcess); await this.safeRemoveLabel(workItemId, this.pmConfig.labels.processed); - if (agentType === 'implementation') { - await this.safeMove(workItemId, this.pmConfig.statuses.inProgress); + if (hooks.moveOnPrepare) { + const destination = + this.pmConfig.statuses[hooks.moveOnPrepare as keyof typeof this.pmConfig.statuses]; + await this.safeMove(workItemId, destination); } } async handleSuccess( workItemId: string, - agentType: string, + hooks: LifecycleHooks, prUrl?: string, progressCommentId?: string, ): Promise { await this.safeAddLabel(workItemId, this.pmConfig.labels.processed); - if (agentType === 'implementation') { - await this.safeMove(workItemId, this.pmConfig.statuses.inReview); - if (prUrl) { - const prTitle = extractPRTitle(prUrl); - let linked = false; - try { - await this.provider.linkPR(workItemId, prUrl, prTitle); - linked = true; - } catch { - // linkPR failed — fall through to comment fallback - } - if (!linked) { - const message = `PR created: ${prUrl}`; - if (progressCommentId) { - // Replace the progress comment with the "PR created" message - await this.safeUpdateOrAddComment(workItemId, progressCommentId, message); - } else { - await this.safeAddComment(workItemId, message); - } + if (hooks.moveOnSuccess) { + const destination = + this.pmConfig.statuses[hooks.moveOnSuccess as keyof typeof this.pmConfig.statuses]; + await this.safeMove(workItemId, destination); + } + + if (hooks.linkPR && prUrl) { + const prTitle = extractPRTitle(prUrl); + let linked = false; + try { + await this.provider.linkPR(workItemId, prUrl, prTitle); + linked = true; + } catch { + // linkPR failed — fall through to comment fallback + } + if (!linked) { + const message = `PR created: ${prUrl}`; + if (progressCommentId) { + // Replace the progress comment with the "PR created" message + await this.safeUpdateOrAddComment(workItemId, progressCommentId, message); + } else { + await this.safeAddComment(workItemId, message); } } } diff --git a/src/triggers/shared/agent-execution.ts b/src/triggers/shared/agent-execution.ts index 86827082..e347d0f9 100644 --- a/src/triggers/shared/agent-execution.ts +++ b/src/triggers/shared/agent-execution.ts @@ -1,3 +1,5 @@ +import { getAgentProfile } from '../../agents/definitions/profiles.js'; +import type { LifecycleHooks } from '../../agents/definitions/schema.js'; import { runAgent } from '../../agents/registry.js'; import { createWorkItem, linkPRToWorkItem } from '../../db/repositories/prWorkItemsRepository.js'; import { updateRunPRNumber } from '../../db/repositories/runsRepository.js'; @@ -99,6 +101,7 @@ async function runPostAgentLifecycle( agentResult: AgentResult, project: ProjectConfig, lifecycle: PMLifecycleManager, + lifecycleHooks: LifecycleHooks, executionConfig: AgentExecutionConfig, ): Promise { const { @@ -129,7 +132,7 @@ async function runPostAgentLifecycle( if (shouldCallHandleSuccess) { await lifecycle.handleSuccess( workItemId, - agentType, + lifecycleHooks, agentResult.prUrl, agentResult.progressCommentId, ); @@ -364,6 +367,18 @@ export async function runAgentExecutionPipeline( const pmConfig = resolveProjectPMConfig(project); const lifecycle = new PMLifecycleManager(pmProvider, pmConfig); + // Load lifecycle hooks from agent profile (best-effort — defaults to no-op on failure) + let lifecycleHooks: LifecycleHooks = {}; + try { + const agentProfile = await getAgentProfile(agentType); + lifecycleHooks = agentProfile.lifecycleHooks; + } catch (err) { + logger.warn('Failed to load agent profile for lifecycle hooks, using defaults', { + agentType, + error: String(err), + }); + } + // Pre-flight integration validation const validation = await validateIntegrations(project.id, agentType); if (!validation.valid) { @@ -429,7 +444,7 @@ export async function runAgentExecutionPipeline( } if (workItemId && !skipPrepareForAgent) { - await lifecycle.prepareForAgent(workItemId, agentType); + await lifecycle.prepareForAgent(workItemId, lifecycleHooks); } const agentResult = await runAgent(agentType, { @@ -459,6 +474,7 @@ export async function runAgentExecutionPipeline( agentResult, project, lifecycle, + lifecycleHooks, executionConfig, ); } diff --git a/tests/unit/backends/adapter.test.ts b/tests/unit/backends/adapter.test.ts index f77357d7..5690a499 100644 --- a/tests/unit/backends/adapter.test.ts +++ b/tests/unit/backends/adapter.test.ts @@ -195,12 +195,14 @@ function makeMockProfile(overrides?: Partial): AgentProfile { allCapabilities: ['fs:read', 'fs:write', 'shell:exec'], needsGitHubToken: false, finishHooks: {}, + lifecycleHooks: {}, fetchContext: vi.fn().mockResolvedValue([]), buildTaskPrompt: () => 'Process the work item', capabilities: { required: ['fs:read'], optional: ['fs:write', 'shell:exec'], }, + getLlmistGadgets: vi.fn().mockReturnValue([]), ...overrides, }; } diff --git a/tests/unit/backends/progress.test.ts b/tests/unit/backends/progress.test.ts index 316e15f4..d7ea50a3 100644 --- a/tests/unit/backends/progress.test.ts +++ b/tests/unit/backends/progress.test.ts @@ -459,7 +459,7 @@ describe('ProgressMonitor — tick behavior', () => { ); }); - it('syncs checklist for implementation agents', async () => { + it('syncs checklist when syncChecklist is true', async () => { const monitor = new ProgressMonitor({ agentType: 'implementation', taskDescription: 'Test task', @@ -468,6 +468,7 @@ describe('ProgressMonitor — tick behavior', () => { customModels: [], logWriter: vi.fn(), trello: { workItemId: 'card1' }, + syncChecklist: true, }); mockGetPMProvider.mockReturnValue(mockPMProvider as unknown as PMProvider); @@ -483,7 +484,7 @@ describe('ProgressMonitor — tick behavior', () => { expect(mockSyncChecklist).toHaveBeenCalledWith('card1'); }); - it('does not sync checklist for non-implementation agents', async () => { + it('does not sync checklist when syncChecklist is not set', async () => { const monitor = new ProgressMonitor({ agentType: 'planning', taskDescription: 'Test task', diff --git a/tests/unit/backends/progressMonitor.test.ts b/tests/unit/backends/progressMonitor.test.ts index 58b35935..1e92da9b 100644 --- a/tests/unit/backends/progressMonitor.test.ts +++ b/tests/unit/backends/progressMonitor.test.ts @@ -352,14 +352,20 @@ describe('ProgressMonitor - tick (via scheduler callback)', () => { expect(mockPMPosterUpdate).toHaveBeenCalledTimes(1); }); - it('syncs todos to checklist for implementation agent with trello', async () => { - await runTick(makeConfig({ trello: { workItemId: 'card-1' } })); + it('syncs todos to checklist when syncChecklist is true and trello is configured', async () => { + await runTick(makeConfig({ syncChecklist: true, trello: { workItemId: 'card-1' } })); expect(mockSyncCompletedTodosToChecklist).toHaveBeenCalledWith('card-1'); }); - it('does not sync todos for non-implementation agents', async () => { - await runTick(makeConfig({ agentType: 'review', trello: { workItemId: 'card-1' } })); + it('does not sync todos when syncChecklist is false', async () => { + await runTick(makeConfig({ syncChecklist: false, trello: { workItemId: 'card-1' } })); + + expect(mockSyncCompletedTodosToChecklist).not.toHaveBeenCalled(); + }); + + it('does not sync todos when syncChecklist is not set (default)', async () => { + await runTick(makeConfig({ trello: { workItemId: 'card-1' } })); expect(mockSyncCompletedTodosToChecklist).not.toHaveBeenCalled(); }); diff --git a/tests/unit/pm/lifecycle.test.ts b/tests/unit/pm/lifecycle.test.ts index c0c519e3..a5cd4f16 100644 --- a/tests/unit/pm/lifecycle.test.ts +++ b/tests/unit/pm/lifecycle.test.ts @@ -388,22 +388,22 @@ describe('pm/lifecycle', () => { describe('prepareForAgent', () => { it('adds processing label and removes ready/processed labels', async () => { - await manager.prepareForAgent('work-item-1', 'splitting'); + await manager.prepareForAgent('work-item-1', {}); expect(mockProvider.addLabel).toHaveBeenCalledWith('work-item-1', 'label-proc'); expect(mockProvider.removeLabel).toHaveBeenCalledWith('work-item-1', 'label-ready'); expect(mockProvider.removeLabel).toHaveBeenCalledWith('work-item-1', 'label-done'); }); - it('moves to inProgress status when agentType is implementation', async () => { - await manager.prepareForAgent('work-item-1', 'implementation'); + it('moves to inProgress status when moveOnPrepare is set to inProgress', async () => { + await manager.prepareForAgent('work-item-1', { moveOnPrepare: 'inProgress' }); expect(mockProvider.addLabel).toHaveBeenCalledWith('work-item-1', 'label-proc'); expect(mockProvider.moveWorkItem).toHaveBeenCalledWith('work-item-1', 'list-progress'); }); - it('does not move work item for non-implementation agents', async () => { - await manager.prepareForAgent('work-item-1', 'splitting'); + it('does not move work item when moveOnPrepare is not set', async () => { + await manager.prepareForAgent('work-item-1', {}); expect(mockProvider.moveWorkItem).not.toHaveBeenCalled(); }); @@ -414,7 +414,7 @@ describe('pm/lifecycle', () => { statuses: {}, }); - await managerNoLabels.prepareForAgent('work-item-1', 'splitting'); + await managerNoLabels.prepareForAgent('work-item-1', {}); expect(mockProvider.addLabel).not.toHaveBeenCalled(); expect(mockProvider.removeLabel).not.toHaveBeenCalled(); @@ -423,22 +423,22 @@ describe('pm/lifecycle', () => { describe('handleSuccess', () => { it('adds processed label', async () => { - await manager.handleSuccess('work-item-1', 'splitting'); + await manager.handleSuccess('work-item-1', {}); expect(mockProvider.addLabel).toHaveBeenCalledWith('work-item-1', 'label-done'); }); - it('moves to inReview status when agentType is implementation', async () => { - await manager.handleSuccess('work-item-1', 'implementation'); + it('moves to inReview status when moveOnSuccess is set to inReview', async () => { + await manager.handleSuccess('work-item-1', { moveOnSuccess: 'inReview' }); expect(mockProvider.addLabel).toHaveBeenCalledWith('work-item-1', 'label-done'); expect(mockProvider.moveWorkItem).toHaveBeenCalledWith('work-item-1', 'list-review'); }); - it('calls linkPR when prUrl is provided for implementation agent', async () => { + it('calls linkPR when prUrl is provided and linkPR hook is true', async () => { await manager.handleSuccess( 'work-item-1', - 'implementation', + { linkPR: true }, 'https://github.com/owner/repo/pull/123', ); @@ -450,27 +450,27 @@ describe('pm/lifecycle', () => { }); it('does not post comment when linkPR succeeds', async () => { - await manager.handleSuccess('work-item-1', 'implementation', 'https://github.com/pr/123'); + await manager.handleSuccess('work-item-1', { linkPR: true }, 'https://github.com/pr/123'); expect(mockProvider.addComment).not.toHaveBeenCalled(); expect(mockProvider.updateComment).not.toHaveBeenCalled(); }); it('does not call linkPR when prUrl is not provided', async () => { - await manager.handleSuccess('work-item-1', 'implementation'); + await manager.handleSuccess('work-item-1', { linkPR: true }); expect(mockProvider.linkPR).not.toHaveBeenCalled(); expect(mockProvider.addComment).not.toHaveBeenCalled(); }); - it('does not move work item for non-implementation agents', async () => { - await manager.handleSuccess('work-item-1', 'splitting'); + it('does not move work item when moveOnSuccess is not set', async () => { + await manager.handleSuccess('work-item-1', {}); expect(mockProvider.moveWorkItem).not.toHaveBeenCalled(); }); - it('does not call linkPR for non-implementation agents even with prUrl', async () => { - await manager.handleSuccess('work-item-1', 'splitting', 'https://github.com/pr/123'); + it('does not call linkPR when linkPR hook is not set even with prUrl', async () => { + await manager.handleSuccess('work-item-1', {}, 'https://github.com/pr/123'); expect(mockProvider.linkPR).not.toHaveBeenCalled(); }); @@ -480,7 +480,7 @@ describe('pm/lifecycle', () => { await manager.handleSuccess( 'work-item-1', - 'implementation', + { linkPR: true }, 'https://github.com/owner/repo/pull/123', ); @@ -500,7 +500,7 @@ describe('pm/lifecycle', () => { await manager.handleSuccess( 'work-item-1', - 'implementation', + { linkPR: true }, 'https://github.com/pr/123', 'comment-abc', ); @@ -520,7 +520,7 @@ describe('pm/lifecycle', () => { await manager.handleSuccess( 'work-item-1', - 'implementation', + { linkPR: true }, 'https://github.com/pr/123', 'comment-abc', ); diff --git a/tests/unit/triggers/agent-execution.test.ts b/tests/unit/triggers/agent-execution.test.ts index 2e751af9..9991c28b 100644 --- a/tests/unit/triggers/agent-execution.test.ts +++ b/tests/unit/triggers/agent-execution.test.ts @@ -46,6 +46,10 @@ vi.mock('../../../src/pm/config.js', () => ({ getJiraConfig: vi.fn(), })); +vi.mock('../../../src/agents/definitions/profiles.js', () => ({ + getAgentProfile: vi.fn().mockResolvedValue({ lifecycleHooks: {} }), +})); + import { runAgent } from '../../../src/agents/registry.js'; import { getJiraConfig, getTrelloConfig } from '../../../src/pm/config.js'; import { getPMProvider } from '../../../src/pm/context.js'; @@ -175,7 +179,7 @@ describe('runAgentExecutionPipeline', () => { it('calls prepareForAgent by default', async () => { await runAgentExecutionPipeline(mockTriggerResult, mockProject, mockConfig); - expect(mockLifecycle.prepareForAgent).toHaveBeenCalledWith('card-123', 'implementation'); + expect(mockLifecycle.prepareForAgent).toHaveBeenCalledWith('card-123', expect.any(Object)); }); it('skips prepareForAgent when skipPrepareForAgent is true', async () => { @@ -212,7 +216,7 @@ describe('runAgentExecutionPipeline', () => { expect(mockLifecycle.handleSuccess).toHaveBeenCalledWith( 'card-123', - 'implementation', + expect.any(Object), 'https://github.com/pr/1', undefined, ); @@ -231,7 +235,7 @@ describe('runAgentExecutionPipeline', () => { expect(mockLifecycle.handleSuccess).toHaveBeenCalledWith( 'card-123', - 'implementation', + expect.any(Object), 'https://github.com/pr/1', 'comment-456', ); @@ -312,7 +316,7 @@ describe('runAgentExecutionPipeline', () => { await runAgentExecutionPipeline(result, mockProject, mockConfig); - expect(mockLifecycle.prepareForAgent).toHaveBeenCalledWith('card-456', 'implementation'); + expect(mockLifecycle.prepareForAgent).toHaveBeenCalledWith('card-456', expect.any(Object)); }); it('uses workItemId when present', async () => { @@ -324,7 +328,7 @@ describe('runAgentExecutionPipeline', () => { await runAgentExecutionPipeline(result, mockProject, mockConfig); - expect(mockLifecycle.prepareForAgent).toHaveBeenCalledWith('issue-789', 'implementation'); + expect(mockLifecycle.prepareForAgent).toHaveBeenCalledWith('issue-789', expect.any(Object)); }); }); diff --git a/tests/unit/triggers/shared/agent-execution.test.ts b/tests/unit/triggers/shared/agent-execution.test.ts index 73ef1b59..5c8fc22d 100644 --- a/tests/unit/triggers/shared/agent-execution.test.ts +++ b/tests/unit/triggers/shared/agent-execution.test.ts @@ -27,6 +27,7 @@ const { mockLookupWorkItemForPR, mockGithubClient, mockParseRepoFullName, + mockGetAgentProfile, } = vi.hoisted(() => ({ mockRunAgent: vi.fn(), mockGetPMProvider: vi.fn(), @@ -72,6 +73,7 @@ const { mockLookupWorkItemForPR: vi.fn().mockResolvedValue(null), mockGithubClient: { getPR: vi.fn().mockResolvedValue({ title: 'feat: test PR' }) }, mockParseRepoFullName: vi.fn().mockReturnValue({ owner: 'acme', repo: 'myapp' }), + mockGetAgentProfile: vi.fn().mockResolvedValue({ lifecycleHooks: {} }), })); vi.mock('../../../../src/agents/registry.js', () => ({ @@ -156,6 +158,10 @@ vi.mock('../../../../src/utils/repo.js', () => ({ parseRepoFullName: mockParseRepoFullName, })); +vi.mock('../../../../src/agents/definitions/profiles.js', () => ({ + getAgentProfile: mockGetAgentProfile, +})); + import { linkPRToWorkItem } from '../../../../src/db/repositories/prWorkItemsRepository.js'; import { runAgentExecutionPipeline } from '../../../../src/triggers/shared/agent-execution.js';