agent-ecosystem/src/main/http/teams.ts

932 lines
32 KiB
TypeScript

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<TeamLaunchRequest, 'teamName'>;
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<HttpServices['teamProvisioningService']> {
if (!services.teamProvisioningService) {
throw new HttpFeatureUnavailableError('Team runtime control is not available in this mode');
}
return services.teamProvisioningService;
}
function getTeamDataService(services: HttpServices): NonNullable<HttpServices['teamDataService']> {
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<string>();
return payloadMembers.map((member) => {
if (!member || typeof member !== 'object') {
throw new HttpBadRequestError('member must be object');
}
const rawMember = member as Record<string, unknown>;
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<string, unknown>) : {};
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<string, unknown>) : {};
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<string, unknown> {
return body && typeof body === 'object' ? (body as Record<string, unknown>) : {};
}
function pickOptionalString(
payload: Record<string, unknown>,
key: string,
fallback: string | undefined,
fieldName: string
): string | undefined {
return Object.hasOwn(payload, key) ? assertOptionalString(payload[key], fieldName) : fallback;
}
function pickOptionalBoolean(
payload: Record<string, unknown>,
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<TeamCreateRequest | null> {
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<string, unknown> {
const payload =
body && typeof body === 'object' && !Array.isArray(body)
? (body as Record<string, unknown>)
: {};
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<HttpServices['memberWorkSyncFeature']> {
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<string, unknown> }>(
'/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<string, unknown> }>(
'/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<string, unknown> }>(
'/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<string, unknown> }>(
'/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<string, unknown> }>(
'/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) });
}
}
);
}