From e996962b64b8b0409531b36992088a2bf9f49d29 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 15 Mar 2026 09:59:35 +0000 Subject: [PATCH] feat(mcp-server): add team provisioning follow-up hooks --- .../a2a/governance-integration.test.ts | 42 ++++ .../modules/a2a/team-provisioning-pipeline.ts | 220 +++++++++++++++--- 2 files changed, 235 insertions(+), 27 deletions(-) diff --git a/services/mcp-server/src/modules/a2a/governance-integration.test.ts b/services/mcp-server/src/modules/a2a/governance-integration.test.ts index f463bfce..f7563f28 100644 --- a/services/mcp-server/src/modules/a2a/governance-integration.test.ts +++ b/services/mcp-server/src/modules/a2a/governance-integration.test.ts @@ -22,6 +22,9 @@ vi.mock('../../lib/extraction-client.js', () => ({ vi.mock('../../lib/jarvis-client.js', () => ({ jarvisMarketplaceGetListing: vi.fn(), jarvisMarketplaceCertify: vi.fn(), + jarvisTeamsListMembers: vi.fn(), + jarvisAgentsList: vi.fn(), + jarvisMemoryCreate: vi.fn(), })); vi.mock('../../lib/lysnrai-client.js', () => ({ @@ -178,4 +181,43 @@ describe('A2A governance integration', () => { expect.objectContaining({ productId: 'lysnrai' }) ); }); + + it('creates a support case when team provisioning is only partially successful', async () => { + const { runTeamProvisioningPipeline } = await import('./team-provisioning-pipeline.js'); + const { jarvisTeamsListMembers, jarvisAgentsList, jarvisMemoryCreate } = + await import('../../lib/jarvis-client.js'); + + vi.mocked(jarvisTeamsListMembers).mockResolvedValue({ + members: [ + { + userId: 'u1', + teamId: 'team_1', + role: 'member', + status: 'invited', + joinedAt: new Date().toISOString(), + }, + ], + total: 1, + } as never); + vi.mocked(jarvisAgentsList).mockResolvedValue({ + agents: [ + { + id: 'agt_1', + role: 'productivity_coach', + }, + ], + total: 1, + } as never); + vi.mocked(jarvisMemoryCreate).mockRejectedValue(new Error('memory down')); + + const report = await runTeamProvisioningPipeline('team_1', 48, buildReq('req_team')); + + expect(report.failed).toBe(1); + expect(supportCasesCreate).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Team provisioning follow-up for team_1', + }), + expect.objectContaining({ productId: 'jarvisjr' }) + ); + }); }); diff --git a/services/mcp-server/src/modules/a2a/team-provisioning-pipeline.ts b/services/mcp-server/src/modules/a2a/team-provisioning-pipeline.ts index 749f2705..5fd594cf 100644 --- a/services/mcp-server/src/modules/a2a/team-provisioning-pipeline.ts +++ b/services/mcp-server/src/modules/a2a/team-provisioning-pipeline.ts @@ -21,6 +21,15 @@ import { type JarvisTeamMemberDoc, type JarvisAgentDoc, } from '../../lib/jarvis-client.js'; +import { + trackRunCompleted, + trackRunFailed, + trackRunStarted, + trackStepCompleted, + trackStepFailed, + trackStepStarted, +} from './run-tracker.js'; +import { createSupportCaseForRun } from './governance.js'; // ── Types ────────────────────────────────────────────────────────────────────── @@ -180,7 +189,7 @@ function assembleProvisioningReport( // ── Pipeline runner ──────────────────────────────────────────────────────────── -async function runTeamProvisioningPipeline( +export async function runTeamProvisioningPipeline( teamId: string, sinceHours: number, req: McpToolRequest @@ -188,37 +197,194 @@ async function runTeamProvisioningPipeline( const runId = randomUUID(); const opts = { token: req.headers.authorization?.slice(7), requestId: req.id }; const sinceMs = sinceHours * 60 * 60 * 1000; + let currentStep: { stepName: 'detect' | 'onboard' | 'report'; order: number } | undefined; - req.log.info({ runId, stepId: 'detect', teamId, sinceHours }, 'NewMemberDetectorAgent start'); - const { all, newMembers } = await detectNewMembers(teamId, sinceMs, opts); - req.log.info( - { runId, stepId: 'detect', total: all.length, newCount: newMembers.length }, - 'NewMemberDetectorAgent done' + await safeTrack(() => + trackRunStarted({ + runId, + productId: 'jarvisjr', + name: 'team-provisioning', + requestId: req.id, + token: opts.token, + input: { teamId, sinceHours }, + }) ); - req.log.info({ runId, stepId: 'onboard', newCount: newMembers.length }, 'OnboardingAgent start'); - const agentsResult = await jarvisAgentsList({ limit: 20 }, opts).catch(() => ({ - agents: [] as JarvisAgentDoc[], - total: 0, - })); - const results: MemberOnboardingResult[] = []; - for (const member of newMembers) { - const result = await onboardMember(member, agentsResult.agents, opts); - results.push(result); + try { + currentStep = { stepName: 'detect', order: 1 }; + const detectStep = currentStep; + await safeTrack(() => + trackStepStarted({ + runId, + productId: 'jarvisjr', + stepName: detectStep.stepName, + order: detectStep.order, + token: opts.token, + requestId: req.id, + input: { teamId, sinceHours }, + }) + ); + req.log.info({ runId, stepId: 'detect', teamId, sinceHours }, 'NewMemberDetectorAgent start'); + const { all, newMembers } = await detectNewMembers(teamId, sinceMs, opts); + await safeTrack(() => + trackStepCompleted({ + runId, + productId: 'jarvisjr', + stepName: detectStep.stepName, + order: detectStep.order, + token: opts.token, + requestId: req.id, + output: { totalMembers: all.length, newMembers: newMembers.length }, + }) + ); + req.log.info( + { runId, stepId: 'detect', total: all.length, newCount: newMembers.length }, + 'NewMemberDetectorAgent done' + ); + + currentStep = { stepName: 'onboard', order: 2 }; + const onboardStep = currentStep; + await safeTrack(() => + trackStepStarted({ + runId, + productId: 'jarvisjr', + stepName: onboardStep.stepName, + order: onboardStep.order, + token: opts.token, + requestId: req.id, + input: { newMembers: newMembers.length }, + }) + ); + req.log.info( + { runId, stepId: 'onboard', newCount: newMembers.length }, + 'OnboardingAgent start' + ); + const agentsResult = await jarvisAgentsList({ limit: 20 }, opts).catch(() => ({ + agents: [] as JarvisAgentDoc[], + total: 0, + })); + const results: MemberOnboardingResult[] = []; + for (const member of newMembers) { + const result = await onboardMember(member, agentsResult.agents, opts); + results.push(result); + } + await safeTrack(() => + trackStepCompleted({ + runId, + productId: 'jarvisjr', + stepName: onboardStep.stepName, + order: onboardStep.order, + token: opts.token, + requestId: req.id, + output: { + seeded: results.filter(r => r.memorySeeded).length, + failed: results.filter(r => r.errors.length > 0).length, + }, + }) + ); + req.log.info( + { runId, stepId: 'onboard', seeded: results.filter(r => r.memorySeeded).length }, + 'OnboardingAgent done' + ); + + currentStep = { stepName: 'report', order: 3 }; + const reportStep = currentStep; + await safeTrack(() => + trackStepStarted({ + runId, + productId: 'jarvisjr', + stepName: reportStep.stepName, + order: reportStep.order, + token: opts.token, + requestId: req.id, + }) + ); + req.log.info({ runId, stepId: 'report' }, 'ProvisioningReportAgent start'); + const report = assembleProvisioningReport(runId, teamId, all.length, results); + await safeTrack(() => + trackStepCompleted({ + runId, + productId: 'jarvisjr', + stepName: reportStep.stepName, + order: reportStep.order, + token: opts.token, + requestId: req.id, + output: { + fullyOnboarded: report.fullyOnboarded, + partiallyOnboarded: report.partiallyOnboarded, + failed: report.failed, + }, + }) + ); + await safeTrack(() => + trackRunCompleted({ + runId, + productId: 'jarvisjr', + name: 'team-provisioning', + requestId: req.id, + token: opts.token, + output: { + fullyOnboarded: report.fullyOnboarded, + partiallyOnboarded: report.partiallyOnboarded, + failed: report.failed, + }, + }) + ); + if (report.failed > 0 || report.partiallyOnboarded > 0) { + await createSupportCaseForRun( + { + productId: 'jarvisjr', + runId, + title: `Team provisioning follow-up for ${teamId}`, + description: report.summary, + priority: report.failed > 0 ? 'high' : 'medium', + tags: ['a2a', 'team-provisioning', 'onboarding'], + }, + opts + ); + } + req.log.info( + { runId, stepId: 'report', summary: report.summary }, + 'ProvisioningReportAgent done' + ); + + return report; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (currentStep) { + const failedStep = currentStep; + await safeTrack(() => + trackStepFailed({ + runId, + productId: 'jarvisjr', + stepName: failedStep.stepName, + order: failedStep.order, + token: opts.token, + requestId: req.id, + error: message, + }) + ); + } + await safeTrack(() => + trackRunFailed({ + runId, + productId: 'jarvisjr', + name: 'team-provisioning', + requestId: req.id, + token: opts.token, + error: message, + }) + ); + throw error; } - req.log.info( - { runId, stepId: 'onboard', seeded: results.filter(r => r.memorySeeded).length }, - 'OnboardingAgent done' - ); +} - req.log.info({ runId, stepId: 'report' }, 'ProvisioningReportAgent start'); - const report = assembleProvisioningReport(runId, teamId, all.length, results); - req.log.info( - { runId, stepId: 'report', summary: report.summary }, - 'ProvisioningReportAgent done' - ); - - return report; +async function safeTrack(fn: () => Promise): Promise { + try { + await fn(); + } catch { + // Tracking must never fail the pipeline itself. + } } // ── MCP tool registration ─────────────────────────────────────────────────────