diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index c2aac6f0..fdd13b1d 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -85,6 +85,7 @@ import { buildActionModeProtocol } from './actionModeInstructions'; import { atomicWriteAsync } from './atomicWrite'; import { peekAutoResumeService } from './AutoResumeService'; import { ClaudeBinaryResolver } from './ClaudeBinaryResolver'; +import { getConfiguredCliCommandLabel } from './cliFlavor'; import { withFileLock } from './fileLock'; import { type ClassifiedMainProcessIdle, @@ -1345,6 +1346,8 @@ ${buildCanonicalSendMessageExample({ to: leadName, summary: 'short update', mess After member_briefing succeeds: - Do NOT send a "ready", "online", "status accepted", or other acknowledgement-only message just to confirm you started successfully. - If bootstrap succeeded and you have no task yet, stay silent and wait for task assignments. +- If bootstrap succeeded and you have no task, produce ZERO assistant text for that turn and end it immediately after the successful tool result. +- Do NOT ask the user or the lead to send you a task ID, task description, or "next task" right after bootstrap. - Only SendMessage the lead after bootstrap when there is a real blocker, a failed bootstrap, an explicit question, an urgent coordination need, or a completed task result to report. - Never send raw tool output, JSON, dict/object dumps, Python-style structs, or internal state payloads to the lead or the user. If you need to report bootstrap/task/tool status, rewrite it as one short natural-language sentence. - When you later receive work or reconnect after a restart, use task_briefing as your compact queue view. Use task_get when you need the full task context before starting a pending/needsFix task or when the in_progress briefing details are not enough. @@ -1415,6 +1418,8 @@ ${actionModeProtocol} After member_briefing succeeds: - Do NOT send a "ready", "online", "status accepted", or other acknowledgement-only message just to confirm you reconnected successfully. - If reconnect bootstrap succeeded and you have no immediate blocker or question, stay silent and continue with your queue. + - If reconnect bootstrap succeeded and you have no immediate blocker, question, or task, produce ZERO assistant text for that turn and end it immediately. + - Do NOT ask the user or the lead to send you a task ID, task description, or "next task" right after reconnect bootstrap. - Never send raw tool output, JSON, dict/object dumps, Python-style structs, or internal state payloads to the lead or the user. If you need to report bootstrap/task/tool status, rewrite it as one short natural-language sentence. - Use task_briefing as your compact queue view. - If task_briefing shows any in_progress task, resume/finish those first. Call task_get only if you need more context than task_briefing already gave you. @@ -12441,6 +12446,7 @@ export class TeamProvisioningService { providerId: TeamProviderId | undefined = 'anthropic' ): Promise<{ warning?: string }> { const resolvedProviderId = resolveTeamProviderId(providerId); + const cliCommandLabel = getConfiguredCliCommandLabel(); try { const versionProbe = await this.spawnProbe( claudePath, @@ -12452,9 +12458,9 @@ export class TeamProvisioningService { if (versionProbe.exitCode !== 0) { const errorText = buildCombinedLogs(versionProbe.stdout, versionProbe.stderr) || - `Claude CLI exited with code ${versionProbe.exitCode ?? 'unknown'} during warm-up`; + `${cliCommandLabel} exited with code ${versionProbe.exitCode ?? 'unknown'} during warm-up`; return { - warning: `Claude CLI binary failed to start correctly. Details: ${errorText}`, + warning: `${cliCommandLabel} binary failed to start correctly. Details: ${errorText}`, }; } } catch (error) { @@ -12465,7 +12471,7 @@ export class TeamProvisioningService { }; } return { - warning: `Claude CLI binary failed to start. Details: ${message}`, + warning: `${cliCommandLabel} binary failed to start. Details: ${message}`, }; } @@ -12527,7 +12533,7 @@ export class TeamProvisioningService { } return { warning: - 'Preflight check for `claude -p` did not complete. ' + + `Preflight check for \`${cliCommandLabel} -p\` did not complete. ` + `Proceeding anyway. Details: ${message}`, }; } @@ -12548,13 +12554,15 @@ export class TeamProvisioningService { const hint = isAuthFailure ? resolvedProviderId === 'codex' ? 'Codex provider is not authenticated for `-p` mode. ' + - 'Run `claude-multimodel auth login --provider codex` and retry.' + + `Authenticate Codex in ${cliCommandLabel} and retry.` + (attempt > 1 ? ` (failed after ${attempt} attempts)` : '') - : 'Claude CLI `-p` mode is not authenticated. ' + - 'Run `claude auth login` (or start `claude` and run `/login`) to authenticate. ' + + : `${cliCommandLabel} \`-p\` mode is not authenticated. ` + + (cliCommandLabel === 'claude' + ? 'Run `claude auth login` (or start `claude` and run `/login`) to authenticate. ' + : `Authenticate Anthropic in ${cliCommandLabel} and retry. `) + 'For automation/headless use, set ANTHROPIC_API_KEY.' + (attempt > 1 ? ` (failed after ${attempt} attempts)` : '') - : `Claude CLI preflight check failed (exit code ${pingProbe.exitCode ?? 'unknown'}).`; + : `${cliCommandLabel} preflight check failed (exit code ${pingProbe.exitCode ?? 'unknown'}).`; return { warning: hint }; } @@ -12595,7 +12603,7 @@ export class TeamProvisioningService { const targetCwd = cwd ?? process.cwd(); const probeResult = await this.getCachedOrProbeResult(targetCwd, 'anthropic'); if (!probeResult?.claudePath) { - throw new Error('Claude CLI not found'); + throw new Error(`${getConfiguredCliCommandLabel()} not found`); } const { env } = await this.buildProvisioningEnv(); const result = await this.spawnProbe( @@ -12608,7 +12616,7 @@ export class TeamProvisioningService { const output = (result.stdout + '\n' + result.stderr).trim(); if (!output) { throw new Error( - `claude --help returned empty output (exit code: ${String(result.exitCode)})` + `${getConfiguredCliCommandLabel()} --help returned empty output (exit code: ${String(result.exitCode)})` ); } this.helpOutputCache = output; @@ -12966,7 +12974,7 @@ export class TeamProvisioningService { const timeoutHandle = setTimeout(() => { settled = true; killProcessTree(child); - reject(new Error(`Timeout running: claude ${args.join(' ')}`)); + reject(new Error(`Timeout running: ${getConfiguredCliCommandLabel()} ${args.join(' ')}`)); }, timeoutMs); const maybeResolveEarly = (): void => { diff --git a/src/main/services/team/cliFlavor.ts b/src/main/services/team/cliFlavor.ts index 787bcec1..5936f527 100644 --- a/src/main/services/team/cliFlavor.ts +++ b/src/main/services/team/cliFlavor.ts @@ -41,3 +41,17 @@ export function getCliFlavorUiOptions(flavor: CliFlavor): CliFlavorUiOptions { }; } } + +export function getCliFlavorCommandLabel(flavor: CliFlavor): string { + switch (flavor) { + case 'agent_teams_orchestrator': + return 'orchestrator-cli'; + case 'claude': + default: + return 'claude'; + } +} + +export function getConfiguredCliCommandLabel(): string { + return getCliFlavorCommandLabel(getConfiguredCliFlavor()); +} diff --git a/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx b/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx index 27bb8dfe..d4be245c 100644 --- a/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx +++ b/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx @@ -135,7 +135,7 @@ function summarizeDetail( ) { return 'CLI binary could not be started'; } - if (lower.includes('preflight check for `claude -p` did not complete')) { + if (lower.includes('preflight check for `') && lower.includes('-p` did not complete')) { return 'CLI preflight did not complete'; } if (lower.includes('not authenticated') || lower.includes('not logged in')) { diff --git a/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts b/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts index 21f67237..626c5476 100644 --- a/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts +++ b/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts @@ -232,6 +232,50 @@ function createRuntimeDetailLines(result: TeamProvisioningPrepareResult): string return [...(result.details ?? []), ...(result.warnings ?? [])]; } +function extractTimedOutPreflightProbeModelId(detail: string): string | null { + const trimmed = detail.trim(); + if (!trimmed) { + return null; + } + if ( + !trimmed.toLowerCase().includes('preflight check for `') || + !trimmed.toLowerCase().includes('-p` did not complete') + ) { + return null; + } + const match = /--model\s+([^\s]+)/i.exec(trimmed); + return match?.[1]?.trim() || null; +} + +function suppressSupersededRuntimeWarnings(params: { + runtimeDetailLines: string[]; + runtimeWarnings: string[]; + modelResultsById: Map; +}): { + runtimeDetailLines: string[]; + runtimeWarnings: string[]; +} { + const suppressedEntries = new Set(); + + for (const warning of params.runtimeWarnings) { + const probedModelId = extractTimedOutPreflightProbeModelId(warning); + if (!probedModelId) { + continue; + } + if (params.modelResultsById.get(probedModelId)?.status !== 'ready') { + continue; + } + suppressedEntries.add(warning); + } + + return { + runtimeDetailLines: params.runtimeDetailLines.filter( + (detail) => !suppressedEntries.has(detail) + ), + runtimeWarnings: params.runtimeWarnings.filter((warning) => !suppressedEntries.has(warning)), + }; +} + function resolveModelResultFromBatch( providerId: TeamProviderId, modelId: string, @@ -351,7 +395,7 @@ export async function runProviderPrepareDiagnostics({ const modelLines = new Map(); let completedCount = 0; let hasFailure = false; - let hasNotes = runtimeWarnings.length > 0; + let hasNotes = false; const modelWarnings: string[] = []; for (const modelId of orderedModelIds) { @@ -436,7 +480,14 @@ export async function runProviderPrepareDiagnostics({ } } - const dedupedWarnings = Array.from(new Set([...runtimeWarnings, ...modelWarnings])); + const filteredRuntime = suppressSupersededRuntimeWarnings({ + runtimeDetailLines, + runtimeWarnings, + modelResultsById, + }); + const dedupedWarnings = Array.from( + new Set([...filteredRuntime.runtimeWarnings, ...modelWarnings]) + ); const selectedModelResultsById = Object.fromEntries( orderedModelIds .map((modelId) => [modelId, modelResultsById.get(modelId)] as const) @@ -446,9 +497,9 @@ export async function runProviderPrepareDiagnostics({ ); return { - status: hasFailure ? 'failed' : hasNotes ? 'notes' : 'ready', + status: hasFailure ? 'failed' : hasNotes || dedupedWarnings.length > 0 ? 'notes' : 'ready', details: [ - ...runtimeDetailLines, + ...filteredRuntime.runtimeDetailLines, ...orderedModelIds.map((modelId) => modelLines.get(modelId) ?? ''), ], warnings: dedupedWarnings, diff --git a/test/main/services/team/TeamProvisioningServicePrepare.test.ts b/test/main/services/team/TeamProvisioningServicePrepare.test.ts index 64e668fb..0addd0bb 100644 --- a/test/main/services/team/TeamProvisioningServicePrepare.test.ts +++ b/test/main/services/team/TeamProvisioningServicePrepare.test.ts @@ -401,7 +401,7 @@ describe('TeamProvisioningService prepare/auth behavior', () => { }); vi.spyOn(svc as any, 'spawnProbe').mockRejectedValue( new Error( - 'Timeout running: claude -p Output only the single word PONG. --output-format text --model gpt-5.3-codex --max-turns 1 --no-session-persistence' + 'Timeout running: orchestrator-cli -p Output only the single word PONG. --output-format text --model gpt-5.3-codex --max-turns 1 --no-session-persistence' ) ); @@ -417,6 +417,26 @@ describe('TeamProvisioningService prepare/auth behavior', () => { ); }); + it('surfaces preflight timeouts with the orchestrator-cli label', async () => { + const svc = new TeamProvisioningService(); + vi.spyOn(svc as any, 'getCachedOrProbeResult').mockResolvedValue({ + claudePath: '/fake/claude', + authSource: 'codex_runtime', + warning: + 'Preflight check for `orchestrator-cli -p` did not complete. Proceeding anyway. Details: Timeout running: orchestrator-cli -p Output only the single word PONG. --output-format text --model gpt-5.4-mini --max-turns 1 --no-session-persistence', + }); + + const result = await svc.prepareForProvisioning(tempRoot, { + forceFresh: true, + providerId: 'codex', + }); + + expect(result.ready).toBe(true); + expect(result.warnings).toContain( + 'Preflight check for `orchestrator-cli -p` did not complete. Proceeding anyway. Details: Timeout running: orchestrator-cli -p Output only the single word PONG. --output-format text --model gpt-5.4-mini --max-turns 1 --no-session-persistence' + ); + }); + it('maps ANTHROPIC_AUTH_TOKEN into ANTHROPIC_API_KEY for headless preflight', async () => { const svc = new TeamProvisioningService(); vi.mocked(resolveInteractiveShellEnv).mockResolvedValue({ diff --git a/test/main/services/team/TeamProvisioningServicePrompts.test.ts b/test/main/services/team/TeamProvisioningServicePrompts.test.ts index 597b31bc..d738d43c 100644 --- a/test/main/services/team/TeamProvisioningServicePrompts.test.ts +++ b/test/main/services/team/TeamProvisioningServicePrompts.test.ts @@ -365,6 +365,20 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () => ); }); + it('add-member spawn prompt explicitly forbids no-task bootstrap chatter', () => { + const prompt = buildAddMemberSpawnMessage('my-team', 'My Team', 'team-lead', { + name: 'alice', + role: 'developer', + }); + + expect(prompt).toContain( + 'If bootstrap succeeded and you have no task, produce ZERO assistant text for that turn and end it immediately after the successful tool result.' + ); + expect(prompt).toContain( + 'Do NOT ask the user or the lead to send you a task ID, task description, or "next task" right after bootstrap.' + ); + }); + it('launchTeam hydration prompt includes task-comment handling guidance by default', async () => { const teamName = 'forward-live-team'; const teamDir = path.join(tempTeamsBase, teamName); @@ -502,7 +516,6 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () => expect(prompt).toContain( 'Correct flow: finish implementation on #X -> task_complete #X -> review_request #X -> reviewer runs review_start #X -> reviewer runs review_approve or review_request_changes on #X.' ); - await svc.cancelProvisioning(runId); }); diff --git a/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts b/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts index 7c69269d..931753f9 100644 --- a/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts +++ b/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts @@ -128,4 +128,36 @@ describe('ProvisioningProviderStatusList', () => { await Promise.resolve(); }); }); + + it('normalizes generic preflight timeout notes without depending on a hardcoded CLI name', async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render( + React.createElement(ProvisioningProviderStatusList, { + checks: [ + { + providerId: 'codex', + status: 'notes', + backendSummary: 'Default adapter', + details: [ + 'Preflight check for `orchestrator-cli -p` did not complete. Proceeding anyway. Details: Timeout running: orchestrator-cli -p Output only the single word PONG. --output-format text --model gpt-5.4-mini --max-turns 1 --no-session-persistence', + ], + }, + ], + }) + ); + await Promise.resolve(); + }); + + expect(host.textContent).toContain('Codex (Default adapter): CLI preflight did not complete'); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); }); diff --git a/test/renderer/components/team/dialogs/providerPrepareDiagnostics.test.ts b/test/renderer/components/team/dialogs/providerPrepareDiagnostics.test.ts index af685938..65feb589 100644 --- a/test/renderer/components/team/dialogs/providerPrepareDiagnostics.test.ts +++ b/test/renderer/components/team/dialogs/providerPrepareDiagnostics.test.ts @@ -196,7 +196,7 @@ describe('runProviderPrepareDiagnostics', () => { ready: true, message: 'CLI is warmed up and ready to launch', warnings: [ - 'Selected model gpt-5.3-codex could not be verified. Timeout running: claude -p Output only the single word PONG. --output-format text --model gpt-5.3-codex --max-turns 1 --no-session-persistence', + 'Selected model gpt-5.3-codex could not be verified. Timeout running: orchestrator-cli -p Output only the single word PONG. --output-format text --model gpt-5.3-codex --max-turns 1 --no-session-persistence', ], }); }); @@ -372,4 +372,45 @@ describe('runProviderPrepareDiagnostics', () => { 'gpt-5.2-codex', ], undefined); }); + + it('suppresses a timed out runtime preflight note when that same model later verifies', async () => { + const prepareProvisioning = vi.fn< + ( + cwd?: string, + providerId?: 'anthropic' | 'codex' | 'gemini', + providerIds?: ('anthropic' | 'codex' | 'gemini')[], + selectedModels?: string[] + ) => Promise + >((_, __, ___, selectedModels) => { + if (!selectedModels || selectedModels.length === 0) { + return Promise.resolve({ + ready: true, + message: 'CLI is ready to launch (see notes)', + warnings: [ + 'Preflight check for `orchestrator-cli -p` did not complete. Proceeding anyway. Details: Timeout running: orchestrator-cli -p Output only the single word PONG. --output-format text --model gpt-5.4-mini --max-turns 1 --no-session-persistence', + ], + }); + } + + return Promise.resolve({ + ready: true, + message: 'CLI is warmed up and ready to launch', + details: [ + 'Selected model gpt-5.4-mini verified for launch.', + 'Selected model gpt-5.4 verified for launch.', + ], + }); + }); + + const result = await runProviderPrepareDiagnostics({ + cwd: '/tmp/project', + providerId: 'codex', + selectedModelIds: ['gpt-5.4-mini', 'gpt-5.4'], + prepareProvisioning, + }); + + expect(result.status).toBe('ready'); + expect(result.warnings).toEqual([]); + expect(result.details).toEqual(['5.4 Mini - verified', '5.4 - verified']); + }); });