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') {
|
if (!payload || typeof payload !== 'object') {
|
||||||
return { success: false, error: 'Invalid payload' };
|
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);
|
const vName = validateTeammateName(name);
|
||||||
if (!vName.valid) return { success: false, error: vName.error ?? 'Invalid member name' };
|
if (!vName.valid) return { success: false, error: vName.error ?? 'Invalid member name' };
|
||||||
if (role !== undefined && typeof role !== 'string') {
|
if (role !== undefined && typeof role !== 'string') {
|
||||||
return { success: false, error: 'role must be a 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 () => {
|
return wrapTeamHandler('addMember', async () => {
|
||||||
const tn = vTeam.value!;
|
const tn = vTeam.value!;
|
||||||
|
|
@ -1863,15 +1870,20 @@ async function handleAddMember(
|
||||||
await getTeamDataService().addMember(tn, {
|
await getTeamDataService().addMember(tn, {
|
||||||
name: memberName,
|
name: memberName,
|
||||||
role: role,
|
role: role,
|
||||||
|
workflow: typeof workflow === 'string' ? workflow.trim() || undefined : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If team is alive, notify the lead to spawn the new teammate
|
// If team is alive, notify the lead to spawn the new teammate
|
||||||
const provisioning = getTeamProvisioningService();
|
const provisioning = getTeamProvisioningService();
|
||||||
if (provisioning.isTeamAlive(tn)) {
|
if (provisioning.isTeamAlive(tn)) {
|
||||||
const roleHint = typeof role === 'string' && role.trim() ? ` with role "${role.trim()}"` : '';
|
const roleHint = typeof role === 'string' && role.trim() ? ` with role "${role.trim()}"` : '';
|
||||||
|
const workflowHint =
|
||||||
|
typeof workflow === 'string' && workflow.trim()
|
||||||
|
? ` Their workflow: ${workflow.trim()}`
|
||||||
|
: '';
|
||||||
const spawnMessage =
|
const spawnMessage =
|
||||||
`A new teammate "${memberName}"${roleHint} has been added to the team. ` +
|
`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 {
|
try {
|
||||||
await provisioning.sendMessageToTeam(tn, spawnMessage);
|
await provisioning.sendMessageToTeam(tn, spawnMessage);
|
||||||
} catch {
|
} 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> {
|
async addMember(teamName: string, request: AddMemberRequest): Promise<void> {
|
||||||
const name = request.name.trim();
|
const name = request.name.trim();
|
||||||
if (!name) {
|
if (!name) {
|
||||||
|
|
@ -639,9 +695,7 @@ export class TeamDataService {
|
||||||
memberName: string,
|
memberName: string,
|
||||||
newRole: string | undefined
|
newRole: string | undefined
|
||||||
): Promise<{ oldRole: string | undefined; changed: boolean }> {
|
): Promise<{ oldRole: string | undefined; changed: boolean }> {
|
||||||
const members = await this.membersMetaStore.getMembers(teamName);
|
const { members, member } = await this.ensureMemberInMeta(teamName, memberName);
|
||||||
const member = members.find((m) => m.name === memberName);
|
|
||||||
if (!member) throw new Error(`Member "${memberName}" not found`);
|
|
||||||
if (member.removedAt) throw new Error(`Member "${memberName}" is removed`);
|
if (member.removedAt) throw new Error(`Member "${memberName}" is removed`);
|
||||||
if (member.agentType === 'team-lead') throw new Error('Cannot change team lead role');
|
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> {
|
async removeMember(teamName: string, memberName: string): Promise<void> {
|
||||||
const members = await this.membersMetaStore.getMembers(teamName);
|
const { members, member } = await this.ensureMemberInMeta(teamName, memberName);
|
||||||
const member = members.find((m) => m.name === memberName);
|
|
||||||
|
|
||||||
if (!member) {
|
|
||||||
throw new Error(`Member "${memberName}" not found`);
|
|
||||||
}
|
|
||||||
if (member.removedAt) {
|
if (member.removedAt) {
|
||||||
throw new Error(`Member "${memberName}" is already removed`);
|
throw new Error(`Member "${memberName}" is already removed`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1426,7 +1426,7 @@ export class TeamProvisioningService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const toMark: InboxMessage[] = [];
|
const toMark: (InboxMessage & { messageId: string })[] = [];
|
||||||
for (const block of deliveredBlocks) {
|
for (const block of deliveredBlocks) {
|
||||||
const matchesBlock = (message: InboxMessage, requireExactText: boolean): boolean => {
|
const matchesBlock = (message: InboxMessage, requireExactText: boolean): boolean => {
|
||||||
if (message.read || message.source !== CROSS_TEAM_SOURCE) return false;
|
if (message.read || message.source !== CROSS_TEAM_SOURCE) return false;
|
||||||
|
|
@ -1442,7 +1442,7 @@ export class TeamProvisioningService {
|
||||||
const matched =
|
const matched =
|
||||||
leadInboxMessages.find((message) => matchesBlock(message, true)) ??
|
leadInboxMessages.find((message) => matchesBlock(message, true)) ??
|
||||||
leadInboxMessages.find((message) => matchesBlock(message, false));
|
leadInboxMessages.find((message) => matchesBlock(message, false));
|
||||||
if (!matched) continue;
|
if (!matched || !this.hasStableMessageId(matched)) continue;
|
||||||
matched.read = true;
|
matched.read = true;
|
||||||
toMark.push(matched);
|
toMark.push(matched);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue