feat: enhance team provisioning process with pre-creation of configuration

- Updated the TeamProvisioningService to pre-create team configuration and member metadata before spawning the CLI, ensuring persistence even in case of failures.
- Modified return messages to reflect the new provisioning flow, indicating that the team configuration has been pre-created.
- Adjusted the steps in the provisioning process to streamline the creation and provisioning of teams, improving overall user experience.
This commit is contained in:
iliya 2026-03-20 14:06:01 +02:00
parent f01defc32c
commit 01a1472e4c

View file

@ -908,21 +908,21 @@ ${buildMemberSpawnPrompt(m, displayName, request.teamName, leadName)
members: request.members, members: request.members,
}); });
return `agent_teams_ui [Agent Team: “${request.teamName}” | Project: “${projectName}” | Lead: “${leadName}”] — team does NOT exist yet. You must create it. return `agent_teams_ui [Agent Team: “${request.teamName}” | Project: “${projectName}” | Lead: “${leadName}”] — team config has been pre-created. Proceed with provisioning.
You are running in a non-interactive CLI session. Do not ask questions. Do everything in a single turn. You are running in a non-interactive CLI session. Do not ask questions. Do everything in a single turn.
CRITICAL: Execute ALL steps directly yourself in sequence. Do NOT delegate any step to a sub-agent via the Agent tool. The ONLY valid use of the Agent tool is spawning individual teammates in step 2. CRITICAL: Execute ALL steps directly yourself in sequence. Do NOT delegate any step to a sub-agent via the Agent tool. The ONLY valid use of the Agent tool is spawning individual teammates in step 2.
CRITICAL: For step 1, use the BUILT-IN TeamCreate tool NOT any mcp__agent-teams__* MCP tool. Do NOT call mcp__agent-teams__team_launch, mcp__agent-teams__team_stop, or any other mcp__agent-teams__ runtime tool during provisioning. MCP board tools (task_create, task_set_status, etc.) are allowed only in step 3. Do NOT call mcp__agent-teams__team_launch, mcp__agent-teams__team_stop, or any other mcp__agent-teams__ runtime tool during provisioning. MCP board tools (task_create, task_set_status, etc.) are allowed only in step 3.
You are ${leadName}, the team lead. You are ${leadName}, the team lead.
Goal: Create and provision a NEW Claude Code agent team${request.members.length === 0 ? ' (solo — lead only)' : ' with live teammates'}. Goal: Provision Claude Code agent team${request.members.length === 0 ? ' (solo — lead only)' : ' with live teammates'}.
The team does NOT exist yet no config, no state, nothing. Step 1 is MANDATORY. The team config has been pre-created (config.json, members metadata). Proceed with member provisioning.
${userPromptBlock} ${userPromptBlock}
${persistentContext} ${persistentContext}
Steps (execute in this exact order do NOT skip any step): Steps (execute in this exact order do NOT skip any step):
1) MANDATORY FIRST STEP: Call the BUILT-IN TeamCreate tool (not any MCP tool) with team_name=${request.teamName}. This creates the team config and in-memory state. Without this step, teammate spawns will FAIL. Do NOT assume the team already exists. 1) The team config has been pre-created. Proceed directly to step 2.
${step2Block} ${step2Block}
@ -2846,7 +2846,7 @@ export class TeamProvisioningService {
waitingTasksSince: null, waitingTasksSince: null,
provisioningComplete: false, provisioningComplete: false,
isLaunch: false, isLaunch: false,
fsPhase: 'waiting_config', fsPhase: 'waiting_members',
leadRelayCapture: null, leadRelayCapture: null,
activeCrossTeamReplyHints: [], activeCrossTeamReplyHints: [],
leadMsgSeq: 0, leadMsgSeq: 0,
@ -2905,7 +2905,7 @@ export class TeamProvisioningService {
'--mcp-config', '--mcp-config',
mcpConfigPath, mcpConfigPath,
'--disallowedTools', '--disallowedTools',
'TeamDelete,TodoWrite', 'TeamDelete,TodoWrite,TeamCreate',
// Explicit --permission-mode overrides user's defaultMode in ~/.claude/settings.json // Explicit --permission-mode overrides user's defaultMode in ~/.claude/settings.json
// (e.g. "acceptEdits") which otherwise takes precedence over CLI flags // (e.g. "acceptEdits") which otherwise takes precedence over CLI flags
...(request.skipPermissions !== false ...(request.skipPermissions !== false
@ -2917,12 +2917,18 @@ export class TeamProvisioningService {
...parseCliArgs(request.extraCliArgs), ...parseCliArgs(request.extraCliArgs),
]; ];
try { try {
await this.preCreateConfig(request);
child = spawnCli(claudePath, spawnArgs, { child = spawnCli(claudePath, spawnArgs, {
cwd: request.cwd, cwd: request.cwd,
env: { ...shellEnv }, env: { ...shellEnv },
stdio: ['pipe', 'pipe', 'pipe'], stdio: ['pipe', 'pipe', 'pipe'],
}); });
} catch (error) { } catch (error) {
// Clean up pre-saved files (may not exist if preCreateConfig failed partway)
const teamDir = path.join(getTeamsBasePath(), request.teamName);
const tasksDir = path.join(getTasksBasePath(), request.teamName);
await fs.promises.rm(teamDir, { recursive: true, force: true }).catch(() => {});
await fs.promises.rm(tasksDir, { recursive: true, force: true }).catch(() => {});
this.runs.delete(runId); this.runs.delete(runId);
this.provisioningRunByTeam.delete(request.teamName); this.provisioningRunByTeam.delete(request.teamName);
throw error; throw error;
@ -2930,6 +2936,7 @@ export class TeamProvisioningService {
updateProgress(run, 'spawning', 'Starting Claude CLI process', { updateProgress(run, 'spawning', 'Starting Claude CLI process', {
pid: child.pid ?? undefined, pid: child.pid ?? undefined,
configReady: true,
}); });
run.onProgress(run.progress); run.onProgress(run.progress);
run.child = child; run.child = child;
@ -7192,6 +7199,41 @@ export class TeamProvisioningService {
} }
} }
/**
* Pre-saves team config.json + members-meta.json + tasks/ dir to disk
* BEFORE spawning CLI. This guarantees the team persists even if CLI
* crashes (e.g. 429 rate limit) before it can call TeamCreate.
*/
private async preCreateConfig(request: TeamCreateRequest): Promise<void> {
const teamDir = path.join(getTeamsBasePath(), request.teamName);
const tasksDir = path.join(getTasksBasePath(), request.teamName);
await fs.promises.mkdir(teamDir, { recursive: true });
await fs.promises.mkdir(tasksDir, { recursive: true });
const config: Record<string, unknown> = {
name: request.displayName?.trim() || request.teamName,
description: request.description?.trim() || undefined,
color: request.color?.trim() || undefined,
};
if (request.cwd?.trim()) {
config.projectPath = request.cwd.trim();
config.projectPathHistory = [request.cwd.trim()];
}
await atomicWriteAsync(path.join(teamDir, 'config.json'), JSON.stringify(config, null, 2));
const joinedAt = Date.now();
await this.membersMetaStore.writeMembers(
request.teamName,
request.members.map((m) => ({
name: m.name.trim(),
role: m.role?.trim() || undefined,
agentType: 'general-purpose' as const,
color: getMemberColorByName(m.name.trim()),
joinedAt,
}))
);
}
private async persistMembersMeta(teamName: string, request: TeamCreateRequest): Promise<void> { private async persistMembersMeta(teamName: string, request: TeamCreateRequest): Promise<void> {
const teammateMembers = request.members.filter((member) => { const teammateMembers = request.members.filter((member) => {
const trimmed = member.name.trim(); const trimmed = member.name.trim();