/* eslint-disable security/detect-non-literal-fs-filename, security/detect-object-injection */ import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import type { TeamTask, TeamTaskChangeSummaryItem, TeamTaskWithKanban } from '../src/shared/types'; process.env.CLAUDE_TEAM_ENABLE_PERSISTED_TASK_CHANGE_CACHE = '0'; const FIRST_STAGE_REQUESTS = 3; const SECOND_STAGE_REQUESTS = 9; const FIRST_STAGE_UNKNOWN_SCAN_LIMIT = 3; const SECOND_STAGE_UNKNOWN_SCAN_LIMIT = 6; const DEFAULT_TEAM_LIMIT = 3; interface Args { teams: string[]; limit: number; } interface PresenceEntry { presence?: string; } interface CandidateSuccess { teamName: string; tasks: TeamTaskWithKanban[]; taskCount: number; changedPresenceCount: number; eligibleCount: number; presenceCounts: Record; } interface CandidateFailure { teamName: string; error: string; } type Candidate = CandidateSuccess | CandidateFailure; interface RuntimeModules { TeamTaskReader: typeof import('../src/main/services/team/TeamTaskReader')['TeamTaskReader']; ChangeExtractorService: typeof import('../src/main/services/team/ChangeExtractorService')['ChangeExtractorService']; TeamMemberLogsFinder: typeof import('../src/main/services/team/TeamMemberLogsFinder')['TeamMemberLogsFinder']; TaskBoundaryParser: typeof import('../src/main/services/team/TaskBoundaryParser')['TaskBoundaryParser']; TaskChangeWorkerClient: typeof import('../src/main/services/team/TaskChangeWorkerClient')['TaskChangeWorkerClient']; buildTeamChangeRequestPlan: typeof import('../src/renderer/components/team/teamChangesRequestPlan')['buildTeamChangeRequestPlan']; TEAM_CHANGES_MAX_REQUESTS: typeof import('../src/renderer/components/team/teamChangesRequestPlan')['TEAM_CHANGES_MAX_REQUESTS']; } interface StageReport { label: string; requested: number; duplicateRequests: string[]; responseItems: number; truncated: boolean; ms: number; deferredBeforeResponse: number; satisfiedAfterStage: number; itemErrors: number; nullItems: number; countableItems: number; fileRows: number; confidenceCounts: Record; sourceKindCounts: Record; firstTaskIds: string[]; } interface TeamSmokeReport { kind: 'team-smoke'; teamName: string; taskCount: number; changedPresenceCount: number; eligibleCount: number; stages: StageReport[]; } interface ForceRefreshSmokeReport { kind: 'force-refresh-smoke'; teamName: string; requested: number; allForceFresh: boolean; responseItems: number; ms: number; taskIds: string[]; } function parseArgs(argv: string[]): Args { const teams: string[] = []; let limit = DEFAULT_TEAM_LIMIT; let index = 0; while (index < argv.length) { const arg = argv[index]; const next = argv[index + 1] ?? ''; if (arg === '--team' || arg === '--teams') { teams.push(...next.split(',').map((teamName) => teamName.trim()).filter(Boolean)); index += 2; continue; } if (arg === '--limit') { const parsedLimit = Number.parseInt(next, 10); if (Number.isFinite(parsedLimit) && parsedLimit > 0) { limit = parsedLimit; } index += 2; continue; } index += 1; } return { teams: [...new Set(teams)], limit }; } async function loadRuntimeModules(): Promise { const { TeamTaskReader } = await import('../src/main/services/team/TeamTaskReader'); const { ChangeExtractorService } = await import( '../src/main/services/team/ChangeExtractorService' ); const { TeamMemberLogsFinder } = await import('../src/main/services/team/TeamMemberLogsFinder'); const { TaskBoundaryParser } = await import('../src/main/services/team/TaskBoundaryParser'); const { TaskChangeWorkerClient } = await import( '../src/main/services/team/TaskChangeWorkerClient' ); const { buildTeamChangeRequestPlan, TEAM_CHANGES_MAX_REQUESTS } = await import( '../src/renderer/components/team/teamChangesRequestPlan' ); return { TeamTaskReader, ChangeExtractorService, TeamMemberLogsFinder, TaskBoundaryParser, TaskChangeWorkerClient, buildTeamChangeRequestPlan, TEAM_CHANGES_MAX_REQUESTS, }; } async function readTeamNames(): Promise { const teamsDir = path.join(os.homedir(), '.claude', 'teams'); const entries = await fs.readdir(teamsDir, { withFileTypes: true }).catch(() => []); return entries .filter((entry) => entry.isDirectory()) .map((entry) => entry.name) .sort((left, right) => left.localeCompare(right)); } async function readPresence(teamName: string): Promise> { const filePath = path.join( os.homedir(), '.claude', 'task-change-presence', `${encodeURIComponent(teamName)}.json` ); try { const parsed: unknown = JSON.parse(await fs.readFile(filePath, 'utf8')); return parsed && typeof parsed === 'object' ? (parsed as Record) : {}; } catch { return {}; } } function overlayPresence( tasks: TeamTask[], presenceByTaskId: Record ): TeamTaskWithKanban[] { return tasks.map((task) => { const presence = presenceByTaskId[task.id]?.presence; if ( presence === 'has_changes' || presence === 'needs_attention' || presence === 'no_changes' || presence === 'unknown' ) { return { ...task, changePresence: presence }; } return task; }); } function increment(counts: Record, rawKey: string | undefined): void { const key = rawKey && rawKey.trim().length > 0 ? rawKey : 'unknown'; counts[key] = (counts[key] ?? 0) + 1; } function isCandidateSuccess(candidate: Candidate): candidate is CandidateSuccess { return !('error' in candidate); } function isCountableSummary(item: TeamTaskChangeSummaryItem): boolean { if (item.error) return true; const changeSet = item.changeSet; if (!changeSet) return false; const fileCount = Array.isArray(changeSet.files) ? changeSet.files.length : 0; const diagnosticCount = Array.isArray(changeSet.reviewDiagnostics) ? changeSet.reviewDiagnostics.length : 0; const warningCount = Array.isArray(changeSet.warnings) ? changeSet.warnings.length : 0; return ( fileCount > 0 || diagnosticCount > 0 || warningCount > 0 ); } function isSatisfiedSummary(item: TeamTaskChangeSummaryItem): boolean { return !item.error && item.changeSet !== null; } function createChangeExtractorService(modules: RuntimeModules): InstanceType< RuntimeModules['ChangeExtractorService'] > { return new modules.ChangeExtractorService( new modules.TeamMemberLogsFinder(), new modules.TaskBoundaryParser(), undefined, undefined, new modules.TaskChangeWorkerClient({ enabled: false }), null ); } async function loadCandidate( modules: RuntimeModules, taskReader: InstanceType, teamName: string ): Promise { try { const rawTasks = await taskReader.getTasks(teamName); const presence = await readPresence(teamName); const tasks = overlayPresence(rawTasks, presence); const eligiblePlan = modules.buildTeamChangeRequestPlan(tasks, 0, false, { maxRequests: modules.TEAM_CHANGES_MAX_REQUESTS, }); const presenceCounts: Record = {}; for (const entry of Object.values(presence)) { increment(presenceCounts, entry.presence); } return { teamName, tasks, taskCount: rawTasks.length, changedPresenceCount: (presenceCounts.has_changes ?? 0) + (presenceCounts.needs_attention ?? 0), eligibleCount: eligiblePlan.eligibleCount, presenceCounts, }; } catch (error) { return { teamName, error: error instanceof Error ? error.message : String(error), }; } } function selectCandidates(candidates: Candidate[], limit: number): CandidateSuccess[] { return candidates .filter(isCandidateSuccess) .sort((left, right) => { const leftScore = left.taskCount + left.changedPresenceCount; const rightScore = right.taskCount + right.changedPresenceCount; return ( rightScore - leftScore || right.changedPresenceCount - left.changedPresenceCount || right.taskCount - left.taskCount || left.teamName.localeCompare(right.teamName) ); }) .slice(0, limit); } function summarizeStageItems( items: TeamTaskChangeSummaryItem[], satisfiedTaskIds: Set ): Omit< StageReport, | 'label' | 'requested' | 'duplicateRequests' | 'responseItems' | 'truncated' | 'ms' | 'deferredBeforeResponse' | 'satisfiedAfterStage' | 'firstTaskIds' > { const confidenceCounts: Record = {}; const sourceKindCounts: Record = {}; let itemErrors = 0; let nullItems = 0; let countableItems = 0; let fileRows = 0; for (const item of items) { if (item.error) itemErrors += 1; if (!item.changeSet) nullItems += 1; if (isCountableSummary(item)) countableItems += 1; if (isSatisfiedSummary(item)) satisfiedTaskIds.add(item.taskId); fileRows += Array.isArray(item.changeSet?.files) ? item.changeSet.files.length : 0; increment(confidenceCounts, item.changeSet?.confidence); increment(sourceKindCounts, item.changeSet?.provenance?.sourceKind); } return { itemErrors, nullItems, countableItems, fileRows, confidenceCounts, sourceKindCounts }; } async function runTeamSmoke( modules: RuntimeModules, team: CandidateSuccess ): Promise { const service = createChangeExtractorService(modules); const satisfiedTaskIds = new Set(); const requestedTaskIds = new Set(); let cursor = 0; const stages: StageReport[] = []; const stageInputs = [ { label: 'stage1-first-paint', maxRequests: FIRST_STAGE_REQUESTS, unknownScanLimit: FIRST_STAGE_UNKNOWN_SCAN_LIMIT, }, { label: 'stage2-expand', maxRequests: SECOND_STAGE_REQUESTS, unknownScanLimit: SECOND_STAGE_UNKNOWN_SCAN_LIMIT, }, { label: 'stage3-full', maxRequests: modules.TEAM_CHANGES_MAX_REQUESTS, unknownScanLimit: undefined, }, ]; for (const stage of stageInputs) { const plan = modules.buildTeamChangeRequestPlan(team.tasks, cursor, false, { maxRequests: stage.maxRequests, unknownScanLimit: stage.unknownScanLimit, satisfiedTaskIds, }); cursor = plan.nextUnknownScanCursor; if (plan.requests.length === 0) break; const duplicateRequests = plan.requests .map((request) => request.taskId) .filter((taskId) => requestedTaskIds.has(taskId)); for (const request of plan.requests) { requestedTaskIds.add(request.taskId); } const startedAt = Date.now(); const response = await service.getTeamTaskChangeSummaries(team.teamName, plan.requests); const summary = summarizeStageItems(response.items, satisfiedTaskIds); stages.push({ label: stage.label, requested: plan.requests.length, duplicateRequests, responseItems: response.items.length, truncated: response.truncated === true, ms: Date.now() - startedAt, deferredBeforeResponse: plan.deferredCount, satisfiedAfterStage: satisfiedTaskIds.size, firstTaskIds: plan.requests.slice(0, 5).map((request) => request.taskId.slice(0, 8)), ...summary, }); if (plan.deferredCount === 0) break; } return { kind: 'team-smoke', teamName: team.teamName, taskCount: team.taskCount, changedPresenceCount: team.changedPresenceCount, eligibleCount: team.eligibleCount, stages, }; } function assertTeamSmoke(report: TeamSmokeReport): void { const problems: string[] = []; if (report.eligibleCount > 0 && report.stages.length === 0) { problems.push('eligible tasks produced no staged requests'); } for (const stage of report.stages) { if (stage.duplicateRequests.length > 0) { problems.push(`${stage.label} duplicated ${stage.duplicateRequests.join(', ')}`); } if (stage.responseItems > stage.requested) { problems.push(`${stage.label} returned more items than requested`); } if (stage.requested === 0) { problems.push(`${stage.label} was recorded with zero requests`); } } const lastStage = report.stages.at(-1); if (lastStage && lastStage.deferredBeforeResponse > 0 && lastStage.label !== 'stage3-full') { problems.push(`${lastStage.label} left deferred work without reaching the full stage`); } if (problems.length > 0) { throw new Error(`Team Changes real-data smoke failed for ${report.teamName}: ${problems.join('; ')}`); } } async function runForceRefreshSmoke( modules: RuntimeModules, team: CandidateSuccess ): Promise { const service = createChangeExtractorService(modules); const plan = modules.buildTeamChangeRequestPlan(team.tasks, 0, true, { maxRequests: FIRST_STAGE_REQUESTS, unknownScanLimit: FIRST_STAGE_UNKNOWN_SCAN_LIMIT, }); const startedAt = Date.now(); const response = await service.getTeamTaskChangeSummaries(team.teamName, plan.requests); return { kind: 'force-refresh-smoke', teamName: team.teamName, requested: plan.requests.length, allForceFresh: plan.requests.every((request) => request.options?.forceFresh === true), responseItems: response.items.length, ms: Date.now() - startedAt, taskIds: plan.requests.map((request) => request.taskId.slice(0, 8)), }; } function assertForceRefreshSmoke(report: ForceRefreshSmokeReport): void { const problems: string[] = []; if (report.requested > 0 && !report.allForceFresh) { problems.push('not every force refresh request carried forceFresh=true'); } if (report.responseItems > report.requested) { problems.push('force refresh returned more items than requested'); } if (problems.length > 0) { throw new Error(`Team Changes force-refresh smoke failed for ${report.teamName}: ${problems.join('; ')}`); } } async function main(): Promise { const args = parseArgs(process.argv.slice(2)); const modules = await loadRuntimeModules(); const taskReader = new modules.TeamTaskReader(); const teamNames = args.teams.length > 0 ? args.teams : await readTeamNames(); const candidates = await Promise.all( teamNames.map((teamName) => loadCandidate(modules, taskReader, teamName)) ); const selected = selectCandidates(candidates, args.limit); const report: unknown[] = [ { kind: 'selection', selected: selected.map( ({ teamName, taskCount, changedPresenceCount, eligibleCount, presenceCounts }) => ({ teamName, taskCount, changedPresenceCount, eligibleCount, presenceCounts, }) ), skipped: candidates.filter((candidate) => !isCandidateSuccess(candidate)), }, ]; for (const team of selected) { const teamReport = await runTeamSmoke(modules, team); assertTeamSmoke(teamReport); report.push(teamReport); } if (selected[0]) { const forceRefreshReport = await runForceRefreshSmoke(modules, selected[0]); assertForceRefreshSmoke(forceRefreshReport); report.push(forceRefreshReport); } console.log(JSON.stringify(report, null, 2)); } void main().then( () => process.exit(0), (error) => { console.error(error instanceof Error ? (error.stack ?? error.message) : String(error)); process.exit(1); } );