From 1949084bd8182bc56fa9b8c5c67fd0f7c5e28abf Mon Sep 17 00:00:00 2001 From: iliya Date: Tue, 10 Mar 2026 17:20:49 +0200 Subject: [PATCH] feat: enhance member handling and validation in team services - Updated handleAddMember function to include optional 'workflow' parameter with validation. - Introduced ensureMemberInMeta method in TeamDataService to migrate members from config.json to meta if necessary. - Refactored addMember and removeMember methods to utilize ensureMemberInMeta for improved member management. - Enhanced TeamProvisioningService to ensure message handling includes workflow context in notifications. --- src/main/ipc/teams.ts | 16 ++++- src/main/services/team/TeamDataService.ts | 66 ++++++++++++++++--- .../services/team/TeamProvisioningService.ts | 4 +- 3 files changed, 74 insertions(+), 12 deletions(-) diff --git a/src/main/ipc/teams.ts b/src/main/ipc/teams.ts index 7f79b2e6..67625ba9 100644 --- a/src/main/ipc/teams.ts +++ b/src/main/ipc/teams.ts @@ -1850,12 +1850,19 @@ async function handleAddMember( if (!payload || typeof payload !== 'object') { return { success: false, error: 'Invalid payload' }; } - const { name, role } = payload as { name?: unknown; role?: unknown }; + const { name, role, workflow } = payload as { + name?: unknown; + role?: unknown; + workflow?: unknown; + }; const vName = validateTeammateName(name); if (!vName.valid) return { success: false, error: vName.error ?? 'Invalid member name' }; if (role !== undefined && typeof role !== 'string') { return { success: false, error: 'role must be a string' }; } + if (workflow !== undefined && typeof workflow !== 'string') { + return { success: false, error: 'workflow must be a string' }; + } return wrapTeamHandler('addMember', async () => { const tn = vTeam.value!; @@ -1863,15 +1870,20 @@ async function handleAddMember( await getTeamDataService().addMember(tn, { name: memberName, role: role, + workflow: typeof workflow === 'string' ? workflow.trim() || undefined : undefined, }); // If team is alive, notify the lead to spawn the new teammate const provisioning = getTeamProvisioningService(); if (provisioning.isTeamAlive(tn)) { const roleHint = typeof role === 'string' && role.trim() ? ` with role "${role.trim()}"` : ''; + const workflowHint = + typeof workflow === 'string' && workflow.trim() + ? ` Their workflow: ${workflow.trim()}` + : ''; const spawnMessage = `A new teammate "${memberName}"${roleHint} has been added to the team. ` + - `Please spawn them immediately using the Task tool with team_name="${tn}" and name="${memberName}".`; + `Please spawn them immediately using the Task tool with team_name="${tn}" and name="${memberName}".${workflowHint}`; try { await provisioning.sendMessageToTeam(tn, spawnMessage); } catch { diff --git a/src/main/services/team/TeamDataService.ts b/src/main/services/team/TeamDataService.ts index 92903f18..2dc5284c 100644 --- a/src/main/services/team/TeamDataService.ts +++ b/src/main/services/team/TeamDataService.ts @@ -599,6 +599,62 @@ export class TeamDataService { } } + /** + * Ensures a member exists in members.meta.json. + * Members can appear in the UI from three sources (see TeamMemberResolver): + * 1. members.meta.json + * 2. config.json members array (CLI-created) + * 3. inbox file presence (CLI-spawned teammates) + * If the member exists in source 2 or 3 but not in meta, migrates it so + * that edit/delete operations work. + */ + private async ensureMemberInMeta( + teamName: string, + memberName: string + ): Promise<{ members: TeamMember[]; member: TeamMember }> { + const members = await this.membersMetaStore.getMembers(teamName); + let member = members.find((m) => m.name === memberName); + + if (!member) { + // Try config.json first — it may have role/workflow info. + const config = await this.configReader.getConfig(teamName); + const configMember = config?.members?.find( + (m) => typeof m?.name === 'string' && m.name.trim() === memberName + ); + + if (configMember) { + member = { + name: configMember.name.trim(), + role: configMember.role, + workflow: configMember.workflow, + agentType: configMember.agentType ?? 'general-purpose', + color: configMember.color ?? getMemberColorByName(configMember.name.trim()), + joinedAt: configMember.joinedAt ?? Date.now(), + cwd: configMember.cwd, + }; + } else { + // Member may exist only via inbox file (CLI-spawned teammate). + // Check if an inbox file exists for this name. + const inboxNames = await this.inboxReader.listInboxNames(teamName); + if (!inboxNames.includes(memberName)) { + throw new Error(`Member "${memberName}" not found`); + } + + member = { + name: memberName, + agentType: 'general-purpose', + color: getMemberColorByName(memberName), + joinedAt: Date.now(), + }; + } + + members.push(member); + await this.membersMetaStore.writeMembers(teamName, members); + } + + return { members, member }; + } + async addMember(teamName: string, request: AddMemberRequest): Promise { const name = request.name.trim(); if (!name) { @@ -639,9 +695,7 @@ export class TeamDataService { memberName: string, newRole: string | undefined ): Promise<{ oldRole: string | undefined; changed: boolean }> { - const members = await this.membersMetaStore.getMembers(teamName); - const member = members.find((m) => m.name === memberName); - if (!member) throw new Error(`Member "${memberName}" not found`); + const { members, member } = await this.ensureMemberInMeta(teamName, memberName); if (member.removedAt) throw new Error(`Member "${memberName}" is removed`); if (member.agentType === 'team-lead') throw new Error('Cannot change team lead role'); @@ -716,12 +770,8 @@ export class TeamDataService { } async removeMember(teamName: string, memberName: string): Promise { - const members = await this.membersMetaStore.getMembers(teamName); - const member = members.find((m) => m.name === memberName); + const { members, member } = await this.ensureMemberInMeta(teamName, memberName); - if (!member) { - throw new Error(`Member "${memberName}" not found`); - } if (member.removedAt) { throw new Error(`Member "${memberName}" is already removed`); } diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 5430a775..d1b98ff7 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -1426,7 +1426,7 @@ export class TeamProvisioningService { return; } - const toMark: InboxMessage[] = []; + const toMark: (InboxMessage & { messageId: string })[] = []; for (const block of deliveredBlocks) { const matchesBlock = (message: InboxMessage, requireExactText: boolean): boolean => { if (message.read || message.source !== CROSS_TEAM_SOURCE) return false; @@ -1442,7 +1442,7 @@ export class TeamProvisioningService { const matched = leadInboxMessages.find((message) => matchesBlock(message, true)) ?? leadInboxMessages.find((message) => matchesBlock(message, false)); - if (!matched) continue; + if (!matched || !this.hasStableMessageId(matched)) continue; matched.read = true; toMark.push(matched); }