fix(team): refine bootstrap and provider diagnostics
This commit is contained in:
parent
fb3d1ceb27
commit
452948b260
8 changed files with 198 additions and 19 deletions
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')) {
|
||||
|
|
|
|||
|
|
@ -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<string, ProviderPrepareDiagnosticsModelResult>;
|
||||
}): {
|
||||
runtimeDetailLines: string[];
|
||||
runtimeWarnings: string[];
|
||||
} {
|
||||
const suppressedEntries = new Set<string>();
|
||||
|
||||
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<string, string>();
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<TeamProvisioningPrepareResult>
|
||||
>((_, __, ___, 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']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue