import { validateTeammateName, validateTeamName } from '@main/ipc/guards'; import { TeamConfigReader } from '@main/services/team/TeamConfigReader'; import { getTeamsBasePath } from '@main/utils/pathDecoder'; import { extractUserFlags, PROTECTED_CLI_FLAGS } from '@shared/utils/cliArgsParser'; import { formatEffortLevelListForProvider, isTeamEffortLevelForProvider, } from '@shared/utils/effortLevels'; import { getErrorMessage } from '@shared/utils/errorHandling'; import { createLogger } from '@shared/utils/logger'; import { migrateProviderBackendId } from '@shared/utils/providerBackend'; import { isTeamProviderId } from '@shared/utils/teamProvider'; import { constants as fsConstants } from 'fs'; import { access } from 'fs/promises'; import { isAbsolute, join } from 'path'; import type { HttpServices } from './index'; import type { MemberWorkSyncReportState } from '@features/member-work-sync/contracts'; import type { EffortLevel, TeamCreateConfigRequest, TeamCreateRequest, TeamFastMode, TeamLaunchRequest, } from '@shared/types/team'; import type { FastifyInstance } from 'fastify'; const logger = createLogger('HTTP:teams'); type LaunchBody = Omit; type CreateTeamBody = TeamCreateConfigRequest; class HttpBadRequestError extends Error {} class HttpFeatureUnavailableError extends Error {} function isMemberWorkSyncReportState(value: string): value is MemberWorkSyncReportState { return value === 'still_working' || value === 'blocked' || value === 'caught_up'; } function getTeamProvisioningService( services: HttpServices ): NonNullable { if (!services.teamProvisioningService) { throw new HttpFeatureUnavailableError('Team runtime control is not available in this mode'); } return services.teamProvisioningService; } function getTeamDataService(services: HttpServices): NonNullable { if (!services.teamDataService) { throw new HttpFeatureUnavailableError('Team data control is not available in this mode'); } return services.teamDataService; } function getStatusCode(error: unknown, fallback: number = 500): number { if (error instanceof HttpBadRequestError) { return 400; } if (error instanceof HttpFeatureUnavailableError) { return 501; } if (error instanceof Error && error.name === 'RuntimeStaleEvidenceError') { return 409; } if (error instanceof Error && error.message.startsWith('Team not found')) { return 404; } if (error instanceof Error && error.message.startsWith('Team already exists')) { return 409; } return fallback; } function shouldLogError(error: unknown): boolean { const statusCode = getStatusCode(error); return ( statusCode >= 500 && !(error instanceof HttpBadRequestError) && !(error instanceof HttpFeatureUnavailableError) ); } function assertProvisioningTeamName(value: unknown): string { const validated = validateTeamName(value); if (!validated.valid) { throw new HttpBadRequestError(validated.error ?? 'Invalid teamName'); } const teamName = validated.value!; const parts = teamName.split('-'); if (teamName.length > 64 || !parts.every((part) => /^[a-z0-9]+$/.test(part))) { throw new HttpBadRequestError('teamName must be kebab-case [a-z0-9-], max 64 chars'); } return teamName; } function assertAbsoluteCwd(cwd: unknown): string { if (typeof cwd !== 'string' || cwd.trim().length === 0) { throw new HttpBadRequestError('cwd must be a non-empty string'); } const normalized = cwd.trim(); if (!isAbsolute(normalized)) { throw new HttpBadRequestError('cwd must be an absolute path'); } return normalized; } function assertOptionalString(value: unknown, fieldName: string): string | undefined { if (value == null) { return undefined; } if (typeof value !== 'string') { throw new HttpBadRequestError(`${fieldName} must be a string`); } const normalized = value.trim(); return normalized.length > 0 ? normalized : undefined; } function assertOptionalBoolean(value: unknown, fieldName: string): boolean | undefined { if (value == null) { return undefined; } if (typeof value !== 'boolean') { throw new HttpBadRequestError(`${fieldName} must be a boolean`); } return value; } function assertOptionalCwd(value: unknown): string | undefined { if (value == null) { return undefined; } const cwd = assertOptionalString(value, 'cwd'); if (!cwd) { return undefined; } if (!isAbsolute(cwd)) { throw new HttpBadRequestError('cwd must be an absolute path'); } return cwd; } function assertOptionalExtraCliArgs(value: unknown): string | undefined { const extraCliArgs = assertOptionalString(value, 'extraCliArgs'); if (!extraCliArgs) { return undefined; } if (extraCliArgs.length > 1024) { throw new HttpBadRequestError('extraCliArgs too long (max 1024)'); } const protectedFlags = extractUserFlags(extraCliArgs).filter((flag) => PROTECTED_CLI_FLAGS.has(flag) ); if (protectedFlags.length > 0) { throw new HttpBadRequestError( `extraCliArgs contains app-managed flags: ${[...new Set(protectedFlags)].join(', ')}` ); } return extraCliArgs; } function assertOptionalEffort( value: unknown, providerId: TeamLaunchRequest['providerId'] ): EffortLevel | undefined { if (value == null) { return undefined; } if (!isTeamEffortLevelForProvider(value, providerId)) { throw new HttpBadRequestError( `effort must be one of: ${formatEffortLevelListForProvider(providerId)}` ); } return value; } function assertOptionalFastMode(value: unknown): TeamFastMode | undefined { if (value == null) { return undefined; } if (value !== 'inherit' && value !== 'on' && value !== 'off') { throw new HttpBadRequestError('fastMode must be one of: inherit, on, off'); } return value; } function parseProviderId(value: unknown): TeamLaunchRequest['providerId'] { if (value == null) { return 'anthropic'; } if (isTeamProviderId(value)) { return value; } throw new HttpBadRequestError('providerId must be anthropic, codex, gemini, or opencode'); } function parseProviderBackendId( providerId: TeamLaunchRequest['providerId'], value: unknown ): TeamLaunchRequest['providerBackendId'] | undefined { const rawProviderBackendId = assertOptionalString(value, 'providerBackendId'); const providerBackendId = migrateProviderBackendId(providerId, rawProviderBackendId); if (rawProviderBackendId && !providerBackendId) { throw new HttpBadRequestError( 'providerBackendId must be one of auto, adapter, api, cli-sdk, or codex-native' ); } return providerBackendId; } function parseCreateMembers(payloadMembers: unknown): TeamCreateConfigRequest['members'] { if (payloadMembers == null) { return []; } if (!Array.isArray(payloadMembers)) { throw new HttpBadRequestError('members must be an array'); } const seenNames = new Set(); return payloadMembers.map((member) => { if (!member || typeof member !== 'object') { throw new HttpBadRequestError('member must be object'); } const rawMember = member as Record; const nameValidation = validateTeammateName(rawMember.name); if (!nameValidation.valid) { throw new HttpBadRequestError(nameValidation.error ?? 'Invalid member name'); } const name = nameValidation.value!; if (seenNames.has(name)) { throw new HttpBadRequestError('member names must be unique'); } seenNames.add(name); const role = assertOptionalString(rawMember.role, 'member role'); const workflow = assertOptionalString(rawMember.workflow, 'member workflow'); if (rawMember.isolation !== undefined && rawMember.isolation !== 'worktree') { throw new HttpBadRequestError('member isolation must be "worktree" when provided'); } const providerId = rawMember.providerId == null ? undefined : parseProviderId(rawMember.providerId); const providerBackendId = parseProviderBackendId(providerId, rawMember.providerBackendId); const model = assertOptionalString(rawMember.model, 'member model'); const effort = assertOptionalEffort(rawMember.effort, providerId); const fastMode = assertOptionalFastMode(rawMember.fastMode); return { name, ...(role ? { role } : {}), ...(workflow ? { workflow } : {}), ...(rawMember.isolation === 'worktree' ? { isolation: 'worktree' as const } : {}), ...(providerId ? { providerId } : {}), ...(providerBackendId ? { providerBackendId } : {}), ...(model ? { model } : {}), ...(effort ? { effort } : {}), ...(fastMode ? { fastMode } : {}), }; }); } function parseLaunchRequest(teamName: string, body: unknown): TeamLaunchRequest { const payload = body && typeof body === 'object' ? (body as Record) : {}; const providerId = parseProviderId(payload.providerId); const prompt = assertOptionalString(payload.prompt, 'prompt'); const providerBackendId = parseProviderBackendId(providerId, payload.providerBackendId); const model = assertOptionalString(payload.model, 'model'); const effort = assertOptionalEffort(payload.effort, providerId); const fastMode = assertOptionalFastMode(payload.fastMode); const clearContext = assertOptionalBoolean(payload.clearContext, 'clearContext'); const skipPermissions = assertOptionalBoolean(payload.skipPermissions, 'skipPermissions'); const worktree = assertOptionalString(payload.worktree, 'worktree'); const extraCliArgs = assertOptionalExtraCliArgs(payload.extraCliArgs); return { teamName, cwd: assertAbsoluteCwd(payload.cwd), providerId, ...(providerBackendId && { providerBackendId, }), ...(prompt && { prompt, }), ...(model && { model, }), ...(effort && { effort, }), ...(fastMode && { fastMode, }), ...(clearContext !== undefined && { clearContext, }), ...(skipPermissions !== undefined && { skipPermissions, }), ...(worktree && { worktree, }), ...(extraCliArgs && { extraCliArgs, }), }; } function parseCreateTeamRequest(body: unknown): TeamCreateConfigRequest { const payload = body && typeof body === 'object' ? (body as Record) : {}; const teamName = assertProvisioningTeamName(payload.teamName); const providerId = payload.providerId == null ? undefined : parseProviderId(payload.providerId); const providerBackendId = parseProviderBackendId(providerId, payload.providerBackendId); const displayName = assertOptionalString(payload.displayName, 'displayName'); const description = assertOptionalString(payload.description, 'description'); const color = assertOptionalString(payload.color, 'color'); const cwd = assertOptionalCwd(payload.cwd); const prompt = assertOptionalString(payload.prompt, 'prompt'); const model = assertOptionalString(payload.model, 'model'); const effort = assertOptionalEffort(payload.effort, providerId); const fastMode = assertOptionalFastMode(payload.fastMode); const limitContext = assertOptionalBoolean(payload.limitContext, 'limitContext'); const skipPermissions = assertOptionalBoolean(payload.skipPermissions, 'skipPermissions'); const worktree = assertOptionalString(payload.worktree, 'worktree'); const extraCliArgs = assertOptionalExtraCliArgs(payload.extraCliArgs); return { teamName, members: parseCreateMembers(payload.members), ...(displayName ? { displayName } : {}), ...(description ? { description } : {}), ...(color ? { color } : {}), ...(cwd ? { cwd } : {}), ...(prompt ? { prompt } : {}), ...(providerId ? { providerId } : {}), ...(providerBackendId ? { providerBackendId } : {}), ...(model ? { model } : {}), ...(effort ? { effort } : {}), ...(fastMode ? { fastMode } : {}), ...(limitContext !== undefined ? { limitContext } : {}), ...(skipPermissions !== undefined ? { skipPermissions } : {}), ...(worktree ? { worktree } : {}), ...(extraCliArgs ? { extraCliArgs } : {}), }; } function getObjectPayload(body: unknown): Record { return body && typeof body === 'object' ? (body as Record) : {}; } function pickOptionalString( payload: Record, key: string, fallback: string | undefined, fieldName: string ): string | undefined { return Object.hasOwn(payload, key) ? assertOptionalString(payload[key], fieldName) : fallback; } function pickOptionalBoolean( payload: Record, key: string, fallback: boolean | undefined, fieldName: string ): boolean | undefined { return Object.hasOwn(payload, key) ? assertOptionalBoolean(payload[key], fieldName) : fallback; } function parseDraftLaunchCreateRequest( savedRequest: TeamCreateRequest, body: unknown ): TeamCreateRequest { const payload = getObjectPayload(body); const cwd = Object.hasOwn(payload, 'cwd') ? assertAbsoluteCwd(payload.cwd) : savedRequest.cwd; if (!cwd) { throw new HttpBadRequestError('cwd is required'); } const providerId = Object.hasOwn(payload, 'providerId') ? parseProviderId(payload.providerId) : (savedRequest.providerId ?? 'anthropic'); const providerBackendId = parseProviderBackendId( providerId, Object.hasOwn(payload, 'providerBackendId') ? payload.providerBackendId : savedRequest.providerBackendId ); const effort = assertOptionalEffort( Object.hasOwn(payload, 'effort') ? payload.effort : savedRequest.effort, providerId ); const fastMode = Object.hasOwn(payload, 'fastMode') ? assertOptionalFastMode(payload.fastMode) : savedRequest.fastMode; const extraCliArgs = Object.hasOwn(payload, 'extraCliArgs') ? assertOptionalExtraCliArgs(payload.extraCliArgs) : savedRequest.extraCliArgs; if (extraCliArgs) { assertOptionalExtraCliArgs(extraCliArgs); } return { teamName: savedRequest.teamName, displayName: savedRequest.displayName, description: savedRequest.description, color: savedRequest.color, members: savedRequest.members, cwd, prompt: pickOptionalString(payload, 'prompt', savedRequest.prompt, 'prompt'), providerId, ...(providerBackendId ? { providerBackendId } : {}), model: pickOptionalString(payload, 'model', savedRequest.model, 'model'), ...(effort ? { effort } : {}), ...(fastMode ? { fastMode } : {}), limitContext: pickOptionalBoolean( payload, 'limitContext', savedRequest.limitContext, 'limitContext' ), skipPermissions: pickOptionalBoolean( payload, 'skipPermissions', savedRequest.skipPermissions, 'skipPermissions' ), worktree: pickOptionalString(payload, 'worktree', savedRequest.worktree, 'worktree'), ...(extraCliArgs ? { extraCliArgs } : {}), }; } async function getDraftSavedRequest( services: HttpServices, teamName: string ): Promise { if (!services.teamDataService) { return null; } const configPath = join(getTeamsBasePath(), teamName, 'config.json'); try { await access(configPath, fsConstants.F_OK); return null; } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { throw error; } } return getTeamDataService(services).getSavedRequest(teamName); } function withRuntimeTeamName(teamName: string, body: unknown): Record { const payload = body && typeof body === 'object' && !Array.isArray(body) ? (body as Record) : {}; const bodyTeamName = typeof payload.teamName === 'string' ? payload.teamName.trim() : ''; if (bodyTeamName && bodyTeamName !== teamName) { throw new HttpBadRequestError('runtime body teamName must match route teamName'); } return { ...payload, teamName }; } function getMemberWorkSyncFeature( services: HttpServices ): NonNullable { if (!services.memberWorkSyncFeature) { throw new HttpBadRequestError('Member work sync feature is unavailable'); } return services.memberWorkSyncFeature; } export function registerTeamRoutes(app: FastifyInstance, services: HttpServices): void { app.get('/api/teams', async (_request, reply) => { try { return reply.send(await getTeamDataService(services).listTeams()); } catch (error) { if (shouldLogError(error)) { logger.error('Error in GET /api/teams:', getErrorMessage(error)); } return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) }); } }); app.post<{ Body: CreateTeamBody }>('/api/teams', async (request, reply) => { try { const createRequest = parseCreateTeamRequest(request.body); await getTeamDataService(services).createTeamConfig(createRequest); return reply.status(201).send({ teamName: createRequest.teamName }); } catch (error) { if (shouldLogError(error)) { logger.error('Error in POST /api/teams:', getErrorMessage(error)); } return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) }); } }); app.get<{ Params: { teamName: string } }>('/api/teams/:teamName', async (request, reply) => { try { const validatedTeamName = validateTeamName(request.params.teamName); if (!validatedTeamName.valid) { return reply.status(400).send({ error: validatedTeamName.error }); } const teamName = validatedTeamName.value!; const draftSavedRequest = await getDraftSavedRequest(services, teamName); if (draftSavedRequest) { return reply.send({ teamName, pendingCreate: true, savedRequest: draftSavedRequest, }); } await services.teamProvisioningService?.repairStaleTaskActivityIntervalsBeforeSnapshot?.( teamName ); return reply.send(await getTeamDataService(services).getTeamData(teamName)); } catch (error) { if (shouldLogError(error)) { logger.error(`Error in GET /api/teams/${request.params.teamName}:`, getErrorMessage(error)); } return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) }); } }); app.post<{ Params: { teamName: string }; Body: LaunchBody }>( '/api/teams/:teamName/launch', async (request, reply) => { try { const validatedTeamName = validateTeamName(request.params.teamName); if (!validatedTeamName.valid) { return reply.status(400).send({ error: validatedTeamName.error }); } const teamName = validatedTeamName.value!; const draftSavedRequest = await getDraftSavedRequest(services, teamName); const response = draftSavedRequest ? await getTeamProvisioningService(services).createTeam( parseDraftLaunchCreateRequest(draftSavedRequest, request.body), () => undefined ) : await getTeamProvisioningService(services).launchTeam( parseLaunchRequest(teamName, request.body), () => undefined ); TeamConfigReader.invalidateListTeamsCache(); return reply.send(response); } catch (error) { const statusCode = getStatusCode(error); if (shouldLogError(error)) { logger.error( `Error in POST /api/teams/${request.params.teamName}/launch:`, getErrorMessage(error) ); } return reply.status(statusCode).send({ error: getErrorMessage(error) }); } } ); app.post<{ Params: { teamName: string } }>( '/api/teams/:teamName/stop', async (request, reply) => { try { const validatedTeamName = validateTeamName(request.params.teamName); if (!validatedTeamName.valid) { return reply.status(400).send({ error: validatedTeamName.error }); } const teamProvisioningService = getTeamProvisioningService(services); await teamProvisioningService.stopTeam(validatedTeamName.value!); return reply.send(await teamProvisioningService.getRuntimeState(validatedTeamName.value!)); } catch (error) { if (shouldLogError(error)) { logger.error( `Error in POST /api/teams/${request.params.teamName}/stop:`, getErrorMessage(error) ); } return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) }); } } ); app.get<{ Params: { teamName: string } }>( '/api/teams/:teamName/runtime', async (request, reply) => { try { const validatedTeamName = validateTeamName(request.params.teamName); if (!validatedTeamName.valid) { return reply.status(400).send({ error: validatedTeamName.error }); } return reply.send( await getTeamProvisioningService(services).getRuntimeState(validatedTeamName.value!) ); } catch (error) { if (shouldLogError(error)) { logger.error( `Error in GET /api/teams/${request.params.teamName}/runtime:`, getErrorMessage(error) ); } return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) }); } } ); app.get<{ Params: { runId: string } }>( '/api/teams/provisioning/:runId', async (request, reply) => { try { const runId = request.params.runId?.trim(); if (!runId) { return reply.status(400).send({ error: 'runId is required' }); } return reply.send(await getTeamProvisioningService(services).getProvisioningStatus(runId)); } catch (error) { const message = getErrorMessage(error); const statusCode = message === 'Unknown runId' ? 404 : getStatusCode(error); if (shouldLogError(error) && statusCode !== 404) { logger.error(`Error in GET /api/teams/provisioning/${request.params.runId}:`, message); } return reply.status(statusCode).send({ error: message }); } } ); app.get('/api/teams/runtime/alive', async (_request, reply) => { try { const teamProvisioningService = getTeamProvisioningService(services); const runtimeStates = await Promise.all( teamProvisioningService .getAliveTeams() .map((teamName) => teamProvisioningService.getRuntimeState(teamName)) ); return reply.send(runtimeStates); } catch (error) { if (shouldLogError(error)) { logger.error('Error in GET /api/teams/runtime/alive:', getErrorMessage(error)); } return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) }); } }); app.post<{ Params: { teamName: string }; Body: Record }>( '/api/teams/:teamName/opencode/runtime/bootstrap-checkin', async (request, reply) => { try { const validatedTeamName = validateTeamName(request.params.teamName); if (!validatedTeamName.valid) { return reply.status(400).send({ error: validatedTeamName.error }); } return reply.send( await getTeamProvisioningService(services).recordOpenCodeRuntimeBootstrapCheckin( withRuntimeTeamName(validatedTeamName.value!, request.body) ) ); } catch (error) { if (shouldLogError(error)) { logger.error( `Error in POST /api/teams/${request.params.teamName}/opencode/runtime/bootstrap-checkin:`, getErrorMessage(error) ); } return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) }); } } ); app.post<{ Params: { teamName: string }; Body: Record }>( '/api/teams/:teamName/opencode/runtime/deliver-message', async (request, reply) => { try { const validatedTeamName = validateTeamName(request.params.teamName); if (!validatedTeamName.valid) { return reply.status(400).send({ error: validatedTeamName.error }); } return reply.send( await getTeamProvisioningService(services).deliverOpenCodeRuntimeMessage( withRuntimeTeamName(validatedTeamName.value!, request.body) ) ); } catch (error) { if (shouldLogError(error)) { logger.error( `Error in POST /api/teams/${request.params.teamName}/opencode/runtime/deliver-message:`, getErrorMessage(error) ); } return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) }); } } ); app.post<{ Params: { teamName: string }; Body: Record }>( '/api/teams/:teamName/opencode/runtime/task-event', async (request, reply) => { try { const validatedTeamName = validateTeamName(request.params.teamName); if (!validatedTeamName.valid) { return reply.status(400).send({ error: validatedTeamName.error }); } return reply.send( await getTeamProvisioningService(services).recordOpenCodeRuntimeTaskEvent( withRuntimeTeamName(validatedTeamName.value!, request.body) ) ); } catch (error) { if (shouldLogError(error)) { logger.error( `Error in POST /api/teams/${request.params.teamName}/opencode/runtime/task-event:`, getErrorMessage(error) ); } return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) }); } } ); app.post<{ Params: { teamName: string }; Body: Record }>( '/api/teams/:teamName/opencode/runtime/heartbeat', async (request, reply) => { try { const validatedTeamName = validateTeamName(request.params.teamName); if (!validatedTeamName.valid) { return reply.status(400).send({ error: validatedTeamName.error }); } return reply.send( await getTeamProvisioningService(services).recordOpenCodeRuntimeHeartbeat( withRuntimeTeamName(validatedTeamName.value!, request.body) ) ); } catch (error) { if (shouldLogError(error)) { logger.error( `Error in POST /api/teams/${request.params.teamName}/opencode/runtime/heartbeat:`, getErrorMessage(error) ); } return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) }); } } ); app.get<{ Params: { teamName: string } }>( '/api/teams/:teamName/member-work-sync/diagnostics', async (request, reply) => { try { const validatedTeamName = validateTeamName(request.params.teamName); if (!validatedTeamName.valid) { return reply.status(400).send({ error: validatedTeamName.error }); } const feature = getMemberWorkSyncFeature(services); const metrics = await feature.getMetrics({ teamName: validatedTeamName.value! }); return reply.send({ teamName: validatedTeamName.value!, generatedAt: new Date().toISOString(), queue: feature.getQueueDiagnostics(), metrics, }); } catch (error) { if (shouldLogError(error)) { logger.error( `Error in GET /api/teams/${request.params.teamName}/member-work-sync/diagnostics:`, getErrorMessage(error) ); } return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) }); } } ); app.get<{ Params: { teamName: string } }>( '/api/teams/:teamName/member-work-sync/metrics', async (request, reply) => { try { const validatedTeamName = validateTeamName(request.params.teamName); if (!validatedTeamName.valid) { return reply.status(400).send({ error: validatedTeamName.error }); } return reply.send( await getMemberWorkSyncFeature(services).getMetrics({ teamName: validatedTeamName.value!, }) ); } catch (error) { if (shouldLogError(error)) { logger.error( `Error in GET /api/teams/${request.params.teamName}/member-work-sync/metrics:`, getErrorMessage(error) ); } return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) }); } } ); app.get<{ Params: { teamName: string; memberName: string } }>( '/api/teams/:teamName/member-work-sync/:memberName', async (request, reply) => { try { const validatedTeamName = validateTeamName(request.params.teamName); if (!validatedTeamName.valid) { return reply.status(400).send({ error: validatedTeamName.error }); } const memberName = request.params.memberName?.trim(); if (!memberName) { return reply.status(400).send({ error: 'memberName is required' }); } return reply.send( await getMemberWorkSyncFeature(services).getStatus({ teamName: validatedTeamName.value!, memberName, }) ); } catch (error) { if (shouldLogError(error)) { logger.error( `Error in GET /api/teams/${request.params.teamName}/member-work-sync/${request.params.memberName}:`, getErrorMessage(error) ); } return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) }); } } ); app.post<{ Params: { teamName: string; memberName: string } }>( '/api/teams/:teamName/member-work-sync/:memberName/refresh', async (request, reply) => { try { const validatedTeamName = validateTeamName(request.params.teamName); if (!validatedTeamName.valid) { return reply.status(400).send({ error: validatedTeamName.error }); } const memberName = request.params.memberName?.trim(); if (!memberName) { return reply.status(400).send({ error: 'memberName is required' }); } return reply.send( await getMemberWorkSyncFeature(services).refreshStatus({ teamName: validatedTeamName.value!, memberName, }) ); } catch (error) { if (shouldLogError(error)) { logger.error( `Error in POST /api/teams/${request.params.teamName}/member-work-sync/${request.params.memberName}/refresh:`, getErrorMessage(error) ); } return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) }); } } ); app.post<{ Params: { teamName: string }; Body: Record }>( '/api/teams/:teamName/member-work-sync/report', async (request, reply) => { try { const validatedTeamName = validateTeamName(request.params.teamName); if (!validatedTeamName.valid) { return reply.status(400).send({ error: validatedTeamName.error }); } const payload = withRuntimeTeamName(validatedTeamName.value!, request.body); const memberName = typeof payload.memberName === 'string' ? payload.memberName.trim() : ''; const state = typeof payload.state === 'string' ? payload.state.trim() : ''; const agendaFingerprint = typeof payload.agendaFingerprint === 'string' ? payload.agendaFingerprint.trim() : ''; if (!memberName || !state || !agendaFingerprint) { return reply.status(400).send({ error: 'memberName, state, and agendaFingerprint are required', }); } if (!isMemberWorkSyncReportState(state)) { return reply .status(400) .send({ error: 'state must be still_working, blocked, or caught_up' }); } const taskIds = Array.isArray(payload.taskIds) ? [ ...new Set( payload.taskIds .filter((taskId): taskId is string => typeof taskId === 'string') .map((taskId) => taskId.trim()) .filter(Boolean) ), ] : undefined; return reply.send( await getMemberWorkSyncFeature(services).report({ teamName: validatedTeamName.value!, memberName, state, agendaFingerprint, ...(typeof payload.reportToken === 'string' ? { reportToken: payload.reportToken } : {}), ...(taskIds?.length ? { taskIds } : {}), ...(typeof payload.note === 'string' ? { note: payload.note } : {}), ...(typeof payload.reportedAt === 'string' ? { reportedAt: payload.reportedAt } : {}), ...(typeof payload.leaseTtlMs === 'number' ? { leaseTtlMs: payload.leaseTtlMs } : {}), source: 'mcp', }) ); } catch (error) { if (shouldLogError(error)) { logger.error( `Error in POST /api/teams/${request.params.teamName}/member-work-sync/report:`, getErrorMessage(error) ); } return reply.status(getStatusCode(error)).send({ error: getErrorMessage(error) }); } } ); }