fix(ci): restore dev validation gates

This commit is contained in:
777genius 2026-05-04 09:12:53 +03:00
parent b92c8c2f54
commit 92a1c2067b
33 changed files with 106 additions and 69 deletions

View file

@ -120,6 +120,7 @@ import {
import { startEventLoopLagMonitor } from './services/infrastructure/EventLoopLagMonitor';
import { HttpServer } from './services/infrastructure/HttpServer';
import { clearAutoResumeService } from './services/team/AutoResumeService';
import { LaunchIoGovernor } from './services/team/LaunchIoGovernor';
import { OpenCodeBridgeCommandClient } from './services/team/opencode/bridge/OpenCodeBridgeCommandClient';
import {
createOpenCodeBridgeCommandLeaseStore,
@ -136,16 +137,15 @@ import {
clearTeamControlApiState,
writeTeamControlApiState,
} from './services/team/TeamControlApiState';
import { TeamInboxReader } from './services/team/TeamInboxReader';
import { getTeamDataWorkerClient } from './services/team/TeamDataWorkerClient';
import { getTeamFsWorkerClient } from './services/team/TeamFsWorkerClient';
import { TeamInboxReader } from './services/team/TeamInboxReader';
import { TeamMemberRuntimeAdvisoryService } from './services/team/TeamMemberRuntimeAdvisoryService';
import {
createTeamReconcileDrainScheduler,
type TeamReconcileTrigger,
} from './services/team/TeamReconcileDrainScheduler';
import { TeamSentMessagesStore } from './services/team/TeamSentMessagesStore';
import { LaunchIoGovernor } from './services/team/LaunchIoGovernor';
import { getAppIconPath } from './utils/appIcon';
import {
getClaudeBasePath,
@ -647,7 +647,7 @@ async function runStartupJobsBounded<T>(
if (isShutdownStarted()) {
return;
}
await run(items[index]!);
await run(items[index]);
}
});
await Promise.allSettled(workers);

View file

@ -125,6 +125,10 @@ import {
initializeAutoResumeService,
planRateLimitAutoResume,
} from '../services/team/AutoResumeService';
import {
cloneLaunchIoGovernorPayload,
type LaunchIoGovernor,
} from '../services/team/LaunchIoGovernor';
import {
buildReplaceMembersDiff,
buildReplaceMembersSummaryMessage,
@ -136,10 +140,6 @@ import { TeamMetaStore } from '../services/team/TeamMetaStore';
import { buildAddMemberSpawnMessage } from '../services/team/TeamProvisioningService';
import { TeamTaskAttachmentStore } from '../services/team/TeamTaskAttachmentStore';
import { TeamWorktreeGitService } from '../services/team/TeamWorktreeGitService';
import {
cloneLaunchIoGovernorPayload,
type LaunchIoGovernor,
} from '../services/team/LaunchIoGovernor';
import {
validateFromField,
@ -983,7 +983,6 @@ async function handleGetData(
} else {
noteHeavyTeamDataWorkerFallback('teams:getData');
data = await getTeamDataService().getTeamData(tn);
dataSource = 'main-unavailable';
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);

View file

@ -23,7 +23,7 @@ import { stripAgentBlocks } from '@shared/constants/agentBlocks';
import { getMemberColorByName, MEMBER_COLOR_HUE } from '@shared/constants/memberColors';
import { isLeadMember } from '@shared/utils/leadDetection';
import { createLogger } from '@shared/utils/logger';
import { Notification as ElectronNotification, nativeImage } from 'electron';
import { nativeImage, Notification as ElectronNotification } from 'electron';
import { EventEmitter } from 'events';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import * as fsp from 'fs/promises';
@ -279,7 +279,7 @@ function truncateNotificationText(value: string, maxLength: number): string {
}
function extractTaskRef(summary: string): string | null {
const match = summary.match(/#([A-Za-z0-9][A-Za-z0-9-]*)/);
const match = /#([A-Za-z0-9][A-Za-z0-9-]*)/.exec(summary);
return match ? `#${match[1]}` : null;
}

View file

@ -1,6 +1,6 @@
import { randomUUID } from 'crypto';
import fs from 'fs';
import path from 'path';
import { randomUUID } from 'crypto';
import { mergeJsonSettingsObjects, parseJsonSettingsObject } from './cliSettingsArgs';
@ -24,7 +24,8 @@ export function splitSettingsJsonArgs(args: string[]): SplitSettingsJsonArgsResu
const settingsFragments: TeamRuntimeSettingsJson[] = [];
const passthroughArgs: string[] = [];
for (let index = 0; index < args.length; index += 1) {
let index = 0;
while (index < args.length) {
const arg = args[index];
if (arg === '--settings') {
const value = args[index + 1];
@ -32,11 +33,11 @@ export function splitSettingsJsonArgs(args: string[]): SplitSettingsJsonArgsResu
const parsed = parseJsonSettingsObject(value);
if (parsed) {
settingsFragments.push(parsed);
index += 1;
index += 2;
continue;
}
passthroughArgs.push(arg, value);
index += 1;
index += 2;
continue;
}
}
@ -52,6 +53,7 @@ export function splitSettingsJsonArgs(args: string[]): SplitSettingsJsonArgsResu
}
passthroughArgs.push(arg);
index += 1;
}
return { settingsFragments, passthroughArgs };
@ -119,7 +121,7 @@ async function writeSettingsFile(
export async function materializeTeamRuntimeSettingsBundle(input: {
teamName: string;
providerId: TeamProviderId;
baseSettings?: Array<TeamRuntimeSettingsJson | null | undefined>;
baseSettings?: (TeamRuntimeSettingsJson | null | undefined)[];
anthropicHelper?: AnthropicTeamApiKeyHelperMaterial | null;
}): Promise<TeamRuntimeSettingsBundle | null> {
const fragments = [...(input.baseSettings ?? [])].filter(

View file

@ -198,12 +198,8 @@ export class TeamFsWorkerClient {
)}`
);
this.failWorker(worker, timeoutError);
try {
// Terminate and recreate on next call - worker may be stuck in native IO.
worker.terminate().catch(() => undefined);
} catch {
// ignore
}
// Terminate and recreate on next call - worker may be stuck in native IO.
void worker.terminate().catch(() => undefined);
reject(timeoutError);
}, WORKER_CALL_TIMEOUT_MS);

View file

@ -13,13 +13,13 @@ import {
BOARD_TASK_CHANGES_DIRNAME,
BOARD_TASK_LOG_FRESHNESS_DIRNAME,
BOARD_TASK_LOG_FRESHNESS_FILE_SUFFIX,
MAX_PENDING_UNKNOWN_ROOT_REFRESH_ATTEMPTS,
MAX_PENDING_UNKNOWN_ROOT_SESSIONS,
PENDING_UNKNOWN_ROOT_SESSION_TTL_MS,
classifyLogSourceWatcherEvent,
getRelativeLogSourceParts,
isAgentTranscriptFileName,
MAX_PENDING_UNKNOWN_ROOT_REFRESH_ATTEMPTS,
MAX_PENDING_UNKNOWN_ROOT_SESSIONS,
normalizeLogSourceSessionId,
PENDING_UNKNOWN_ROOT_SESSION_TTL_MS,
} from './teamLogSourceWatchScope';
import type { TeamLogSourceLiveContext, TeamMemberLogsFinder } from './TeamMemberLogsFinder';
@ -420,18 +420,21 @@ export class TeamLogSourceTracker {
});
if (action.kind === 'task-freshness') {
this.handleTaskFreshnessSignalChange(
teamName,
current.projectDir,
changedPath,
BOARD_TASK_LOG_FRESHNESS_DIRNAME
) ||
if (
!this.handleTaskFreshnessSignalChange(
teamName,
current.projectDir,
changedPath,
BOARD_TASK_LOG_FRESHNESS_DIRNAME
)
) {
this.handleTaskFreshnessSignalChange(
teamName,
current.projectDir,
changedPath,
BOARD_TASK_CHANGE_FRESHNESS_DIRNAME
);
}
return;
}

View file

@ -11,15 +11,15 @@ import {
lineHasAgentTeamsTaskBoundaryToolName,
} from './agentTeamsToolNames';
import {
readBootstrapLaunchSnapshot,
choosePreferredLaunchSnapshot,
readBootstrapLaunchSnapshot,
} from './TeamBootstrapStateReader';
import { TeamConfigReader } from './TeamConfigReader';
import { TeamInboxReader } from './TeamInboxReader';
import { TeamLaunchStateStore } from './TeamLaunchStateStore';
import { buildTeamLogWatchSessionIds, extractRuntimeSessionIds } from './teamLogSourceWatchScope';
import { TeamMembersMetaStore } from './TeamMembersMetaStore';
import { TeamTranscriptProjectResolver } from './TeamTranscriptProjectResolver';
import { buildTeamLogWatchSessionIds, extractRuntimeSessionIds } from './teamLogSourceWatchScope';
import type {
MemberLogSummary,

View file

@ -109,20 +109,20 @@ import * as path from 'path';
import pidusage from 'pidusage';
import * as readline from 'readline';
import { mergeJsonSettingsArgs, parseJsonSettingsObject } from '../runtime/cliSettingsArgs';
import {
ANTHROPIC_HELPER_MODE_COMPETING_AUTH_ENV_KEYS,
type AnthropicTeamApiKeyHelperMaterial,
CLAUDE_TEAM_ANTHROPIC_API_KEY_HELPER_SETTINGS_PATH_ENV,
CLAUDE_TEAM_ANTHROPIC_AUTH_MODE_API_KEY_HELPER,
CLAUDE_TEAM_ANTHROPIC_AUTH_MODE_ENV,
DISABLE_ANTHROPIC_TEAM_API_KEY_HELPER_ENV,
cleanupAnthropicTeamApiKeyHelperForTeam,
cleanupAnthropicTeamApiKeyHelperMaterial,
cleanupStaleAnthropicTeamApiKeyHelpers,
DISABLE_ANTHROPIC_TEAM_API_KEY_HELPER_ENV,
materializeAnthropicTeamApiKeyHelper,
verifyAnthropicTeamApiKeyHelperMaterial,
type AnthropicTeamApiKeyHelperMaterial,
} from '../runtime/anthropicTeamApiKeyHelper';
import { mergeJsonSettingsArgs, parseJsonSettingsObject } from '../runtime/cliSettingsArgs';
import {
type GeminiRuntimeAuthState,
resolveGeminiRuntimeAuth,
@ -359,9 +359,9 @@ import type {
LeadContextUsage,
MemberLaunchState,
MemberSpawnLivenessSource,
MemberSpawnStatusesSnapshot,
MemberSpawnStatus,
MemberSpawnStatusEntry,
MemberSpawnStatusesSnapshot,
PersistedTeamLaunchMemberState,
PersistedTeamLaunchPhase,
PersistedTeamLaunchSnapshot,
@ -1100,22 +1100,26 @@ function filterOutSettingsPathArgs(
return [...args];
}
const filtered: string[] = [];
for (let index = 0; index < args.length; index += 1) {
let index = 0;
while (index < args.length) {
const arg = args[index];
if (arg === '--settings' && args[index + 1] === settingsPath) {
index += 1;
index += 2;
continue;
}
if (arg === `--settings=${settingsPath}`) {
index += 1;
continue;
}
filtered.push(arg);
index += 1;
}
return filtered;
}
function hasPathBasedSettingsArgs(args: string[]): boolean {
for (let index = 0; index < args.length; index += 1) {
let index = 0;
while (index < args.length) {
const arg = args[index];
if (arg === '--settings') {
const value = args[index + 1];
@ -1123,17 +1127,20 @@ function hasPathBasedSettingsArgs(args: string[]): boolean {
if (!parseJsonSettingsObject(value)) {
return true;
}
index += 1;
index += 2;
continue;
}
if (typeof value !== 'string') {
return true;
}
index += 1;
continue;
}
const prefix = '--settings=';
if (arg.startsWith(prefix) && !parseJsonSettingsObject(arg.slice(prefix.length))) {
return true;
}
index += 1;
}
return false;
}
@ -4989,6 +4996,10 @@ export class TeamProvisioningService {
this.transcriptProjectResolver = new TeamTranscriptProjectResolver({
getConfig: (teamName) => this.configReader.getConfigSnapshot(teamName),
});
this.scheduleStaleAnthropicTeamApiKeyHelperCleanup();
}
private scheduleStaleAnthropicTeamApiKeyHelperCleanup(): void {
void cleanupStaleAnthropicTeamApiKeyHelpers({
baseClaudeDir: getClaudeBasePath(),
maxAgeMs: 14 * 24 * 60 * 60 * 1000,
@ -11090,8 +11101,7 @@ export class TeamProvisioningService {
const existingRequest = this.memberSpawnStatusesInFlightByTeam.get(teamName);
if (
existingRequest &&
existingRequest.generationAtStart === generationAtStart &&
existingRequest?.generationAtStart === generationAtStart &&
existingRequest.runIdAtStart === run.runId
) {
const snapshot = await existingRequest.promise;
@ -11201,8 +11211,7 @@ export class TeamProvisioningService {
const generationAtStart = this.getRuntimeSnapshotCacheGeneration(teamName);
const existingRequest = this.agentRuntimeSnapshotInFlightByTeam.get(teamName);
if (
existingRequest &&
existingRequest.generationAtStart === generationAtStart &&
existingRequest?.generationAtStart === generationAtStart &&
existingRequest.runIdAtStart === runId
) {
return existingRequest.promise;
@ -18732,8 +18741,7 @@ export class TeamProvisioningService {
const generationAtStart = this.getRuntimeSnapshotCacheGeneration(teamName);
const existingRequest = this.liveTeamAgentRuntimeMetadataInFlightByTeam.get(teamName);
if (
existingRequest &&
existingRequest.generationAtStart === generationAtStart &&
existingRequest?.generationAtStart === generationAtStart &&
existingRequest.runIdAtStart === runId
) {
return this.cloneLiveTeamAgentRuntimeMetadata(await existingRequest.promise);

View file

@ -3,10 +3,10 @@ import { createHash, randomUUID } from 'crypto';
import { promises as fs } from 'fs';
import * as path from 'path';
import type { FileLockOptions } from '../../fileLock';
import { VersionedJsonStore, VersionedJsonStoreError } from './VersionedJsonStore';
import type { FileLockOptions } from '../../fileLock';
export const OPENCODE_RUNTIME_STORE_MANIFEST_SCHEMA_VERSION = 1;
export const OPENCODE_RUNTIME_STORE_RECEIPT_SCHEMA_VERSION = 1;

View file

@ -148,8 +148,8 @@ interface TeamSummaryCacheEntry {
}
type CachedTaskReadResult =
| { task: Record<string, unknown>; skipReason?: undefined }
| { task?: undefined; skipReason: string };
| { task: Record<string, unknown>; skipReason?: never }
| { task?: never; skipReason: string };
interface TaskFileCacheEntry {
fingerprint: string;

View file

@ -216,3 +216,5 @@ export const BaseItem = memo(
);
}
);
BaseItem.displayName = 'BaseItem';

View file

@ -282,3 +282,5 @@ export const ExecutionTrace: React.FC<ExecutionTraceProps> = React.memo(
);
}
);
ExecutionTrace.displayName = 'ExecutionTrace';

View file

@ -236,3 +236,5 @@ export const LinkedToolItem = memo(
);
}
);
LinkedToolItem.displayName = 'LinkedToolItem';

View file

@ -215,3 +215,5 @@ export const MetricsPill = memo(
);
}
);
MetricsPill.displayName = 'MetricsPill';

View file

@ -81,3 +81,5 @@ export const SlashItem = memo(
);
}
);
SlashItem.displayName = 'SlashItem';

View file

@ -598,3 +598,5 @@ export const SubagentItem: React.FC<SubagentItemProps> = React.memo(
);
}
);
SubagentItem.displayName = 'SubagentItem';

View file

@ -264,3 +264,5 @@ export const TeammateMessageItem = memo(
);
}
);
TeammateMessageItem.displayName = 'TeammateMessageItem';

View file

@ -82,3 +82,5 @@ export const TextItem: React.FC<TextItemProps> = React.memo(
);
}
);
TextItem.displayName = 'TextItem';

View file

@ -82,3 +82,5 @@ export const ThinkingItem: React.FC<ThinkingItemProps> = React.memo(
);
}
);
ThinkingItem.displayName = 'ThinkingItem';

View file

@ -59,3 +59,5 @@ export const CollapsibleOutputSection = memo(
);
}
);
CollapsibleOutputSection.displayName = 'CollapsibleOutputSection';

View file

@ -1112,3 +1112,5 @@ export const DateGroupedSessions = memo((): React.JSX.Element => {
</div>
);
});
DateGroupedSessions.displayName = 'DateGroupedSessions';

View file

@ -117,3 +117,5 @@ export const MemberBadge = memo(
);
}
);
MemberBadge.displayName = 'MemberBadge';

View file

@ -128,12 +128,12 @@ import {
} from './sidebar/teamSidebarUiState';
import { ClaudeLogsSection } from './ClaudeLogsSection';
import { CollapsibleTeamSection } from './CollapsibleTeamSection';
import { ProcessesSection } from './ProcessesSection';
import { getLaunchJoinMilestonesFromMembers, getLaunchJoinState } from './provisioningSteps';
import { TeamProvisioningBanner } from './TeamProvisioningBanner';
import { deriveLeadContextButtonLabel } from './leadContextLoadGuards';
import { LeadSessionDetailGate } from './LeadSessionDetailGate';
import { LiveRuntimeStatusBridge } from './LiveRuntimeStatusBridge';
import { ProcessesSection } from './ProcessesSection';
import { getLaunchJoinMilestonesFromMembers, getLaunchJoinState } from './provisioningSteps';
import { TeamProvisioningBanner } from './TeamProvisioningBanner';
import { loadTeamSessionMetadata } from './teamSessionFetchGuards';
import { TeamSessionsSection } from './TeamSessionsSection';
@ -1847,7 +1847,7 @@ export const TeamDetailView = memo(function TeamDetailView({
const pendingTeamSectionFocus = useStore((s) => s.pendingTeamSectionFocus);
const clearTeamSectionFocus = useStore((s) => s.clearTeamSectionFocus);
useEffect(() => {
if (!pendingTeamSectionFocus || pendingTeamSectionFocus.teamName !== teamName) return;
if (pendingTeamSectionFocus?.teamName !== teamName) return;
const sectionId =
pendingTeamSectionFocus.section === 'members'

View file

@ -79,3 +79,5 @@ export const ReplyQuoteBlock = memo(
);
}
);
ReplyQuoteBlock.displayName = 'ReplyQuoteBlock';

View file

@ -458,5 +458,7 @@ export const KanbanGridLayout = memo(
}
);
KanbanGridLayout.displayName = 'KanbanGridLayout';
export { SKELETON_HIDE_DELAY_MS, SKELETON_HIDE_DELAY_MS_ON_MODE_SWITCH };
/* eslint-enable tailwindcss/no-custom-classname -- stable class hooks remain scoped to this file. */

View file

@ -58,3 +58,5 @@ export const CurrentTaskIndicator = memo(
);
}
);
CurrentTaskIndicator.displayName = 'CurrentTaskIndicator';

View file

@ -27,3 +27,5 @@ export const MemberPresenceDot = memo(
);
}
);
MemberPresenceDot.displayName = 'MemberPresenceDot';

View file

@ -36,11 +36,11 @@ export interface TeamRuntimeDisplayRow {
actionsAllowed: false;
}
type SpawnDegradation = {
interface SpawnDegradation {
reason: string;
diagnostic?: string;
diagnosticSeverity: TeamAgentRuntimeDiagnosticSeverity;
};
}
const ACTIVE_SPAWN_STATUSES = new Set(['waiting', 'spawning']);

View file

@ -48,6 +48,8 @@ import {
isTeamDataRefreshPending,
selectTeamDataForName,
} from './slices/teamSlice';
import { createUISlice } from './slices/uiSlice';
import { createUpdateSlice } from './slices/updateSlice';
import {
decideProcessFanoutDryRun,
decideProcessFanoutMode,
@ -58,8 +60,6 @@ import {
noteTeamRefreshFanout,
type TeamRefreshFanoutOperation,
} from './teamRefreshFanoutDiagnostics';
import { createUISlice } from './slices/uiSlice';
import { createUpdateSlice } from './slices/updateSlice';
import type { DetectedError } from '../types/data';
import type { AppState } from './types';

View file

@ -60,7 +60,7 @@ export interface TeamRefreshFanoutStructuredCount {
phase: TeamRefreshFanoutPhase;
}
export interface TeamRefreshFanoutSummaryRow extends TeamRefreshFanoutStructuredCount {}
export type TeamRefreshFanoutSummaryRow = TeamRefreshFanoutStructuredCount;
export interface TeamRefreshFanoutSummary {
generatedAt: number;
@ -92,7 +92,7 @@ function createEmptyBucket(): TeamRefreshFanoutBucket {
function ensureTeamBucket(teamName: string): TeamRefreshFanoutBucket {
if (!buckets.has(teamName) && buckets.size >= MAX_TEAM_REFRESH_DIAGNOSTIC_TEAMS) {
const oldestKey = buckets.keys().next().value as string | undefined;
const oldestKey = buckets.keys().next().value;
if (oldestKey) {
buckets.delete(oldestKey);
}

View file

@ -1,5 +1,5 @@
import type { EnhancedChunk } from '@main/types';
import type { NotificationTarget, TeamEventType } from './notifications';
import type { EnhancedChunk } from '@main/types';
export interface TeamMember {
name: string;

View file

@ -235,10 +235,7 @@ liveDescribe('Mixed provider team launch live e2e', () => {
await waitUntilWithDiagnostics(async () => {
const status = await harness!.svc.getMemberSpawnStatuses(teamName!);
if (
status.teamLaunchState === 'failed' ||
status.teamLaunchState === 'partial_failure'
) {
if (status.teamLaunchState === 'partial_failure') {
throw new Error(await formatMixedLaunchDiagnostics(harness!, teamName!, progressEvents));
}
for (const memberName of ['alice', 'cody', 'oscar'] as const) {
@ -268,8 +265,8 @@ liveDescribe('Mixed provider team launch live e2e', () => {
const laneIndex = await readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName);
expect(
Object.values(laneIndex.lanes).some(
(lane) => lane.state === 'active' && lane.memberName === 'oscar'
Object.entries(laneIndex.lanes).some(
([laneId, lane]) => lane.state === 'active' && laneId === 'secondary:opencode:oscar'
)
).toBe(true);
},

View file

@ -25,7 +25,7 @@ vi.mock('@main/services/team/ClaudeBinaryResolver', () => ({
vi.mock('@main/utils/childProcess', () => ({
execCli: vi.fn(async (_binaryPath: string | null, args: string[]) => {
if (args[0] === 'model') {
if (args.includes('model') && args.includes('list')) {
return {
stdout: JSON.stringify({
schemaVersion: 1,
@ -54,7 +54,7 @@ vi.mock('@main/utils/childProcess', () => ({
stderr: '',
};
}
if (args[0] === 'runtime') {
if (args.includes('runtime') && args.includes('status')) {
return {
stdout: JSON.stringify({
providers: {