diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 33daae73..ef1f62e7 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -674,6 +674,8 @@ const TEAM_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,127}$/; const RUN_TIMEOUT_MS = 300_000; const VERIFY_TIMEOUT_MS = 15_000; const MCP_PREFLIGHT_INITIALIZE_TIMEOUT_MS = 45_000; +const PROVIDER_MODEL_LIST_TIMEOUT_MS = 30_000; +const PROVIDER_RUNTIME_STATUS_TIMEOUT_MS = 20_000; const TASK_ACTIVITY_RUNTIME_PAUSE_GRACE_MS = 5_000; function asRuntimeRecord(value: unknown): Record { @@ -1344,16 +1346,77 @@ function extractJsonObjectFromCli(raw: string): T { const trimmed = raw.trim(); try { return JSON.parse(trimmed) as T; - } catch { - const start = trimmed.indexOf('{'); - const end = trimmed.lastIndexOf('}'); - if (start >= 0 && end > start) { - return JSON.parse(trimmed.slice(start, end + 1)) as T; + } catch (initialError) { + const candidates: T[] = []; + let lastParseError: unknown = null; + for (let start = trimmed.indexOf('{'); start >= 0; start = trimmed.indexOf('{', start + 1)) { + const end = findJsonObjectEnd(trimmed, start); + if (end < 0) { + continue; + } + try { + candidates.push(JSON.parse(trimmed.slice(start, end + 1)) as T); + } catch (error) { + lastParseError = error; + } + } + + let providerResponse: T | null = null; + for (let index = candidates.length - 1; index >= 0; index -= 1) { + const record = candidates[index] as Record | null; + const providers = record && typeof record === 'object' ? record.providers : null; + if (providers && typeof providers === 'object' && !Array.isArray(providers)) { + providerResponse = candidates[index]; + break; + } + } + if (providerResponse) { + return providerResponse; + } + if (candidates.length > 0) { + throw new Error('No provider JSON object found in CLI output'); + } + if (lastParseError instanceof Error) { + throw lastParseError; + } + if (trimmed.includes('{') && initialError instanceof Error) { + throw initialError; } throw new Error('No JSON object found in CLI output'); } } +function findJsonObjectEnd(source: string, start: number): number { + let depth = 0; + let inString = false; + let escaped = false; + for (let index = start; index < source.length; index += 1) { + const char = source[index]; + if (inString) { + if (escaped) { + escaped = false; + } else if (char === '\\') { + escaped = true; + } else if (char === '"') { + inString = false; + } + continue; + } + + if (char === '"') { + inString = true; + } else if (char === '{') { + depth += 1; + } else if (char === '}') { + depth -= 1; + if (depth === 0) { + return index; + } + } + } + return -1; +} + function getExplicitLaunchModelSelection(model: string | undefined): string | undefined { const trimmed = model?.trim(); if (!trimmed || isDefaultProviderModelSelection(trimmed)) { @@ -7256,7 +7319,7 @@ export class TeamProvisioningService { { cwd: params.cwd, env: params.env, - timeout: 10_000, + timeout: PROVIDER_MODEL_LIST_TIMEOUT_MS, } ); const runtimeStatusPromise = @@ -20440,28 +20503,39 @@ export class TeamProvisioningService { providerArgs: string[] = [], limitContext: boolean ): Promise { - const { stdout } = await execCli( - claudePath, - buildProviderCliCommandArgs(providerArgs, [ - 'model', - 'list', - '--json', - '--provider', - providerId, - ]), - { - cwd, - env, - timeout: 10_000, - } - ); let parsed: ProviderModelListCommandResponse; try { + const { stdout } = await execCli( + claudePath, + buildProviderCliCommandArgs(providerArgs, [ + 'model', + 'list', + '--json', + '--provider', + providerId, + ]), + { + cwd, + env, + timeout: PROVIDER_MODEL_LIST_TIMEOUT_MS, + } + ); parsed = extractJsonObjectFromCli(stdout); } catch (error) { + const fallbackDefaultModel = await this.resolveProviderDefaultModelFromRuntimeStatus( + claudePath, + cwd, + providerId, + env, + providerArgs, + limitContext + ).catch(() => null); + if (fallbackDefaultModel) { + return fallbackDefaultModel; + } const message = error instanceof Error ? error.message : String(error); throw new Error( - `Failed to parse runtime default model list for ${getTeamProviderLabel(providerId)} (${providerId}): ${message}` + `Failed to load runtime default model list for ${getTeamProviderLabel(providerId)} (${providerId}): ${message}` ); } const defaultModel = parsed.providers?.[providerId]?.defaultModel; @@ -20482,6 +20556,46 @@ export class TeamProvisioningService { return normalizedDefaultModel; } + private async resolveProviderDefaultModelFromRuntimeStatus( + claudePath: string, + cwd: string, + providerId: TeamProviderId, + env: NodeJS.ProcessEnv, + providerArgs: string[] = [], + limitContext: boolean + ): Promise { + const { stdout } = await execCli( + claudePath, + buildProviderCliCommandArgs(providerArgs, [ + 'runtime', + 'status', + '--json', + '--provider', + providerId, + ]), + { + cwd, + env, + timeout: PROVIDER_RUNTIME_STATUS_TIMEOUT_MS, + } + ); + const parsed = extractJsonObjectFromCli(stdout); + const providerStatus = parsed.providers?.[providerId] ?? null; + const modelCatalog = + providerStatus?.modelCatalog?.providerId === providerId ? providerStatus.modelCatalog : null; + const defaultLaunchModel = modelCatalog?.defaultLaunchModel?.trim() || null; + + if (providerId === 'anthropic') { + return resolveAnthropicLaunchModel({ + limitContext, + availableLaunchModels: modelCatalog?.models.map((model) => model.launchModel) ?? [], + defaultLaunchModel, + }); + } + + return defaultLaunchModel; + } + private async materializeEffectiveTeamMemberSpecs(params: { claudePath: string; cwd: string; @@ -20605,6 +20719,111 @@ export class TeamProvisioningService { ); } + private async materializeOpenCodeRuntimeAdapterDefaults< + TRequest extends TeamCreateRequest | TeamLaunchRequest, + >(params: { + request: TRequest; + members: TeamCreateRequest['members']; + }): Promise<{ + request: TRequest; + members: TeamCreateRequest['members']; + }> { + const effectiveMembers = buildEffectiveTeamMemberSpecs(params.members, { + providerId: params.request.providerId, + model: params.request.model, + effort: params.request.effort, + }); + const explicitRootModel = getExplicitLaunchModelSelection(params.request.model); + const memberModels = [ + ...new Set( + effectiveMembers + .map((member) => member.model?.trim()) + .filter((model): model is string => Boolean(model)) + ), + ]; + if (!explicitRootModel && memberModels.length > 1) { + throw new Error( + 'OpenCode runtime adapter launch supports one selected model per lane. Select one team model or align OpenCode teammate models.' + ); + } + const inheritedRootModel = explicitRootModel ? undefined : memberModels[0]; + const rootModel = explicitRootModel ?? inheritedRootModel; + const needsMemberModel = effectiveMembers.some((member) => { + const providerId = normalizeTeamMemberProviderId(member.providerId) ?? 'opencode'; + return providerId === 'opencode' && !member.model?.trim(); + }); + if (rootModel && !needsMemberModel) { + return { + request: { + ...params.request, + model: rootModel, + } as TRequest, + members: effectiveMembers, + }; + } + if (rootModel) { + return { + request: { + ...params.request, + model: rootModel, + } as TRequest, + members: effectiveMembers.map((member) => { + const providerId = normalizeTeamMemberProviderId(member.providerId) ?? 'opencode'; + if (providerId !== 'opencode' || member.model?.trim()) { + return member; + } + return { + ...member, + model: rootModel, + }; + }), + }; + } + + const claudePath = await ClaudeBinaryResolver.resolve(); + if (!claudePath) { + throw buildMissingCliError(); + } + const provisioningEnv = await this.buildProvisioningEnv( + 'opencode', + params.request.providerBackendId + ); + if (provisioningEnv.warning) { + throw new Error(provisioningEnv.warning); + } + const resolvedDefaultModel = await this.resolveProviderDefaultModel( + claudePath, + params.request.cwd, + 'opencode', + provisioningEnv.env, + provisioningEnv.providerArgs ?? [], + params.request.limitContext === true + ); + const normalizedDefaultModel = resolvedDefaultModel?.trim(); + if (!normalizedDefaultModel) { + throw new Error( + 'Could not resolve the runtime default model for OpenCode teammates. Select an explicit model and retry.' + ); + } + + return { + request: { + ...params.request, + model: normalizedDefaultModel, + } as TRequest, + members: effectiveMembers.map((member) => { + const providerId = normalizeTeamMemberProviderId(member.providerId) ?? 'opencode'; + if (providerId !== 'opencode' || member.model?.trim()) { + return member; + } + return { + ...member, + model: normalizedDefaultModel, + }; + }), + }; + } + private async resolveOpenCodeMemberWorkspacesForRuntime(params: { teamName: string; baseCwd: string; @@ -22228,46 +22447,47 @@ export class TeamProvisioningService { } await ensureCwdExists(request.cwd); - const effectiveMembers = await this.resolveOpenCodeMemberWorkspacesForRuntime({ - teamName: request.teamName, - baseCwd: request.cwd, - leadProviderId: request.providerId, - members: buildEffectiveTeamMemberSpecs(request.members, { - providerId: request.providerId, - model: request.model, - effort: request.effort, - }), + const materialized = await this.materializeOpenCodeRuntimeAdapterDefaults({ + request, + members: request.members, }); - const teamDir = path.join(getTeamsBasePath(), request.teamName); - const tasksDir = path.join(getTasksBasePath(), request.teamName); + const launchRequest = materialized.request; + const effectiveMembers = await this.resolveOpenCodeMemberWorkspacesForRuntime({ + teamName: launchRequest.teamName, + baseCwd: launchRequest.cwd, + leadProviderId: launchRequest.providerId, + members: materialized.members, + }); + const teamDir = path.join(getTeamsBasePath(), launchRequest.teamName); + const tasksDir = path.join(getTasksBasePath(), launchRequest.teamName); await fs.promises.mkdir(teamDir, { recursive: true }); await fs.promises.mkdir(tasksDir, { recursive: true }); - await this.teamMetaStore.writeMeta(request.teamName, { - displayName: request.displayName, - description: request.description, - color: request.color, - cwd: request.cwd, - prompt: request.prompt, - providerId: request.providerId, - providerBackendId: request.providerBackendId, - model: request.model, - effort: request.effort, - skipPermissions: request.skipPermissions, - worktree: request.worktree, - extraCliArgs: request.extraCliArgs, - limitContext: request.limitContext, + await this.teamMetaStore.writeMeta(launchRequest.teamName, { + displayName: launchRequest.displayName, + description: launchRequest.description, + color: launchRequest.color, + cwd: launchRequest.cwd, + prompt: launchRequest.prompt, + providerId: launchRequest.providerId, + providerBackendId: launchRequest.providerBackendId, + model: launchRequest.model, + effort: launchRequest.effort, + skipPermissions: launchRequest.skipPermissions, + worktree: launchRequest.worktree, + extraCliArgs: launchRequest.extraCliArgs, + limitContext: launchRequest.limitContext, createdAt: Date.now(), }); const membersToWrite = this.buildMembersMetaWritePayload(effectiveMembers); - await this.membersMetaStore.writeMembers(request.teamName, membersToWrite, { - providerBackendId: request.providerBackendId, + await this.membersMetaStore.writeMembers(launchRequest.teamName, membersToWrite, { + providerBackendId: launchRequest.providerBackendId, }); - await this.writeOpenCodeTeamConfig(request, effectiveMembers); + await this.writeOpenCodeTeamConfig(launchRequest, effectiveMembers); return this.runOpenCodeTeamRuntimeAdapterLaunch({ - request, + request: launchRequest, members: effectiveMembers, - prompt: request.prompt?.trim() ?? '', + prompt: launchRequest.prompt?.trim() ?? '', sourceWarning: undefined, onProgress, }); @@ -22291,17 +22511,18 @@ export class TeamProvisioningService { configRaw, request.providerId ); - const effectiveMembers = await this.resolveOpenCodeMemberWorkspacesForRuntime({ - teamName: request.teamName, - baseCwd: request.cwd, - leadProviderId: request.providerId, - members: buildEffectiveTeamMemberSpecs(members, { - providerId: request.providerId, - model: request.model, - effort: request.effort, - }), + const materialized = await this.materializeOpenCodeRuntimeAdapterDefaults({ + request, + members, }); - await this.updateConfigProjectPath(request.teamName, request.cwd); + const launchRequest = materialized.request; + const effectiveMembers = await this.resolveOpenCodeMemberWorkspacesForRuntime({ + teamName: launchRequest.teamName, + baseCwd: launchRequest.cwd, + leadProviderId: launchRequest.providerId, + members: materialized.members, + }); + await this.updateConfigProjectPath(launchRequest.teamName, launchRequest.cwd); let existingTasks: TeamTask[] = []; try { @@ -22312,14 +22533,14 @@ export class TeamProvisioningService { ); } const prompt = buildDeterministicLaunchHydrationPrompt( - request, + launchRequest, effectiveMembers, existingTasks, false ); return this.runOpenCodeTeamRuntimeAdapterLaunch({ - request, + request: launchRequest, members: effectiveMembers, prompt, sourceWarning: warning, @@ -22695,7 +22916,7 @@ export class TeamProvisioningService { name: member.name, providerId: 'opencode', providerBackendId: undefined, - model: member.model?.trim() || undefined, + model: member.model?.trim() || evidence?.model?.trim() || undefined, effort: member.effort, cwd: member.cwd?.trim() || undefined, laneId: 'primary', @@ -26842,9 +27063,10 @@ export class TeamProvisioningService { } const activeRunMember = this.findEffectiveRunMember(run, memberName); const activeRunModel = activeRunMember?.model?.trim(); + const evidenceModel = currentRuntimeAdapterRun?.members?.[memberName]?.model?.trim(); const activeRunProviderId = normalizeOptionalTeamProviderId(activeRunMember?.providerId) ?? - inferTeamProviderIdFromModel(activeRunModel); + inferTeamProviderIdFromModel(activeRunModel ?? evidenceModel); const effectiveProviderId = activeRunProviderId ?? persistedMember.providerId; const currentRuntimeAdapterEvidence = currentRuntimeAdapterRun?.members?.[memberName]; upsertMetadata(memberName, { @@ -26865,9 +27087,11 @@ export class TeamProvisioningService { persistedMember.lastRuntimeAliveAt, ...(activeRunModel ? { model: activeRunModel } - : persistedMember.model?.trim() - ? { model: persistedMember.model.trim() } - : {}), + : evidenceModel + ? { model: evidenceModel } + : persistedMember.model?.trim() + ? { model: persistedMember.model.trim() } + : {}), ...(typeof currentRuntimeAdapterEvidence?.runtimePid === 'number' && currentRuntimeAdapterEvidence.runtimePid > 0 ? { metricsPid: currentRuntimeAdapterEvidence.runtimePid } diff --git a/src/main/services/team/runtime/TeamRuntimeAdapter.ts b/src/main/services/team/runtime/TeamRuntimeAdapter.ts index d98a8cf6..808e7ce8 100644 --- a/src/main/services/team/runtime/TeamRuntimeAdapter.ts +++ b/src/main/services/team/runtime/TeamRuntimeAdapter.ts @@ -74,6 +74,7 @@ export type TeamRuntimePrepareResult = TeamRuntimePrepareSuccess | TeamRuntimePr export interface TeamRuntimeMemberLaunchEvidence { memberName: string; providerId: TeamRuntimeProviderId; + model?: string; launchState: MemberLaunchState; agentToolAccepted: boolean; runtimeAlive: boolean; diff --git a/test/main/services/team/TeamProvisioningServicePrepare.test.ts b/test/main/services/team/TeamProvisioningServicePrepare.test.ts index 45404b49..f340c039 100644 --- a/test/main/services/team/TeamProvisioningServicePrepare.test.ts +++ b/test/main/services/team/TeamProvisioningServicePrepare.test.ts @@ -2821,6 +2821,332 @@ describe('TeamProvisioningService prepare/auth behavior', () => { vi.mocked(console.warn).mockClear(); }); + it('resolves the OpenCode default model when CLI JSON is surrounded by noisy structured logs', async () => { + const modelList = { + schemaVersion: 1, + providers: { + opencode: { + defaultModel: 'opencode/big-pickle', + models: [ + { + id: 'opencode/big-pickle', + label: 'Big Pickle', + description: 'Default OpenCode free model', + }, + { + id: 'opencode/minimax-m2.5-free', + label: 'MiniMax M2.5 Free', + description: 'Free OpenCode model', + }, + ], + }, + }, + }; + execCliMock.mockResolvedValue({ + stdout: [ + 'debug {"event":"starting model list"}', + JSON.stringify(modelList), + 'debug {"providers":"log-only"}', + ].join('\n'), + stderr: '', + exitCode: 0, + }); + + const svc = new TeamProvisioningService(); + const serviceWithDefaultModelResolver = svc as unknown as { + resolveProviderDefaultModel: ( + claudePath: string, + cwd: string, + providerId: string, + env: NodeJS.ProcessEnv, + providerArgs: string[], + limitContext: boolean + ) => Promise; + }; + await expect( + serviceWithDefaultModelResolver.resolveProviderDefaultModel( + '/fake/claude', + tempRoot, + 'opencode', + { PATH: '/usr/bin' }, + [], + false + ) + ).resolves.toBe('opencode/big-pickle'); + }); + + it('falls back to OpenCode runtime status when the default model list is truncated', async () => { + execCliMock.mockImplementation(async (_binaryPath: string | null, args: string[]) => { + if (args[0] === 'model' && args[1] === 'list' && args.includes('opencode')) { + return { + stdout: [ + '{', + ' "schemaVersion": 1,', + ' "providers": {', + ' "opencode": {', + ' "defaultModel": "opencode/big-pickle",', + ' "models": [', + ' {"id":"opencode/big-pickle","label":"Big Pickle","description":"Free"}', + ].join('\n'), + stderr: '', + exitCode: 0, + }; + } + if (args[0] === 'runtime' && args[1] === 'status' && args.includes('opencode')) { + return { + stdout: JSON.stringify({ + providers: { + opencode: { + providerId: 'opencode', + modelCatalog: { + schemaVersion: 1, + providerId: 'opencode', + source: 'app-server', + status: 'ready', + fetchedAt: new Date(0).toISOString(), + staleAt: new Date(60_000).toISOString(), + defaultModelId: 'opencode/big-pickle', + defaultLaunchModel: 'opencode/big-pickle', + models: [ + { + id: 'opencode/big-pickle', + launchModel: 'opencode/big-pickle', + displayName: 'Big Pickle', + hidden: false, + supportedReasoningEfforts: [], + defaultReasoningEffort: null, + inputModalities: ['text'], + supportsPersonality: true, + isDefault: true, + upgrade: false, + source: 'app-server', + badgeLabel: 'Free', + statusMessage: null, + metadata: { free: true }, + }, + ], + diagnostics: { + configReadState: 'ready', + appServerState: 'healthy', + }, + }, + }, + }, + }), + stderr: '', + exitCode: 0, + }; + } + return defaultExecCliMockImplementation(_binaryPath, args); + }); + + const svc = new TeamProvisioningService(); + const serviceWithDefaultModelResolver = svc as unknown as { + resolveProviderDefaultModel: ( + claudePath: string, + cwd: string, + providerId: string, + env: NodeJS.ProcessEnv, + providerArgs: string[], + limitContext: boolean + ) => Promise; + }; + + await expect( + serviceWithDefaultModelResolver.resolveProviderDefaultModel( + '/fake/claude', + tempRoot, + 'opencode', + { PATH: '/usr/bin' }, + [], + false + ) + ).resolves.toBe('opencode/big-pickle'); + }); + + it('falls back to OpenCode runtime status when the default model list command fails', async () => { + execCliMock.mockImplementation(async (_binaryPath: string | null, args: string[]) => { + if (args[0] === 'model' && args[1] === 'list' && args.includes('opencode')) { + const error = new Error('stdout maxBuffer exceeded'); + Object.assign(error, { stdout: '{"providers":', stderr: '' }); + throw error; + } + if (args[0] === 'runtime' && args[1] === 'status' && args.includes('opencode')) { + return { + stdout: JSON.stringify({ + providers: { + opencode: { + providerId: 'opencode', + modelCatalog: { + schemaVersion: 1, + providerId: 'opencode', + source: 'app-server', + status: 'ready', + fetchedAt: new Date(0).toISOString(), + staleAt: new Date(60_000).toISOString(), + defaultModelId: 'opencode/big-pickle', + defaultLaunchModel: 'opencode/big-pickle', + models: [], + diagnostics: { + configReadState: 'ready', + appServerState: 'healthy', + }, + }, + }, + }, + }), + stderr: '', + exitCode: 0, + }; + } + return defaultExecCliMockImplementation(_binaryPath, args); + }); + + const svc = new TeamProvisioningService(); + const serviceWithDefaultModelResolver = svc as unknown as { + resolveProviderDefaultModel: ( + claudePath: string, + cwd: string, + providerId: string, + env: NodeJS.ProcessEnv, + providerArgs: string[], + limitContext: boolean + ) => Promise; + }; + + await expect( + serviceWithDefaultModelResolver.resolveProviderDefaultModel( + '/fake/claude', + tempRoot, + 'opencode', + { PATH: '/usr/bin' }, + [], + false + ) + ).resolves.toBe('opencode/big-pickle'); + }); + + it('materializes pure OpenCode runtime adapter Default selections before launch', async () => { + execCliMock.mockImplementation(async (_binaryPath: string | null, args: string[]) => { + if (args[0] === 'model' && args[1] === 'list' && args.includes('opencode')) { + return { + stdout: JSON.stringify({ + schemaVersion: 1, + providers: { + opencode: { + defaultModel: 'opencode/big-pickle', + models: [ + { + id: 'opencode/big-pickle', + label: 'Big Pickle', + description: 'Free OpenCode model', + }, + ], + }, + }, + }), + stderr: '', + exitCode: 0, + }; + } + return defaultExecCliMockImplementation(_binaryPath, args); + }); + + const svc = new TeamProvisioningService(); + const serviceWithMaterializer = svc as unknown as { + materializeOpenCodeRuntimeAdapterDefaults: (params: { + request: { + teamName: string; + cwd: string; + providerId: 'opencode'; + skipPermissions: boolean; + model?: string; + }; + members: Array<{ + name: string; + providerId?: 'opencode'; + model?: string; + }>; + }) => Promise<{ + request: { model?: string }; + members: Array<{ name: string; model?: string }>; + }>; + }; + + const result = await serviceWithMaterializer.materializeOpenCodeRuntimeAdapterDefaults({ + request: { + teamName: 'default-opencode-team', + cwd: tempRoot, + providerId: 'opencode', + skipPermissions: true, + }, + members: [ + { + name: 'atlas', + providerId: 'opencode', + }, + ], + }); + + expect(result.request.model).toBe('opencode/big-pickle'); + expect(result.members).toEqual([ + { + name: 'atlas', + providerId: 'opencode', + model: 'opencode/big-pickle', + effort: undefined, + }, + ]); + expect(execCliMock).toHaveBeenCalledWith( + '/fake/claude', + ['model', 'list', '--json', '--provider', 'opencode'], + expect.objectContaining({ cwd: tempRoot }) + ); + }); + + it('materializes pure OpenCode runtime adapter root model from a saved teammate model', async () => { + const svc = new TeamProvisioningService(); + const serviceWithMaterializer = svc as unknown as { + materializeOpenCodeRuntimeAdapterDefaults: (params: { + request: { + teamName: string; + cwd: string; + providerId: 'opencode'; + skipPermissions: boolean; + model?: string; + }; + members: Array<{ + name: string; + providerId?: 'opencode'; + model?: string; + }>; + }) => Promise<{ + request: { model?: string }; + members: Array<{ name: string; model?: string }>; + }>; + }; + + const result = await serviceWithMaterializer.materializeOpenCodeRuntimeAdapterDefaults({ + request: { + teamName: 'saved-opencode-team', + cwd: tempRoot, + providerId: 'opencode', + skipPermissions: true, + }, + members: [ + { + name: 'atlas', + providerId: 'opencode', + model: 'opencode/big-pickle', + }, + ], + }); + + expect(result.request.model).toBe('opencode/big-pickle'); + expect(result.members[0]?.model).toBe('opencode/big-pickle'); + expect(execCliMock).not.toHaveBeenCalled(); + }); + it('maps ANTHROPIC_AUTH_TOKEN into ANTHROPIC_API_KEY for headless preflight', async () => { const svc = new TeamProvisioningService(); vi.mocked(resolveInteractiveShellEnv).mockResolvedValue({