fix: standardize file naming conventions and improve error handling in team provisioning
- Updated file naming in release workflow to use consistent dot notation for better clarity. - Enhanced RELEASE.md to reflect updated file names for download links. - Added logic to filter out the "user" pseudo-member in TeamMemberResolver to prevent confusion in team configurations. - Improved error handling in TeamProvisioningService to avoid caching authentication failures and ensure accurate readiness status. - Introduced visual tone handling in ProvisioningProgressBlock for better user feedback on errors. Made-with: Cursor
This commit is contained in:
parent
71e88fd267
commit
2615ed0ad7
7 changed files with 181 additions and 72 deletions
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
|
|
@ -277,10 +277,10 @@ jobs:
|
||||||
DOWNLOAD_BASE="https://github.com/${REPO}/releases/download/v${VERSION}"
|
DOWNLOAD_BASE="https://github.com/${REPO}/releases/download/v${VERSION}"
|
||||||
|
|
||||||
declare -A FILES=(
|
declare -A FILES=(
|
||||||
["Claude-Agent-Teams-UI-arm64.dmg"]="Claude-Agent-Teams-UI-${VERSION}-arm64.dmg"
|
["Claude-Agent-Teams-UI-arm64.dmg"]="Claude.Agent.Teams.UI-${VERSION}-arm64.dmg"
|
||||||
["Claude-Agent-Teams-UI-x64.dmg"]="Claude-Agent-Teams-UI-${VERSION}.dmg"
|
["Claude-Agent-Teams-UI-x64.dmg"]="Claude.Agent.Teams.UI-${VERSION}.dmg"
|
||||||
["Claude-Agent-Teams-UI-Setup.exe"]="Claude-Agent-Teams-UI-Setup-${VERSION}.exe"
|
["Claude-Agent-Teams-UI-Setup.exe"]="Claude.Agent.Teams.UI.Setup.${VERSION}.exe"
|
||||||
["Claude-Agent-Teams-UI.AppImage"]="Claude-Agent-Teams-UI-${VERSION}.AppImage"
|
["Claude-Agent-Teams-UI.AppImage"]="Claude.Agent.Teams.UI-${VERSION}.AppImage"
|
||||||
["Claude-Agent-Teams-UI-amd64.deb"]="claude-agent-teams-ui_${VERSION}_amd64.deb"
|
["Claude-Agent-Teams-UI-amd64.deb"]="claude-agent-teams-ui_${VERSION}_amd64.deb"
|
||||||
["Claude-Agent-Teams-UI-x86_64.rpm"]="claude-agent-teams-ui-${VERSION}.x86_64.rpm"
|
["Claude-Agent-Teams-UI-x86_64.rpm"]="claude-agent-teams-ui-${VERSION}.x86_64.rpm"
|
||||||
["Claude-Agent-Teams-UI.pacman"]="claude-agent-teams-ui-${VERSION}.pacman"
|
["Claude-Agent-Teams-UI.pacman"]="claude-agent-teams-ui-${VERSION}.pacman"
|
||||||
|
|
|
||||||
|
|
@ -67,23 +67,23 @@ EOF
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/777genius/claude_agent_teams_ui/releases/download/v<VERSION>/Claude-Agent-Teams-UI-<VERSION>-arm64.dmg">
|
<a href="https://github.com/777genius/claude_agent_teams_ui/releases/download/v<VERSION>/Claude.Agent.Teams.UI-<VERSION>-arm64.dmg">
|
||||||
<img src="https://img.shields.io/badge/macOS_Apple_Silicon-.dmg-000000?style=for-the-badge&logo=apple&logoColor=white" alt="macOS Apple Silicon" />
|
<img src="https://img.shields.io/badge/macOS_Apple_Silicon-.dmg-000000?style=for-the-badge&logo=apple&logoColor=white" alt="macOS Apple Silicon" />
|
||||||
</a>
|
</a>
|
||||||
<br />
|
<br />
|
||||||
<a href="https://github.com/777genius/claude_agent_teams_ui/releases/download/v<VERSION>/Claude-Agent-Teams-UI-<VERSION>.dmg">
|
<a href="https://github.com/777genius/claude_agent_teams_ui/releases/download/v<VERSION>/Claude.Agent.Teams.UI-<VERSION>.dmg">
|
||||||
<img src="https://img.shields.io/badge/macOS_Intel-.dmg-434343?style=for-the-badge&logo=apple&logoColor=white" alt="macOS Intel" />
|
<img src="https://img.shields.io/badge/macOS_Intel-.dmg-434343?style=for-the-badge&logo=apple&logoColor=white" alt="macOS Intel" />
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/777genius/claude_agent_teams_ui/releases/download/v<VERSION>/Claude-Agent-Teams-UI-Setup-<VERSION>.exe">
|
<a href="https://github.com/777genius/claude_agent_teams_ui/releases/download/v<VERSION>/Claude.Agent.Teams.UI.Setup.<VERSION>.exe">
|
||||||
<img src="https://img.shields.io/badge/Windows-Download_.exe-0078D4?style=for-the-badge&logo=windows&logoColor=white" alt="Windows" />
|
<img src="https://img.shields.io/badge/Windows-Download_.exe-0078D4?style=for-the-badge&logo=windows&logoColor=white" alt="Windows" />
|
||||||
</a>
|
</a>
|
||||||
<br />
|
<br />
|
||||||
<sub>May trigger SmartScreen — click "More info" → "Run anyway"</sub>
|
<sub>May trigger SmartScreen — click "More info" → "Run anyway"</sub>
|
||||||
</td>
|
</td>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/777genius/claude_agent_teams_ui/releases/download/v<VERSION>/Claude-Agent-Teams-UI-<VERSION>.AppImage">
|
<a href="https://github.com/777genius/claude_agent_teams_ui/releases/download/v<VERSION>/Claude.Agent.Teams.UI-<VERSION>.AppImage">
|
||||||
<img src="https://img.shields.io/badge/Linux-Download_.AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black" alt="Linux AppImage" />
|
<img src="https://img.shields.io/badge/Linux-Download_.AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black" alt="Linux AppImage" />
|
||||||
</a>
|
</a>
|
||||||
<br />
|
<br />
|
||||||
|
|
@ -123,12 +123,12 @@ electron-builder generates these artifacts per platform:
|
||||||
|
|
||||||
| Platform | Versioned Name | Stable Name (for /latest/download) |
|
| Platform | Versioned Name | Stable Name (for /latest/download) |
|
||||||
|------------------|--------------------------------------------------|--------------------------------------------|
|
|------------------|--------------------------------------------------|--------------------------------------------|
|
||||||
| macOS arm64 DMG | `Claude-Agent-Teams-UI-<VER>-arm64.dmg` | `Claude-Agent-Teams-UI-arm64.dmg` |
|
| macOS arm64 DMG | `Claude.Agent.Teams.UI-<VER>-arm64.dmg` | `Claude-Agent-Teams-UI-arm64.dmg` |
|
||||||
| macOS x64 DMG | `Claude-Agent-Teams-UI-<VER>.dmg` | `Claude-Agent-Teams-UI-x64.dmg` |
|
| macOS x64 DMG | `Claude.Agent.Teams.UI-<VER>.dmg` | `Claude-Agent-Teams-UI-x64.dmg` |
|
||||||
| macOS arm64 ZIP | `Claude-Agent-Teams-UI-<VER>-arm64-mac.zip` | — |
|
| macOS arm64 ZIP | `Claude.Agent.Teams.UI-<VER>-arm64-mac.zip` | — |
|
||||||
| macOS x64 ZIP | `Claude-Agent-Teams-UI-<VER>-mac.zip` | — |
|
| macOS x64 ZIP | `Claude.Agent.Teams.UI-<VER>-mac.zip` | — |
|
||||||
| Windows | `Claude-Agent-Teams-UI-Setup-<VER>.exe` | `Claude-Agent-Teams-UI-Setup.exe` |
|
| Windows | `Claude.Agent.Teams.UI.Setup.<VER>.exe` | `Claude-Agent-Teams-UI-Setup.exe` |
|
||||||
| Linux AppImage | `Claude-Agent-Teams-UI-<VER>.AppImage` | `Claude-Agent-Teams-UI.AppImage` |
|
| Linux AppImage | `Claude.Agent.Teams.UI-<VER>.AppImage` | `Claude-Agent-Teams-UI.AppImage` |
|
||||||
| Linux deb | `claude-agent-teams-ui_<VER>_amd64.deb` | `Claude-Agent-Teams-UI-amd64.deb` |
|
| Linux deb | `claude-agent-teams-ui_<VER>_amd64.deb` | `Claude-Agent-Teams-UI-amd64.deb` |
|
||||||
| Linux rpm | `claude-agent-teams-ui-<VER>.x86_64.rpm` | `Claude-Agent-Teams-UI-x86_64.rpm` |
|
| Linux rpm | `claude-agent-teams-ui-<VER>.x86_64.rpm` | `Claude-Agent-Teams-UI-x86_64.rpm` |
|
||||||
| Linux pacman | `claude-agent-teams-ui-<VER>.pacman` | `Claude-Agent-Teams-UI.pacman` |
|
| Linux pacman | `claude-agent-teams-ui-<VER>.pacman` | `Claude-Agent-Teams-UI.pacman` |
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,10 @@ export class TeamMemberResolver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "user" is a built-in pseudo-member in Claude Code's team framework
|
||||||
|
// (recipient of SendMessage to "user"). It's not a real AI teammate.
|
||||||
|
names.delete('user');
|
||||||
|
|
||||||
const members: ResolvedTeamMember[] = [];
|
const members: ResolvedTeamMember[] = [];
|
||||||
for (const name of names) {
|
for (const name of names) {
|
||||||
const ownedTasks = tasks.filter((task) => task.owner === name);
|
const ownedTasks = tasks.filter((task) => task.owner === name);
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ const LOG_PROGRESS_THROTTLE_MS = 300;
|
||||||
const UI_LOGS_TAIL_LIMIT = 128 * 1024;
|
const UI_LOGS_TAIL_LIMIT = 128 * 1024;
|
||||||
const SHELL_ENV_TIMEOUT_MS = 12000;
|
const SHELL_ENV_TIMEOUT_MS = 12000;
|
||||||
const CLI_PREPARE_TIMEOUT_MS = 10000;
|
const CLI_PREPARE_TIMEOUT_MS = 10000;
|
||||||
|
const PROBE_CACHE_TTL_MS = 60_000;
|
||||||
const PREFLIGHT_TIMEOUT_MS = 30000;
|
const PREFLIGHT_TIMEOUT_MS = 30000;
|
||||||
const PREFLIGHT_AUTH_RETRY_DELAY_MS = 2000;
|
const PREFLIGHT_AUTH_RETRY_DELAY_MS = 2000;
|
||||||
const PREFLIGHT_AUTH_MAX_RETRIES = 2;
|
const PREFLIGHT_AUTH_MAX_RETRIES = 2;
|
||||||
|
|
@ -565,6 +566,7 @@ Constraints:
|
||||||
- Do NOT use TodoWrite.
|
- Do NOT use TodoWrite.
|
||||||
- Do NOT send shutdown_request messages (SendMessage type: "shutdown_request" is FORBIDDEN).
|
- Do NOT send shutdown_request messages (SendMessage type: "shutdown_request" is FORBIDDEN).
|
||||||
- Do NOT shut down, terminate, or clean up the team or its members.
|
- Do NOT shut down, terminate, or clean up the team or its members.
|
||||||
|
- Do NOT spawn or create a member named "user". "user" is a reserved system name for the human operator — it is NOT a teammate.
|
||||||
- Keep assistant text minimal.
|
- Keep assistant text minimal.
|
||||||
- NEVER send duplicate messages to the same member. One SendMessage per member per topic is enough.
|
- NEVER send duplicate messages to the same member. One SendMessage per member per topic is enough.
|
||||||
- Keep the task board high-signal: avoid creating tasks for trivial micro-items.
|
- Keep the task board high-signal: avoid creating tasks for trivial micro-items.
|
||||||
|
|
@ -683,6 +685,7 @@ Constraints:
|
||||||
- Do NOT use TodoWrite.
|
- Do NOT use TodoWrite.
|
||||||
- Do NOT send shutdown_request messages (SendMessage type: "shutdown_request" is FORBIDDEN).
|
- Do NOT send shutdown_request messages (SendMessage type: "shutdown_request" is FORBIDDEN).
|
||||||
- Do NOT shut down, terminate, or clean up the team or its members.
|
- Do NOT shut down, terminate, or clean up the team or its members.
|
||||||
|
- Do NOT spawn or create a member named "user". "user" is a reserved system name for the human operator — it is NOT a teammate.
|
||||||
- Keep assistant text minimal.
|
- Keep assistant text minimal.
|
||||||
- NEVER send duplicate messages to the same member. One SendMessage per member per topic is enough.
|
- NEVER send duplicate messages to the same member. One SendMessage per member per topic is enough.
|
||||||
- Keep the task board high-signal: avoid creating tasks for trivial micro-items.
|
- Keep the task board high-signal: avoid creating tasks for trivial micro-items.
|
||||||
|
|
@ -795,7 +798,12 @@ function buildCliExitError(code: number | null, stdoutText: string, stderrText:
|
||||||
const trimmed = buildCombinedLogs(stdoutText, stderrText).trim();
|
const trimmed = buildCombinedLogs(stdoutText, stderrText).trim();
|
||||||
if (trimmed.length > 0) {
|
if (trimmed.length > 0) {
|
||||||
if (trimmed.toLowerCase().includes('please run /login')) {
|
if (trimmed.toLowerCase().includes('please run /login')) {
|
||||||
return 'CLI output indicates that `-p` mode is not authenticated. `claude -p` typically requires `ANTHROPIC_API_KEY` (Agent SDK). `/login` is interactive-only and does not fix `-p`.';
|
return (
|
||||||
|
'Claude CLI reports it is not authenticated ("Please run /login"). ' +
|
||||||
|
'Run `claude auth login` (or start `claude` and run `/login`) to authenticate, then retry. ' +
|
||||||
|
'For automation/headless use, prefer `claude setup-token` and export `CLAUDE_CODE_OAUTH_TOKEN`, ' +
|
||||||
|
'or set `ANTHROPIC_API_KEY` for `-p` mode.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return trimmed.slice(-4000);
|
return trimmed.slice(-4000);
|
||||||
}
|
}
|
||||||
|
|
@ -809,9 +817,9 @@ function buildCliExitError(code: number | null, stdoutText: string, stderrText:
|
||||||
|
|
||||||
interface CachedProbeResult {
|
interface CachedProbeResult {
|
||||||
claudePath: string;
|
claudePath: string;
|
||||||
env: NodeJS.ProcessEnv;
|
|
||||||
authSource: ProvisioningAuthSource;
|
authSource: ProvisioningAuthSource;
|
||||||
warning?: string;
|
warning?: string;
|
||||||
|
cachedAtMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cachedProbeResult: CachedProbeResult | null = null;
|
let cachedProbeResult: CachedProbeResult | null = null;
|
||||||
|
|
@ -860,12 +868,21 @@ export class TeamProvisioningService {
|
||||||
|
|
||||||
async warmup(): Promise<void> {
|
async warmup(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
if (cachedProbeResult && Date.now() - cachedProbeResult.cachedAtMs < PROBE_CACHE_TTL_MS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const claudePath = await ClaudeBinaryResolver.resolve();
|
const claudePath = await ClaudeBinaryResolver.resolve();
|
||||||
if (!claudePath) return;
|
if (!claudePath) return;
|
||||||
const { env, authSource } = await this.buildProvisioningEnv();
|
const { env, authSource } = await this.buildProvisioningEnv();
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
const probe = await this.probeClaudeRuntime(claudePath, cwd, env);
|
const probe = await this.probeClaudeRuntime(claudePath, cwd, env);
|
||||||
cachedProbeResult = { claudePath, env, authSource, warning: probe.warning };
|
const warning = probe.warning;
|
||||||
|
if (warning && this.isAuthFailureWarning(warning)) {
|
||||||
|
// Don't pin auth failures in cache — user may log in after startup.
|
||||||
|
cachedProbeResult = null;
|
||||||
|
} else {
|
||||||
|
cachedProbeResult = { claudePath, authSource, warning, cachedAtMs: Date.now() };
|
||||||
|
}
|
||||||
logger.info('CLI warmup completed');
|
logger.info('CLI warmup completed');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`CLI warmup failed: ${error instanceof Error ? error.message : String(error)}`);
|
logger.warn(`CLI warmup failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
|
@ -880,15 +897,21 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cachedProbeResult) {
|
if (cachedProbeResult) {
|
||||||
const { warning, authSource } = cachedProbeResult;
|
const ageMs = Date.now() - cachedProbeResult.cachedAtMs;
|
||||||
const warnings: string[] = [];
|
if (ageMs >= PROBE_CACHE_TTL_MS) {
|
||||||
if (warning) warnings.push(warning);
|
cachedProbeResult = null;
|
||||||
const isAuthFailure = warning ? this.isAuthFailureWarning(warning) : false;
|
} else {
|
||||||
return {
|
const { warning, authSource } = cachedProbeResult;
|
||||||
ready: !warning || authSource !== 'none' || !isAuthFailure,
|
const warnings: string[] = [];
|
||||||
message: 'CLI is warmed up and ready to launch',
|
if (warning) warnings.push(warning);
|
||||||
warnings: warnings.length > 0 ? warnings : undefined,
|
const isAuthFailure = warning ? this.isAuthFailureWarning(warning) : false;
|
||||||
};
|
const ready = !warning || authSource !== 'none' || !isAuthFailure;
|
||||||
|
return {
|
||||||
|
ready,
|
||||||
|
message: ready ? 'CLI is warmed up and ready to launch' : warning || 'CLI is not ready',
|
||||||
|
warnings: warnings.length > 0 ? warnings : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const claudePath = await ClaudeBinaryResolver.resolve();
|
const claudePath = await ClaudeBinaryResolver.resolve();
|
||||||
|
|
@ -948,6 +971,19 @@ export class TeamProvisioningService {
|
||||||
warnings.push(probe.warning);
|
warnings.push(probe.warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache successful/non-auth-failure results so dialogs don't rerun preflight repeatedly.
|
||||||
|
// Avoid caching auth failures — user may authenticate externally and retry without app restart.
|
||||||
|
if (!probe.warning || !this.isAuthFailureWarning(probe.warning)) {
|
||||||
|
cachedProbeResult = {
|
||||||
|
claudePath,
|
||||||
|
authSource,
|
||||||
|
warning: probe.warning,
|
||||||
|
cachedAtMs: Date.now(),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
cachedProbeResult = null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ready: true,
|
ready: true,
|
||||||
message: 'CLI is warmed up and ready to launch',
|
message: 'CLI is warmed up and ready to launch',
|
||||||
|
|
@ -957,12 +993,15 @@ export class TeamProvisioningService {
|
||||||
|
|
||||||
private isAuthFailureWarning(text: string): boolean {
|
private isAuthFailureWarning(text: string): boolean {
|
||||||
const lower = text.toLowerCase();
|
const lower = text.toLowerCase();
|
||||||
|
const has401 = /(^|\D)401(\D|$)/.test(lower);
|
||||||
return (
|
return (
|
||||||
lower.includes('not authenticated') ||
|
lower.includes('not authenticated') ||
|
||||||
lower.includes('not logged in') ||
|
lower.includes('not logged in') ||
|
||||||
lower.includes('please run /login') ||
|
lower.includes('please run /login') ||
|
||||||
lower.includes('missing api key') ||
|
lower.includes('missing api key') ||
|
||||||
lower.includes('invalid api key')
|
lower.includes('invalid api key') ||
|
||||||
|
lower.includes('unauthorized') ||
|
||||||
|
has401
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -988,8 +1027,8 @@ export class TeamProvisioningService {
|
||||||
killProcessTree(run.child);
|
killProcessTree(run.child);
|
||||||
const progress = updateProgress(run, 'failed', 'Authentication failed — CLI requires login', {
|
const progress = updateProgress(run, 'failed', 'Authentication failed — CLI requires login', {
|
||||||
error:
|
error:
|
||||||
'Claude CLI is not authenticated. Run `claude` in a terminal to complete login, ' +
|
'Claude CLI is not authenticated. Run `claude auth login` (or start `claude` and run `/login`) ' +
|
||||||
'or set ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN and try again.',
|
'to authenticate, or set ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN and try again.',
|
||||||
cliLogsTail: extractLogsTail(run.stdoutBuffer, run.stderrBuffer),
|
cliLogsTail: extractLogsTail(run.stdoutBuffer, run.stderrBuffer),
|
||||||
});
|
});
|
||||||
run.onProgress(progress);
|
run.onProgress(progress);
|
||||||
|
|
@ -2138,6 +2177,9 @@ export class TeamProvisioningService {
|
||||||
.map((part) => part.text as string);
|
.map((part) => part.text as string);
|
||||||
if (textParts.length > 0) {
|
if (textParts.length > 0) {
|
||||||
const text = textParts.join('');
|
const text = textParts.join('');
|
||||||
|
// Auth failures sometimes show up as assistant text (e.g. "401", "Please run /login")
|
||||||
|
// rather than stderr or a result.subtype=error. Detect early to avoid false "ready".
|
||||||
|
this.handleAuthFailureInOutput(run, text, 'assistant');
|
||||||
logger.debug(`[${run.teamName}] assistant: ${text.slice(0, 200)}`);
|
logger.debug(`[${run.teamName}] assistant: ${text.slice(0, 200)}`);
|
||||||
// During provisioning (before provisioningComplete), accumulate for live UI preview.
|
// During provisioning (before provisioningComplete), accumulate for live UI preview.
|
||||||
// Emission is handled by the throttled emitLogsProgress() in the stdout data handler.
|
// Emission is handled by the throttled emitLogsProgress() in the stdout data handler.
|
||||||
|
|
@ -2269,6 +2311,21 @@ export class TeamProvisioningService {
|
||||||
// Guard: must be set synchronously BEFORE any await to prevent
|
// Guard: must be set synchronously BEFORE any await to prevent
|
||||||
// double-invocation from filesystem monitor + stream-json racing.
|
// double-invocation from filesystem monitor + stream-json racing.
|
||||||
if (run.provisioningComplete || run.cancelRequested) return;
|
if (run.provisioningComplete || run.cancelRequested) return;
|
||||||
|
|
||||||
|
// Prevent false "ready" when auth failure was printed as assistant text or logs
|
||||||
|
// but the filesystem monitor observed files on disk.
|
||||||
|
const authFailureText = [
|
||||||
|
buildCombinedLogs(run.stdoutBuffer, run.stderrBuffer),
|
||||||
|
run.provisioningOutputParts.length > 0 ? run.provisioningOutputParts.join('\n') : '',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n')
|
||||||
|
.trim();
|
||||||
|
if (authFailureText && this.isAuthFailureWarning(authFailureText)) {
|
||||||
|
this.handleAuthFailureInOutput(run, authFailureText, 'pre-complete');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
run.provisioningComplete = true;
|
run.provisioningComplete = true;
|
||||||
this.setLeadActivity(run, 'idle');
|
this.setLeadActivity(run, 'idle');
|
||||||
|
|
||||||
|
|
@ -3441,26 +3498,16 @@ export class TeamProvisioningService {
|
||||||
cwd: string,
|
cwd: string,
|
||||||
env: NodeJS.ProcessEnv
|
env: NodeJS.ProcessEnv
|
||||||
): Promise<{ warning?: string }> {
|
): Promise<{ warning?: string }> {
|
||||||
// Stage 1 + Stage 2 attempt #1 in parallel.
|
// Stage 1: verify binary works (awaited first for clearer errors)
|
||||||
// Rationale: both are independent process spawns and the combined wall time
|
// Important: keep this sequential with Stage 2 to avoid auth/credential-store races
|
||||||
// is dominated by startup/IO. We still prioritize the stage-1 error message.
|
// when multiple `claude` processes start simultaneously (most visible on Windows).
|
||||||
const versionProbePromise = this.spawnProbe(
|
const versionProbe = await this.spawnProbe(
|
||||||
claudePath,
|
claudePath,
|
||||||
['--version'],
|
['--version'],
|
||||||
cwd,
|
cwd,
|
||||||
env,
|
env,
|
||||||
CLI_PREPARE_TIMEOUT_MS
|
CLI_PREPARE_TIMEOUT_MS
|
||||||
);
|
);
|
||||||
const pingAttempt1Promise = this.spawnProbe(
|
|
||||||
claudePath,
|
|
||||||
['-p', 'Reply with the single word PONG and nothing else', '--output-format', 'text'],
|
|
||||||
cwd,
|
|
||||||
env,
|
|
||||||
PREFLIGHT_TIMEOUT_MS
|
|
||||||
);
|
|
||||||
|
|
||||||
// Stage 1: verify binary works (awaited first for clearer errors)
|
|
||||||
const versionProbe = await versionProbePromise;
|
|
||||||
if (versionProbe.exitCode !== 0) {
|
if (versionProbe.exitCode !== 0) {
|
||||||
const errorText =
|
const errorText =
|
||||||
buildCombinedLogs(versionProbe.stdout, versionProbe.stderr) ||
|
buildCombinedLogs(versionProbe.stdout, versionProbe.stderr) ||
|
||||||
|
|
@ -3472,21 +3519,13 @@ export class TeamProvisioningService {
|
||||||
for (let attempt = 1; attempt <= PREFLIGHT_AUTH_MAX_RETRIES; attempt++) {
|
for (let attempt = 1; attempt <= PREFLIGHT_AUTH_MAX_RETRIES; attempt++) {
|
||||||
let pingProbe: { exitCode: number | null; stdout: string; stderr: string } | null = null;
|
let pingProbe: { exitCode: number | null; stdout: string; stderr: string } | null = null;
|
||||||
try {
|
try {
|
||||||
pingProbe =
|
pingProbe = await this.spawnProbe(
|
||||||
attempt === 1
|
claudePath,
|
||||||
? await pingAttempt1Promise
|
['-p', 'Reply with the single word PONG and nothing else', '--output-format', 'text'],
|
||||||
: await this.spawnProbe(
|
cwd,
|
||||||
claudePath,
|
env,
|
||||||
[
|
PREFLIGHT_TIMEOUT_MS
|
||||||
'-p',
|
);
|
||||||
'Reply with the single word PONG and nothing else',
|
|
||||||
'--output-format',
|
|
||||||
'text',
|
|
||||||
],
|
|
||||||
cwd,
|
|
||||||
env,
|
|
||||||
PREFLIGHT_TIMEOUT_MS
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
if (attempt < PREFLIGHT_AUTH_MAX_RETRIES) {
|
if (attempt < PREFLIGHT_AUTH_MAX_RETRIES) {
|
||||||
|
|
@ -3524,13 +3563,24 @@ export class TeamProvisioningService {
|
||||||
if (isAuthFailure || pingProbe.exitCode !== 0) {
|
if (isAuthFailure || pingProbe.exitCode !== 0) {
|
||||||
const hint = isAuthFailure
|
const hint = isAuthFailure
|
||||||
? 'Claude CLI `-p` mode is not authenticated. ' +
|
? 'Claude CLI `-p` mode is not authenticated. ' +
|
||||||
'Set ANTHROPIC_API_KEY, or run `claude setup-token` to generate a long-lived OAuth token, ' +
|
'Run `claude auth login` (or start `claude` and run `/login`) to authenticate. ' +
|
||||||
'then export it as CLAUDE_CODE_OAUTH_TOKEN.' +
|
'For automation/headless use, set ANTHROPIC_API_KEY or run `claude setup-token` ' +
|
||||||
|
'and export CLAUDE_CODE_OAUTH_TOKEN.' +
|
||||||
(attempt > 1 ? ` (failed after ${attempt} attempts)` : '')
|
(attempt > 1 ? ` (failed after ${attempt} attempts)` : '')
|
||||||
: `Claude CLI preflight check failed (exit code ${pingProbe.exitCode ?? 'unknown'}).`;
|
: `Claude CLI preflight check failed (exit code ${pingProbe.exitCode ?? 'unknown'}).`;
|
||||||
return { warning: hint };
|
return { warning: hint };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pongCandidate = pingProbe.stdout.trim() || pingProbe.stderr.trim();
|
||||||
|
const isPong = pongCandidate.toUpperCase() === 'PONG';
|
||||||
|
if (!isPong) {
|
||||||
|
return {
|
||||||
|
warning:
|
||||||
|
'Preflight ping completed but did not return the expected PONG. ' +
|
||||||
|
`Output: ${combinedOutput || '(empty)'}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (attempt > 1) {
|
if (attempt > 1) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Preflight auth succeeded on attempt ${attempt} (previous attempt had auth failure)`
|
`Preflight auth succeeded on attempt ${attempt} (previous attempt had auth failure)`
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ export interface ProvisioningProgressBlockProps {
|
||||||
title: string;
|
title: string;
|
||||||
/** Optional status message */
|
/** Optional status message */
|
||||||
message?: string | null;
|
message?: string | null;
|
||||||
|
/** Visual tone (e.g. highlight errors) */
|
||||||
|
tone?: 'default' | 'error';
|
||||||
/** Index of the current step in STEP_ORDER (0-based), or -1 if unknown */
|
/** Index of the current step in STEP_ORDER (0-based), or -1 if unknown */
|
||||||
currentStepIndex: number;
|
currentStepIndex: number;
|
||||||
/** Show spinner next to title */
|
/** Show spinner next to title */
|
||||||
|
|
@ -66,6 +68,7 @@ function useElapsedTimer(startedAt?: string): string | null {
|
||||||
export const ProvisioningProgressBlock = ({
|
export const ProvisioningProgressBlock = ({
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
|
tone = 'default',
|
||||||
currentStepIndex,
|
currentStepIndex,
|
||||||
loading = false,
|
loading = false,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
|
@ -78,6 +81,7 @@ export const ProvisioningProgressBlock = ({
|
||||||
const elapsed = useElapsedTimer(startedAt);
|
const elapsed = useElapsedTimer(startedAt);
|
||||||
const [logsOpen, setLogsOpen] = useState(false);
|
const [logsOpen, setLogsOpen] = useState(false);
|
||||||
const outputScrollRef = useRef<HTMLDivElement>(null);
|
const outputScrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isError = tone === 'error';
|
||||||
|
|
||||||
// Auto-scroll assistant output
|
// Auto-scroll assistant output
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -90,6 +94,7 @@ export const ProvisioningProgressBlock = ({
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-md border border-[var(--color-border)] bg-[var(--color-surface-raised)] px-3 py-2',
|
'rounded-md border border-[var(--color-border)] bg-[var(--color-surface-raised)] px-3 py-2',
|
||||||
|
isError && 'border-red-500/40 bg-red-500/10',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -119,7 +124,16 @@ export const ProvisioningProgressBlock = ({
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{message ? <p className="mt-1.5 text-xs text-[var(--color-text-muted)]">{message}</p> : null}
|
{message ? (
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
'mt-1.5 text-xs',
|
||||||
|
isError ? 'text-red-200' : 'text-[var(--color-text-muted)]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
<div className="mt-2 flex items-center gap-1 overflow-x-auto pb-0.5">
|
<div className="mt-2 flex items-center gap-1 overflow-x-auto pb-0.5">
|
||||||
{STEP_ORDER.filter((s): s is ProvisioningStep => s !== 'ready').map((step, index) => {
|
{STEP_ORDER.filter((s): s is ProvisioningStep => s !== 'ready').map((step, index) => {
|
||||||
const isDone = currentStepIndex >= 0 && index < currentStepIndex;
|
const isDone = currentStepIndex >= 0 && index < currentStepIndex;
|
||||||
|
|
@ -155,7 +169,10 @@ export const ProvisioningProgressBlock = ({
|
||||||
<p className="mb-1 text-[11px] font-medium text-[var(--color-text-muted)]">Live output</p>
|
<p className="mb-1 text-[11px] font-medium text-[var(--color-text-muted)]">Live output</p>
|
||||||
<div
|
<div
|
||||||
ref={outputScrollRef}
|
ref={outputScrollRef}
|
||||||
className="max-h-[400px] overflow-y-auto rounded border border-[var(--color-border)] bg-[var(--color-surface)] p-2"
|
className={cn(
|
||||||
|
'max-h-[400px] overflow-y-auto rounded border border-[var(--color-border)] bg-[var(--color-surface)] p-2',
|
||||||
|
isError && 'border-red-500/40'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<MarkdownViewer content={assistantOutput} bare maxHeight="max-h-none" />
|
<MarkdownViewer content={assistantOutput} bare maxHeight="max-h-none" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -87,16 +87,29 @@ export const TeamProvisioningBanner = ({
|
||||||
|
|
||||||
if (isFailed) {
|
if (isFailed) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-3 flex items-center gap-2 rounded-md border border-red-500/40 bg-red-500/10 px-3 py-2">
|
<div className="mb-3">
|
||||||
<p className="flex-1 text-xs text-red-200">{progress.message}</p>
|
<div className="mb-2 flex items-center gap-2 rounded-md border border-red-500/40 bg-red-500/10 px-3 py-2">
|
||||||
<Button
|
<p className="flex-1 text-xs text-red-200">{progress.message}</p>
|
||||||
variant="outline"
|
<Button
|
||||||
size="sm"
|
variant="outline"
|
||||||
className="h-6 shrink-0 border-red-500/40 px-2 text-xs text-red-300 hover:bg-red-500/10 hover:text-red-200"
|
size="sm"
|
||||||
onClick={() => setDismissed(true)}
|
className="h-6 shrink-0 border-red-500/40 px-2 text-xs text-red-300 hover:bg-red-500/10 hover:text-red-200"
|
||||||
>
|
onClick={() => setDismissed(true)}
|
||||||
<X size={12} />
|
>
|
||||||
</Button>
|
<X size={12} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ProvisioningProgressBlock
|
||||||
|
title="Launch failed"
|
||||||
|
message={progress.error ?? null}
|
||||||
|
tone="error"
|
||||||
|
currentStepIndex={progressStepIndex >= 0 ? progressStepIndex : -1}
|
||||||
|
startedAt={progress.startedAt}
|
||||||
|
pid={progress.pid}
|
||||||
|
cliLogsTail={progress.cliLogsTail}
|
||||||
|
assistantOutput={progress.assistantOutput}
|
||||||
|
onCancel={null}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,4 +40,29 @@ describe('TeamMemberResolver', () => {
|
||||||
expect(lead?.role).toBe('lead');
|
expect(lead?.role).toBe('lead');
|
||||||
expect(lead?.agentType).toBe('team-lead');
|
expect(lead?.agentType).toBe('team-lead');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('filters out "user" pseudo-member even when present in config, meta, or inboxNames', () => {
|
||||||
|
const resolver = new TeamMemberResolver();
|
||||||
|
const config: TeamConfig = {
|
||||||
|
name: 'Team',
|
||||||
|
members: [
|
||||||
|
{ name: 'team-lead', agentType: 'team-lead', role: 'lead' },
|
||||||
|
{ name: 'user', agentType: 'general-purpose' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const metaMembers: TeamConfig['members'] = [
|
||||||
|
{ name: 'user', agentType: 'general-purpose' },
|
||||||
|
{ name: 'alice', role: 'dev', agentType: 'general-purpose' },
|
||||||
|
];
|
||||||
|
const inboxNames = ['user', 'alice'];
|
||||||
|
const tasks: TeamTask[] = [];
|
||||||
|
const messages: InboxMessage[] = [];
|
||||||
|
|
||||||
|
const members = resolver.resolveMembers(config, metaMembers, inboxNames, tasks, messages);
|
||||||
|
const names = members.map((m) => m.name);
|
||||||
|
|
||||||
|
expect(names).not.toContain('user');
|
||||||
|
expect(names).toContain('team-lead');
|
||||||
|
expect(names).toContain('alice');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue