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.
This commit is contained in:
parent
bfcdff5955
commit
1949084bd8
3 changed files with 74 additions and 12 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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<void> {
|
||||
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`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue