import { promises as fs } from 'fs'; import * as os from 'os'; import * as path from 'path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { WorkspaceTrustCoordinator, WorkspaceTrustExecutionPlan, } from '../../../../src/features/workspace-trust/core/application/WorkspaceTrustCoordinator'; import { ClaudeBinaryResolver } from '../../../../src/main/services/team/ClaudeBinaryResolver'; import { TeamConfigReader } from '../../../../src/main/services/team/TeamConfigReader'; import { getMixedLaunchFallbackRecoveryError, TeamProvisioningService, } from '../../../../src/main/services/team/TeamProvisioningService'; import type { OpenCodeTeamRuntimeMessageInput, OpenCodeTeamRuntimeMessageResult, } from '../../../../src/main/services/team/runtime'; import { TeamRuntimeAdapterRegistry, type TeamLaunchRuntimeAdapter, type TeamRuntimeLaunchInput, type TeamRuntimeMemberLaunchEvidence, type TeamRuntimeMemberSpec, type TeamRuntimeLaunchResult, type TeamRuntimePrepareResult, type TeamRuntimeReconcileInput, type TeamRuntimeReconcileResult, type TeamRuntimeStopInput, type TeamRuntimeStopResult, } from '../../../../src/main/services/team/runtime/TeamRuntimeAdapter'; import { encodePath, extractBaseDir, getProjectsBasePath, getTeamsBasePath, setClaudeBasePathOverride, } from '../../../../src/main/utils/pathDecoder'; import { createPersistedLaunchSnapshot } from '../../../../src/main/services/team/TeamLaunchStateEvaluator'; import { getOpenCodeRuntimeManifestPath, getOpenCodeRuntimeLaneIndexPath, readOpenCodeRuntimeLaneIndex, setOpenCodeRuntimeActiveRunManifest, upsertOpenCodeRuntimeLaneIndexEntry, } from '../../../../src/main/services/team/opencode/store/OpenCodeRuntimeManifestEvidenceReader'; import { createRuntimeStoreManifestStore, createRuntimeStoreReceiptStore, OPENCODE_RUNTIME_STORE_DESCRIPTORS, RuntimeStoreBatchWriter, } from '../../../../src/main/services/team/opencode/store/RuntimeStoreManifest'; import type { InboxMessage, TaskRef, TeamProvisioningProgress } from '../../../../src/shared/types'; const LAUNCH_MATRIX_SAFE_E2E_TIMEOUT_MS = 60_000; const WORKSPACE_TRUST_TEST_ENV_NAMES = [ 'AGENT_TEAMS_WORKSPACE_TRUST', 'AGENT_TEAMS_WORKSPACE_TRUST_PREFLIGHT', 'AGENT_TEAMS_WORKSPACE_TRUST_CLAUDE_PTY', 'AGENT_TEAMS_WORKSPACE_TRUST_CODEX_ARGS', 'AGENT_TEAMS_WORKSPACE_TRUST_CODEX_SETTINGS', 'AGENT_TEAMS_WORKSPACE_TRUST_RETRY', ] as const; type WorkspaceTrustTestEnvName = (typeof WORKSPACE_TRUST_TEST_ENV_NAMES)[number]; describe('Team agent launch matrix safe e2e', () => { let tempDir: string; let tempClaudeRoot: string; let projectPath: string; let originalClaudeCliPath: string | undefined; let originalWorkspaceTrustEnv: Partial>; beforeEach(async () => { TeamConfigReader.clearCacheForTests(); ClaudeBinaryResolver.clearCache(); originalClaudeCliPath = process.env.CLAUDE_CLI_PATH; originalWorkspaceTrustEnv = snapshotWorkspaceTrustTestEnv(); tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agent-launch-matrix-e2e-')); tempClaudeRoot = path.join(tempDir, '.claude'); projectPath = path.join(tempDir, 'project'); await fs.mkdir(projectPath, { recursive: true }); await fs.mkdir(tempClaudeRoot, { recursive: true }); setClaudeBasePathOverride(tempClaudeRoot); }); afterEach(async () => { TeamConfigReader.clearCacheForTests(); restoreOptionalEnvValue('CLAUDE_CLI_PATH', originalClaudeCliPath); restoreWorkspaceTrustTestEnv(originalWorkspaceTrustEnv); ClaudeBinaryResolver.clearCache(); setClaudeBasePathOverride(null); await removeTempDirWithRetries(tempDir); }); it('launches a pure OpenCode team through the runtime adapter and exposes live members', async () => { const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const progressEvents: TeamProvisioningProgress[] = []; const { runId } = await svc.createTeam( { teamName: 'pure-opencode-safe-e2e', cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [ { name: 'alice', role: 'Developer', providerId: 'opencode' }, { name: 'bob', role: 'Reviewer', providerId: 'opencode' }, ], }, (progress) => progressEvents.push(progress) ); expect(runId).toBe(adapter.launchInputs[0]?.runId); expect(adapter.launchInputs).toHaveLength(1); expect(adapter.launchInputs[0]?.expectedMembers.map((member) => member.name)).toEqual([ 'alice', 'bob', ]); expect(progressEvents.at(-1)).toMatchObject({ state: 'ready', message: 'OpenCode team launch is ready', }); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot('pure-opencode-safe-e2e'); expect(runtimeSnapshot.members.alice).toMatchObject({ alive: true, providerId: 'opencode', runtimeModel: 'opencode/big-pickle', }); expect(runtimeSnapshot.members.bob).toMatchObject({ alive: true, providerId: 'opencode', runtimeModel: 'opencode/big-pickle', }); await expect( fs.readFile(path.join(getTeamsBasePath(), 'pure-opencode-safe-e2e', 'launch-state.json'), { encoding: 'utf8', }) ).resolves.toContain('"teamLaunchState": "clean_success"'); }); it('accepts pure OpenCode runtime bootstrap check-ins during adapter launch', async () => { const svc = new TeamProvisioningService(); const adapter = new BootstrapCheckingOpenCodeRuntimeAdapter(svc); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const { runId } = await svc.createTeam( { teamName: 'pure-opencode-bootstrap-during-launch-safe-e2e', cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); expect(runId).toBe(adapter.launchInputs[0]?.runId); expect(adapter.bootstrapCheckins).toEqual([ { memberName: 'alice', runId, state: 'accepted', }, ]); const statuses = await svc.getMemberSpawnStatuses( 'pure-opencode-bootstrap-during-launch-safe-e2e' ); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', bootstrapConfirmed: true, }); }); it('keeps failed OpenCode runtime adapter launches out of alive teams', async () => { const adapter = new FakeOpenCodeRuntimeAdapter('partial_failure'); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const progressEvents: TeamProvisioningProgress[] = []; await svc.createTeam( { teamName: 'failed-opencode-safe-e2e', cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, (progress) => progressEvents.push(progress) ); expect(progressEvents.at(-1)).toMatchObject({ state: 'failed', message: 'OpenCode team launch failed readiness gate', }); expect(svc.isTeamAlive('failed-opencode-safe-e2e')).toBe(false); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot('failed-opencode-safe-e2e'); expect(runtimeSnapshot.members.alice).toMatchObject({ alive: false, providerId: 'opencode', runtimeModel: 'opencode/big-pickle', }); }); it('launches an existing pure OpenCode team config through the runtime adapter', async () => { await writeOpenCodeTeamConfig({ teamName: 'existing-opencode-safe-e2e', projectPath, members: ['alice', 'bob'], }); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const progressEvents: TeamProvisioningProgress[] = []; const { runId } = await svc.launchTeam( { teamName: 'existing-opencode-safe-e2e', cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, }, (progress) => progressEvents.push(progress) ); expect(runId).toBe(adapter.launchInputs[0]?.runId); expect(adapter.launchInputs[0]?.expectedMembers.map((member) => member.name)).toEqual([ 'alice', 'bob', ]); expect(progressEvents.at(-1)).toMatchObject({ state: 'ready', message: 'OpenCode team launch is ready', }); const statuses = await svc.getMemberSpawnStatuses('existing-opencode-safe-e2e'); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); }); it('materializes members metadata before relaunching a legacy pure team config', async () => { const teamName = 'legacy-pure-config-repair-safe-e2e'; await writePureAnthropicTeamConfigWithMembers({ teamName, projectPath, members: ['alice', 'bob'], }); const svc = new TeamProvisioningService(); vi.spyOn(svc as any, 'normalizeTeamConfigForLaunch').mockImplementation(async () => { throw new Error('stop after compatibility repair'); }); await expect( svc.launchTeam( { teamName, cwd: projectPath, providerId: 'anthropic', model: 'sonnet', skipPermissions: true, }, () => undefined ) ).rejects.toThrow('stop after compatibility repair'); const membersMeta = JSON.parse( await fs.readFile(path.join(getTeamsBasePath(), teamName, 'members.meta.json'), 'utf8') ) as { members: Array<{ name: string; providerId?: string; model?: string }> }; expect(membersMeta.members).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'alice', providerId: 'anthropic', model: 'haiku' }), expect.objectContaining({ name: 'bob', providerId: 'anthropic', model: 'sonnet' }), ]) ); }); it('fails unsafe old mixed OpenCode config without launch-state or members metadata mutation', async () => { const teamName = 'legacy-mixed-config-unsafe-safe-e2e'; await writeMixedTeamConfigWithoutOpenCodeProviderMetadata({ teamName, projectPath }); const svc = new TeamProvisioningService(); const normalizeSpy = vi.spyOn(svc as any, 'normalizeTeamConfigForLaunch'); await expect( svc.launchTeam( { teamName, cwd: projectPath, providerId: 'codex', providerBackendId: 'codex-native', model: 'gpt-5.4', skipPermissions: true, }, () => undefined ) ).rejects.toThrow(getMixedLaunchFallbackRecoveryError()); expect(normalizeSpy).not.toHaveBeenCalled(); await expect( fs.readFile(path.join(getTeamsBasePath(), teamName, 'members.meta.json'), 'utf8') ).rejects.toThrow(); await expect( fs.readFile(path.join(getTeamsBasePath(), teamName, 'launch-state.json'), 'utf8') ).rejects.toThrow(); }); it('keeps permission-pending OpenCode members pending instead of reading the team as fully ready', async () => { const adapter = new FakeOpenCodeRuntimeAdapter('partial_pending'); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const progressEvents: TeamProvisioningProgress[] = []; await svc.createTeam( { teamName: 'permission-opencode-safe-e2e', cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: false, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, (progress) => progressEvents.push(progress) ); expect(progressEvents.at(-1)).toMatchObject({ state: 'ready', message: 'OpenCode team launch is waiting for runtime evidence or permissions', messageSeverity: 'warning', }); expect(svc.isTeamAlive('permission-opencode-safe-e2e')).toBe(true); const statuses = await svc.getMemberSpawnStatuses('permission-opencode-safe-e2e'); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.alice).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_permission', runtimeAlive: false, pendingPermissionRequestIds: ['perm-alice'], }); expect(statuses.summary?.pendingCount).toBe(1); }); it('blocks createTeam at workspace trust preflight before spawn and preserves existing launch state', async () => { forceWorkspaceTrustPreflightEnv(); process.env.CLAUDE_CLI_PATH = await writeFakeClaudeCli(tempDir); ClaudeBinaryResolver.clearCache(); const teamName = 'workspace-trust-create-blocked-safe-e2e'; const staleLaunchStatePath = path.join(getTeamsBasePath(), teamName, 'launch-state.json'); const staleLaunchState = { version: 2, teamName, updatedAt: '2026-05-13T00:00:00.000Z', leadSessionId: 'previous-lead-session', launchPhase: 'finished', expectedMembers: ['alice'], bootstrapExpectedMembers: ['alice'], members: {}, summary: { confirmedCount: 1, pendingCount: 0, failedCount: 0, runtimeAlivePendingCount: 0, shellOnlyPendingCount: 0, runtimeProcessPendingCount: 0, runtimeCandidatePendingCount: 0, noRuntimePendingCount: 0, permissionPendingCount: 0, }, teamLaunchState: 'clean_success', }; await writeJsonFile(staleLaunchStatePath, staleLaunchState); const errorMessage = `Claude workspace trust was not confirmed for ${projectPath}`; const { coordinator, execute, planFull } = createBlockedWorkspaceTrustCoordinator({ errorMessage, rawTail: 'Unexpected Claude startup screen', }); const svc = new TeamProvisioningService(); svc.setWorkspaceTrustCoordinator(coordinator); const progressEvents: TeamProvisioningProgress[] = []; await expect( svc.createTeam( { teamName, cwd: projectPath, providerId: 'anthropic', model: 'sonnet', skipPermissions: true, members: [{ name: 'alice', role: 'Reviewer', providerId: 'anthropic', model: 'haiku' }], }, (progress) => progressEvents.push(progress) ) ).rejects.toThrow(errorMessage); expect(progressEvents.map((progress) => progress.message)).toContain( 'Preparing workspace trust' ); expect(progressEvents.at(-1)).toMatchObject({ state: 'failed', message: 'Workspace trust required', error: errorMessage, }); expect(progressEvents.at(-1)?.launchDiagnostics).toEqual([ expect.objectContaining({ severity: 'error', code: 'workspace_trust_preflight', label: 'Workspace trust preflight blocked launch', detail: errorMessage, }), ]); expect(planFull).toHaveBeenCalledTimes(1); expect(execute).toHaveBeenCalledTimes(1); const executePlan = execute.mock.calls[0]?.[0]; expect(executePlan).toBeDefined(); expect(executePlan?.workspaces.map((workspace) => workspace.cwd)).toContain(projectPath); await expect(fs.readFile(staleLaunchStatePath, 'utf8')).resolves.toBe( `${JSON.stringify(staleLaunchState, null, 2)}\n` ); await expect( fs.access(path.join(getTeamsBasePath(), teamName, 'config.json')) ).rejects.toThrow(); const manifest = await readLatestLaunchFailureManifest(teamName); expect(manifest).toMatchObject({ reason: 'launch_progress_failed', classification: { code: 'workspace_trust_required' }, progress: { state: 'failed', message: 'Workspace trust required', error: errorMessage, }, flags: { isLaunch: false, workspaceTrustPreflight: { strategyResults: [ expect.objectContaining({ status: 'blocked', errorCode: 'workspace_trust_preflight_not_confirmed', errorMessage, rawTail: 'Unexpected Claude startup screen', }), ], }, }, }); expect(manifest.launchDiagnostics).toEqual([ expect.objectContaining({ code: 'workspace_trust_preflight', severity: 'error', detail: errorMessage, }), ]); }); it('blocks launchTeam at workspace trust preflight and restores the prelaunch config backup', async () => { forceWorkspaceTrustPreflightEnv(); process.env.CLAUDE_CLI_PATH = await writeFakeClaudeCli(tempDir); ClaudeBinaryResolver.clearCache(); const teamName = 'workspace-trust-launch-blocked-safe-e2e'; const originalProjectPath = path.join(tempDir, 'original-project'); const nextProjectPath = path.join(tempDir, 'next-project'); await fs.mkdir(originalProjectPath, { recursive: true }); await fs.mkdir(nextProjectPath, { recursive: true }); const originalConfig = await writeAnthropicTeamConfig({ teamName, projectPath: originalProjectPath, members: ['alice', 'bob'], }); const errorMessage = `Claude workspace trust was not confirmed for ${nextProjectPath}`; const { coordinator, execute } = createBlockedWorkspaceTrustCoordinator({ errorMessage, evidence: ['workspace trust preflight blocked launch'], }); const svc = new TeamProvisioningService(); svc.setWorkspaceTrustCoordinator(coordinator); const progressEvents: TeamProvisioningProgress[] = []; await expect( svc.launchTeam( { teamName, cwd: nextProjectPath, providerId: 'anthropic', model: 'sonnet', skipPermissions: true, }, (progress) => progressEvents.push(progress) ) ).rejects.toThrow(errorMessage); expect(execute).toHaveBeenCalledTimes(1); expect(progressEvents.at(-1)).toMatchObject({ state: 'failed', message: 'Workspace trust required', error: errorMessage, }); await expect( fs.readFile(path.join(getTeamsBasePath(), teamName, 'config.json'), 'utf8') ).resolves.toBe(originalConfig); const manifest = await readLatestLaunchFailureManifest(teamName); expect(manifest).toMatchObject({ classification: { code: 'workspace_trust_required' }, flags: { isLaunch: true, workspaceTrustPreflight: { strategyResults: [ expect.objectContaining({ status: 'blocked', errorMessage, }), ], }, }, }); expect(manifest.progress.launchDiagnostics).toEqual([ expect.objectContaining({ code: 'workspace_trust_preflight', severity: 'error', detail: errorMessage, }), ]); }); it('preserves mixed OpenCode per-member outcomes after a partial runtime adapter launch', async () => { const adapter = new FakeOpenCodeRuntimeAdapter('partial_failure', { alice: 'confirmed', bob: 'permission', tom: 'failed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await svc.createTeam( { teamName: 'mixed-opencode-safe-e2e', cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: false, members: [ { name: 'alice', role: 'Developer', providerId: 'opencode' }, { name: 'bob', role: 'Reviewer', providerId: 'opencode' }, { name: 'tom', role: 'Developer', providerId: 'opencode' }, ], }, () => undefined ); expect(svc.isTeamAlive('mixed-opencode-safe-e2e')).toBe(false); const statuses = await svc.getMemberSpawnStatuses('mixed-opencode-safe-e2e'); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', runtimeAlive: true, bootstrapConfirmed: true, }); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_permission', runtimeAlive: false, pendingPermissionRequestIds: ['perm-bob'], }); expect(statuses.statuses.tom).toMatchObject({ status: 'error', launchState: 'failed_to_start', runtimeAlive: false, hardFailure: true, hardFailureReason: 'fake_open_code_launch_failure', }); expect(statuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 1, failedCount: 1, }); }); it('stops a pure OpenCode runtime adapter team and clears alive tracking', async () => { const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await svc.createTeam( { teamName: 'stoppable-opencode-safe-e2e', cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); expect(svc.isTeamAlive('stoppable-opencode-safe-e2e')).toBe(true); svc.stopTeam('stoppable-opencode-safe-e2e'); await waitForCondition(() => adapter.stopInputs.length === 1); await waitForCondition(() => !svc.isTeamAlive('stoppable-opencode-safe-e2e')); expect(adapter.stopInputs[0]).toMatchObject({ teamName: 'stoppable-opencode-safe-e2e', providerId: 'opencode', reason: 'user_requested', force: true, }); }); it('stops one pure OpenCode runtime adapter team without disconnecting another team', async () => { const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await svc.createTeam( { teamName: 'pure-opencode-stop-isolated-a-safe-e2e', cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await svc.createTeam( { teamName: 'pure-opencode-stop-isolated-b-safe-e2e', cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'bob', role: 'Reviewer', providerId: 'opencode' }], }, () => undefined ); expect(svc.isTeamAlive('pure-opencode-stop-isolated-a-safe-e2e')).toBe(true); expect(svc.isTeamAlive('pure-opencode-stop-isolated-b-safe-e2e')).toBe(true); svc.stopTeam('pure-opencode-stop-isolated-a-safe-e2e'); await waitForCondition(() => adapter.stopInputs.length === 1); await waitForCondition(() => !svc.isTeamAlive('pure-opencode-stop-isolated-a-safe-e2e')); expect(svc.isTeamAlive('pure-opencode-stop-isolated-b-safe-e2e')).toBe(true); expect(adapter.stopInputs[0]).toMatchObject({ teamName: 'pure-opencode-stop-isolated-a-safe-e2e', providerId: 'opencode', reason: 'user_requested', }); const survivingStatuses = await svc.getMemberSpawnStatuses( 'pure-opencode-stop-isolated-b-safe-e2e' ); expect(survivingStatuses.teamLaunchState).toBe('clean_success'); expect(survivingStatuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); const survivingSnapshot = await svc.getTeamAgentRuntimeSnapshot( 'pure-opencode-stop-isolated-b-safe-e2e' ); expect(survivingSnapshot.members.bob).toMatchObject({ alive: true, providerId: 'opencode', runtimeModel: 'opencode/big-pickle', }); }); it('lists only still-running OpenCode runtime adapter teams after one team stops', async () => { const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await svc.createTeam( { teamName: 'pure-opencode-alive-list-a-safe-e2e', cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await svc.createTeam( { teamName: 'pure-opencode-alive-list-b-safe-e2e', cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'bob', role: 'Reviewer', providerId: 'opencode' }], }, () => undefined ); expect(svc.getAliveTeams().sort()).toEqual([ 'pure-opencode-alive-list-a-safe-e2e', 'pure-opencode-alive-list-b-safe-e2e', ]); svc.stopTeam('pure-opencode-alive-list-a-safe-e2e'); await waitForCondition(() => !svc.isTeamAlive('pure-opencode-alive-list-a-safe-e2e')); expect(svc.getAliveTeams()).toEqual(['pure-opencode-alive-list-b-safe-e2e']); const statuses = await svc.getMemberSpawnStatuses('pure-opencode-alive-list-b-safe-e2e'); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('reports pure OpenCode runtime state as alive before stop and offline after stop', async () => { const teamName = 'pure-opencode-runtime-state-stop-safe-e2e'; const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const { runId } = await svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); const runningState = await svc.getRuntimeState(teamName); expect(runningState).toMatchObject({ teamName, isAlive: true, runId, progress: { state: 'ready', message: 'OpenCode team launch is ready', }, }); svc.stopTeam(teamName); await waitForCondition(() => !svc.isTeamAlive(teamName)); const stoppedState = await svc.getRuntimeState(teamName); expect(stoppedState).toMatchObject({ teamName, isAlive: false, }); }); it('stops the stale pure OpenCode primary runtime before same-team relaunch', async () => { const teamName = 'pure-opencode-relaunch-stops-stale-runtime-safe-e2e'; const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const first = await svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); expect(svc.isTeamAlive(teamName)).toBe(true); expect(adapter.stopInputs).toHaveLength(0); const firstSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(firstSnapshot).toMatchObject({ runId: first.runId, members: { alice: { alive: true, providerId: 'opencode', runtimeModel: 'opencode/big-pickle', }, }, }); const second = await svc.launchTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, }, () => undefined ); expect(second.runId).not.toBe(first.runId); expect(adapter.launchInputs.map((input) => input.runId)).toEqual([first.runId, second.runId]); expect(adapter.stopInputs).toHaveLength(1); expect(adapter.stopInputs[0]).toMatchObject({ runId: first.runId, teamName, laneId: 'primary', providerId: 'opencode', reason: 'user_requested', force: true, }); expect(svc.isTeamAlive(teamName)).toBe(true); const snapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(snapshot).toMatchObject({ runId: second.runId, members: { alice: { alive: true, providerId: 'opencode', runtimeModel: 'opencode/big-pickle', }, }, }); svc.stopTeam(teamName); await waitForCondition(() => adapter.stopInputs.length === 2); expect(adapter.stopInputs[1]).toMatchObject({ runId: second.runId, teamName, laneId: 'primary', providerId: 'opencode', reason: 'user_requested', force: true, }); await waitForCondition(() => !svc.isTeamAlive(teamName)); }); it('relaunches one pure OpenCode team without stopping another live OpenCode team', async () => { const relaunchTeamName = 'pure-opencode-relaunch-isolated-a-safe-e2e'; const survivingTeamName = 'pure-opencode-relaunch-isolated-b-safe-e2e'; const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const relaunchFirst = await svc.createTeam( { teamName: relaunchTeamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); const surviving = await svc.createTeam( { teamName: survivingTeamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'bob', role: 'Reviewer', providerId: 'opencode' }], }, () => undefined ); const relaunchSecond = await svc.launchTeam( { teamName: relaunchTeamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, }, () => undefined ); expect(adapter.stopInputs).toHaveLength(1); expect(adapter.stopInputs[0]).toMatchObject({ runId: relaunchFirst.runId, teamName: relaunchTeamName, laneId: 'primary', providerId: 'opencode', }); expect(svc.getAliveTeams().sort()).toEqual([relaunchTeamName, survivingTeamName].sort()); const relaunchedSnapshot = await svc.getTeamAgentRuntimeSnapshot(relaunchTeamName); expect(relaunchedSnapshot).toMatchObject({ runId: relaunchSecond.runId, members: { alice: { alive: true, providerId: 'opencode', runtimeModel: 'opencode/big-pickle', }, }, }); const survivingSnapshot = await svc.getTeamAgentRuntimeSnapshot(survivingTeamName); expect(survivingSnapshot).toMatchObject({ runId: surviving.runId, members: { bob: { alive: true, providerId: 'opencode', runtimeModel: 'opencode/big-pickle', }, }, }); }); it('serializes same-team pure OpenCode relaunch behind an in-flight launch before replacing the current run', async () => { const teamName = 'pure-opencode-relaunch-queued-safe-e2e'; const adapter = new BlockingOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const firstPromise = svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); const firstRunId = adapter.pendingLaunchInputs[0]?.runId; expect(firstRunId).toBeTruthy(); const secondPromise = svc.launchTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, }, () => undefined ); await Promise.resolve(); expect(adapter.pendingLaunchInputs).toHaveLength(1); expect(adapter.stopInputs).toHaveLength(0); adapter.releaseLaunches(); await expect(firstPromise).resolves.toEqual({ runId: firstRunId }); const second = await secondPromise; const secondRunId = second.runId; expect(secondRunId).toBeTruthy(); expect(secondRunId).not.toBe(firstRunId); expect(svc.isTeamAlive(teamName)).toBe(true); expect(svc.getAliveTeams()).toEqual([teamName]); expect(adapter.launchInputs.map((input) => input.runId)).toEqual([firstRunId, secondRunId]); expect(adapter.stopInputs).toHaveLength(1); expect(adapter.stopInputs[0]).toMatchObject({ runId: firstRunId, teamName, laneId: 'primary', providerId: 'opencode', reason: 'user_requested', force: true, }); const snapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(snapshot).toMatchObject({ runId: secondRunId, members: { alice: { alive: true, providerId: 'opencode', runtimeModel: 'opencode/big-pickle', }, }, }); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { primary: { state: 'active' }, }, } ); }); it('keeps relaunch waiting while the previous same-team OpenCode runtime stop is slow', async () => { const teamName = 'pure-opencode-relaunch-slow-stop-safe-e2e'; const adapter = new BlockingStopOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const firstPromise = svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); const firstRunId = adapter.pendingLaunchInputs[0]?.runId; expect(firstRunId).toBeTruthy(); adapter.releaseLaunches(); await expect(firstPromise).resolves.toEqual({ runId: firstRunId }); await waitForCondition(() => adapter.launchInputs.length === 1); expect(svc.isTeamAlive(teamName)).toBe(true); const secondPromise = svc.launchTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, }, () => undefined ); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs[0]).toMatchObject({ runId: firstRunId, teamName, laneId: 'primary', cwd: projectPath, reason: 'user_requested', force: true, }); await expect(svc.getProvisioningStatus(firstRunId!)).resolves.toMatchObject({ runId: firstRunId, teamName, state: 'disconnected', message: 'Stopping OpenCode team through runtime adapter', }); expect(adapter.pendingLaunchInputs).toHaveLength(1); expect(adapter.launchInputs).toHaveLength(1); expect(svc.getAliveTeams()).toEqual([]); adapter.releaseStops(); const second = await secondPromise; expect(second.runId).toBeTruthy(); expect(second.runId).not.toBe(firstRunId); await waitForCondition(() => adapter.launchInputs.length === 2); expect(svc.isTeamAlive(teamName)).toBe(true); expect(svc.getAliveTeams()).toEqual([teamName]); await expect(svc.getProvisioningStatus(second.runId)).resolves.toMatchObject({ runId: second.runId, teamName, state: 'ready', message: 'OpenCode team launch is ready', }); const snapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(snapshot).toMatchObject({ runId: second.runId, members: { alice: { alive: true, providerId: 'opencode', runtimeModel: 'opencode/big-pickle', }, }, }); }); it('serializes manual stop and same-team OpenCode relaunch behind a slow runtime stop', async () => { const teamName = 'pure-opencode-stop-then-relaunch-slow-stop-safe-e2e'; const adapter = new BlockingStopOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const firstPromise = svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); const firstRunId = adapter.pendingLaunchInputs[0]?.runId; expect(firstRunId).toBeTruthy(); adapter.releaseLaunches(); await expect(firstPromise).resolves.toEqual({ runId: firstRunId }); await waitForCondition(() => adapter.launchInputs.length === 1); expect(svc.getAliveTeams()).toEqual([teamName]); svc.stopTeam(teamName); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs[0]).toMatchObject({ runId: firstRunId, teamName, laneId: 'primary', cwd: projectPath, reason: 'user_requested', force: true, }); expect(svc.getAliveTeams()).toEqual([]); const secondPromise = svc.launchTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, }, () => undefined ); await Promise.resolve(); expect(adapter.pendingLaunchInputs).toHaveLength(1); expect(adapter.launchInputs).toHaveLength(1); adapter.releaseStops(); const second = await secondPromise; expect(second.runId).toBeTruthy(); expect(second.runId).not.toBe(firstRunId); await waitForCondition(() => adapter.launchInputs.length === 2); expect(svc.getAliveTeams()).toEqual([teamName]); await expect(svc.getProvisioningStatus(second.runId)).resolves.toMatchObject({ runId: second.runId, teamName, state: 'ready', message: 'OpenCode team launch is ready', }); }); it('keeps slow OpenCode stop scoped to one team while another team relaunches', async () => { const stoppingTeamName = 'pure-opencode-cross-team-slow-stop-a-safe-e2e'; const relaunchTeamName = 'pure-opencode-cross-team-slow-stop-b-safe-e2e'; const adapter = new BlockingStopOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const stoppingCreate = svc.createTeam( { teamName: stoppingTeamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); adapter.releaseLaunches(); const stopping = await stoppingCreate; const relaunchFirst = await svc.createTeam( { teamName: relaunchTeamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await waitForCondition(() => adapter.launchInputs.length === 2); expect(svc.getAliveTeams().sort()).toEqual([relaunchTeamName, stoppingTeamName].sort()); svc.stopTeam(stoppingTeamName); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs[0]).toMatchObject({ runId: stopping.runId, teamName: stoppingTeamName, laneId: 'primary', reason: 'user_requested', }); expect(svc.isTeamAlive(stoppingTeamName)).toBe(false); expect(svc.isTeamAlive(relaunchTeamName)).toBe(true); const relaunchSecondPromise = svc.launchTeam( { teamName: relaunchTeamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, }, () => undefined ); await waitForCondition(() => adapter.stopInputs.length === 2); expect(adapter.stopInputs[1]).toMatchObject({ runId: relaunchFirst.runId, teamName: relaunchTeamName, laneId: 'primary', reason: 'user_requested', }); expect(adapter.launchInputs).toHaveLength(2); adapter.releaseStops(); const relaunchSecond = await relaunchSecondPromise; await waitForCondition(() => adapter.launchInputs.length === 3); expect(relaunchSecond.runId).not.toBe(relaunchFirst.runId); expect(svc.isTeamAlive(stoppingTeamName)).toBe(false); expect(svc.isTeamAlive(relaunchTeamName)).toBe(true); }); it('dedupes duplicate manual OpenCode stops while the runtime stop is still pending', async () => { const teamName = 'pure-opencode-duplicate-stop-slow-stop-safe-e2e'; const adapter = new BlockingStopOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const createPromise = svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); const runId = adapter.pendingLaunchInputs[0]?.runId; expect(runId).toBeTruthy(); adapter.releaseLaunches(); await expect(createPromise).resolves.toEqual({ runId }); await waitForCondition(() => adapter.launchInputs.length === 1); svc.stopTeam(teamName); svc.stopTeam(teamName); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs[0]).toMatchObject({ runId, teamName, laneId: 'primary', reason: 'user_requested', }); expect(svc.getAliveTeams()).toEqual([]); await Promise.resolve(); expect(adapter.stopInputs).toHaveLength(1); adapter.releaseStops(); await waitForCondition(() => { const status = (svc as any).runtimeAdapterProgressByRunId.get(runId); return status?.state === 'disconnected' && status.message === 'OpenCode team stopped'; }); expect(adapter.stopInputs).toHaveLength(1); expect(svc.isTeamAlive(teamName)).toBe(false); }); it('does not resurrect a same-team OpenCode relaunch after stopAllTeams during slow replacement stop', async () => { const teamName = 'pure-opencode-relaunch-stop-all-during-slow-stop-safe-e2e'; const adapter = new BlockingStopOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const firstPromise = svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); const firstRunId = adapter.pendingLaunchInputs[0]?.runId; expect(firstRunId).toBeTruthy(); adapter.releaseLaunches(); await expect(firstPromise).resolves.toEqual({ runId: firstRunId }); await waitForCondition(() => adapter.launchInputs.length === 1); const relaunchPromise = svc.launchTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, }, () => undefined ); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs[0]).toMatchObject({ runId: firstRunId, teamName, laneId: 'primary', reason: 'user_requested', }); expect(adapter.launchInputs).toHaveLength(1); svc.stopAllTeams(); adapter.releaseStops(); const relaunch = await relaunchPromise; expect(relaunch.runId).toBeTruthy(); expect(relaunch.runId).not.toBe(firstRunId); expect(adapter.launchInputs).toHaveLength(1); await expect(svc.getProvisioningStatus(relaunch.runId)).resolves.toMatchObject({ runId: relaunch.runId, teamName, state: 'cancelled', message: 'Provisioning cancelled by user', }); expect(svc.isTeamAlive(teamName)).toBe(false); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: {}, } ); }); it('allows a fresh OpenCode launch after stopAllTeams cancelled a queued same-team relaunch', async () => { const teamName = 'pure-opencode-launch-after-stop-all-cancelled-relaunch-safe-e2e'; const adapter = new BlockingStopOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const firstPromise = svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); const firstRunId = adapter.pendingLaunchInputs[0]?.runId; expect(firstRunId).toBeTruthy(); adapter.releaseLaunches(); await expect(firstPromise).resolves.toEqual({ runId: firstRunId }); await waitForCondition(() => adapter.launchInputs.length === 1); const cancelledRelaunchPromise = svc.launchTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, }, () => undefined ); await waitForCondition(() => adapter.stopInputs.length === 1); svc.stopAllTeams(); adapter.releaseStops(); const cancelledRelaunch = await cancelledRelaunchPromise; expect(adapter.launchInputs).toHaveLength(1); await expect(svc.getProvisioningStatus(cancelledRelaunch.runId)).resolves.toMatchObject({ runId: cancelledRelaunch.runId, teamName, state: 'cancelled', }); expect(svc.isTeamAlive(teamName)).toBe(false); const freshLaunch = await svc.launchTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, }, () => undefined ); expect(freshLaunch.runId).toBeTruthy(); expect(freshLaunch.runId).not.toBe(firstRunId); expect(freshLaunch.runId).not.toBe(cancelledRelaunch.runId); await waitForCondition(() => adapter.launchInputs.length === 2); expect(adapter.launchInputs[1]).toMatchObject({ runId: freshLaunch.runId, teamName, providerId: 'opencode', cwd: projectPath, }); await expect(svc.getProvisioningStatus(freshLaunch.runId)).resolves.toMatchObject({ runId: freshLaunch.runId, teamName, state: 'ready', message: 'OpenCode team launch is ready', }); expect(svc.isTeamAlive(teamName)).toBe(true); const snapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(snapshot).toMatchObject({ runId: freshLaunch.runId, members: { alice: { alive: true, providerId: 'opencode', runtimeModel: 'opencode/big-pickle', }, }, }); }); it('stopAllTeams does not double-stop an already stopping OpenCode team and still stops live siblings', async () => { const stoppingTeamName = 'pure-opencode-stop-all-already-stopping-a-safe-e2e'; const liveTeamName = 'pure-opencode-stop-all-already-stopping-b-safe-e2e'; const adapter = new BlockingStopOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const stoppingCreate = svc.createTeam( { teamName: stoppingTeamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); const stoppingRunId = adapter.pendingLaunchInputs[0]?.runId; expect(stoppingRunId).toBeTruthy(); adapter.releaseLaunches(); await expect(stoppingCreate).resolves.toEqual({ runId: stoppingRunId }); const live = await svc.createTeam( { teamName: liveTeamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'bob', role: 'Reviewer', providerId: 'opencode' }], }, () => undefined ); await waitForCondition(() => adapter.launchInputs.length === 2); expect(svc.getAliveTeams().sort()).toEqual([liveTeamName, stoppingTeamName].sort()); svc.stopTeam(stoppingTeamName); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs[0]).toMatchObject({ runId: stoppingRunId, teamName: stoppingTeamName, laneId: 'primary', reason: 'user_requested', }); expect(svc.isTeamAlive(stoppingTeamName)).toBe(false); expect(svc.isTeamAlive(liveTeamName)).toBe(true); svc.stopAllTeams(); await waitForCondition(() => adapter.stopInputs.length === 2); expect(adapter.stopInputs).toEqual( expect.arrayContaining([ expect.objectContaining({ runId: stoppingRunId, teamName: stoppingTeamName, laneId: 'primary', reason: 'user_requested', }), expect.objectContaining({ runId: live.runId, teamName: liveTeamName, laneId: 'primary', reason: 'user_requested', }), ]) ); expect(adapter.stopInputs.filter((input) => input.teamName === stoppingTeamName)).toHaveLength( 1 ); expect(svc.getAliveTeams()).toEqual([]); adapter.releaseStops(); await waitForCondition(() => !svc.isTeamAlive(liveTeamName)); expect(svc.isTeamAlive(stoppingTeamName)).toBe(false); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), stoppingTeamName)).resolves .toMatchObject({ lanes: {}, }); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), liveTeamName)).resolves .toMatchObject({ lanes: {}, }); }); it('cancels an in-flight pure OpenCode runtime adapter launch without letting late success resurrect it', async () => { const teamName = 'pure-opencode-cancel-inflight-runtime-safe-e2e'; const adapter = new BlockingOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const createPromise = svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); const runId = adapter.pendingLaunchInputs[0]?.runId; expect(runId).toBeTruthy(); await svc.cancelProvisioning(runId!); expect(adapter.stopInputs).toHaveLength(1); expect(adapter.stopInputs[0]).toMatchObject({ runId, teamName, laneId: 'primary', providerId: 'opencode', reason: 'user_requested', force: true, }); await expect(svc.getProvisioningStatus(runId!)).resolves.toMatchObject({ runId, teamName, state: 'cancelled', message: 'Provisioning cancelled by user', }); expect(svc.isTeamAlive(teamName)).toBe(false); adapter.releaseLaunches(); await expect(createPromise).resolves.toEqual({ runId }); await waitForCondition(() => adapter.launchInputs.length === 1); expect(svc.isTeamAlive(teamName)).toBe(false); expect(svc.getAliveTeams()).not.toContain(teamName); const state = await svc.getRuntimeState(teamName); expect(state).toMatchObject({ teamName, isAlive: false, runId: null, }); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).not.toBe('clean_success'); }); it('shows cancelled status immediately when manual cancel waits on a slow OpenCode stop', async () => { const teamName = 'pure-opencode-cancel-slow-stop-status-safe-e2e'; const adapter = new BlockingStopOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const createPromise = svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); const runId = adapter.pendingLaunchInputs[0]?.runId; expect(runId).toBeTruthy(); const cancelPromise = svc.cancelProvisioning(runId!); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs[0]).toMatchObject({ runId, teamName, laneId: 'primary', cwd: projectPath, reason: 'user_requested', force: true, }); await expect(svc.getProvisioningStatus(runId!)).resolves.toMatchObject({ runId, teamName, state: 'cancelled', message: 'Provisioning cancelled by user', }); expect(svc.isTeamAlive(teamName)).toBe(false); adapter.releaseLaunches(); await expect(createPromise).resolves.toEqual({ runId }); await waitForCondition(() => adapter.launchInputs.length === 1); await expect(svc.getProvisioningStatus(runId!)).resolves.toMatchObject({ runId, teamName, state: 'cancelled', }); adapter.releaseStops(); await expect(cancelPromise).resolves.toBeUndefined(); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: {}, } ); expect(svc.getAliveTeams()).not.toContain(teamName); }); it('cancels one in-flight pure OpenCode launch without cancelling another OpenCode team', async () => { const cancelledTeamName = 'pure-opencode-cancel-inflight-isolated-a-safe-e2e'; const survivingTeamName = 'pure-opencode-cancel-inflight-isolated-b-safe-e2e'; const adapter = new BlockingOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const cancelledPromise = svc.createTeam( { teamName: cancelledTeamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); const survivingPromise = svc.createTeam( { teamName: survivingTeamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'bob', role: 'Reviewer', providerId: 'opencode' }], }, () => undefined ); await waitForCondition(() => adapter.pendingLaunchInputs.length === 2); const cancelledRunId = adapter.pendingLaunchInputs.find( (input) => input.teamName === cancelledTeamName )?.runId; const survivingRunId = adapter.pendingLaunchInputs.find( (input) => input.teamName === survivingTeamName )?.runId; expect(cancelledRunId).toBeTruthy(); expect(survivingRunId).toBeTruthy(); await svc.cancelProvisioning(cancelledRunId!); expect(adapter.stopInputs).toHaveLength(1); expect(adapter.stopInputs[0]).toMatchObject({ runId: cancelledRunId, teamName: cancelledTeamName, laneId: 'primary', providerId: 'opencode', reason: 'user_requested', force: true, }); expect(svc.isTeamAlive(cancelledTeamName)).toBe(false); expect(svc.isTeamAlive(survivingTeamName)).toBe(false); adapter.releaseLaunches(); await expect(cancelledPromise).resolves.toEqual({ runId: cancelledRunId }); await expect(survivingPromise).resolves.toEqual({ runId: survivingRunId }); await waitForCondition(() => adapter.launchInputs.length === 2); expect(svc.isTeamAlive(cancelledTeamName)).toBe(false); expect(svc.isTeamAlive(survivingTeamName)).toBe(true); expect(svc.getAliveTeams()).toEqual([survivingTeamName]); await expect(svc.getProvisioningStatus(cancelledRunId!)).resolves.toMatchObject({ runId: cancelledRunId, teamName: cancelledTeamName, state: 'cancelled', }); const survivingState = await svc.getRuntimeState(survivingTeamName); expect(survivingState).toMatchObject({ teamName: survivingTeamName, isAlive: true, runId: survivingRunId, progress: { state: 'ready', message: 'OpenCode team launch is ready', }, }); const cancelledStatuses = await svc.getMemberSpawnStatuses(cancelledTeamName); expect(cancelledStatuses.teamLaunchState).not.toBe('clean_success'); const survivingStatuses = await svc.getMemberSpawnStatuses(survivingTeamName); expect(survivingStatuses.teamLaunchState).toBe('clean_success'); expect(survivingStatuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('manual cancel with slow OpenCode stop stays scoped while another in-flight team succeeds', async () => { const cancelledTeamName = 'pure-opencode-cancel-slow-stop-isolated-a-safe-e2e'; const survivingTeamName = 'pure-opencode-cancel-slow-stop-isolated-b-safe-e2e'; const adapter = new BlockingStopOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const cancelledPromise = svc.createTeam( { teamName: cancelledTeamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); const survivingPromise = svc.createTeam( { teamName: survivingTeamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'bob', role: 'Reviewer', providerId: 'opencode' }], }, () => undefined ); await waitForCondition(() => adapter.pendingLaunchInputs.length === 2); const cancelledRunId = adapter.pendingLaunchInputs.find( (input) => input.teamName === cancelledTeamName )?.runId; const survivingRunId = adapter.pendingLaunchInputs.find( (input) => input.teamName === survivingTeamName )?.runId; expect(cancelledRunId).toBeTruthy(); expect(survivingRunId).toBeTruthy(); const cancelPromise = svc.cancelProvisioning(cancelledRunId!); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs[0]).toMatchObject({ runId: cancelledRunId, teamName: cancelledTeamName, laneId: 'primary', cwd: projectPath, reason: 'user_requested', force: true, }); await expect(svc.getProvisioningStatus(cancelledRunId!)).resolves.toMatchObject({ runId: cancelledRunId, teamName: cancelledTeamName, state: 'cancelled', }); await expect(svc.getProvisioningStatus(survivingRunId!)).resolves.toMatchObject({ runId: survivingRunId, teamName: survivingTeamName, state: 'spawning', }); expect(svc.getAliveTeams()).toEqual([]); adapter.releaseLaunches(); await expect(cancelledPromise).resolves.toEqual({ runId: cancelledRunId }); await expect(survivingPromise).resolves.toEqual({ runId: survivingRunId }); await waitForCondition(() => adapter.launchInputs.length === 2); expect(svc.isTeamAlive(cancelledTeamName)).toBe(false); expect(svc.isTeamAlive(survivingTeamName)).toBe(true); expect(svc.getAliveTeams()).toEqual([survivingTeamName]); await expect(svc.getProvisioningStatus(cancelledRunId!)).resolves.toMatchObject({ state: 'cancelled', }); await expect(svc.getProvisioningStatus(survivingRunId!)).resolves.toMatchObject({ state: 'ready', message: 'OpenCode team launch is ready', }); adapter.releaseStops(); await expect(cancelPromise).resolves.toBeUndefined(); const cancelledStatuses = await svc.getMemberSpawnStatuses(cancelledTeamName); expect(cancelledStatuses.teamLaunchState).not.toBe('clean_success'); const survivingStatuses = await svc.getMemberSpawnStatuses(survivingTeamName); expect(survivingStatuses.teamLaunchState).toBe('clean_success'); expect(survivingStatuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('rejects cancel for a ready pure OpenCode runtime adapter team without stopping it', async () => { const teamName = 'pure-opencode-cancel-ready-reject-safe-e2e'; const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const { runId } = await svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await expect(svc.cancelProvisioning(runId)).rejects.toThrow( 'Provisioning cannot be cancelled in current state' ); expect(adapter.stopInputs).toEqual([]); expect(svc.isTeamAlive(teamName)).toBe(true); const state = await svc.getRuntimeState(teamName); expect(state).toMatchObject({ teamName, isAlive: true, runId, progress: { state: 'ready', message: 'OpenCode team launch is ready', }, }); }); it('does not stop a live OpenCode team when cancelling an unknown run id', async () => { const teamName = 'pure-opencode-cancel-unknown-run-safe-e2e'; const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await expect(svc.cancelProvisioning('missing-run-id')).rejects.toThrow('Unknown runId'); expect(adapter.stopInputs).toHaveLength(0); expect(svc.isTeamAlive(teamName)).toBe(true); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('stopAllTeams cancels an in-flight pure OpenCode runtime adapter launch without late success resurrecting it', async () => { const teamName = 'pure-opencode-stop-all-inflight-runtime-safe-e2e'; const adapter = new BlockingOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const createPromise = svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); const runId = adapter.pendingLaunchInputs[0]?.runId; expect(runId).toBeTruthy(); svc.stopAllTeams(); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs[0]).toMatchObject({ runId, teamName, laneId: 'primary', providerId: 'opencode', reason: 'user_requested', force: true, }); await expect(svc.getProvisioningStatus(runId!)).resolves.toMatchObject({ runId, teamName, state: 'cancelled', message: 'Provisioning cancelled by user', }); expect(svc.isTeamAlive(teamName)).toBe(false); adapter.releaseLaunches(); await expect(createPromise).resolves.toEqual({ runId }); await waitForCondition(() => adapter.launchInputs.length === 1); expect(svc.isTeamAlive(teamName)).toBe(false); expect(svc.getAliveTeams()).not.toContain(teamName); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: {}, } ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).not.toBe('clean_success'); expect(statuses.statuses.alice?.launchState).not.toBe('confirmed_alive'); }); it('allows a fresh OpenCode launch after stopAllTeams cancelled an in-flight create', async () => { const teamName = 'pure-opencode-launch-after-stop-all-cancelled-create-safe-e2e'; const adapter = new BlockingOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const createPromise = svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); const cancelledRunId = adapter.pendingLaunchInputs[0]?.runId; expect(cancelledRunId).toBeTruthy(); svc.stopAllTeams(); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs[0]).toMatchObject({ runId: cancelledRunId, teamName, laneId: 'primary', providerId: 'opencode', reason: 'user_requested', force: true, }); await expect(svc.getProvisioningStatus(cancelledRunId!)).resolves.toMatchObject({ runId: cancelledRunId, teamName, state: 'cancelled', }); adapter.releaseLaunches(); await expect(createPromise).resolves.toEqual({ runId: cancelledRunId }); await waitForCondition(() => adapter.launchInputs.length === 1); expect(svc.isTeamAlive(teamName)).toBe(false); const freshLaunch = await svc.launchTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, }, () => undefined ); expect(freshLaunch.runId).toBeTruthy(); expect(freshLaunch.runId).not.toBe(cancelledRunId); await waitForCondition(() => adapter.launchInputs.length === 2); expect(adapter.launchInputs[1]).toMatchObject({ runId: freshLaunch.runId, teamName, providerId: 'opencode', cwd: projectPath, }); await expect(svc.getProvisioningStatus(freshLaunch.runId)).resolves.toMatchObject({ runId: freshLaunch.runId, teamName, state: 'ready', message: 'OpenCode team launch is ready', }); expect(svc.isTeamAlive(teamName)).toBe(true); }); it('shows cancelled status immediately when stopAllTeams waits on a slow OpenCode stop', async () => { const teamName = 'pure-opencode-stop-all-slow-stop-status-safe-e2e'; const adapter = new BlockingStopOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const createPromise = svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); const runId = adapter.pendingLaunchInputs[0]?.runId; expect(runId).toBeTruthy(); svc.stopAllTeams(); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs[0]).toMatchObject({ runId, teamName, laneId: 'primary', cwd: projectPath, reason: 'user_requested', force: true, }); await expect(svc.getProvisioningStatus(runId!)).resolves.toMatchObject({ runId, teamName, state: 'cancelled', message: 'Provisioning cancelled by user', }); expect(svc.getAliveTeams()).not.toContain(teamName); adapter.releaseLaunches(); await expect(createPromise).resolves.toEqual({ runId }); await waitForCondition(() => adapter.launchInputs.length === 1); await expect(svc.getProvisioningStatus(runId!)).resolves.toMatchObject({ runId, teamName, state: 'cancelled', }); expect(svc.isTeamAlive(teamName)).toBe(false); adapter.releaseStops(); await waitForCondition(() => adapter.stopInputs.length === 1); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: {}, } ); }); it('shows cancelled status immediately for multiple teams when OpenCode stops are slow', async () => { const firstTeamName = 'pure-opencode-stop-all-slow-stop-multi-a-safe-e2e'; const secondTeamName = 'pure-opencode-stop-all-slow-stop-multi-b-safe-e2e'; const adapter = new BlockingStopOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const firstPromise = svc.createTeam( { teamName: firstTeamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); const secondPromise = svc.createTeam( { teamName: secondTeamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'bob', role: 'Reviewer', providerId: 'opencode' }], }, () => undefined ); await waitForCondition(() => adapter.pendingLaunchInputs.length === 2); const firstRunId = adapter.pendingLaunchInputs.find( (input) => input.teamName === firstTeamName )?.runId; const secondRunId = adapter.pendingLaunchInputs.find( (input) => input.teamName === secondTeamName )?.runId; expect(firstRunId).toBeTruthy(); expect(secondRunId).toBeTruthy(); svc.stopAllTeams(); await waitForCondition(() => adapter.stopInputs.length === 2); expect(adapter.stopInputs.map((input) => input.teamName).sort()).toEqual([ firstTeamName, secondTeamName, ]); expect(adapter.stopInputs).toEqual( expect.arrayContaining([ expect.objectContaining({ runId: firstRunId, teamName: firstTeamName, laneId: 'primary', cwd: projectPath, reason: 'user_requested', force: true, }), expect.objectContaining({ runId: secondRunId, teamName: secondTeamName, laneId: 'primary', cwd: projectPath, reason: 'user_requested', force: true, }), ]) ); await expect(svc.getProvisioningStatus(firstRunId!)).resolves.toMatchObject({ runId: firstRunId, teamName: firstTeamName, state: 'cancelled', }); await expect(svc.getProvisioningStatus(secondRunId!)).resolves.toMatchObject({ runId: secondRunId, teamName: secondTeamName, state: 'cancelled', }); expect(svc.getAliveTeams()).toEqual([]); adapter.releaseLaunches(); await expect(firstPromise).resolves.toEqual({ runId: firstRunId }); await expect(secondPromise).resolves.toEqual({ runId: secondRunId }); await waitForCondition(() => adapter.launchInputs.length === 2); await expect(svc.getProvisioningStatus(firstRunId!)).resolves.toMatchObject({ state: 'cancelled', }); await expect(svc.getProvisioningStatus(secondRunId!)).resolves.toMatchObject({ state: 'cancelled', }); expect(svc.getAliveTeams()).toEqual([]); adapter.releaseStops(); await waitForCondition(() => adapter.stopInputs.length === 2); await expect( readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), firstTeamName) ).resolves.toMatchObject({ lanes: {}, }); await expect( readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), secondTeamName) ).resolves.toMatchObject({ lanes: {}, }); }); it('stopAllTeams cancels multiple in-flight pure OpenCode launches without cross-team resurrection', async () => { const firstTeamName = 'pure-opencode-stop-all-inflight-multi-a-safe-e2e'; const secondTeamName = 'pure-opencode-stop-all-inflight-multi-b-safe-e2e'; const adapter = new BlockingOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const firstPromise = svc.createTeam( { teamName: firstTeamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); const secondPromise = svc.createTeam( { teamName: secondTeamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'bob', role: 'Reviewer', providerId: 'opencode' }], }, () => undefined ); await waitForCondition(() => adapter.pendingLaunchInputs.length === 2); const firstRunId = adapter.pendingLaunchInputs.find( (input) => input.teamName === firstTeamName )?.runId; const secondRunId = adapter.pendingLaunchInputs.find( (input) => input.teamName === secondTeamName )?.runId; expect(firstRunId).toBeTruthy(); expect(secondRunId).toBeTruthy(); svc.stopAllTeams(); await waitForCondition(() => adapter.stopInputs.length === 2); expect(adapter.stopInputs.map((input) => input.teamName).sort()).toEqual([ firstTeamName, secondTeamName, ]); expect(adapter.stopInputs.map((input) => input.laneId)).toEqual(['primary', 'primary']); await expect(svc.getProvisioningStatus(firstRunId!)).resolves.toMatchObject({ runId: firstRunId, teamName: firstTeamName, state: 'cancelled', }); await expect(svc.getProvisioningStatus(secondRunId!)).resolves.toMatchObject({ runId: secondRunId, teamName: secondTeamName, state: 'cancelled', }); adapter.releaseLaunches(); await expect(firstPromise).resolves.toEqual({ runId: firstRunId }); await expect(secondPromise).resolves.toEqual({ runId: secondRunId }); await waitForCondition(() => adapter.launchInputs.length === 2); expect(svc.getAliveTeams()).toEqual([]); await expect( readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), firstTeamName) ).resolves.toMatchObject({ lanes: {}, }); await expect( readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), secondTeamName) ).resolves.toMatchObject({ lanes: {}, }); }); it('stops pure OpenCode and mixed secondary runtime teams during stopAllTeams', async () => { const pureTeamName = 'pure-opencode-stop-all-safe-e2e'; const mixedTeamName = 'mixed-opencode-stop-all-safe-e2e'; await writeMixedTeamConfig({ teamName: mixedTeamName, projectPath }); await writeTeamMeta(mixedTeamName, projectPath); await writeMembersMeta(mixedTeamName); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await svc.createTeam( { teamName: pureTeamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); const mixedRun = createMixedLiveRun({ teamName: mixedTeamName, projectPath }); mixedRun.child = { kill: () => undefined }; trackLiveRun(svc, mixedRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(mixedRun); await waitForCondition(() => adapter.launchInputs.length === 3); await waitForCondition(() => mixedRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); expect(svc.getAliveTeams().sort()).toEqual([mixedTeamName, pureTeamName].sort()); svc.stopAllTeams(); await waitForCondition(() => adapter.stopInputs.length === 3); await waitForCondition(() => svc.getAliveTeams().length === 0); expect(adapter.stopInputs.map((input) => input.teamName).sort()).toEqual([ mixedTeamName, mixedTeamName, pureTeamName, ]); expect(adapter.stopInputs.map((input) => input.laneId).sort()).toEqual([ 'primary', 'secondary:opencode:bob', 'secondary:opencode:tom', ]); }); it('launches mixed OpenCode lanes after a fresh abandoned lane index lock', async () => { const teamName = 'mixed-opencode-abandoned-lane-lock-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); const lockPath = `${getOpenCodeRuntimeLaneIndexPath(getTeamsBasePath(), teamName)}.lock`; const abandonedPid = 424_242; await fs.mkdir(path.dirname(lockPath), { recursive: true }); await fs.writeFile(lockPath, `${abandonedPid}\n${Date.now()}\n`, 'utf8'); const killSpy = vi.spyOn(process, 'kill').mockImplementation(((pid: number | string) => { if (pid === abandonedPid) { const error = new Error('process is gone') as NodeJS.ErrnoException; error.code = 'ESRCH'; throw error; } return true; }) as typeof process.kill); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); run.child = { kill: () => undefined }; trackLiveRun(svc, run); try { await (svc as any).launchMixedSecondaryLaneIfNeeded(run); } finally { killSpy.mockRestore(); } await waitForCondition(() => adapter.launchInputs.length === 2); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { 'secondary:opencode:bob': { state: 'active' }, 'secondary:opencode:tom': { state: 'active' }, }, } ); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); expect(run.mixedSecondaryLanes.map((lane: { state: string }) => lane.state)).toEqual([ 'finished', 'finished', ]); }); it('stopAllTeams stops in-flight mixed OpenCode secondary lanes without late failure degrading launch state', async () => { const teamName = 'mixed-opencode-stop-all-inflight-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); const adapter = new RejectingBlockingOpenCodeRuntimeAdapter('late fake shutdown bridge failure'); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); run.child = { kill: () => undefined }; trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); svc.stopAllTeams(); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs.map((input) => input.laneId).sort()).toEqual([ 'secondary:opencode:bob', ]); expect(svc.isTeamAlive(teamName)).toBe(false); adapter.releaseLaunches(); await waitForCondition(() => adapter.rejectedLaunchCount === 1); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: {}, } ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).not.toBe('partial_failure'); expect(statuses.statuses.bob).toMatchObject({ hardFailure: false }); expect(statuses.statuses.bob?.launchState).not.toBe('failed_to_start'); expect(statuses.statuses.tom).toMatchObject({ hardFailure: false }); expect(statuses.statuses.tom?.launchState).not.toBe('failed_to_start'); }); it('allows fresh mixed OpenCode secondary lanes after stopAllTeams cancelled in-flight handoff', async () => { const teamName = 'mixed-opencode-fresh-after-stop-all-cancelled-handoff-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const cancelledRun = createMixedLiveRun({ teamName, projectPath }); cancelledRun.child = { kill: () => undefined }; trackLiveRun(svc, cancelledRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(cancelledRun); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); svc.stopAllTeams(); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs.map((input) => input.laneId).sort()).toEqual([ 'secondary:opencode:bob', ]); expect(svc.isTeamAlive(teamName)).toBe(false); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 1); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: {}, } ); const freshRun = createMixedLiveRun({ teamName, projectPath }); freshRun.runId = `${cancelledRun.runId}-fresh`; freshRun.detectedSessionId = 'lead-session-fresh'; freshRun.child = { kill: () => undefined }; trackLiveRun(svc, freshRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(freshRun); await waitForCondition(() => adapter.launchInputs.length === 3); await waitForCondition(() => freshRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); const snapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(snapshot).toMatchObject({ runId: freshRun.runId, members: { bob: { alive: true, providerId: 'opencode', laneKind: 'secondary', }, tom: { alive: true, providerId: 'opencode', laneKind: 'secondary', }, }, }); }, 120_000); it('stopAllTeams stops in-flight mixed OpenCode secondary lanes for multiple teams', async () => { const firstTeamName = 'mixed-opencode-stop-all-inflight-multi-a-safe-e2e'; const secondTeamName = 'mixed-opencode-stop-all-inflight-multi-b-safe-e2e'; await writeMixedTeamConfig({ teamName: firstTeamName, projectPath }); await writeTeamMeta(firstTeamName, projectPath); await writeMembersMeta(firstTeamName); await writeMixedTeamConfig({ teamName: secondTeamName, projectPath }); await writeTeamMeta(secondTeamName, projectPath); await writeMembersMeta(secondTeamName); const adapter = new RejectingBlockingOpenCodeRuntimeAdapter('late fake multi shutdown failure'); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const firstRun = createMixedLiveRun({ teamName: firstTeamName, projectPath }); const secondRun = createMixedLiveRun({ teamName: secondTeamName, projectPath }); firstRun.child = { kill: () => undefined }; secondRun.child = { kill: () => undefined }; trackLiveRun(svc, firstRun); trackLiveRun(svc, secondRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(firstRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(secondRun); await waitForCondition(() => adapter.pendingLaunchInputs.length === 2); svc.stopAllTeams(); await waitForCondition(() => adapter.stopInputs.length === 2); expect(adapter.stopInputs.map((input) => input.teamName).sort()).toEqual([ firstTeamName, secondTeamName, ]); expect(adapter.stopInputs.map((input) => input.laneId).sort()).toEqual([ 'secondary:opencode:bob', 'secondary:opencode:bob', ]); expect(svc.getAliveTeams()).toEqual([]); adapter.releaseLaunches(); await waitForCondition(() => adapter.rejectedLaunchCount === 2); await expect( readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), firstTeamName) ).resolves.toMatchObject({ lanes: {}, }); await expect( readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), secondTeamName) ).resolves.toMatchObject({ lanes: {}, }); const firstStatuses = await svc.getMemberSpawnStatuses(firstTeamName); const secondStatuses = await svc.getMemberSpawnStatuses(secondTeamName); expect(firstStatuses.teamLaunchState).not.toBe('partial_failure'); expect(secondStatuses.teamLaunchState).not.toBe('partial_failure'); expect(firstStatuses.statuses.bob?.launchState).not.toBe('failed_to_start'); expect(firstStatuses.statuses.tom?.launchState).not.toBe('failed_to_start'); expect(secondStatuses.statuses.bob?.launchState).not.toBe('failed_to_start'); expect(secondStatuses.statuses.tom?.launchState).not.toBe('failed_to_start'); }); it('recovers mixed Codex/OpenCode launch truth from persisted state after service restart', async () => { const teamName = 'mixed-persisted-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ providerId: 'codex', providerBackendId: 'codex-native', model: 'gpt-5.4-mini', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'codex', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), tom: mixedMemberState({ providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'runtime_pending_permission', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: false, hardFailure: false, pendingPermissionRequestIds: ['perm-tom'], }), }, }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob', 'tom']); expect(statuses.summary).toMatchObject({ confirmedCount: 2, pendingCount: 1, failedCount: 0, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); expect(statuses.statuses.tom).toMatchObject({ launchState: 'runtime_pending_permission', pendingPermissionRequestIds: ['perm-tom'], }); const runtimeSnapshot = await restartedService.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.providerBackendId).toBe('codex-native'); expect(runtimeSnapshot.members.alice).toMatchObject({ providerId: 'codex', providerBackendId: 'codex-native', laneKind: 'primary', runtimeModel: 'gpt-5.4-mini', }); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:bob', laneKind: 'secondary', runtimeModel: 'opencode/minimax-m2.5-free', }); expect(runtimeSnapshot.members.tom).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:tom', laneKind: 'secondary', runtimeModel: 'opencode/nemotron-3-super-free', }); }); it('recovers mixed Anthropic/OpenCode launch truth from persisted state after service restart', async () => { const teamName = 'mixed-persisted-anthropic-opencode-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), tom: mixedMemberState({ providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: false, hardFailure: false, }), }, }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob', 'tom']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, pendingCount: 1, failedCount: 0, runtimeAlivePendingCount: 0, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }); const runtimeSnapshot = await restartedService.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.alice).toMatchObject({ providerId: 'anthropic', laneKind: 'primary', runtimeModel: 'haiku', }); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:bob', laneKind: 'secondary', runtimeModel: 'opencode/minimax-m2.5-free', }); expect(runtimeSnapshot.members.tom).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:tom', laneKind: 'secondary', runtimeModel: 'opencode/nemotron-3-super-free', }); }); it('does not resurrect removed OpenCode secondary teammates in mixed Anthropic launch recovery', async () => { const teamName = 'mixed-anthropic-removed-opencode-stale-state-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic', removedMembers: ['tom'] }); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), tom: mixedMemberState({ providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'stale removed OpenCode lane failure', }), }, }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, pendingCount: 0, failedCount: 0, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toBeUndefined(); const runtimeSnapshot = await restartedService.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'opencode', laneKind: 'secondary', runtimeModel: 'opencode/minimax-m2.5-free', }); expect(runtimeSnapshot.members.tom).toBeUndefined(); }); it('keeps active suffixed OpenCode secondary teammates in mixed Anthropic recovery', async () => { const teamName = 'mixed-anthropic-suffixed-opencode-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic', removedMembers: ['bob', 'tom'], extraMembers: [ { name: 'bob-2', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', }, ], }); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), 'bob-2': mixedMemberState({ name: 'bob-2', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob-2', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), }, }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob-2']); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.bob).toBeUndefined(); expect(statuses.statuses.tom).toBeUndefined(); expect(statuses.statuses['bob-2']).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); const runtimeSnapshot = await restartedService.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.bob).toBeUndefined(); expect(runtimeSnapshot.members.tom).toBeUndefined(); expect(runtimeSnapshot.members['bob-2']).toMatchObject({ providerId: 'opencode', laneKind: 'secondary', runtimeModel: 'opencode/minimax-m2.5-free', }); }); it('ignores stale active OpenCode lane index entries for removed teammates in mixed Anthropic recovery', async () => { const teamName = 'mixed-anthropic-removed-stale-lane-index-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic', removedMembers: ['bob', 'tom'], }); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), }, }); await upsertActiveOpenCodeRuntimeLaneForTest({ teamName, laneId: 'secondary:opencode:bob', }); await upsertActiveOpenCodeRuntimeLaneForTest({ teamName, laneId: 'secondary:opencode:tom', }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice']); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toBeUndefined(); expect(statuses.statuses.tom).toBeUndefined(); const runtimeSnapshot = await restartedService.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.alice).toMatchObject({ providerId: 'anthropic', runtimeModel: 'haiku', }); expect(runtimeSnapshot.members.bob).toBeUndefined(); expect(runtimeSnapshot.members.tom).toBeUndefined(); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { 'secondary:opencode:bob': { state: 'active' }, 'secondary:opencode:tom': { state: 'active' }, }, } ); }); it('recovers pure Anthropic status and model metadata from persisted state after service restart', async () => { const teamName = 'pure-persisted-anthropic-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), }, }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, pendingCount: 0, failedCount: 0, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); const runtimeSnapshot = await restartedService.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.alice).toMatchObject({ providerId: 'anthropic', laneKind: 'primary', alive: false, runtimeModel: 'haiku', }); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'anthropic', laneKind: 'primary', alive: false, runtimeModel: 'sonnet', }); }); it('recovers pure Anthropic partial failure from persisted state after service restart', async () => { const teamName = 'pure-persisted-anthropic-failure-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Anthropic pane exited before bootstrap', }), }, }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 0, failedCount: 1, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Anthropic pane exited before bootstrap', }); const runtimeSnapshot = await restartedService.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'anthropic', laneKind: 'primary', alive: false, runtimeModel: 'sonnet', }); }); it('does not resurrect removed pure Anthropic teammates from stale persisted launch state', async () => { const teamName = 'pure-anthropic-removed-member-stale-state-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName, { removedMembers: ['bob'] }); await writePureAnthropicTeamLaunchState({ teamName, expectedMembers: ['alice', 'bob'], members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'stale removed member failure', }), }, }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice']); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 0, failedCount: 0, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toBeUndefined(); const runtimeSnapshot = await restartedService.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.alice).toMatchObject({ providerId: 'anthropic', runtimeModel: 'haiku', }); expect(runtimeSnapshot.members.bob).toBeUndefined(); }); it('keeps active suffixed pure Anthropic teammates when the removed base member is stale', async () => { const teamName = 'pure-anthropic-suffixed-active-member-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName, { removedMembers: ['bob'], extraMembers: [{ name: 'bob-2', providerId: 'anthropic', model: 'sonnet' }], }); await writePureAnthropicTeamLaunchState({ teamName, expectedMembers: ['alice', 'bob-2'], members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), 'bob-2': mixedMemberState({ name: 'bob-2', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), }, }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob-2']); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.bob).toBeUndefined(); expect(statuses.statuses['bob-2']).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); const runtimeSnapshot = await restartedService.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.bob).toBeUndefined(); expect(runtimeSnapshot.members['bob-2']).toMatchObject({ providerId: 'anthropic', runtimeModel: 'sonnet', }); }); it('filters removed pure Anthropic teammates from bootstrap-only launch recovery', async () => { const teamName = 'pure-anthropic-removed-bootstrap-state-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName, { removedMembers: ['bob'] }); await writeBootstrapState(teamName, [ { name: 'alice', status: 'bootstrap_confirmed', lastAttemptAt: Date.parse('2026-04-23T10:00:00.000Z'), lastObservedAt: Date.parse('2026-04-23T10:00:05.000Z'), }, { name: 'bob', status: 'failed', lastAttemptAt: Date.parse('2026-04-23T10:00:00.000Z'), lastObservedAt: Date.parse('2026-04-23T10:00:04.000Z'), failureReason: 'stale removed bootstrap failure', }, ]); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice']); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 0, failedCount: 0, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', bootstrapConfirmed: true, hardFailure: false, }); expect(statuses.statuses.bob).toBeUndefined(); }); it('recovers pure Anthropic runtime-pending bootstrap from persisted state after service restart', async () => { const teamName = 'pure-persisted-anthropic-bootstrap-pending-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: false, hardFailure: false, }), }, }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 1, failedCount: 0, runtimeAlivePendingCount: 0, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }); }); it('recovers pure Anthropic runtime-pending permission from persisted state after service restart', async () => { const teamName = 'pure-persisted-anthropic-permission-pending-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_permission', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: false, hardFailure: false, pendingPermissionRequestIds: ['perm-bob'], }), }, }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 1, failedCount: 0, runtimeAlivePendingCount: 0, }); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_permission', runtimeAlive: false, pendingPermissionRequestIds: ['perm-bob'], hardFailure: false, }); }); it('keeps active pure Anthropic starting teammates pending after service restart', async () => { const teamName = 'pure-active-anthropic-starting-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }), }, }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.launchPhase).toBe('active'); expect(statuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 1, failedCount: 0, }); expect(statuses.statuses.bob).toMatchObject({ status: 'spawning', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, hardFailure: false, }); const runtimeSnapshot = await restartedService.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'anthropic', alive: false, runtimeModel: 'sonnet', }); }); it('fails finished pure Anthropic starting teammates after service restart', async () => { const teamName = 'pure-finished-anthropic-never-spawned-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'finished', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }), }, }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.launchPhase).toBe('finished'); expect(statuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 0, failedCount: 1, }); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, hardFailure: true, hardFailureReason: 'Teammate was never spawned during launch.', }); const runtimeSnapshot = await restartedService.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'anthropic', alive: false, runtimeModel: 'sonnet', }); }); it('keeps active pure Anthropic missing member state pending after service restart', async () => { const teamName = 'pure-active-anthropic-missing-state-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', expectedMembers: ['alice', 'bob'], members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), }, }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.launchPhase).toBe('active'); expect(statuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 1, failedCount: 0, }); expect(statuses.statuses.bob).toMatchObject({ status: 'spawning', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, hardFailure: false, }); }); it('fails finished pure Anthropic missing member state after service restart', async () => { const teamName = 'pure-finished-anthropic-missing-state-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'finished', expectedMembers: ['alice', 'bob'], members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), }, }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.launchPhase).toBe('finished'); expect(statuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 0, failedCount: 1, }); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, hardFailure: true, hardFailureReason: 'Teammate was never spawned during launch.', }); }); it('recovers legacy pure Anthropic partial launch marker without leaving missing teammates joining', async () => { const teamName = 'legacy-pure-anthropic-partial-marker-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writeLegacyPartialLaunchState({ teamName, expectedMembers: ['alice', 'bob'], confirmedMembers: ['alice'], missingMembers: ['bob'], }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.launchPhase).toBe('reconciled'); expect(statuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 0, failedCount: 1, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: true, hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Legacy partial launch marker reported teammate missing.', }); const runtimeSnapshot = await restartedService.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'anthropic', alive: false, runtimeModel: 'sonnet', }); }); it('keeps finished pure Anthropic runtime-pending bootstrap teammates pending after service restart', async () => { const teamName = 'pure-finished-anthropic-bootstrap-pending-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'finished', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: false, hardFailure: false, }), }, }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.launchPhase).toBe('finished'); expect(statuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 1, failedCount: 0, runtimeAlivePendingCount: 0, }); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }); }); it('keeps finished pure Anthropic runtime-pending permission teammates pending after service restart', async () => { const teamName = 'pure-finished-anthropic-permission-pending-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'finished', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_permission', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: false, hardFailure: false, pendingPermissionRequestIds: ['perm-bob'], }), }, }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.launchPhase).toBe('finished'); expect(statuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 1, failedCount: 0, runtimeAlivePendingCount: 0, }); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_permission', agentToolAccepted: true, runtimeAlive: false, pendingPermissionRequestIds: ['perm-bob'], hardFailure: false, }); }); it('recovers mixed Anthropic and Gemini failure with split OpenCode lane truth after service restart', async () => { const teamName = 'mixed-persisted-anthropic-gemini-failure-opencode-split-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), reviewer: mixedMemberState({ providerId: 'gemini', model: 'gemini-2.5-flash', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'gemini', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Gemini pane exited before bootstrap', }), bob: mixedMemberState({ providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), tom: mixedMemberState({ providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'runtime_pending_permission', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: false, hardFailure: false, pendingPermissionRequestIds: ['perm-tom'], }), }, }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'reviewer', 'bob', 'tom']); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, pendingCount: 1, failedCount: 1, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.reviewer).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Gemini pane exited before bootstrap', }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_permission', hardFailure: false, pendingPermissionRequestIds: ['perm-tom'], }); const runtimeSnapshot = await restartedService.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.alice).toMatchObject({ providerId: 'anthropic', laneKind: 'primary', runtimeModel: 'haiku', }); expect(runtimeSnapshot.members.reviewer).toMatchObject({ providerId: 'gemini', laneKind: 'primary', alive: false, runtimeModel: 'gemini-2.5-flash', }); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'opencode', laneKind: 'secondary', runtimeModel: 'opencode/minimax-m2.5-free', }); expect(runtimeSnapshot.members.tom).toMatchObject({ providerId: 'opencode', laneKind: 'secondary', runtimeModel: 'opencode/nemotron-3-super-free', }); }); it('recovers mixed Gemini failure and split OpenCode lane truth after service restart', async () => { const teamName = 'mixed-persisted-gemini-failure-opencode-split-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName, { includeGeminiPrimary: true }); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ providerId: 'codex', providerBackendId: 'codex-native', model: 'gpt-5.4-mini', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'codex', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), reviewer: mixedMemberState({ providerId: 'gemini', model: 'gemini-2.5-flash', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'gemini', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Gemini pane exited before bootstrap', }), bob: mixedMemberState({ providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), tom: mixedMemberState({ providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'runtime_pending_permission', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: false, hardFailure: false, pendingPermissionRequestIds: ['perm-tom'], }), }, }); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'reviewer', 'bob', 'tom']); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, pendingCount: 1, failedCount: 1, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.reviewer).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Gemini pane exited before bootstrap', }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_permission', hardFailure: false, pendingPermissionRequestIds: ['perm-tom'], }); const runtimeSnapshot = await restartedService.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.reviewer).toMatchObject({ providerId: 'gemini', laneKind: 'primary', alive: false, runtimeModel: 'gemini-2.5-flash', }); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'opencode', laneKind: 'secondary', runtimeModel: 'opencode/minimax-m2.5-free', }); expect(runtimeSnapshot.members.tom).toMatchObject({ providerId: 'opencode', laneKind: 'secondary', runtimeModel: 'opencode/nemotron-3-super-free', }); }); it('exposes shared OpenCode side-lane runtime memory in the team runtime snapshot', async () => { const teamName = 'mixed-opencode-runtime-memory-safe-e2e'; const sharedHostPid = 24_242; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ providerId: 'codex', providerBackendId: 'codex-native', model: 'gpt-5.4-mini', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'codex', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), }, }); const svc = new TeamProvisioningService(); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'bob', { alive: true, metricsPid: sharedHostPid, model: 'opencode/minimax-m2.5-free', }, ], ]); (svc as any).readProcessRssBytesByPid = async () => new Map([[sharedHostPid, 183.9 * 1024 * 1024]]); await waitForCondition(async () => { const snapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); return snapshot.members.bob?.alive === true; }); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:bob', laneKind: 'secondary', alive: true, restartable: false, pid: sharedHostPid, runtimeModel: 'opencode/minimax-m2.5-free', rssBytes: 183.9 * 1024 * 1024, }); expect(runtimeSnapshot.members.bob.providerBackendId).toBeUndefined(); }); it('keeps OpenCode side-lane pid and memory visible after mixed failure recovery', async () => { const teamName = 'mixed-gemini-failure-opencode-memory-safe-e2e'; const sharedHostPid = 31_313; const sharedRssBytes = 211.4 * 1024 * 1024; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName, { includeGeminiPrimary: true }); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ providerId: 'codex', providerBackendId: 'codex-native', model: 'gpt-5.4-mini', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'codex', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), reviewer: mixedMemberState({ providerId: 'gemini', model: 'gemini-2.5-flash', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'gemini', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Gemini pane exited before bootstrap', }), bob: mixedMemberState({ providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), tom: mixedMemberState({ providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'runtime_pending_permission', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: false, hardFailure: false, pendingPermissionRequestIds: ['perm-tom'], }), }, }); const svc = new TeamProvisioningService(); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'bob', { alive: true, metricsPid: sharedHostPid, model: 'opencode/minimax-m2.5-free', }, ], [ 'tom', { alive: true, metricsPid: sharedHostPid, model: 'opencode/nemotron-3-super-free', }, ], ]); (svc as any).readProcessRssBytesByPid = async () => new Map([[sharedHostPid, sharedRssBytes]]); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.reviewer).toMatchObject({ providerId: 'gemini', laneKind: 'primary', alive: false, runtimeModel: 'gemini-2.5-flash', }); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:bob', laneKind: 'secondary', alive: true, restartable: false, pid: sharedHostPid, runtimeModel: 'opencode/minimax-m2.5-free', rssBytes: sharedRssBytes, }); expect(runtimeSnapshot.members.tom).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:tom', laneKind: 'secondary', alive: true, restartable: false, pid: sharedHostPid, runtimeModel: 'opencode/nemotron-3-super-free', rssBytes: sharedRssBytes, }); }); it('keeps OpenCode side-lane pid and memory visible after Anthropic mixed recovery', async () => { const teamName = 'mixed-anthropic-opencode-memory-safe-e2e'; const sharedHostPid = 41_414; const sharedRssBytes = 207.6 * 1024 * 1024; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), tom: mixedMemberState({ providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'runtime_pending_permission', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: false, hardFailure: false, pendingPermissionRequestIds: ['perm-tom'], }), }, }); const svc = new TeamProvisioningService(); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'bob', { alive: true, metricsPid: sharedHostPid, model: 'opencode/minimax-m2.5-free', }, ], [ 'tom', { alive: true, metricsPid: sharedHostPid, model: 'opencode/nemotron-3-super-free', }, ], ]); (svc as any).readProcessRssBytesByPid = async () => new Map([[sharedHostPid, sharedRssBytes]]); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.alice).toMatchObject({ providerId: 'anthropic', laneKind: 'primary', runtimeModel: 'haiku', }); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:bob', laneKind: 'secondary', alive: true, restartable: false, pid: sharedHostPid, runtimeModel: 'opencode/minimax-m2.5-free', rssBytes: sharedRssBytes, }); expect(runtimeSnapshot.members.tom).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:tom', laneKind: 'secondary', alive: true, restartable: false, pid: sharedHostPid, runtimeModel: 'opencode/nemotron-3-super-free', rssBytes: sharedRssBytes, }); }); it('keeps OpenCode side-lane pid and memory visible after Anthropic and Gemini mixed failure recovery', async () => { const teamName = 'mixed-anthropic-gemini-failure-opencode-memory-safe-e2e'; const sharedHostPid = 51_515; const sharedRssBytes = 219.2 * 1024 * 1024; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), reviewer: mixedMemberState({ providerId: 'gemini', model: 'gemini-2.5-flash', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'gemini', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Gemini pane exited before bootstrap', }), bob: mixedMemberState({ providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), tom: mixedMemberState({ providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'runtime_pending_permission', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: false, hardFailure: false, pendingPermissionRequestIds: ['perm-tom'], }), }, }); const svc = new TeamProvisioningService(); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'bob', { alive: true, metricsPid: sharedHostPid, model: 'opencode/minimax-m2.5-free', }, ], [ 'tom', { alive: true, metricsPid: sharedHostPid, model: 'opencode/nemotron-3-super-free', }, ], ]); (svc as any).readProcessRssBytesByPid = async () => new Map([[sharedHostPid, sharedRssBytes]]); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.alice).toMatchObject({ providerId: 'anthropic', laneKind: 'primary', runtimeModel: 'haiku', }); expect(runtimeSnapshot.members.reviewer).toMatchObject({ providerId: 'gemini', laneKind: 'primary', alive: false, runtimeModel: 'gemini-2.5-flash', }); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:bob', laneKind: 'secondary', alive: true, restartable: false, pid: sharedHostPid, runtimeModel: 'opencode/minimax-m2.5-free', rssBytes: sharedRssBytes, }); expect(runtimeSnapshot.members.tom).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:tom', laneKind: 'secondary', alive: true, restartable: false, pid: sharedHostPid, runtimeModel: 'opencode/nemotron-3-super-free', rssBytes: sharedRssBytes, }); }); it('infers OpenCode runtime provider from model after restart when provider metadata is missing', async () => { const teamName = 'mixed-opencode-model-inference-safe-e2e'; const sharedHostPid = 24_243; await writeMixedTeamConfigWithoutOpenCodeProviderMetadata({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ providerId: 'codex', providerBackendId: 'codex-native', model: 'gpt-5.4-mini', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'codex', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }), }, }); const restartedService = new TeamProvisioningService(); (restartedService as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'bob', { alive: true, metricsPid: sharedHostPid, model: 'opencode/minimax-m2.5-free', }, ], ]); (restartedService as any).readProcessRssBytesByPid = async () => new Map([[sharedHostPid, 188.4 * 1024 * 1024]]); const runtimeSnapshot = await restartedService.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:bob', laneKind: 'secondary', alive: true, restartable: false, pid: sharedHostPid, runtimeModel: 'opencode/minimax-m2.5-free', rssBytes: 188.4 * 1024 * 1024, }); expect(runtimeSnapshot.members.bob.providerBackendId).toBeUndefined(); }); it('clears stale never-spawned OpenCode side-lane failures when live runtime metadata proves the member is alive', async () => { const teamName = 'mixed-opencode-stale-failure-clears-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ providerId: 'codex', providerBackendId: 'codex-native', model: 'gpt-5.4-mini', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'codex', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Teammate was never spawned during launch.', }), }, }); const svc = new TeamProvisioningService(); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'bob', { alive: true, model: 'opencode/minimax-m2.5-free', livenessKind: 'runtime_process', }, ], ]); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 1, failedCount: 0, runtimeAlivePendingCount: 1, }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'runtime_pending_bootstrap', runtimeAlive: true, hardFailure: false, runtimeModel: 'opencode/minimax-m2.5-free', }); expect(statuses.statuses.bob.hardFailureReason).toBeUndefined(); expect(statuses.statuses.bob.error).toBeUndefined(); }); it('promotes starting OpenCode side-lane members to runtime-pending when live metadata sees the process', async () => { const teamName = 'mixed-opencode-starting-promotes-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ providerId: 'codex', providerBackendId: 'codex-native', model: 'gpt-5.4-mini', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'codex', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }), }, }); const svc = new TeamProvisioningService(); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'bob', { alive: true, model: 'opencode/minimax-m2.5-free', livenessKind: 'runtime_process', }, ], ]); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 1, failedCount: 0, runtimeAlivePendingCount: 1, }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: true, livenessSource: 'process', hardFailure: false, runtimeModel: 'opencode/minimax-m2.5-free', }); }); it('does not clear definitive OpenCode side-lane failures from unrelated live runtime metadata', async () => { const teamName = 'mixed-opencode-definitive-failure-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ providerId: 'codex', providerBackendId: 'codex-native', model: 'gpt-5.4-mini', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'codex', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'OpenCode raw model id "minimax-m2.5-free" was not found.', }), }, }); const svc = new TeamProvisioningService(); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'bob', { alive: true, model: 'opencode/minimax-m2.5-free', }, ], ]); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.summary).toMatchObject({ confirmedCount: 1, failedCount: 1, }); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', runtimeAlive: false, hardFailure: true, hardFailureReason: 'OpenCode raw model id "minimax-m2.5-free" was not found.', runtimeModel: 'opencode/minimax-m2.5-free', }); }); it('runs mixed live secondary OpenCode lanes and preserves primary Codex status', async () => { const teamName = 'mixed-live-lanes-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'permission', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); const initialSnapshot = await (svc as any).launchMixedSecondaryLaneIfNeeded(run); expect(initialSnapshot).toMatchObject({ teamName, launchPhase: 'active', teamLaunchState: 'partial_pending', }); expect(initialSnapshot.members.alice).toMatchObject({ providerId: 'codex', laneKind: 'primary', launchState: 'confirmed_alive', }); expect(initialSnapshot.members.bob).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:bob', laneKind: 'secondary', launchState: 'starting', }); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await waitForCondition( () => run.memberSpawnStatuses.get('bob')?.launchState === 'confirmed_alive' ); await waitForCondition( () => run.memberSpawnStatuses.get('tom')?.launchState === 'runtime_pending_permission' ); expect(adapter.launchInputs.map((input) => input.laneId).sort()).toEqual([ 'secondary:opencode:bob', 'secondary:opencode:tom', ]); expect(adapter.launchInputs).toEqual( expect.arrayContaining([ expect.objectContaining({ laneId: 'secondary:opencode:bob', model: 'opencode/minimax-m2.5-free', expectedMembers: [expect.objectContaining({ name: 'bob', providerId: 'opencode' })], }), expect.objectContaining({ laneId: 'secondary:opencode:tom', model: 'opencode/nemotron-3-super-free', expectedMembers: [expect.objectContaining({ name: 'tom', providerId: 'opencode' })], }), ]) ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, pendingCount: 1, failedCount: 0, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', runtimeAlive: true, bootstrapConfirmed: true, }); expect(statuses.statuses.tom).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_permission', runtimeAlive: false, pendingPermissionRequestIds: ['perm-tom'], }); }); it('keeps mixed launch pending while Codex primary is still joining and OpenCode lanes are ready', async () => { const teamName = 'mixed-codex-starting-opencode-ready-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); run.memberSpawnStatuses.set('alice', { status: 'starting', launchState: 'starting', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await waitForCondition( () => run.memberSpawnStatuses.get('bob')?.launchState === 'confirmed_alive' ); await waitForCondition( () => run.memberSpawnStatuses.get('tom')?.launchState === 'confirmed_alive' ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, pendingCount: 1, failedCount: 0, }); expect(statuses.statuses.alice).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', runtimeAlive: true, bootstrapConfirmed: true, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', runtimeAlive: true, bootstrapConfirmed: true, }); }); it('keeps Anthropic mixed launch pending while primary is still joining and OpenCode lanes are ready', async () => { const teamName = 'mixed-anthropic-starting-opencode-ready-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); trackLiveRun(svc, run); run.memberSpawnStatuses.set('alice', { status: 'starting', launchState: 'starting', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await waitForCondition( () => run.memberSpawnStatuses.get('bob')?.launchState === 'confirmed_alive' ); await waitForCondition( () => run.memberSpawnStatuses.get('tom')?.launchState === 'confirmed_alive' ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, pendingCount: 1, failedCount: 0, }); expect(statuses.statuses.alice).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', runtimeAlive: true, bootstrapConfirmed: true, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', runtimeAlive: true, bootstrapConfirmed: true, }); }); it('keeps Anthropic mixed launch pending while primary awaits permission and OpenCode lanes are ready', async () => { const teamName = 'mixed-anthropic-permission-opencode-ready-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); trackLiveRun(svc, run); run.memberSpawnStatuses.set('alice', { status: 'online', launchState: 'runtime_pending_permission', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: false, hardFailure: false, pendingPermissionRequestIds: ['perm-alice'], lastRuntimeAliveAt: '2026-04-23T10:00:00.000Z', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await waitForCondition( () => run.memberSpawnStatuses.get('bob')?.launchState === 'confirmed_alive' ); await waitForCondition( () => run.memberSpawnStatuses.get('tom')?.launchState === 'confirmed_alive' ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, pendingCount: 1, failedCount: 0, runtimeAlivePendingCount: 0, }); expect(statuses.statuses.alice).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_permission', runtimeAlive: false, bootstrapConfirmed: false, pendingPermissionRequestIds: ['perm-alice'], hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', runtimeAlive: true, bootstrapConfirmed: true, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', runtimeAlive: true, bootstrapConfirmed: true, }); }); it('keeps Anthropic primary online while mixed OpenCode lanes split ready and bootstrap pending', async () => { const teamName = 'mixed-anthropic-opencode-split-bootstrap-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'launching', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await waitForCondition( () => run.memberSpawnStatuses.get('bob')?.launchState === 'confirmed_alive' ); await waitForCondition( () => run.memberSpawnStatuses.get('tom')?.launchState === 'runtime_pending_bootstrap' ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, pendingCount: 1, failedCount: 0, runtimeAlivePendingCount: 0, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.alice).toMatchObject({ providerId: 'anthropic', laneKind: 'primary', alive: true, runtimeModel: 'haiku', }); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'opencode', laneKind: 'secondary', alive: true, runtimeModel: 'opencode/minimax-m2.5-free', }); expect(runtimeSnapshot.members.tom).toMatchObject({ providerId: 'opencode', laneKind: 'secondary', alive: false, runtimeModel: 'opencode/nemotron-3-super-free', }); }); it('keeps mixed Anthropic launch partial when Gemini primary fails and OpenCode lanes split ready and bootstrap pending', async () => { const teamName = 'mixed-anthropic-gemini-failed-opencode-split-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'launching', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); const reviewer = { name: 'reviewer', role: 'Reviewer', providerId: 'gemini', model: 'gemini-2.5-flash', }; run.expectedMembers = ['alice', 'reviewer']; run.effectiveMembers = [...run.effectiveMembers, reviewer]; run.allEffectiveMembers = [ ...run.effectiveMembers, ...run.allEffectiveMembers.filter( (member: { providerId?: string }) => member.providerId === 'opencode' ), ]; run.memberSpawnStatuses.set('reviewer', { status: 'error', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Gemini pane exited before bootstrap', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await waitForCondition( () => run.memberSpawnStatuses.get('bob')?.launchState === 'confirmed_alive' ); await waitForCondition( () => run.memberSpawnStatuses.get('tom')?.launchState === 'runtime_pending_bootstrap' ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, pendingCount: 1, failedCount: 1, runtimeAlivePendingCount: 0, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.reviewer).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Gemini pane exited before bootstrap', }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.alice).toMatchObject({ providerId: 'anthropic', laneKind: 'primary', alive: true, runtimeModel: 'haiku', }); expect(runtimeSnapshot.members.reviewer).toMatchObject({ providerId: 'gemini', laneKind: 'primary', alive: false, runtimeModel: 'gemini-2.5-flash', }); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'opencode', laneKind: 'secondary', alive: true, runtimeModel: 'opencode/minimax-m2.5-free', }); expect(runtimeSnapshot.members.tom).toMatchObject({ providerId: 'opencode', laneKind: 'secondary', alive: false, runtimeModel: 'opencode/nemotron-3-super-free', }); }); it('keeps OpenCode side-lane pid and memory visible during mixed Anthropic launch when Gemini failed and a sibling lane is still bootstrapping', async () => { const teamName = 'mixed-anthropic-gemini-bootstrap-memory-safe-e2e'; const sharedHostPid = 52_525; const sharedRssBytes = 221.7 * 1024 * 1024; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'launching', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); const reviewer = { name: 'reviewer', role: 'Reviewer', providerId: 'gemini', model: 'gemini-2.5-flash', }; run.expectedMembers = ['alice', 'reviewer']; run.effectiveMembers = [...run.effectiveMembers, reviewer]; run.allEffectiveMembers = [ ...run.effectiveMembers, ...run.allEffectiveMembers.filter( (member: { providerId?: string }) => member.providerId === 'opencode' ), ]; run.memberSpawnStatuses.set('reviewer', { status: 'error', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Gemini pane exited before bootstrap', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await waitForCondition( () => run.memberSpawnStatuses.get('bob')?.launchState === 'confirmed_alive' ); await waitForCondition( () => run.memberSpawnStatuses.get('tom')?.launchState === 'runtime_pending_bootstrap' ); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'bob', { alive: true, metricsPid: sharedHostPid, model: 'opencode/minimax-m2.5-free', }, ], [ 'tom', { alive: true, metricsPid: sharedHostPid, model: 'opencode/nemotron-3-super-free', }, ], ]); (svc as any).readProcessRssBytesByPid = async () => new Map([[sharedHostPid, sharedRssBytes]]); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.alice).toMatchObject({ providerId: 'anthropic', laneKind: 'primary', runtimeModel: 'haiku', }); expect(runtimeSnapshot.members.reviewer).toMatchObject({ providerId: 'gemini', laneKind: 'primary', alive: false, runtimeModel: 'gemini-2.5-flash', }); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:bob', laneKind: 'secondary', alive: true, restartable: false, pid: sharedHostPid, runtimeModel: 'opencode/minimax-m2.5-free', rssBytes: sharedRssBytes, }); expect(runtimeSnapshot.members.tom).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:tom', laneKind: 'secondary', alive: true, restartable: false, pid: sharedHostPid, runtimeModel: 'opencode/nemotron-3-super-free', rssBytes: sharedRssBytes, }); }); it('keeps mixed launch partial when Gemini primary fails and OpenCode lanes split ready and pending', async () => { const teamName = 'mixed-gemini-failed-opencode-split-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'permission', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); const reviewer = { name: 'reviewer', role: 'Reviewer', providerId: 'gemini', model: 'gemini-2.5-flash', }; run.expectedMembers = ['alice', 'reviewer']; run.effectiveMembers = [...run.effectiveMembers, reviewer]; run.allEffectiveMembers = [ ...run.effectiveMembers, ...run.allEffectiveMembers.filter( (member: { providerId?: string }) => member.providerId === 'opencode' ), ]; run.memberSpawnStatuses.set('reviewer', { status: 'error', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Gemini pane exited before bootstrap', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await waitForCondition( () => run.memberSpawnStatuses.get('bob')?.launchState === 'confirmed_alive' ); await waitForCondition( () => run.memberSpawnStatuses.get('tom')?.launchState === 'runtime_pending_permission' ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, pendingCount: 1, failedCount: 1, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.reviewer).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Gemini pane exited before bootstrap', }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_permission', hardFailure: false, pendingPermissionRequestIds: ['perm-tom'], }); }); it('keeps Codex primary online when a mixed OpenCode secondary lane fails', async () => { const teamName = 'mixed-live-secondary-failure-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'failed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, failedCount: 1, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'fake_open_code_launch_failure', }); }); it('keeps Anthropic primary online when a mixed OpenCode secondary lane fails', async () => { const teamName = 'mixed-anthropic-secondary-failure-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'failed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, failedCount: 1, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'fake_open_code_launch_failure', }); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.alice).toMatchObject({ providerId: 'anthropic', laneKind: 'primary', alive: true, runtimeModel: 'haiku', }); }); it('does not expose removed OpenCode secondary teammates from live mixed Anthropic launch status', async () => { const teamName = 'mixed-anthropic-live-removed-secondary-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic', removedMembers: ['bob'], }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); removeMixedOpenCodeLaneForTest(run, 'bob'); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 1); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); expect(adapter.launchInputs.map((input) => input.expectedMembers.map((member) => member.name))).toEqual([ ['tom'], ]); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'tom']); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, pendingCount: 0, failedCount: 0, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toBeUndefined(); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.tom).toMatchObject({ providerId: 'opencode', runtimeModel: 'opencode/nemotron-3-super-free', }); expect(runtimeSnapshot.members.bob).toBeUndefined(); }); it('does not re-add removed OpenCode secondary teammates from stale live runtime metadata in mixed Anthropic status', async () => { const teamName = 'mixed-anthropic-stale-live-metadata-removed-secondary-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic', removedMembers: ['bob'], }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'bob', { alive: true, metricsPid: 44_001, model: 'opencode/minimax-m2.5-free', }, ], [ 'tom', { alive: true, metricsPid: 44_002, model: 'opencode/nemotron-3-super-free', }, ], ]); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); removeMixedOpenCodeLaneForTest(run, 'bob'); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 1); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); expect(adapter.launchInputs.map((input) => input.expectedMembers.map((member) => member.name))).toEqual([ ['tom'], ]); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'tom']); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toBeUndefined(); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.tom).toMatchObject({ providerId: 'opencode', runtimeModel: 'opencode/nemotron-3-super-free', }); expect(runtimeSnapshot.members.bob).toBeUndefined(); }); it('does not expose removed pure Anthropic teammates from live launch status', async () => { const teamName = 'pure-anthropic-live-removed-member-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName, { removedMembers: ['bob'] }); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice']); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 0, failedCount: 0, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toBeUndefined(); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.alice).toMatchObject({ providerId: 'anthropic', runtimeModel: 'haiku', }); expect(runtimeSnapshot.members.bob).toBeUndefined(); }); it('does not re-add removed pure Anthropic teammates from stale live runtime metadata', async () => { const teamName = 'pure-anthropic-stale-live-metadata-removed-member-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName, { removedMembers: ['bob'] }); const svc = new TeamProvisioningService(); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'alice', { alive: true, model: 'haiku', }, ], [ 'bob', { alive: true, model: 'sonnet', }, ], ]); const run = createPureAnthropicLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice']); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toBeUndefined(); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.alice).toMatchObject({ providerId: 'anthropic', runtimeModel: 'haiku', }); expect(runtimeSnapshot.members.bob).toBeUndefined(); }); it('does not map stale base Anthropic runtime metadata onto an active suffixed Anthropic teammate', async () => { const teamName = 'pure-anthropic-stale-base-runtime-suffixed-member-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName, { removedMembers: ['bob'], extraMembers: [{ name: 'bob-2', providerId: 'anthropic', model: 'sonnet' }], }); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', expectedMembers: ['alice', 'bob-2'], members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), 'bob-2': mixedMemberState({ name: 'bob-2', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }), }, }); const svc = new TeamProvisioningService(); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ ['alice', { alive: true, model: 'haiku' }], ['bob', { alive: true, model: 'sonnet' }], ]); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob-2']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toBeUndefined(); expect(statuses.statuses['bob-2']).toMatchObject({ status: 'spawning', launchState: 'starting', runtimeAlive: false, hardFailure: false, }); }); it('does not map stale base OpenCode runtime metadata onto an active suffixed teammate in mixed Anthropic recovery', async () => { const teamName = 'mixed-anthropic-stale-base-runtime-suffixed-opencode-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic', removedMembers: ['bob'], extraMembers: [ { name: 'bob-2', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', }, ], }); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), 'bob-2': mixedMemberState({ name: 'bob-2', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob-2', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }), }, }); const svc = new TeamProvisioningService(); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ ['alice', { alive: true, model: 'haiku' }], ['bob', { alive: true, model: 'opencode/minimax-m2.5-free' }], ]); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob-2']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toBeUndefined(); expect(statuses.statuses['bob-2']).toMatchObject({ status: 'spawning', launchState: 'starting', runtimeAlive: false, hardFailure: false, }); }); it('maps suffixed Anthropic runtime metadata onto the canonical pure Anthropic teammate', async () => { const teamName = 'pure-anthropic-suffixed-runtime-canonical-member-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', expectedMembers: ['alice', 'bob'], members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }), }, }); const svc = new TeamProvisioningService(); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ ['alice', { alive: true, model: 'haiku', livenessKind: 'confirmed_bootstrap' }], ['bob-2', { alive: true, model: 'sonnet', livenessKind: 'runtime_process' }], ]); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: true, hardFailure: false, livenessSource: 'process', }); expect(statuses.statuses['bob-2']).toBeUndefined(); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'anthropic', alive: true, runtimeModel: 'sonnet', }); expect(runtimeSnapshot.members['bob-2']).toBeUndefined(); }); it('maps suffixed OpenCode runtime metadata onto the canonical mixed Anthropic teammate', async () => { const teamName = 'mixed-anthropic-suffixed-runtime-canonical-opencode-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }), }, }); const svc = new TeamProvisioningService(); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ ['alice', { alive: true, model: 'haiku', livenessKind: 'confirmed_bootstrap' }], [ 'bob-2', { alive: true, model: 'opencode/minimax-m2.5-free', livenessKind: 'runtime_process', }, ], ]); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: true, hardFailure: false, livenessSource: 'process', }); expect(statuses.statuses['bob-2']).toBeUndefined(); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'opencode', alive: true, runtimeModel: 'opencode/minimax-m2.5-free', }); expect(runtimeSnapshot.members['bob-2']).toBeUndefined(); }); it('maps suffixed lead inbox heartbeat onto the canonical pure Anthropic teammate', async () => { const teamName = 'pure-anthropic-suffixed-heartbeat-canonical-member-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writeLeadInboxMessages(teamName, [ { from: 'bob-2', text: 'heartbeat', timestamp: '2026-04-23T10:00:10.000Z', messageId: 'msg-bob-2-heartbeat', }, ]); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); run.memberSpawnStatuses.set('bob', { status: 'spawning', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, updatedAt: '2026-04-23T10:00:00.000Z', }); trackLiveRun(svc, run); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', bootstrapConfirmed: true, hardFailure: false, livenessSource: 'heartbeat', }); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('does not map stale base lead inbox heartbeat onto an active suffixed pure Anthropic teammate', async () => { const teamName = 'pure-anthropic-stale-base-heartbeat-suffixed-member-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName, { removedMembers: ['bob'], extraMembers: [{ name: 'bob-2', providerId: 'anthropic', model: 'sonnet' }], }); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', expectedMembers: ['alice', 'bob-2'], members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), 'bob-2': mixedMemberState({ name: 'bob-2', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }), }, }); await writeLeadInboxMessages(teamName, [ { from: 'bob', text: 'heartbeat', timestamp: '2026-04-23T10:00:10.000Z', messageId: 'msg-stale-bob-heartbeat', }, ]); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob-2']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toBeUndefined(); expect(statuses.statuses['bob-2']).toMatchObject({ status: 'spawning', launchState: 'starting', bootstrapConfirmed: false, hardFailure: false, }); }); it('maps suffixed lead inbox heartbeat onto the canonical mixed Anthropic OpenCode teammate', async () => { const teamName = 'mixed-anthropic-suffixed-heartbeat-canonical-opencode-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName, updatedAt: '2026-04-23T10:00:00.000Z', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }), }, }); await writeLeadInboxMessages(teamName, [ { from: 'bob-2', text: 'heartbeat', timestamp: '2026-04-23T10:00:10.000Z', messageId: 'msg-mixed-bob-2-heartbeat', }, ]); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toMatchObject({ status: 'spawning', launchState: 'starting', bootstrapConfirmed: false, hardFailure: false, }); expect(statuses.statuses.bob?.lastHeartbeatAt).toBeUndefined(); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('does not map stale base lead inbox heartbeat onto an active suffixed mixed Anthropic teammate', async () => { const teamName = 'mixed-anthropic-stale-base-heartbeat-suffixed-opencode-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic', removedMembers: ['bob'], extraMembers: [ { name: 'bob-2', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', }, ], }); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), 'bob-2': mixedMemberState({ name: 'bob-2', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob-2', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }), }, }); await writeLeadInboxMessages(teamName, [ { from: 'bob', text: 'heartbeat', timestamp: '2026-04-23T10:00:10.000Z', messageId: 'msg-mixed-stale-bob-heartbeat', }, ]); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob-2']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toBeUndefined(); expect(statuses.statuses['bob-2']).toMatchObject({ status: 'spawning', launchState: 'starting', bootstrapConfirmed: false, hardFailure: false, }); }); it('maps suffixed lead inbox bootstrap failure onto the canonical pure Anthropic teammate', async () => { const teamName = 'pure-anthropic-suffixed-failure-canonical-member-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: '2026-04-23T10:00:00.000Z', }), }, }); await writeLeadInboxMessages(teamName, [ { from: 'bob-2', text: 'Bootstrap failed: unsupported model', timestamp: '2026-04-23T10:00:10.000Z', messageId: 'msg-bob-2-bootstrap-failed', }, ]); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Bootstrap failed: unsupported model', }); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('does not map stale base lead inbox bootstrap failure onto an active suffixed pure Anthropic teammate', async () => { const teamName = 'pure-anthropic-stale-base-failure-suffixed-member-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName, { removedMembers: ['bob'], extraMembers: [{ name: 'bob-2', providerId: 'anthropic', model: 'sonnet' }], }); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', expectedMembers: ['alice', 'bob-2'], members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), 'bob-2': mixedMemberState({ name: 'bob-2', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }), }, }); await writeLeadInboxMessages(teamName, [ { from: 'bob', text: 'Bootstrap failed: unsupported model', timestamp: '2026-04-23T10:00:10.000Z', messageId: 'msg-stale-bob-bootstrap-failed', }, ]); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob-2']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toBeUndefined(); expect(statuses.statuses['bob-2']).toMatchObject({ status: 'spawning', launchState: 'starting', bootstrapConfirmed: false, hardFailure: false, }); }); it('maps suffixed lead inbox bootstrap failure onto the canonical mixed Anthropic OpenCode teammate', async () => { const teamName = 'mixed-anthropic-suffixed-failure-canonical-opencode-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName, updatedAt: '2026-04-23T10:00:00.000Z', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: '2026-04-23T10:00:00.000Z', }), }, }); await writeLeadInboxMessages(teamName, [ { from: 'bob-2', text: 'Bootstrap failed: unsupported model', timestamp: '2026-04-23T10:00:10.000Z', messageId: 'msg-mixed-bob-2-bootstrap-failed', }, ]); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Bootstrap failed: unsupported model', }); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('does not map stale base lead inbox bootstrap failure onto an active suffixed mixed Anthropic teammate', async () => { const teamName = 'mixed-anthropic-stale-base-failure-suffixed-opencode-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic', removedMembers: ['bob'], extraMembers: [ { name: 'bob-2', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', }, ], }); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), 'bob-2': mixedMemberState({ name: 'bob-2', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob-2', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }), }, }); await writeLeadInboxMessages(teamName, [ { from: 'bob', text: 'Bootstrap failed: unsupported model', timestamp: '2026-04-23T10:00:10.000Z', messageId: 'msg-mixed-stale-bob-bootstrap-failed', }, ]); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob-2']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toBeUndefined(); expect(statuses.statuses['bob-2']).toMatchObject({ status: 'spawning', launchState: 'starting', bootstrapConfirmed: false, hardFailure: false, }); }); it('ignores stale suffixed lead inbox heartbeat from an older pure Anthropic launch attempt', async () => { const teamName = 'pure-anthropic-old-suffixed-heartbeat-current-attempt-safe-e2e'; const firstSpawnAcceptedAt = new Date(Date.now() - 1_000).toISOString(); const staleMessageAt = new Date(Date.now() - 2_000).toISOString(); await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt, }), }, }); await writeLeadInboxMessages(teamName, [ { from: 'bob-2', text: 'heartbeat', timestamp: staleMessageAt, messageId: 'msg-old-bob-2-heartbeat', }, ]); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', bootstrapConfirmed: false, hardFailure: false, }); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('ignores stale suffixed lead inbox bootstrap failure from an older pure Anthropic launch attempt', async () => { const teamName = 'pure-anthropic-old-suffixed-failure-current-attempt-safe-e2e'; const firstSpawnAcceptedAt = new Date(Date.now() - 1_000).toISOString(); const staleMessageAt = new Date(Date.now() - 2_000).toISOString(); await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt, }), }, }); await writeLeadInboxMessages(teamName, [ { from: 'bob-2', text: 'Bootstrap failed: unsupported model', timestamp: staleMessageAt, messageId: 'msg-old-bob-2-bootstrap-failed', }, ]); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', bootstrapConfirmed: false, hardFailure: false, }); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('ignores stale suffixed lead inbox heartbeat from an older mixed Anthropic OpenCode launch attempt', async () => { const teamName = 'mixed-anthropic-old-suffixed-heartbeat-current-opencode-safe-e2e'; const firstSpawnAcceptedAt = new Date(Date.now() - 1_000).toISOString(); const staleMessageAt = new Date(Date.now() - 2_000).toISOString(); await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName, updatedAt: '2026-04-23T10:00:00.000Z', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt, }), }, }); await writeLeadInboxMessages(teamName, [ { from: 'bob-2', text: 'heartbeat', timestamp: staleMessageAt, messageId: 'msg-mixed-old-bob-2-heartbeat', }, ]); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry( new TeamRuntimeAdapterRegistry([new FakeOpenCodeRuntimeAdapter()]) ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', bootstrapConfirmed: false, hardFailure: false, }); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('ignores stale suffixed lead inbox bootstrap failure from an older mixed Anthropic OpenCode launch attempt', async () => { const teamName = 'mixed-anthropic-old-suffixed-failure-current-opencode-safe-e2e'; const firstSpawnAcceptedAt = new Date(Date.now() - 1_000).toISOString(); const staleMessageAt = new Date(Date.now() - 2_000).toISOString(); await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName, updatedAt: '2026-04-23T10:00:00.000Z', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt, }), }, }); await writeLeadInboxMessages(teamName, [ { from: 'bob-2', text: 'Bootstrap failed: unsupported model', timestamp: staleMessageAt, messageId: 'msg-mixed-old-bob-2-bootstrap-failed', }, ]); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry( new TeamRuntimeAdapterRegistry([new FakeOpenCodeRuntimeAdapter()]) ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', bootstrapConfirmed: false, hardFailure: false, }); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('uses newer suffixed heartbeat over older pure Anthropic bootstrap failure during persisted reconcile', async () => { const teamName = 'pure-anthropic-newer-heartbeat-over-old-failure-safe-e2e'; const firstSpawnAcceptedAt = new Date(Date.now() - 3_000).toISOString(); const olderSignalAt = new Date(Date.now() - 2_000).toISOString(); const newerSignalAt = new Date(Date.now() - 1_000).toISOString(); await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt, }), }, }); await writeLeadInboxMessages(teamName, [ { from: 'bob-2', text: 'Bootstrap failed: unsupported model', timestamp: olderSignalAt, messageId: 'msg-old-failure-before-heartbeat', }, { from: 'bob-2', text: 'heartbeat', timestamp: newerSignalAt, messageId: 'msg-new-heartbeat-after-failure', }, ]); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', bootstrapConfirmed: true, hardFailure: false, lastHeartbeatAt: newerSignalAt, }); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('uses newer suffixed bootstrap failure over older pure Anthropic heartbeat during persisted reconcile', async () => { const teamName = 'pure-anthropic-newer-failure-over-old-heartbeat-safe-e2e'; const firstSpawnAcceptedAt = new Date(Date.now() - 3_000).toISOString(); const olderSignalAt = new Date(Date.now() - 2_000).toISOString(); const newerSignalAt = new Date(Date.now() - 1_000).toISOString(); await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt, }), }, }); await writeLeadInboxMessages(teamName, [ { from: 'bob-2', text: 'heartbeat', timestamp: olderSignalAt, messageId: 'msg-old-heartbeat-before-failure', }, { from: 'bob-2', text: 'Bootstrap failed: unsupported model', timestamp: newerSignalAt, messageId: 'msg-new-failure-after-heartbeat', }, ]); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Bootstrap failed: unsupported model', }); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('uses newer suffixed heartbeat over older mixed Anthropic OpenCode bootstrap failure during persisted reconcile', async () => { const teamName = 'mixed-anthropic-newer-heartbeat-over-old-failure-safe-e2e'; const firstSpawnAcceptedAt = new Date(Date.now() - 3_000).toISOString(); const olderSignalAt = new Date(Date.now() - 2_000).toISOString(); const newerSignalAt = new Date(Date.now() - 1_000).toISOString(); await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName, updatedAt: '2026-04-23T10:00:00.000Z', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt, }), }, }); await writeLeadInboxMessages(teamName, [ { from: 'bob-2', text: 'Bootstrap failed: unsupported model', timestamp: olderSignalAt, messageId: 'msg-mixed-old-failure-before-heartbeat', }, { from: 'bob-2', text: 'heartbeat', timestamp: newerSignalAt, messageId: 'msg-mixed-new-heartbeat-after-failure', }, ]); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry( new TeamRuntimeAdapterRegistry([new FakeOpenCodeRuntimeAdapter()]) ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', bootstrapConfirmed: false, hardFailure: false, }); expect(statuses.statuses.bob?.lastHeartbeatAt).toBeUndefined(); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('uses newer suffixed bootstrap failure over older mixed Anthropic OpenCode heartbeat during persisted reconcile', async () => { const teamName = 'mixed-anthropic-newer-failure-over-old-heartbeat-safe-e2e'; const firstSpawnAcceptedAt = new Date(Date.now() - 3_000).toISOString(); const olderSignalAt = new Date(Date.now() - 2_000).toISOString(); const newerSignalAt = new Date(Date.now() - 1_000).toISOString(); await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName, updatedAt: '2026-04-23T10:00:00.000Z', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt, }), }, }); await writeLeadInboxMessages(teamName, [ { from: 'bob-2', text: 'heartbeat', timestamp: olderSignalAt, messageId: 'msg-mixed-old-heartbeat-before-failure', }, { from: 'bob-2', text: 'Bootstrap failed: unsupported model', timestamp: newerSignalAt, messageId: 'msg-mixed-new-failure-after-heartbeat', }, ]); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry( new TeamRuntimeAdapterRegistry([new FakeOpenCodeRuntimeAdapter()]) ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Bootstrap failed: unsupported model', }); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('uses greater same-timestamp heartbeat messageId over pure Anthropic bootstrap failure during persisted reconcile', async () => { const teamName = 'pure-anthropic-same-time-heartbeat-wins-safe-e2e'; const firstSpawnAcceptedAt = new Date(Date.now() - 2_000).toISOString(); const signalAt = new Date(Date.now() - 1_000).toISOString(); await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt, }), }, }); await writeLeadInboxMessages(teamName, [ { from: 'bob-2', text: 'Bootstrap failed: unsupported model', timestamp: signalAt, messageId: 'msg-001-same-time-failure', }, { from: 'bob-2', text: 'heartbeat', timestamp: signalAt, messageId: 'msg-002-same-time-heartbeat', }, ]); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', bootstrapConfirmed: true, hardFailure: false, lastHeartbeatAt: signalAt, }); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('uses greater same-timestamp bootstrap failure messageId over pure Anthropic heartbeat during persisted reconcile', async () => { const teamName = 'pure-anthropic-same-time-failure-wins-safe-e2e'; const firstSpawnAcceptedAt = new Date(Date.now() - 2_000).toISOString(); const signalAt = new Date(Date.now() - 1_000).toISOString(); await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt, }), }, }); await writeLeadInboxMessages(teamName, [ { from: 'bob-2', text: 'heartbeat', timestamp: signalAt, messageId: 'msg-001-same-time-heartbeat', }, { from: 'bob-2', text: 'Bootstrap failed: unsupported model', timestamp: signalAt, messageId: 'msg-002-same-time-failure', }, ]); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Bootstrap failed: unsupported model', }); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('uses greater same-timestamp heartbeat messageId over mixed Anthropic OpenCode bootstrap failure during persisted reconcile', async () => { const teamName = 'mixed-anthropic-same-time-heartbeat-wins-safe-e2e'; const firstSpawnAcceptedAt = new Date(Date.now() - 2_000).toISOString(); const signalAt = new Date(Date.now() - 1_000).toISOString(); await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName, updatedAt: '2026-04-23T10:00:00.000Z', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt, }), }, }); await writeLeadInboxMessages(teamName, [ { from: 'bob-2', text: 'Bootstrap failed: unsupported model', timestamp: signalAt, messageId: 'msg-mixed-001-same-time-failure', }, { from: 'bob-2', text: 'heartbeat', timestamp: signalAt, messageId: 'msg-mixed-002-same-time-heartbeat', }, ]); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry( new TeamRuntimeAdapterRegistry([new FakeOpenCodeRuntimeAdapter()]) ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', bootstrapConfirmed: false, hardFailure: false, }); expect(statuses.statuses.bob?.lastHeartbeatAt).toBeUndefined(); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('uses greater same-timestamp bootstrap failure messageId over mixed Anthropic OpenCode heartbeat during persisted reconcile', async () => { const teamName = 'mixed-anthropic-same-time-failure-wins-safe-e2e'; const firstSpawnAcceptedAt = new Date(Date.now() - 2_000).toISOString(); const signalAt = new Date(Date.now() - 1_000).toISOString(); await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName, updatedAt: '2026-04-23T10:00:00.000Z', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt, }), }, }); await writeLeadInboxMessages(teamName, [ { from: 'bob-2', text: 'heartbeat', timestamp: signalAt, messageId: 'msg-mixed-001-same-time-heartbeat', }, { from: 'bob-2', text: 'Bootstrap failed: unsupported model', timestamp: signalAt, messageId: 'msg-mixed-002-same-time-failure', }, ]); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry( new TeamRuntimeAdapterRegistry([new FakeOpenCodeRuntimeAdapter()]) ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Bootstrap failed: unsupported model', }); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('clears false never-spawned pure Anthropic failure when live runtime proves the teammate exists', async () => { const teamName = 'pure-anthropic-never-spawned-live-runtime-recovered-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Teammate was never spawned during launch.', }), }, }); const svc = new TeamProvisioningService(); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ ['alice', { alive: true, model: 'haiku', livenessKind: 'confirmed_bootstrap' }], ['bob-2', { alive: true, model: 'sonnet', livenessKind: 'runtime_process' }], ]); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: false, hardFailure: false, livenessSource: 'process', runtimeModel: 'sonnet', }); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('does not clear explicit pure Anthropic bootstrap failure just because runtime metadata is alive', async () => { const teamName = 'pure-anthropic-hard-failure-live-runtime-not-cleared-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'failed_to_start', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Bootstrap failed: unsupported model', firstSpawnAcceptedAt: '2026-04-23T10:00:00.000Z', }), }, }); const svc = new TeamProvisioningService(); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ ['alice', { alive: true, model: 'haiku' }], ['bob-2', { alive: true, model: 'sonnet' }], ]); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Bootstrap failed: unsupported model', runtimeModel: 'sonnet', }); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('clears false never-spawned Anthropic primary failure in mixed launch when live runtime is alive', async () => { const teamName = 'mixed-anthropic-never-spawned-primary-live-runtime-recovered-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName, updatedAt: '2026-04-23T10:00:00.000Z', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Teammate was never spawned during launch.', }), bob: mixedMemberState({ name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), tom: mixedMemberState({ name: 'tom', providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), }, }); const svc = new TeamProvisioningService(); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([['alice', { alive: true, model: 'haiku', livenessKind: 'runtime_process' }]]); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob', 'tom']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, pendingCount: 1, failedCount: 0, }); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: false, hardFailure: false, livenessSource: 'process', runtimeModel: 'haiku', }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('does not clear explicit Anthropic primary bootstrap failure in mixed launch when runtime metadata is alive', async () => { const teamName = 'mixed-anthropic-hard-primary-failure-live-runtime-not-cleared-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName, updatedAt: '2026-04-23T10:00:00.000Z', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'failed_to_start', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Bootstrap failed: unsupported model', firstSpawnAcceptedAt: '2026-04-23T10:00:00.000Z', }), bob: mixedMemberState({ name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), tom: mixedMemberState({ name: 'tom', providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), }, }); const svc = new TeamProvisioningService(); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([['alice', { alive: true, model: 'haiku' }]]); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob', 'tom']); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, pendingCount: 0, failedCount: 1, }); expect(statuses.statuses.alice).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Bootstrap failed: unsupported model', runtimeModel: 'haiku', }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('confirms pure Anthropic teammate bootstrap from member transcript when inbox and runtime are silent', async () => { const teamName = 'pure-anthropic-transcript-success-safe-e2e'; const acceptedAt = new Date(Date.now() - 5_000).toISOString(); const successAt = new Date(Date.now() - 4_000).toISOString(); await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: acceptedAt, }), }, }); await writeMemberTranscript({ projectPath, sessionId: 'bob-transcript-success', records: [ { timestamp: acceptedAt, teamName, agentName: 'bob', type: 'user', message: { role: 'user', content: `You are bootstrapping into team "${teamName}" as member "bob".`, }, }, { timestamp: successAt, teamName, agentName: 'bob', type: 'user', message: { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'member-briefing-bob', content: `Member briefing for bob on team "${teamName}" (${teamName}).\nTask briefing for bob:\nNo actionable tasks.`, is_error: false, }, ], }, }, ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', bootstrapConfirmed: true, hardFailure: false, lastHeartbeatAt: successAt, }); expect(statuses.statuses.alice).toMatchObject({ status: 'spawning', launchState: 'starting', }); }); it('fails pure Anthropic teammate bootstrap from member transcript API error when inbox is silent', async () => { const teamName = 'pure-anthropic-transcript-failure-safe-e2e'; const acceptedAt = new Date(Date.now() - 5_000).toISOString(); const errorAt = new Date(Date.now() - 4_000).toISOString(); await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: acceptedAt, }), }, }); await writeMemberTranscript({ projectPath, sessionId: 'bob-transcript-failure', records: [ { timestamp: acceptedAt, teamName, agentName: 'bob', type: 'user', message: { role: 'user', content: `You are bootstrapping into team "${teamName}" as member "bob".`, }, }, { timestamp: errorAt, teamName, agentName: 'bob', type: 'assistant', isApiErrorMessage: true, message: { role: 'assistant', content: [ { type: 'text', text: 'API Error: 400 {"detail":"The requested Anthropic model is not available for your account."}', }, ], }, }, ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, }); expect(statuses.statuses.bob?.hardFailureReason).toContain('requested Anthropic model'); }); it('confirms Anthropic primary bootstrap from transcript in mixed launch without changing OpenCode teammates', async () => { const teamName = 'mixed-anthropic-transcript-primary-success-safe-e2e'; const acceptedAt = new Date(Date.now() - 5_000).toISOString(); const successAt = new Date(Date.now() - 4_000).toISOString(); await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName, updatedAt: '2026-04-23T10:00:00.000Z', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: acceptedAt, }), bob: mixedMemberState({ name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), tom: mixedMemberState({ name: 'tom', providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), }, }); await writeMemberTranscript({ projectPath, sessionId: 'alice-transcript-success', records: [ { timestamp: acceptedAt, teamName, agentName: 'alice', type: 'user', message: { role: 'user', content: `You are bootstrapping into team "${teamName}" as member "alice".`, }, }, { timestamp: successAt, teamName, agentName: 'alice', type: 'assistant', message: { role: 'assistant', content: [ { type: 'text', text: `Member briefing for alice on team "${teamName}" (${teamName}).\nTask briefing for alice:\nNo actionable tasks.`, }, ], }, }, ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob', 'tom']); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', bootstrapConfirmed: true, hardFailure: false, lastHeartbeatAt: successAt, }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('fails Anthropic primary bootstrap from transcript in mixed launch without degrading OpenCode teammates', async () => { const teamName = 'mixed-anthropic-transcript-primary-failure-safe-e2e'; const acceptedAt = new Date(Date.now() - 5_000).toISOString(); const errorAt = new Date(Date.now() - 4_000).toISOString(); await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName, updatedAt: '2026-04-23T10:00:00.000Z', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: acceptedAt, }), bob: mixedMemberState({ name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), tom: mixedMemberState({ name: 'tom', providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), }, }); await writeMemberTranscript({ projectPath, sessionId: 'alice-transcript-failure', records: [ { timestamp: acceptedAt, teamName, agentName: 'alice', type: 'user', message: { role: 'user', content: `You are bootstrapping into team "${teamName}" as member "alice".`, }, }, { timestamp: errorAt, teamName, agentName: 'alice', type: 'assistant', isApiErrorMessage: true, message: { role: 'assistant', content: [ { type: 'text', text: 'API Error: 400 {"detail":"The requested Anthropic model is not available for your account."}', }, ], }, }, ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob', 'tom']); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.alice).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, }); expect(statuses.statuses.alice?.hardFailureReason).toContain('requested Anthropic model'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('confirms pure Anthropic teammate bootstrap from suffixed member transcript agentName', async () => { const teamName = 'pure-anthropic-suffixed-transcript-success-safe-e2e'; const acceptedAt = new Date(Date.now() - 5_000).toISOString(); const successAt = new Date(Date.now() - 4_000).toISOString(); await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: acceptedAt, }), }, }); await writeMemberTranscript({ projectPath, sessionId: 'bob-2-transcript-success', records: [ { timestamp: acceptedAt, teamName, agentName: 'bob-2', type: 'user', message: { role: 'user', content: `You are bootstrapping into team "${teamName}" as member "bob".`, }, }, { timestamp: successAt, teamName, agentName: 'bob-2', type: 'assistant', message: { role: 'assistant', content: [ { type: 'text', text: `Member briefing for bob on team "${teamName}" (${teamName}).\nTask briefing for bob:\nNo actionable tasks.`, }, ], }, }, ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', bootstrapConfirmed: true, hardFailure: false, lastHeartbeatAt: successAt, }); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('fails pure Anthropic teammate bootstrap from suffixed member transcript API error', async () => { const teamName = 'pure-anthropic-suffixed-transcript-failure-safe-e2e'; const acceptedAt = new Date(Date.now() - 5_000).toISOString(); const errorAt = new Date(Date.now() - 4_000).toISOString(); await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: acceptedAt, }), }, }); await writeMemberTranscript({ projectPath, sessionId: 'bob-2-transcript-failure', records: [ { timestamp: acceptedAt, teamName, agentName: 'bob-2', type: 'user', message: { role: 'user', content: `You are bootstrapping into team "${teamName}" as member "bob".`, }, }, { timestamp: errorAt, teamName, agentName: 'bob-2', type: 'assistant', isApiErrorMessage: true, message: { role: 'assistant', content: [ { type: 'text', text: 'API Error: 400 {"detail":"The requested Anthropic model is not available for your account."}', }, ], }, }, ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, }); expect(statuses.statuses.bob?.hardFailureReason).toContain('requested Anthropic model'); expect(statuses.statuses['bob-2']).toBeUndefined(); }); it('confirms suffixed Anthropic primary transcript agentName in mixed launch without changing OpenCode teammates', async () => { const teamName = 'mixed-anthropic-suffixed-transcript-primary-success-safe-e2e'; const acceptedAt = new Date(Date.now() - 5_000).toISOString(); const successAt = new Date(Date.now() - 4_000).toISOString(); await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName, updatedAt: '2026-04-23T10:00:00.000Z', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: acceptedAt, }), bob: mixedMemberState({ name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), tom: mixedMemberState({ name: 'tom', providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), }, }); await writeMemberTranscript({ projectPath, sessionId: 'alice-2-transcript-success', records: [ { timestamp: acceptedAt, teamName, agentName: 'alice-2', type: 'user', message: { role: 'user', content: `You are bootstrapping into team "${teamName}" as member "alice".`, }, }, { timestamp: successAt, teamName, agentName: 'alice-2', type: 'assistant', message: { role: 'assistant', content: [ { type: 'text', text: `Member briefing for alice on team "${teamName}" (${teamName}).\nTask briefing for alice:\nNo actionable tasks.`, }, ], }, }, ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob', 'tom']); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', bootstrapConfirmed: true, hardFailure: false, lastHeartbeatAt: successAt, }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('fails suffixed Anthropic primary transcript agentName in mixed launch without degrading OpenCode teammates', async () => { const teamName = 'mixed-anthropic-suffixed-transcript-primary-failure-safe-e2e'; const acceptedAt = new Date(Date.now() - 5_000).toISOString(); const errorAt = new Date(Date.now() - 4_000).toISOString(); await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName, updatedAt: '2026-04-23T10:00:00.000Z', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: acceptedAt, }), bob: mixedMemberState({ name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), tom: mixedMemberState({ name: 'tom', providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), }, }); await writeMemberTranscript({ projectPath, sessionId: 'alice-2-transcript-failure', records: [ { timestamp: acceptedAt, teamName, agentName: 'alice-2', type: 'user', message: { role: 'user', content: `You are bootstrapping into team "${teamName}" as member "alice".`, }, }, { timestamp: errorAt, teamName, agentName: 'alice-2', type: 'assistant', isApiErrorMessage: true, message: { role: 'assistant', content: [ { type: 'text', text: 'API Error: 400 {"detail":"The requested Anthropic model is not available for your account."}', }, ], }, }, ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob', 'tom']); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.alice).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, }); expect(statuses.statuses.alice?.hardFailureReason).toContain('requested Anthropic model'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('uses newer pure Anthropic transcript success over older lexically-later transcript failure', async () => { const teamName = 'pure-anthropic-newer-transcript-success-wins-safe-e2e'; const acceptedAt = new Date(Date.now() - 6_000).toISOString(); const olderAt = new Date(Date.now() - 5_000).toISOString(); const newerAt = new Date(Date.now() - 4_000).toISOString(); await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: acceptedAt, }), }, }); await writeMemberTranscript({ projectPath, sessionId: 'zz-old-bob-failure', records: [ bootstrapTranscriptRecord({ timestamp: acceptedAt, teamName, memberName: 'bob' }), bootstrapFailureTranscriptRecord({ timestamp: olderAt, teamName, memberName: 'bob' }), ], }); await writeMemberTranscript({ projectPath, sessionId: 'aa-new-bob-success', records: [ bootstrapTranscriptRecord({ timestamp: acceptedAt, teamName, memberName: 'bob' }), bootstrapSuccessTranscriptRecord({ timestamp: newerAt, teamName, memberName: 'bob' }), ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', bootstrapConfirmed: true, hardFailure: false, lastHeartbeatAt: newerAt, }); }); it('uses newer pure Anthropic transcript failure over older lexically-later transcript success', async () => { const teamName = 'pure-anthropic-newer-transcript-failure-wins-safe-e2e'; const acceptedAt = new Date(Date.now() - 6_000).toISOString(); const olderAt = new Date(Date.now() - 5_000).toISOString(); const newerAt = new Date(Date.now() - 4_000).toISOString(); await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: acceptedAt, }), }, }); await writeMemberTranscript({ projectPath, sessionId: 'zz-old-bob-success', records: [ bootstrapTranscriptRecord({ timestamp: acceptedAt, teamName, memberName: 'bob' }), bootstrapSuccessTranscriptRecord({ timestamp: olderAt, teamName, memberName: 'bob' }), ], }); await writeMemberTranscript({ projectPath, sessionId: 'aa-new-bob-failure', records: [ bootstrapTranscriptRecord({ timestamp: acceptedAt, teamName, memberName: 'bob' }), bootstrapFailureTranscriptRecord({ timestamp: newerAt, teamName, memberName: 'bob' }), ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, }); expect(statuses.statuses.bob?.hardFailureReason).toContain('requested Anthropic model'); }); it('uses newer mixed Anthropic primary transcript success over older lexically-later failure', async () => { const teamName = 'mixed-anthropic-newer-transcript-success-wins-safe-e2e'; const acceptedAt = new Date(Date.now() - 6_000).toISOString(); const olderAt = new Date(Date.now() - 5_000).toISOString(); const newerAt = new Date(Date.now() - 4_000).toISOString(); await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName, updatedAt: '2026-04-23T10:00:00.000Z', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: acceptedAt, }), bob: mixedMemberState({ name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), tom: mixedMemberState({ name: 'tom', providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), }, }); await writeMemberTranscript({ projectPath, sessionId: 'zz-old-alice-failure', records: [ bootstrapTranscriptRecord({ timestamp: acceptedAt, teamName, memberName: 'alice' }), bootstrapFailureTranscriptRecord({ timestamp: olderAt, teamName, memberName: 'alice' }), ], }); await writeMemberTranscript({ projectPath, sessionId: 'aa-new-alice-success', records: [ bootstrapTranscriptRecord({ timestamp: acceptedAt, teamName, memberName: 'alice' }), bootstrapSuccessTranscriptRecord({ timestamp: newerAt, teamName, memberName: 'alice' }), ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob', 'tom']); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', bootstrapConfirmed: true, hardFailure: false, lastHeartbeatAt: newerAt, }); }); it('uses newer mixed Anthropic primary transcript failure over older lexically-later success', async () => { const teamName = 'mixed-anthropic-newer-transcript-failure-wins-safe-e2e'; const acceptedAt = new Date(Date.now() - 6_000).toISOString(); const olderAt = new Date(Date.now() - 5_000).toISOString(); const newerAt = new Date(Date.now() - 4_000).toISOString(); await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName, updatedAt: '2026-04-23T10:00:00.000Z', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: acceptedAt, }), bob: mixedMemberState({ name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), tom: mixedMemberState({ name: 'tom', providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), }, }); await writeMemberTranscript({ projectPath, sessionId: 'zz-old-alice-success', records: [ bootstrapTranscriptRecord({ timestamp: acceptedAt, teamName, memberName: 'alice' }), bootstrapSuccessTranscriptRecord({ timestamp: olderAt, teamName, memberName: 'alice' }), ], }); await writeMemberTranscript({ projectPath, sessionId: 'aa-new-alice-failure', records: [ bootstrapTranscriptRecord({ timestamp: acceptedAt, teamName, memberName: 'alice' }), bootstrapFailureTranscriptRecord({ timestamp: newerAt, teamName, memberName: 'alice' }), ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'bob', 'tom']); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.alice).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, }); expect(statuses.statuses.alice?.hardFailureReason).toContain('requested Anthropic model'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('falls back to project-root Anthropic transcript success when member log discovery fails', async () => { const teamName = 'pure-anthropic-project-root-transcript-success-fallback-safe-e2e'; const acceptedAt = new Date(Date.now() - 6_000).toISOString(); const successAt = new Date(Date.now() - 4_000).toISOString(); await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: acceptedAt, }), }, }); await writeMemberTranscript({ projectPath, sessionId: 'bob-project-root-success', records: [ bootstrapTranscriptRecord({ timestamp: acceptedAt, teamName, memberName: 'bob' }), bootstrapSuccessTranscriptRecord({ timestamp: successAt, teamName, memberName: 'bob' }), ], }); const svc = new TeamProvisioningService(); (svc as any).memberLogsFinder = { findMemberLogs: async () => { throw new Error('fake member log discovery failure'); }, }; const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', bootstrapConfirmed: true, hardFailure: false, lastHeartbeatAt: successAt, }); }); it('falls back to project-root Anthropic transcript failure when member log discovery fails', async () => { const teamName = 'pure-anthropic-project-root-transcript-failure-fallback-safe-e2e'; const acceptedAt = new Date(Date.now() - 6_000).toISOString(); const failureAt = new Date(Date.now() - 4_000).toISOString(); await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: acceptedAt, }), }, }); await writeMemberTranscript({ projectPath, sessionId: 'bob-project-root-failure', records: [ bootstrapTranscriptRecord({ timestamp: acceptedAt, teamName, memberName: 'bob' }), bootstrapFailureTranscriptRecord({ timestamp: failureAt, teamName, memberName: 'bob' }), ], }); const svc = new TeamProvisioningService(); (svc as any).memberLogsFinder = { findMemberLogs: async () => { throw new Error('fake member log discovery failure'); }, }; const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, }); expect(statuses.statuses.bob?.hardFailureReason).toContain('requested Anthropic model'); }); it('ignores malformed Anthropic transcript lines and recovers bootstrap success', async () => { const teamName = 'pure-anthropic-malformed-transcript-success-safe-e2e'; const acceptedAt = new Date(Date.now() - 6_000).toISOString(); const successAt = new Date(Date.now() - 4_000).toISOString(); await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: acceptedAt, }), }, }); await writeRawMemberTranscript({ projectPath, sessionId: 'bob-malformed-success', lines: [ JSON.stringify( bootstrapTranscriptRecord({ timestamp: acceptedAt, teamName, memberName: 'bob' }) ), '{"timestamp": "not complete"', JSON.stringify( bootstrapSuccessTranscriptRecord({ timestamp: successAt, teamName, memberName: 'bob' }) ), 'warning: claude cli emitted a non-json trailer', ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', bootstrapConfirmed: true, hardFailure: false, lastHeartbeatAt: successAt, }); }); it('ignores malformed Anthropic transcript lines and recovers bootstrap failure', async () => { const teamName = 'pure-anthropic-malformed-transcript-failure-safe-e2e'; const acceptedAt = new Date(Date.now() - 6_000).toISOString(); const failureAt = new Date(Date.now() - 4_000).toISOString(); await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); await writePureAnthropicTeamLaunchState({ teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: acceptedAt, }), }, }); await writeRawMemberTranscript({ projectPath, sessionId: 'bob-malformed-failure', lines: [ JSON.stringify( bootstrapTranscriptRecord({ timestamp: acceptedAt, teamName, memberName: 'bob' }) ), 'partial-json-line {', JSON.stringify( bootstrapFailureTranscriptRecord({ timestamp: failureAt, teamName, memberName: 'bob' }) ), 'non-json stderr trailer', ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, }); expect(statuses.statuses.bob?.hardFailureReason).toContain('requested Anthropic model'); }); it('ignores stale Anthropic transcript success from before the current spawn attempt', async () => { const teamName = 'pure-anthropic-stale-transcript-success-safe-e2e'; const staleAt = new Date(Date.now() - 8_000).toISOString(); const acceptedAt = new Date(Date.now() - 4_000).toISOString(); await writePureAnthropicPendingBobFixture({ teamName, projectPath, acceptedAt }); await writeMemberTranscript({ projectPath, sessionId: 'bob-stale-success', records: [ bootstrapTranscriptRecord({ timestamp: staleAt, teamName, memberName: 'bob' }), bootstrapSuccessTranscriptRecord({ timestamp: staleAt, teamName, memberName: 'bob' }), ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', bootstrapConfirmed: false, hardFailure: false, }); }); it('ignores stale Anthropic transcript failure from before the current spawn attempt', async () => { const teamName = 'pure-anthropic-stale-transcript-failure-safe-e2e'; const staleAt = new Date(Date.now() - 8_000).toISOString(); const acceptedAt = new Date(Date.now() - 4_000).toISOString(); await writePureAnthropicPendingBobFixture({ teamName, projectPath, acceptedAt }); await writeMemberTranscript({ projectPath, sessionId: 'bob-stale-failure', records: [ bootstrapTranscriptRecord({ timestamp: staleAt, teamName, memberName: 'bob' }), bootstrapFailureTranscriptRecord({ timestamp: staleAt, teamName, memberName: 'bob' }), ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', bootstrapConfirmed: false, hardFailure: false, }); }); it('ignores invalid-timestamp Anthropic transcript success when filtering a current spawn attempt', async () => { const teamName = 'pure-anthropic-invalid-timestamp-success-safe-e2e'; const acceptedAt = new Date(Date.now() - 4_000).toISOString(); await writePureAnthropicPendingBobFixture({ teamName, projectPath, acceptedAt }); await writeMemberTranscript({ projectPath, sessionId: 'bob-invalid-timestamp-success', records: [ bootstrapTranscriptRecord({ timestamp: 'not-a-date', teamName, memberName: 'bob' }), bootstrapSuccessTranscriptRecord({ timestamp: 'not-a-date', teamName, memberName: 'bob', }), ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', bootstrapConfirmed: false, hardFailure: false, }); }); it('ignores invalid-timestamp Anthropic transcript failure when filtering a current spawn attempt', async () => { const teamName = 'pure-anthropic-invalid-timestamp-failure-safe-e2e'; const acceptedAt = new Date(Date.now() - 4_000).toISOString(); await writePureAnthropicPendingBobFixture({ teamName, projectPath, acceptedAt }); await writeMemberTranscript({ projectPath, sessionId: 'bob-invalid-timestamp-failure', records: [ bootstrapTranscriptRecord({ timestamp: 'not-a-date', teamName, memberName: 'bob' }), bootstrapFailureTranscriptRecord({ timestamp: 'not-a-date', teamName, memberName: 'bob', }), ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', bootstrapConfirmed: false, hardFailure: false, }); }); it('ignores unrelated no-agentName Anthropic transcript failure in the same project root', async () => { const teamName = 'pure-anthropic-unrelated-no-agent-transcript-failure-safe-e2e'; const acceptedAt = new Date(Date.now() - 4_000).toISOString(); const errorAt = new Date(Date.now() - 2_000).toISOString(); await writePureAnthropicPendingBobFixture({ teamName, projectPath, acceptedAt }); await writeMemberTranscript({ projectPath, sessionId: 'unrelated-no-agent-failure', records: [genericTranscriptApiErrorRecord({ timestamp: errorAt })], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', bootstrapConfirmed: false, hardFailure: false, }); }); it('accepts no-agentName Anthropic transcript failure when the file has matching bootstrap context', async () => { const teamName = 'pure-anthropic-contextual-no-agent-transcript-failure-safe-e2e'; const acceptedAt = new Date(Date.now() - 4_000).toISOString(); const errorAt = new Date(Date.now() - 2_000).toISOString(); await writePureAnthropicPendingBobFixture({ teamName, projectPath, acceptedAt }); await writeMemberTranscript({ projectPath, sessionId: 'bob-contextual-no-agent-failure', records: [ withoutAgentName( bootstrapTranscriptRecord({ timestamp: acceptedAt, teamName, memberName: 'bob' }) ), genericTranscriptApiErrorRecord({ timestamp: errorAt }), ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, }); expect(statuses.statuses.bob?.hardFailureReason).toContain('requested Anthropic model'); }); it('ignores unrelated no-agentName Anthropic primary transcript failure in mixed launch', async () => { const teamName = 'mixed-anthropic-unrelated-no-agent-transcript-failure-safe-e2e'; const acceptedAt = new Date(Date.now() - 4_000).toISOString(); const errorAt = new Date(Date.now() - 2_000).toISOString(); await writeMixedAnthropicPendingAliceFixture({ teamName, projectPath, acceptedAt }); await writeMemberTranscript({ projectPath, sessionId: 'mixed-unrelated-no-agent-failure', records: [genericTranscriptApiErrorRecord({ timestamp: errorAt })], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.alice).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', bootstrapConfirmed: false, hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('accepts no-agentName Anthropic primary transcript failure in mixed launch with matching context', async () => { const teamName = 'mixed-anthropic-contextual-no-agent-transcript-failure-safe-e2e'; const acceptedAt = new Date(Date.now() - 4_000).toISOString(); const errorAt = new Date(Date.now() - 2_000).toISOString(); await writeMixedAnthropicPendingAliceFixture({ teamName, projectPath, acceptedAt }); await writeMemberTranscript({ projectPath, sessionId: 'mixed-alice-contextual-no-agent-failure', records: [ withoutAgentName( bootstrapTranscriptRecord({ timestamp: acceptedAt, teamName, memberName: 'alice' }) ), genericTranscriptApiErrorRecord({ timestamp: errorAt }), ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.alice).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, }); expect(statuses.statuses.alice?.hardFailureReason).toContain('requested Anthropic model'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('accepts attributed pure Anthropic no-agentName transcript failure without bootstrap context', async () => { const teamName = 'pure-anthropic-attributed-no-agent-transcript-failure-safe-e2e'; const acceptedAt = new Date(Date.now() - 4_000).toISOString(); const errorAt = new Date(Date.now() - 2_000).toISOString(); const sessionId = 'bob-attributed-no-agent-failure'; await writePureAnthropicPendingBobFixture({ teamName, projectPath, acceptedAt }); await writeMemberTranscript({ projectPath, sessionId, records: [genericTranscriptApiErrorRecord({ timestamp: errorAt })], }); const svc = new TeamProvisioningService(); (svc as any).memberLogsFinder = { findMemberLogs: async () => [ { filePath: getMemberTranscriptPath(projectPath, sessionId), }, ], }; const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, }); expect(statuses.statuses.bob?.hardFailureReason).toContain('requested Anthropic model'); }); it('accepts attributed mixed Anthropic no-agentName transcript failure without degrading OpenCode teammates', async () => { const teamName = 'mixed-anthropic-attributed-no-agent-transcript-failure-safe-e2e'; const acceptedAt = new Date(Date.now() - 4_000).toISOString(); const errorAt = new Date(Date.now() - 2_000).toISOString(); const sessionId = 'alice-attributed-no-agent-failure'; await writeMixedAnthropicPendingAliceFixture({ teamName, projectPath, acceptedAt }); await writeMemberTranscript({ projectPath, sessionId, records: [genericTranscriptApiErrorRecord({ timestamp: errorAt })], }); const svc = new TeamProvisioningService(); (svc as any).memberLogsFinder = { findMemberLogs: async () => [ { filePath: getMemberTranscriptPath(projectPath, sessionId), }, ], }; const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.alice).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, }); expect(statuses.statuses.alice?.hardFailureReason).toContain('requested Anthropic model'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('maps shared pure Anthropic transcript outcomes to the matching agentName only', async () => { const teamName = 'pure-anthropic-shared-transcript-agent-attribution-safe-e2e'; const acceptedAt = new Date(Date.now() - 4_000).toISOString(); const aliceSuccessAt = new Date(Date.now() - 2_500).toISOString(); const bobFailureAt = new Date(Date.now() - 2_000).toISOString(); await writePureAnthropicPendingMembersFixture({ teamName, projectPath, acceptedAt }); await writeMemberTranscript({ projectPath, sessionId: 'shared-agent-attributed-outcomes', records: [ bootstrapTranscriptRecord({ timestamp: acceptedAt, teamName, memberName: 'alice', agentName: 'alice', }), bootstrapSuccessTranscriptRecord({ timestamp: aliceSuccessAt, teamName, memberName: 'alice', agentName: 'alice', }), bootstrapTranscriptRecord({ timestamp: acceptedAt, teamName, memberName: 'bob', agentName: 'bob', }), bootstrapFailureTranscriptRecord({ timestamp: bobFailureAt, teamName, memberName: 'bob', agentName: 'bob', }), ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', bootstrapConfirmed: true, hardFailure: false, lastHeartbeatAt: aliceSuccessAt, }); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, }); expect(statuses.statuses.bob?.hardFailureReason).toContain('requested Anthropic model'); }); it('does not apply one anonymous shared Anthropic transcript failure to multiple pending members', async () => { const teamName = 'pure-anthropic-shared-anonymous-failure-ambiguous-safe-e2e'; const acceptedAt = new Date(Date.now() - 4_000).toISOString(); const errorAt = new Date(Date.now() - 2_000).toISOString(); await writePureAnthropicPendingMembersFixture({ teamName, projectPath, acceptedAt }); await writeMemberTranscript({ projectPath, sessionId: 'shared-ambiguous-anonymous-failure', records: [ withoutAgentName( bootstrapTranscriptRecord({ timestamp: acceptedAt, teamName, memberName: 'alice' }) ), withoutAgentName( bootstrapTranscriptRecord({ timestamp: acceptedAt, teamName, memberName: 'bob' }) ), genericTranscriptApiErrorRecord({ timestamp: errorAt }), ], }); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.alice).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', bootstrapConfirmed: false, hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', bootstrapConfirmed: false, hardFailure: false, }); }); it('marks an Agent tool call without team_name as an ephemeral spawn failure', async () => { const teamName = 'agent-missing-team-name-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); run.expectedMembers = ['alice', 'bob']; run.memberSpawnStatuses.set('bob', { status: 'waiting', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); (svc as any).captureTeamSpawnEvents(run, [ { type: 'tool_use', name: 'Agent', id: 'tool-bob-missing-team', input: { name: 'bob' }, }, ]); expect(console.warn).toHaveBeenCalledWith( expect.any(String), expect.stringContaining('missing team_name') ); (console.warn as unknown as { mockClear: () => void }).mockClear(); expect(run.memberSpawnStatuses.get('bob')).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: expect.stringContaining('missing team_name'), }); expect(run.memberSpawnToolUseIds.has('tool-bob-missing-team')).toBe(false); }); it('ignores an Agent tool call routed to a different team during launch capture', async () => { const teamName = 'agent-wrong-team-capture-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); run.expectedMembers = ['alice', 'bob']; run.memberSpawnStatuses.set('bob', { status: 'waiting', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); (svc as any).captureTeamSpawnEvents(run, [ { type: 'tool_use', name: 'Agent', id: 'tool-bob-other-team', input: { team_name: 'other-team', name: 'bob' }, }, ]); expect(run.memberSpawnStatuses.get('bob')).toMatchObject({ status: 'waiting', launchState: 'starting', agentToolAccepted: false, hardFailure: false, }); expect(run.memberSpawnToolUseIds.has('tool-bob-other-team')).toBe(false); }); it('marks a valid Agent tool call for this team as spawning and advances to members joining', async () => { const teamName = 'agent-valid-spawn-capture-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); const progressEvents: TeamProvisioningProgress[] = []; run.progress = { ...run.progress, state: 'configuring', message: 'Preparing launch', }; run.onProgress = (progress: TeamProvisioningProgress) => progressEvents.push(progress); run.expectedMembers = ['alice', 'bob']; run.memberSpawnStatuses.set('bob', { status: 'waiting', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); (svc as any).captureTeamSpawnEvents(run, [ { type: 'tool_use', name: 'Agent', id: 'tool-bob-valid-spawn', input: { team_name: teamName, name: 'bob' }, }, ]); expect(run.memberSpawnStatuses.get('bob')).toMatchObject({ status: 'spawning', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }); expect(run.memberSpawnToolUseIds.get('tool-bob-valid-spawn')).toBe('bob'); expect(progressEvents.at(-1)).toMatchObject({ state: 'assembling', message: 'Spawning member bob...', }); }); it('does not reset an online teammate when a duplicate Agent tool call is captured', async () => { const teamName = 'agent-duplicate-online-capture-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); const progressEvents: TeamProvisioningProgress[] = []; run.progress = { ...run.progress, state: 'configuring', message: 'Preparing launch', }; run.onProgress = (progress: TeamProvisioningProgress) => progressEvents.push(progress); const before = run.memberSpawnStatuses.get('alice'); (svc as any).captureTeamSpawnEvents(run, [ { type: 'tool_use', name: 'Agent', id: 'tool-alice-duplicate-spawn', input: { team_name: teamName, name: 'alice' }, }, ]); expect(run.memberSpawnStatuses.get('alice')).toMatchObject({ status: before.status, launchState: before.launchState, agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }); expect(run.memberSpawnToolUseIds.has('tool-alice-duplicate-spawn')).toBe(false); expect(run.provisioningOutputParts.join('\n')).toContain( 'respawn blocked as duplicate - teammate already online' ); expect(progressEvents).toEqual([]); }); it('ignores an Agent tool call without name instead of creating a phantom teammate', async () => { const teamName = 'agent-missing-name-capture-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); run.expectedMembers = ['alice', 'bob']; (svc as any).captureTeamSpawnEvents(run, [ { type: 'tool_use', name: 'Agent', id: 'tool-missing-name', input: { team_name: teamName }, }, ]); expect(console.warn).toHaveBeenCalledWith( expect.any(String), expect.stringContaining('missing name') ); (console.warn as unknown as { mockClear: () => void }).mockClear(); expect(run.memberSpawnStatuses.has('')).toBe(false); expect(run.memberSpawnStatuses.has('undefined')).toBe(false); expect(run.memberSpawnToolUseIds.has('tool-missing-name')).toBe(false); }); it('moves a spawned teammate to bootstrap-pending when Agent tool result is accepted', async () => { const teamName = 'agent-tool-result-accepted-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); run.expectedMembers = ['alice', 'bob']; run.memberSpawnStatuses.set('bob', { status: 'spawning', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); run.activeToolCalls.set('tool-bob-result-accepted', { memberName: 'bob', toolUseId: 'tool-bob-result-accepted', toolName: 'Agent', preview: 'Spawn teammate bob', startedAt: '2026-04-23T10:00:00.000Z', state: 'running', source: 'runtime', }); run.memberSpawnToolUseIds.set('tool-bob-result-accepted', 'bob'); (svc as any).finishRuntimeToolActivity( run, 'tool-bob-result-accepted', [{ type: 'text', text: 'Agent spawn accepted' }], false ); expect(run.memberSpawnStatuses.get('bob')).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }); expect(run.memberSpawnStatuses.get('bob')?.firstSpawnAcceptedAt).toBeTruthy(); expect(run.memberSpawnToolUseIds.has('tool-bob-result-accepted')).toBe(false); expect(run.provisioningOutputParts.join('\n')).toContain( 'spawn accepted, waiting for teammate check-in' ); }); it('fails a spawned teammate when Agent tool result returns an error', async () => { const teamName = 'agent-tool-result-error-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); const progressEvents: TeamProvisioningProgress[] = []; run.progress = { ...run.progress, state: 'assembling', message: 'Members joining', }; run.onProgress = (progress: TeamProvisioningProgress) => progressEvents.push(progress); run.expectedMembers = ['alice', 'bob']; run.memberSpawnStatuses.set('bob', { status: 'spawning', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); run.activeToolCalls.set('tool-bob-result-error', { memberName: 'bob', toolUseId: 'tool-bob-result-error', toolName: 'Agent', preview: 'Spawn teammate bob', startedAt: '2026-04-23T10:00:00.000Z', state: 'running', source: 'runtime', }); run.memberSpawnToolUseIds.set('tool-bob-result-error', 'bob'); (svc as any).finishRuntimeToolActivity( run, 'tool-bob-result-error', [{ type: 'text', text: 'spawn denied by runtime' }], true ); expect(run.memberSpawnStatuses.get('bob')).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: expect.stringContaining('spawn denied by runtime'), }); expect(run.memberSpawnToolUseIds.has('tool-bob-result-error')).toBe(false); expect(run.provisioningOutputParts.join('\n')).toContain( 'Teammate "bob" failed to start: spawn denied by runtime' ); expect(progressEvents.at(-1)).toMatchObject({ state: 'assembling', message: 'Failed to start member bob', }); }); it('restarts a pure Anthropic teammate through the primary runtime without touching siblings', async () => { const teamName = 'pure-anthropic-manual-restart-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); let sentRestartMessage = ''; (svc as any).sendMessageToRun = async (_run: unknown, message: string) => { sentRestartMessage = message; }; (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map(); await svc.restartMember(teamName, 'bob'); expect(sentRestartMessage).toContain('bob'); expect(sentRestartMessage).toContain(teamName); expect(run.pendingMemberRestarts.has('bob')).toBe(true); expect(run.memberSpawnStatuses.get('alice')).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(run.memberSpawnStatuses.get('bob')).toMatchObject({ status: 'spawning', launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }); expect(run.provisioningOutputParts.join('\n')).toContain('manual restart requested from UI'); }); it('keeps a pure Anthropic restart pending after Agent accepts the spawn but before heartbeat', async () => { const teamName = 'pure-anthropic-restart-accepted-pending-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); (svc as any).sendMessageToRun = async () => undefined; (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map(); await svc.restartMember(teamName, 'bob'); run.activeToolCalls.set('tool-bob-restart-accepted', { memberName: 'bob', toolUseId: 'tool-bob-restart-accepted', toolName: 'Agent', preview: 'Restart teammate bob', startedAt: '2026-04-23T10:00:00.000Z', state: 'running', source: 'runtime', }); run.memberSpawnToolUseIds.set('tool-bob-restart-accepted', 'bob'); (svc as any).finishRuntimeToolActivity( run, 'tool-bob-restart-accepted', [{ type: 'text', text: 'Agent spawn accepted' }], false ); expect(run.pendingMemberRestarts.has('bob')).toBe(true); expect(run.memberSpawnStatuses.get('bob')).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }); expect(run.memberSpawnStatuses.get('alice')).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('fails a pure Anthropic restart cleanly when the lead runtime cannot receive the command', async () => { const teamName = 'pure-anthropic-restart-send-failure-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); (svc as any).sendMessageToRun = async () => { throw new Error('lead stdin is closed'); }; (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map(); await expect(svc.restartMember(teamName, 'bob')).rejects.toThrow('lead stdin is closed'); expect(run.pendingMemberRestarts.has('bob')).toBe(false); expect(run.memberSpawnStatuses.get('bob')).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'lead stdin is closed', }); expect(run.memberSpawnStatuses.get('alice')).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('rejects a duplicate pure Anthropic restart while the first restart is still pending', async () => { const teamName = 'pure-anthropic-duplicate-restart-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); let sendCount = 0; (svc as any).sendMessageToRun = async () => { sendCount += 1; }; (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map(); await svc.restartMember(teamName, 'bob'); await expect(svc.restartMember(teamName, 'bob')).rejects.toThrow( 'Restart for teammate "bob" is already in progress' ); expect(sendCount).toBe(1); expect(run.pendingMemberRestarts.has('bob')).toBe(true); expect(run.memberSpawnStatuses.get('bob')).toMatchObject({ status: 'spawning', launchState: 'starting', hardFailure: false, }); }); it('clears stale Agent tracking for the restarted teammate without clearing sibling tool calls', async () => { const teamName = 'pure-anthropic-restart-clears-stale-tool-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); (svc as any).sendMessageToRun = async () => undefined; (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map(); run.activeToolCalls.set('old-bob-tool', { memberName: 'bob', toolUseId: 'old-bob-tool', toolName: 'Agent', preview: 'Old bob spawn', startedAt: '2026-04-23T09:59:00.000Z', state: 'running', source: 'runtime', }); run.activeToolCalls.set('alice-tool', { memberName: 'alice', toolUseId: 'alice-tool', toolName: 'Read', preview: 'Alice is working', startedAt: '2026-04-23T09:59:00.000Z', state: 'running', source: 'runtime', }); run.memberSpawnToolUseIds.set('old-bob-tool', 'bob'); run.memberSpawnToolUseIds.set('alice-tool', 'alice'); await svc.restartMember(teamName, 'bob'); expect(run.activeToolCalls.has('old-bob-tool')).toBe(false); expect(run.activeToolCalls.has('alice-tool')).toBe(true); expect(run.memberSpawnToolUseIds.has('old-bob-tool')).toBe(false); expect(run.memberSpawnToolUseIds.get('alice-tool')).toBe('alice'); expect(run.provisioningOutputParts.join('\n')).toContain( 'cleared stale spawn tool tracking before manual restart' ); }); it('keeps manual restart state isolated to the targeted team', async () => { const firstTeamName = 'pure-anthropic-restart-team-a-safe-e2e'; const secondTeamName = 'pure-anthropic-restart-team-b-safe-e2e'; await writePureAnthropicTeamConfig({ teamName: firstTeamName, projectPath }); await writePureAnthropicTeamMeta(firstTeamName, projectPath); await writePureAnthropicMembersMeta(firstTeamName); await writePureAnthropicTeamConfig({ teamName: secondTeamName, projectPath }); await writePureAnthropicTeamMeta(secondTeamName, projectPath); await writePureAnthropicMembersMeta(secondTeamName); const svc = new TeamProvisioningService(); const firstRun = createPureAnthropicLiveRun({ teamName: firstTeamName, projectPath }); const secondRun = createPureAnthropicLiveRun({ teamName: secondTeamName, projectPath }); trackLiveRun(svc, firstRun); trackLiveRun(svc, secondRun); (svc as any).sendMessageToRun = async () => undefined; (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map(); await svc.restartMember(firstTeamName, 'bob'); expect(firstRun.memberSpawnStatuses.get('bob')).toMatchObject({ status: 'spawning', launchState: 'starting', hardFailure: false, }); expect(secondRun.memberSpawnStatuses.get('bob')).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); const firstStatuses = await svc.getMemberSpawnStatuses(firstTeamName); const secondStatuses = await svc.getMemberSpawnStatuses(secondTeamName); expect(firstStatuses.teamLaunchState).toBe('partial_pending'); expect(secondStatuses.teamLaunchState).toBe('clean_success'); }); it('rejects restart for a removed pure Anthropic teammate without changing sibling statuses', async () => { const teamName = 'pure-anthropic-restart-removed-member-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName, { removedMembers: ['bob'] }); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); await expect(svc.restartMember(teamName, 'bob')).rejects.toThrow( 'Member "bob" has been removed' ); expect(run.pendingMemberRestarts.has('bob')).toBe(false); expect(run.memberSpawnStatuses.get('alice')).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(run.memberSpawnStatuses.get('bob')).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('rejects restart for the team lead without creating a member restart', async () => { const teamName = 'pure-anthropic-restart-lead-reject-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); await expect(svc.restartMember(teamName, 'team-lead')).rejects.toThrow( 'Lead restart is not supported from member controls' ); expect(run.pendingMemberRestarts.size).toBe(0); expect(run.memberSpawnStatuses.get('alice')).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(run.memberSpawnStatuses.get('bob')).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('rejects restart after a pure Anthropic team was stopped', async () => { const teamName = 'pure-anthropic-restart-after-stop-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); run.child = { kill: () => undefined }; trackLiveRun(svc, run); svc.stopTeam(teamName); expect(svc.isTeamAlive(teamName)).toBe(false); await expect(svc.restartMember(teamName, 'bob')).rejects.toThrow( `Team "${teamName}" is not currently running` ); expect(run.pendingMemberRestarts.has('bob')).toBe(false); }); it('stops one live pure Anthropic team without disconnecting another tracked team', async () => { const firstTeamName = 'pure-anthropic-stop-team-a-safe-e2e'; const secondTeamName = 'pure-anthropic-stop-team-b-safe-e2e'; await writePureAnthropicTeamConfig({ teamName: firstTeamName, projectPath }); await writePureAnthropicTeamMeta(firstTeamName, projectPath); await writePureAnthropicMembersMeta(firstTeamName); await writePureAnthropicTeamConfig({ teamName: secondTeamName, projectPath }); await writePureAnthropicTeamMeta(secondTeamName, projectPath); await writePureAnthropicMembersMeta(secondTeamName); const svc = new TeamProvisioningService(); const firstRun = createPureAnthropicLiveRun({ teamName: firstTeamName, projectPath }); const secondRun = createPureAnthropicLiveRun({ teamName: secondTeamName, projectPath }); firstRun.child = { kill: () => undefined }; secondRun.child = { kill: () => undefined }; trackLiveRun(svc, firstRun); trackLiveRun(svc, secondRun); svc.stopTeam(firstTeamName); expect(svc.isTeamAlive(firstTeamName)).toBe(false); expect(svc.isTeamAlive(secondTeamName)).toBe(true); expect(firstRun.cancelRequested).toBe(true); expect(secondRun.cancelRequested).toBe(false); const secondStatuses = await svc.getMemberSpawnStatuses(secondTeamName); expect(secondStatuses.teamLaunchState).toBe('clean_success'); expect(secondStatuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); expect(secondStatuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); }); it('keeps pure Anthropic runtime state isolated when one of two teams stops', async () => { const stoppedTeamName = 'pure-anthropic-runtime-state-stopped-safe-e2e'; const liveTeamName = 'pure-anthropic-runtime-state-live-safe-e2e'; await writePureAnthropicTeamConfig({ teamName: stoppedTeamName, projectPath }); await writePureAnthropicTeamMeta(stoppedTeamName, projectPath); await writePureAnthropicMembersMeta(stoppedTeamName); await writePureAnthropicTeamConfig({ teamName: liveTeamName, projectPath }); await writePureAnthropicTeamMeta(liveTeamName, projectPath); await writePureAnthropicMembersMeta(liveTeamName); const svc = new TeamProvisioningService(); const stoppedRun = createPureAnthropicLiveRun({ teamName: stoppedTeamName, projectPath }); const liveRun = createPureAnthropicLiveRun({ teamName: liveTeamName, projectPath }); stoppedRun.child = { pid: 61101, kill: () => undefined, stdin: { writable: true } }; liveRun.child = { pid: 61201, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, stoppedRun); trackLiveRun(svc, liveRun); expect(await svc.getRuntimeState(stoppedTeamName)).toMatchObject({ teamName: stoppedTeamName, isAlive: true, runId: stoppedRun.runId, progress: { state: 'finalizing', }, }); expect(await svc.getRuntimeState(liveTeamName)).toMatchObject({ teamName: liveTeamName, isAlive: true, runId: liveRun.runId, progress: { state: 'finalizing', }, }); svc.stopTeam(stoppedTeamName); expect(await svc.getRuntimeState(stoppedTeamName)).toMatchObject({ teamName: stoppedTeamName, isAlive: false, runId: null, progress: null, }); expect(await svc.getRuntimeState(liveTeamName)).toMatchObject({ teamName: liveTeamName, isAlive: true, runId: liveRun.runId, progress: { state: 'finalizing', }, }); expect(svc.getAliveTeams()).toEqual([liveTeamName]); }); it('stops all tracked pure Anthropic teams and clears lead activity', async () => { const firstTeamName = 'pure-anthropic-stop-all-a-safe-e2e'; const secondTeamName = 'pure-anthropic-stop-all-b-safe-e2e'; await writePureAnthropicTeamConfig({ teamName: firstTeamName, projectPath }); await writePureAnthropicTeamMeta(firstTeamName, projectPath); await writePureAnthropicMembersMeta(firstTeamName); await writePureAnthropicTeamConfig({ teamName: secondTeamName, projectPath }); await writePureAnthropicTeamMeta(secondTeamName, projectPath); await writePureAnthropicMembersMeta(secondTeamName); const svc = new TeamProvisioningService(); const firstRun = createPureAnthropicLiveRun({ teamName: firstTeamName, projectPath }); const secondRun = createPureAnthropicLiveRun({ teamName: secondTeamName, projectPath }); firstRun.child = { pid: 62101, kill: () => undefined, stdin: { writable: true } }; secondRun.child = { pid: 62201, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, firstRun); trackLiveRun(svc, secondRun); expect(svc.getAliveTeams().sort()).toEqual([firstTeamName, secondTeamName].sort()); svc.stopAllTeams(); expect(svc.getAliveTeams()).toEqual([]); expect(firstRun.cancelRequested).toBe(true); expect(secondRun.cancelRequested).toBe(true); expect(svc.getLeadActivityState(firstTeamName)).toEqual({ state: 'offline', runId: null, }); expect(svc.getLeadActivityState(secondTeamName)).toEqual({ state: 'offline', runId: null, }); }); it('sends a user message only to the targeted pure Anthropic team', async () => { const firstTeamName = 'pure-anthropic-message-team-a-safe-e2e'; const secondTeamName = 'pure-anthropic-message-team-b-safe-e2e'; await writePureAnthropicTeamConfig({ teamName: firstTeamName, projectPath }); await writePureAnthropicTeamMeta(firstTeamName, projectPath); await writePureAnthropicMembersMeta(firstTeamName); await writePureAnthropicTeamConfig({ teamName: secondTeamName, projectPath }); await writePureAnthropicTeamMeta(secondTeamName, projectPath); await writePureAnthropicMembersMeta(secondTeamName); const svc = new TeamProvisioningService(); const firstRun = createPureAnthropicLiveRun({ teamName: firstTeamName, projectPath }); const secondRun = createPureAnthropicLiveRun({ teamName: secondTeamName, projectPath }); firstRun.child = { stdin: { writable: true } }; secondRun.child = { stdin: { writable: true } }; trackLiveRun(svc, firstRun); trackLiveRun(svc, secondRun); const delivered: Array<{ teamName: string; message: string }> = []; (svc as any).sendMessageToRun = async (run: { teamName: string }, message: string) => { delivered.push({ teamName: run.teamName, message }); }; await svc.sendMessageToTeam(secondTeamName, 'please review the latest task'); expect(delivered).toEqual([ { teamName: secondTeamName, message: 'please review the latest task', }, ]); expect(svc.isTeamAlive(firstTeamName)).toBe(true); expect(svc.isTeamAlive(secondTeamName)).toBe(true); expect(firstRun.memberSpawnStatuses.get('bob')).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(secondRun.memberSpawnStatuses.get('bob')).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('serializes attachments only into the targeted pure Anthropic lead stdin', async () => { const firstTeamName = 'pure-anthropic-attachment-team-a-safe-e2e'; const secondTeamName = 'pure-anthropic-attachment-team-b-safe-e2e'; await writePureAnthropicTeamConfig({ teamName: firstTeamName, projectPath }); await writePureAnthropicTeamMeta(firstTeamName, projectPath); await writePureAnthropicMembersMeta(firstTeamName); await writePureAnthropicTeamConfig({ teamName: secondTeamName, projectPath }); await writePureAnthropicTeamMeta(secondTeamName, projectPath); await writePureAnthropicMembersMeta(secondTeamName); const svc = new TeamProvisioningService(); const firstRun = createPureAnthropicLiveRun({ teamName: firstTeamName, projectPath }); const secondRun = createPureAnthropicLiveRun({ teamName: secondTeamName, projectPath }); const firstWrites: string[] = []; const secondWrites: string[] = []; firstRun.child = { stdin: createWritableStdin(firstWrites) }; secondRun.child = { stdin: createWritableStdin(secondWrites) }; trackLiveRun(svc, firstRun); trackLiveRun(svc, secondRun); await svc.sendMessageToTeam(secondTeamName, 'review the attached files', [ { filename: 'notes.txt', mimeType: 'text/plain', data: Buffer.from('line one\nline two', 'utf8').toString('base64'), }, { filename: 'brief.pdf', mimeType: 'application/pdf', data: 'JVBERi0xLjQ=', }, { filename: 'screenshot.png', mimeType: 'image/png', data: 'iVBORw0KGgo=', }, ]); expect(firstWrites).toEqual([]); expect(secondWrites).toHaveLength(1); const payload = JSON.parse(secondWrites[0].trim()) as { message: { content: Array> }; }; expect(payload.message.content).toMatchObject([ { type: 'text', text: 'review the attached files' }, { type: 'document', source: { type: 'text', media_type: 'text/plain', data: 'line one\nline two' }, title: 'notes.txt', }, { type: 'document', source: { type: 'base64', media_type: 'application/pdf', data: 'JVBERi0xLjQ=' }, title: 'brief.pdf', }, { type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'iVBORw0KGgo=' }, }, ]); expect(svc.isTeamAlive(firstTeamName)).toBe(true); expect(svc.isTeamAlive(secondTeamName)).toBe(true); }); it('routes messages to the current pure Anthropic run after same-team relaunch', async () => { const teamName = 'pure-anthropic-message-current-run-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const staleRun = createPureAnthropicLiveRun({ teamName, projectPath }); const currentRun = createPureAnthropicLiveRun({ teamName, projectPath }); staleRun.runId = `run-${teamName}-stale`; currentRun.runId = `run-${teamName}-current`; const staleWrites: string[] = []; const currentWrites: string[] = []; staleRun.child = { stdin: createWritableStdin(staleWrites) }; currentRun.child = { stdin: createWritableStdin(currentWrites) }; trackLiveRun(svc, staleRun); trackLiveRun(svc, currentRun); await svc.sendMessageToTeam(teamName, 'use the latest run only'); expect(staleWrites).toEqual([]); expect(currentWrites).toHaveLength(1); const payload = JSON.parse(currentWrites[0].trim()) as { message: { content: Array> }; }; expect(payload.message.content).toMatchObject([ { type: 'text', text: 'use the latest run only' }, ]); expect(svc.getLeadActivityState(teamName)).toEqual({ state: 'active', runId: currentRun.runId, }); }); it('sends a user message only to the targeted Anthropic and Gemini mixed team', async () => { const firstTeamName = 'mixed-anthropic-gemini-message-team-a-safe-e2e'; const secondTeamName = 'mixed-anthropic-gemini-message-team-b-safe-e2e'; await writeMixedTeamConfig({ teamName: firstTeamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(firstTeamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(firstTeamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeMixedTeamConfig({ teamName: secondTeamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(secondTeamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(secondTeamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const svc = new TeamProvisioningService(); const firstRun = createMixedLiveRun({ teamName: firstTeamName, projectPath, primaryProviderId: 'anthropic', }); const secondRun = createMixedLiveRun({ teamName: secondTeamName, projectPath, primaryProviderId: 'anthropic', }); addGeminiPrimaryToMixedRun(firstRun); addGeminiPrimaryToMixedRun(secondRun); firstRun.child = { stdin: { writable: true } }; secondRun.child = { stdin: { writable: true } }; trackLiveRun(svc, firstRun); trackLiveRun(svc, secondRun); const delivered: Array<{ teamName: string; message: string }> = []; (svc as any).sendMessageToRun = async (run: { teamName: string }, message: string) => { delivered.push({ teamName: run.teamName, message }); }; await svc.sendMessageToTeam(secondTeamName, 'review mixed launch state'); expect(delivered).toEqual([ { teamName: secondTeamName, message: 'review mixed launch state', }, ]); expect(svc.isTeamAlive(firstTeamName)).toBe(true); expect(svc.isTeamAlive(secondTeamName)).toBe(true); expect(firstRun.memberSpawnStatuses.get('reviewer')).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(secondRun.memberSpawnStatuses.get('reviewer')).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('routes messages to the current Anthropic and Gemini mixed run after same-team relaunch', async () => { const teamName = 'mixed-anthropic-gemini-message-current-run-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const svc = new TeamProvisioningService(); const staleRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); const currentRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); addGeminiPrimaryToMixedRun(staleRun); addGeminiPrimaryToMixedRun(currentRun); staleRun.runId = `run-${teamName}-stale`; currentRun.runId = `run-${teamName}-current`; const staleWrites: string[] = []; const currentWrites: string[] = []; staleRun.child = { stdin: createWritableStdin(staleWrites) }; currentRun.child = { stdin: createWritableStdin(currentWrites) }; trackLiveRun(svc, staleRun); trackLiveRun(svc, currentRun); await svc.sendMessageToTeam(teamName, 'use the latest mixed run only'); expect(staleWrites).toEqual([]); expect(currentWrites).toHaveLength(1); const payload = JSON.parse(currentWrites[0].trim()) as { message: { content: Array> }; }; expect(payload.message.content).toMatchObject([ { type: 'text', text: 'use the latest mixed run only' }, ]); expect(svc.getLeadActivityState(teamName)).toEqual({ state: 'active', runId: currentRun.runId, }); }); it('routes direct OpenCode member messages to the current Anthropic and Gemini mixed run after same-team relaunch', async () => { const teamName = 'mixed-anthropic-gemini-direct-message-current-run-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const staleRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); const currentRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); addGeminiPrimaryToMixedRun(staleRun); addGeminiPrimaryToMixedRun(currentRun); staleRun.runId = `run-${teamName}-stale`; currentRun.runId = `run-${teamName}-current`; markMixedOpenCodeLaneConfirmedForTest(currentRun, 'bob'); trackLiveRun(svc, staleRun); trackLiveRun(svc, currentRun); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'use current run for direct opencode message', messageId: 'msg-current-direct-opencode', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ runId: currentRun.runId, teamName, laneId: 'secondary:opencode:bob', memberName: 'bob', cwd: projectPath, text: 'use current run for direct opencode message', messageId: 'msg-current-direct-opencode', }); expect(adapter.messageInputs[0]?.runId).not.toBe(staleRun.runId); }); it('routes direct OpenCode member messages only to the targeted live mixed OpenCode lane', async () => { const firstTeamName = 'mixed-opencode-direct-message-live-team-a-safe-e2e'; const secondTeamName = 'mixed-opencode-direct-message-live-team-b-safe-e2e'; await writeMixedTeamConfig({ teamName: firstTeamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(firstTeamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(firstTeamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeMixedTeamConfig({ teamName: secondTeamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(secondTeamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(secondTeamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const firstRun = createMixedLiveRun({ teamName: firstTeamName, projectPath, primaryProviderId: 'anthropic', }); const secondRun = createMixedLiveRun({ teamName: secondTeamName, projectPath, primaryProviderId: 'anthropic', }); addGeminiPrimaryToMixedRun(firstRun); addGeminiPrimaryToMixedRun(secondRun); firstRun.child = { stdin: { writable: true } }; secondRun.child = { stdin: { writable: true } }; markMixedOpenCodeLaneConfirmedForTest(secondRun, 'bob'); trackLiveRun(svc, firstRun); trackLiveRun(svc, secondRun); await expect( svc.deliverOpenCodeMemberMessage(secondTeamName, { memberName: 'bob', text: 'send to the second live mixed opencode lane only', messageId: 'msg-live-mixed-opencode-team-b', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ runId: secondRun.runId, teamName: secondTeamName, laneId: 'secondary:opencode:bob', memberName: 'bob', cwd: projectPath, text: 'send to the second live mixed opencode lane only', messageId: 'msg-live-mixed-opencode-team-b', }); expect(adapter.messageInputs[0]?.runId).not.toBe(firstRun.runId); expect(svc.isTeamAlive(firstTeamName)).toBe(true); expect(svc.isTeamAlive(secondTeamName)).toBe(true); }); it('routes direct OpenCode member messages to a fresh Anthropic and Gemini mixed relaunch after cancelling an in-flight handoff', async () => { const cancelledTeamName = 'mixed-anthropic-gemini-direct-after-cancelled-handoff-safe-e2e'; const survivingTeamName = 'mixed-anthropic-gemini-direct-survives-cancelled-handoff-safe-e2e'; await writeMixedTeamConfig({ teamName: cancelledTeamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(cancelledTeamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(cancelledTeamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeMixedTeamConfig({ teamName: survivingTeamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(survivingTeamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(survivingTeamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const cancelledRun = createMixedLiveRun({ teamName: cancelledTeamName, projectPath, primaryProviderId: 'anthropic', }); const survivingRun = createMixedLiveRun({ teamName: survivingTeamName, projectPath, primaryProviderId: 'anthropic', }); addGeminiPrimaryToMixedRun(cancelledRun); addGeminiPrimaryToMixedRun(survivingRun); cancelledRun.child = { kill: () => undefined }; survivingRun.child = { kill: () => undefined }; trackLiveRun(svc, cancelledRun); trackLiveRun(svc, survivingRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(cancelledRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(survivingRun); await waitForCondition(() => adapter.pendingLaunchInputs.some((input) => input.teamName === cancelledTeamName) ); await svc.cancelProvisioning(cancelledRun.runId); await waitForCondition( () => adapter.stopInputs.filter((input) => input.teamName === cancelledTeamName).length === 1 ); expect(adapter.stopInputs.some((input) => input.teamName === survivingTeamName)).toBe(false); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 3); await waitForCondition(() => survivingRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); const freshRun = createMixedLiveRun({ teamName: cancelledTeamName, projectPath, primaryProviderId: 'anthropic', }); freshRun.runId = `${cancelledRun.runId}-fresh`; freshRun.detectedSessionId = 'lead-session-fresh'; freshRun.child = { kill: () => undefined }; addGeminiPrimaryToMixedRun(freshRun); trackLiveRun(svc, freshRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(freshRun); await waitForCondition(() => adapter.launchInputs.length === 5); await waitForCondition(() => freshRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await expect( svc.deliverOpenCodeMemberMessage(cancelledTeamName, { memberName: 'bob', text: 'send to fresh mixed relaunch after cancelled handoff', messageId: 'msg-fresh-mixed-opencode-after-cancelled-handoff', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); await expect( svc.deliverOpenCodeMemberMessage(survivingTeamName, { memberName: 'tom', text: 'send to surviving sibling mixed lane', messageId: 'msg-surviving-mixed-opencode-after-cancelled-handoff', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(2); expect(adapter.messageInputs[0]).toMatchObject({ runId: latestOpenCodeLaunchRunId( adapter, cancelledTeamName, 'secondary:opencode:bob' ), teamName: cancelledTeamName, laneId: 'secondary:opencode:bob', memberName: 'bob', cwd: projectPath, text: 'send to fresh mixed relaunch after cancelled handoff', messageId: 'msg-fresh-mixed-opencode-after-cancelled-handoff', }); expect(adapter.messageInputs[1]).toMatchObject({ runId: latestOpenCodeLaunchRunId( adapter, survivingTeamName, 'secondary:opencode:tom' ), teamName: survivingTeamName, laneId: 'secondary:opencode:tom', memberName: 'tom', cwd: projectPath, text: 'send to surviving sibling mixed lane', messageId: 'msg-surviving-mixed-opencode-after-cancelled-handoff', }); expect(adapter.messageInputs.map((input) => input.runId)).not.toContain(cancelledRun.runId); }); it('does not deliver direct OpenCode member messages to a cancelled mixed handoff after late launch completion while a sibling stays live', async () => { const cancelledTeamName = 'mixed-direct-cancelled-late-completion-safe-e2e'; const survivingTeamName = 'mixed-direct-sibling-after-cancelled-late-completion-safe-e2e'; await writeMixedTeamConfig({ teamName: cancelledTeamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(cancelledTeamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(cancelledTeamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeMixedTeamConfig({ teamName: survivingTeamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(survivingTeamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(survivingTeamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const cancelledRun = createMixedLiveRun({ teamName: cancelledTeamName, projectPath, primaryProviderId: 'anthropic', }); const survivingRun = createMixedLiveRun({ teamName: survivingTeamName, projectPath, primaryProviderId: 'anthropic', }); addGeminiPrimaryToMixedRun(cancelledRun); addGeminiPrimaryToMixedRun(survivingRun); cancelledRun.child = { kill: () => undefined }; survivingRun.child = { kill: () => undefined }; trackLiveRun(svc, cancelledRun); trackLiveRun(svc, survivingRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(cancelledRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(survivingRun); await waitForCondition(() => adapter.pendingLaunchInputs.some((input) => input.teamName === cancelledTeamName) ); await svc.cancelProvisioning(cancelledRun.runId); await waitForCondition( () => adapter.stopInputs.filter((input) => input.teamName === cancelledTeamName).length === 1 ); expect(adapter.stopInputs.some((input) => input.teamName === survivingTeamName)).toBe(false); expect(svc.isTeamAlive(cancelledTeamName)).toBe(false); expect(svc.isTeamAlive(survivingTeamName)).toBe(true); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 3); await waitForCondition(() => survivingRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await expect( svc.deliverOpenCodeMemberMessage(cancelledTeamName, { memberName: 'bob', text: 'must not reach cancelled mixed handoff after late launch', messageId: 'msg-cancelled-mixed-late-launch-direct', }) ).resolves.toEqual({ delivered: false, reason: 'opencode_runtime_not_active', }); await expect( svc.deliverOpenCodeMemberMessage(survivingTeamName, { memberName: 'bob', text: 'sibling still receives direct message after cancelled launch', messageId: 'msg-sibling-after-cancelled-late-launch-direct', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ runId: latestOpenCodeLaunchRunId( adapter, survivingTeamName, 'secondary:opencode:bob' ), teamName: survivingTeamName, laneId: 'secondary:opencode:bob', memberName: 'bob', cwd: projectPath, text: 'sibling still receives direct message after cancelled launch', messageId: 'msg-sibling-after-cancelled-late-launch-direct', }); expect(adapter.messageInputs[0]?.runId).not.toBe(cancelledRun.runId); }); it('routes direct OpenCode member messages to the alive mixed run when stale provisioning state remains', async () => { const teamName = 'mixed-direct-message-stale-provisioning-alive-run-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const currentRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); addGeminiPrimaryToMixedRun(currentRun); currentRun.runId = `run-${teamName}-current`; markMixedOpenCodeLaneConfirmedForTest(currentRun, 'bob'); trackLiveRun(svc, currentRun); injectStaleTerminalProvisioningRun(svc, teamName, `run-${teamName}-stale`); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'use alive mixed run despite stale provisioning', messageId: 'msg-stale-provisioning-alive-mixed', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ runId: currentRun.runId, teamName, laneId: 'secondary:opencode:bob', memberName: 'bob', cwd: projectPath, text: 'use alive mixed run despite stale provisioning', messageId: 'msg-stale-provisioning-alive-mixed', }); }); it('routes direct OpenCode member messages to the current pure OpenCode run after same-team relaunch', async () => { const teamName = 'pure-opencode-direct-message-current-run-safe-e2e'; const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const first = await svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); const second = await svc.launchTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, }, () => undefined ); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'alice', text: 'use current pure opencode run only', messageId: 'msg-current-pure-opencode', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ runId: second.runId, teamName, laneId: 'primary', memberName: 'alice', cwd: projectPath, text: 'use current pure opencode run only', messageId: 'msg-current-pure-opencode', }); expect(adapter.messageInputs[0]?.runId).not.toBe(first.runId); }); it('routes direct OpenCode member messages only to the targeted live pure OpenCode team', async () => { const firstTeamName = 'pure-opencode-direct-message-live-team-a-safe-e2e'; const secondTeamName = 'pure-opencode-direct-message-live-team-b-safe-e2e'; const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const first = await svc.createTeam( { teamName: firstTeamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); const second = await svc.createTeam( { teamName: secondTeamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await expect( svc.deliverOpenCodeMemberMessage(secondTeamName, { memberName: 'alice', text: 'send to the second live pure opencode team only', messageId: 'msg-live-pure-opencode-team-b', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ runId: second.runId, teamName: secondTeamName, laneId: 'primary', memberName: 'alice', cwd: projectPath, text: 'send to the second live pure opencode team only', messageId: 'msg-live-pure-opencode-team-b', }); expect(adapter.messageInputs[0]?.runId).not.toBe(first.runId); expect(svc.isTeamAlive(firstTeamName)).toBe(true); expect(svc.isTeamAlive(secondTeamName)).toBe(true); }); it('routes direct OpenCode member messages to the alive pure OpenCode run when stale provisioning state remains', async () => { const teamName = 'pure-opencode-direct-message-stale-provisioning-alive-run-safe-e2e'; const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const current = await svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); injectStaleTerminalProvisioningRun(svc, teamName, `run-${teamName}-stale`); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'alice', text: 'use alive pure opencode run despite stale provisioning', messageId: 'msg-stale-provisioning-alive-pure', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ runId: current.runId, teamName, laneId: 'primary', memberName: 'alice', cwd: projectPath, text: 'use alive pure opencode run despite stale provisioning', messageId: 'msg-stale-provisioning-alive-pure', }); }); it('inherits OpenCode runtime delivery taskRefs end-to-end when the visible reply omits them', async () => { const teamName = 'pure-opencode-runtime-delivery-taskrefs-inherit-safe-e2e'; const taskRef: TaskRef = { teamName, taskId: 'task-runtime-delivery-1', displayId: 'abcd1234', }; const adapter = new VisibleReplyOpenCodeRuntimeAdapter({ replySource: 'runtime_delivery', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const launch = await svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'alice', text: 'reply about #abcd1234 without manually carrying metadata', messageId: 'msg-taskrefs-inherit-e2e', replyRecipient: 'user', actionMode: 'ask', source: 'manual', taskRefs: [taskRef], }) ).resolves.toMatchObject({ delivered: true, accepted: true, responsePending: false, responseState: 'responded_visible_message', ledgerStatus: 'responded', visibleReplyMessageId: 'reply-msg-taskrefs-inherit-e2e', visibleReplyCorrelation: 'relayOfMessageId', }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ runId: launch.runId, teamName, laneId: 'primary', memberName: 'alice', messageId: 'msg-taskrefs-inherit-e2e', taskRefs: [taskRef], }); const userInbox = await readInboxRows(teamName, 'user'); expect(userInbox).toHaveLength(1); expect(userInbox[0]).toMatchObject({ from: 'alice', to: 'user', source: 'runtime_delivery', messageId: 'reply-msg-taskrefs-inherit-e2e', relayOfMessageId: 'msg-taskrefs-inherit-e2e', taskRefs: [taskRef], }); }); it('does not attach taskRefs end-to-end to explicit non-runtime visible replies', async () => { const teamName = 'pure-opencode-runtime-delivery-taskrefs-non-runtime-safe-e2e'; const taskRef: TaskRef = { teamName, taskId: 'task-runtime-delivery-2', displayId: 'dcba4321', }; const adapter = new VisibleReplyOpenCodeRuntimeAdapter({ replySource: 'lead_process', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'alice', text: 'this reply has a misleading non-runtime source', messageId: 'msg-taskrefs-non-runtime-e2e', replyRecipient: 'user', actionMode: 'ask', source: 'manual', taskRefs: [taskRef], }) ).resolves.toMatchObject({ delivered: true, accepted: true, responsePending: true, responseState: 'responded_visible_message', reason: 'visible_reply_missing_task_refs', }); const userInbox = await readInboxRows(teamName, 'user'); expect(userInbox).toHaveLength(1); expect(userInbox[0]).toMatchObject({ from: 'alice', to: 'user', source: 'lead_process', messageId: 'reply-msg-taskrefs-non-runtime-e2e', relayOfMessageId: 'msg-taskrefs-non-runtime-e2e', }); expect(userInbox[0]?.taskRefs).toBeUndefined(); }); it('delivers direct OpenCode member messages to recovered pure OpenCode lanes after service restart', async () => { const teamName = 'pure-opencode-direct-message-recovered-lane-safe-e2e'; const launchAdapter = new FakeOpenCodeRuntimeAdapter(); const firstService = new TeamProvisioningService(); firstService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([launchAdapter])); const launch = await firstService.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); const messageAdapter = new FakeOpenCodeRuntimeAdapter(); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([messageAdapter])); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'alice', text: 'message recovered pure opencode lane', messageId: 'msg-recovered-pure-opencode', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(messageAdapter.messageInputs).toHaveLength(1); expect(messageAdapter.messageInputs[0]).toMatchObject({ runId: launch.runId, teamName, laneId: 'primary', memberName: 'alice', cwd: projectPath, text: 'message recovered pure opencode lane', messageId: 'msg-recovered-pure-opencode', }); expect(messageAdapter.messageInputs[0]?.runId).toBe(launchAdapter.launchInputs[0]?.runId); }); it('delivers direct OpenCode member messages to recovered pure OpenCode lanes despite stale terminal provisioning state', async () => { const teamName = 'pure-opencode-direct-message-recovered-stale-terminal-safe-e2e'; await writeOpenCodeTeamConfig({ teamName, projectPath, members: ['alice'] }); await writeOpenCodeMembersMeta(teamName, { members: ['alice'] }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'primary', state: 'active', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); injectStaleTerminalProvisioningRun(svc, teamName, `run-${teamName}-stale`); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'alice', text: 'message recovered pure lane despite stale terminal state', messageId: 'msg-recovered-pure-stale-terminal', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ teamName, laneId: 'primary', memberName: 'alice', cwd: projectPath, text: 'message recovered pure lane despite stale terminal state', messageId: 'msg-recovered-pure-stale-terminal', }); expect(adapter.messageInputs[0]?.runId).toBeUndefined(); }); it('delivers direct OpenCode member messages to recovered pure OpenCode lanes when config is missing', async () => { const teamName = 'pure-opencode-direct-message-meta-only-recovered-safe-e2e'; await writeOpenCodeTeamMeta(teamName, projectPath); await writeOpenCodeMembersMeta(teamName, { members: ['alice'], memberCwd: projectPath, }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'primary', state: 'active', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'alice', text: 'message pure opencode recovered from meta only', messageId: 'msg-meta-only-pure-opencode', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ teamName, laneId: 'primary', memberName: 'alice', cwd: projectPath, text: 'message pure opencode recovered from meta only', messageId: 'msg-meta-only-pure-opencode', }); expect(adapter.messageInputs[0]?.runId).toBeUndefined(); }); it('keeps recovered pure OpenCode direct messages isolated across teams with the same member name', async () => { const activeTeamName = 'pure-opencode-direct-message-cross-team-active-safe-e2e'; const degradedTeamName = 'pure-opencode-direct-message-cross-team-degraded-safe-e2e'; const activeProjectPath = path.join(tempDir, 'project-active-pure'); const degradedProjectPath = path.join(tempDir, 'project-degraded-pure'); await fs.mkdir(activeProjectPath, { recursive: true }); await fs.mkdir(degradedProjectPath, { recursive: true }); await writeOpenCodeTeamMeta(activeTeamName, activeProjectPath); await writeOpenCodeMembersMeta(activeTeamName, { members: ['alice'], memberCwd: activeProjectPath, }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName: activeTeamName, laneId: 'primary', state: 'active', }); await writeOpenCodeTeamMeta(degradedTeamName, degradedProjectPath); await writeOpenCodeMembersMeta(degradedTeamName, { members: ['alice'], memberCwd: degradedProjectPath, }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName: degradedTeamName, laneId: 'primary', state: 'degraded', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( svc.deliverOpenCodeMemberMessage(activeTeamName, { memberName: 'alice', text: 'message only active pure team', messageId: 'msg-cross-team-active-pure', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); await expect( svc.deliverOpenCodeMemberMessage(degradedTeamName, { memberName: 'alice', text: 'must not reach degraded pure team', messageId: 'msg-cross-team-degraded-pure', }) ).resolves.toEqual({ delivered: false, reason: 'opencode_runtime_not_active', }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ teamName: activeTeamName, laneId: 'primary', memberName: 'alice', cwd: activeProjectPath, text: 'message only active pure team', messageId: 'msg-cross-team-active-pure', }); }); it('does not deliver direct OpenCode member messages to stopped pure OpenCode teams', async () => { const teamName = 'pure-opencode-direct-message-stopped-safe-e2e'; const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); svc.stopTeam(teamName); await waitForCondition(() => adapter.stopInputs.length === 1); await waitForCondition(() => !svc.isTeamAlive(teamName)); expect((await readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).lanes).toEqual({}); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'alice', text: 'must not reach stopped pure opencode', messageId: 'msg-stopped-pure-opencode', }) ).resolves.toEqual({ delivered: false, reason: 'opencode_runtime_not_active', }); expect(adapter.messageInputs).toEqual([]); }); it('does not deliver direct OpenCode member messages while a pure OpenCode stop is in flight', async () => { const teamName = 'pure-opencode-direct-message-stop-in-flight-safe-e2e'; const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); let releaseStop: () => void = () => undefined; const stopRelease = new Promise((resolve) => { releaseStop = resolve; }); adapter.stop = (async (input) => { adapter.stopInputs.push(input); await stopRelease; return { runId: input.runId, teamName: input.teamName, stopped: true, members: {}, warnings: [], diagnostics: ['delayed fake stop'], }; }) as typeof adapter.stop; svc.stopTeam(teamName); await waitForCondition(() => adapter.stopInputs.length === 1); try { await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'alice', text: 'must not reach pure opencode while stop is in flight', messageId: 'msg-pure-opencode-stop-in-flight', }) ).resolves.toEqual({ delivered: false, reason: 'opencode_runtime_not_active', }); expect(adapter.messageInputs).toEqual([]); } finally { releaseStop(); await waitForCondition(() => !svc.isTeamAlive(teamName)); } }); it('does not deliver direct OpenCode member messages to removed pure OpenCode teammates', async () => { const teamName = 'pure-opencode-direct-message-removed-safe-e2e'; const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const { runId } = await svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [ { name: 'alice', role: 'Developer', providerId: 'opencode' }, { name: 'bob', role: 'Reviewer', providerId: 'opencode' }, ], }, () => undefined ); await writeOpenCodeMembersMeta(teamName, { members: ['alice', 'bob'], removedMembers: ['alice'], }); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'alice', text: 'must not reach removed pure alice', messageId: 'msg-removed-pure-alice', }) ).resolves.toEqual({ delivered: false, reason: 'recipient_removed', }); expect(adapter.messageInputs).toEqual([]); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'active pure bob still receives message', messageId: 'msg-active-pure-bob', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ runId, teamName, laneId: 'primary', memberName: 'bob', text: 'active pure bob still receives message', messageId: 'msg-active-pure-bob', }); }); it('delivers direct OpenCode member messages to re-added pure OpenCode teammates when meta is active', async () => { const teamName = 'pure-opencode-direct-message-readded-safe-e2e'; await writeOpenCodeTeamConfig({ teamName, projectPath, members: ['alice'], removedMembers: ['alice'], }); await writeOpenCodeMembersMeta(teamName, { members: ['alice'] }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'primary', state: 'active', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'alice', text: 're-added pure alice should receive message', messageId: 'msg-readded-pure-alice', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ teamName, laneId: 'primary', memberName: 'alice', cwd: projectPath, text: 're-added pure alice should receive message', messageId: 'msg-readded-pure-alice', }); }); it('delivers direct OpenCode member messages to recovered pure lanes with case-insensitive member input after service restart', async () => { const teamName = 'pure-opencode-direct-message-case-insensitive-recovered-safe-e2e'; await writeOpenCodeTeamConfig({ teamName, projectPath, members: ['alice'] }); await writeOpenCodeMembersMeta(teamName, { members: ['alice'] }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'primary', state: 'active', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'ALICE', text: 'case-insensitive alice reaches recovered pure lane', messageId: 'msg-case-insensitive-recovered-pure-alice', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ teamName, laneId: 'primary', memberName: 'alice', cwd: projectPath, text: 'case-insensitive alice reaches recovered pure lane', messageId: 'msg-case-insensitive-recovered-pure-alice', }); expect(adapter.messageInputs[0]?.runId).toBeUndefined(); }); it('does not deliver direct OpenCode member messages when members meta removed a pure teammate but config and lane index are stale active after service restart', async () => { const teamName = 'pure-opencode-direct-message-meta-removed-config-stale-safe-e2e'; await writeOpenCodeTeamConfig({ teamName, projectPath, members: ['alice', 'bob'] }); await writeOpenCodeMembersMeta(teamName, { members: ['alice', 'bob'], removedMembers: ['alice'], }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'primary', state: 'active', diagnostics: ['stale active primary lane while members meta removed alice'], }); const adapter = new FakeOpenCodeRuntimeAdapter(); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'alice', text: 'meta removed alice must not receive message despite stale active config', messageId: 'msg-meta-removed-config-stale-pure-alice', }) ).resolves.toEqual({ delivered: false, reason: 'recipient_removed', }); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'active pure bob still receives message despite stale removed sibling', messageId: 'msg-meta-removed-config-stale-pure-bob', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ teamName, laneId: 'primary', memberName: 'bob', cwd: projectPath, text: 'active pure bob still receives message despite stale removed sibling', messageId: 'msg-meta-removed-config-stale-pure-bob', }); expect(adapter.messageInputs[0]?.runId).toBeUndefined(); }); it('does not deliver direct OpenCode member messages to unknown pure OpenCode teammates', async () => { const teamName = 'pure-opencode-direct-message-unknown-safe-e2e'; const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await svc.createTeam( { teamName, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'ghost', text: 'must not reach unknown pure member', messageId: 'msg-unknown-pure-opencode', }) ).resolves.toEqual({ delivered: false, reason: 'recipient_is_not_opencode', }); expect(adapter.messageInputs).toEqual([]); }); it('does not deliver direct OpenCode member messages to degraded recovered pure OpenCode primary lanes', async () => { const teamName = 'pure-opencode-direct-message-degraded-lane-safe-e2e'; await writeOpenCodeTeamConfig({ teamName, projectPath, members: ['alice'] }); await writeOpenCodeMembersMeta(teamName, { members: ['alice'] }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'primary', state: 'degraded', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'alice', text: 'must not reach degraded pure opencode lane', messageId: 'msg-degraded-pure-opencode', }) ).resolves.toEqual({ delivered: false, reason: 'opencode_runtime_not_active', }); expect(adapter.messageInputs).toEqual([]); }); it('does not deliver direct OpenCode member messages to degraded pure OpenCode lanes despite stale terminal provisioning state', async () => { const teamName = 'pure-opencode-direct-message-degraded-stale-terminal-safe-e2e'; await writeOpenCodeTeamConfig({ teamName, projectPath, members: ['alice'] }); await writeOpenCodeMembersMeta(teamName, { members: ['alice'] }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'primary', state: 'degraded', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); injectStaleTerminalProvisioningRun(svc, teamName, `run-${teamName}-stale`); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'alice', text: 'must not reach degraded pure opencode despite stale terminal state', messageId: 'msg-degraded-pure-stale-terminal', }) ).resolves.toEqual({ delivered: false, reason: 'opencode_runtime_not_active', }); expect(adapter.messageInputs).toEqual([]); }); it('does not deliver direct OpenCode member messages to stopped mixed OpenCode secondary lanes', async () => { const teamName = 'mixed-opencode-direct-message-stopped-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); run.child = { kill: () => undefined }; trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); svc.stopTeam(teamName); await waitForCondition(() => adapter.stopInputs.length === 2); await waitForCondition(() => !svc.isTeamAlive(teamName)); await waitForCondition(async () => { const laneIndex = await readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName); return Object.keys(laneIndex.lanes).length === 0; }); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'must not reach stopped mixed opencode lane', messageId: 'msg-stopped-mixed-opencode', }) ).resolves.toEqual({ delivered: false, reason: 'opencode_runtime_not_active', }); expect(adapter.messageInputs).toEqual([]); }); it('does not scan or deliver orphaned mixed OpenCode lanes after app restart when team is stopped', async () => { const teamName = 'mixed-opencode-stopped-orphaned-lane-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:bob', state: 'active', diagnostics: ['orphaned lane from previous app session'], }); await setOpenCodeRuntimeActiveRunManifest({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:bob', runId: 'orphaned-opencode-run-bob', }); const inboxDir = path.join(getTeamsBasePath(), teamName, 'inboxes'); await fs.mkdir(inboxDir, { recursive: true }); await fs.writeFile( path.join(inboxDir, 'bob.json'), `${JSON.stringify( [ { from: 'user', to: 'bob', text: 'must not be delivered while parent team is stopped', timestamp: '2026-04-23T10:01:00.000Z', read: false, messageId: 'msg-stopped-orphaned-lane-bob', }, ], null, 2 )}\n`, 'utf8' ); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', }); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect(restartedService.scanOpenCodePromptDeliveryWatchdog(teamName)).resolves.toBe(0); expect(adapter.stopInputs).toHaveLength(1); expect(adapter.stopInputs[0]).toMatchObject({ runId: 'orphaned-opencode-run-bob', teamName, laneId: 'secondary:opencode:bob', providerId: 'opencode', reason: 'cleanup', force: true, }); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject({ lanes: {}, }); await expect( restartedService.relayOpenCodeMemberInboxMessages(teamName, 'bob') ).resolves.toMatchObject({ relayed: 0, failed: 1, lastDelivery: { delivered: false, reason: 'opencode_runtime_not_active' }, }); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'direct message must not reach orphaned lane', messageId: 'msg-stopped-orphaned-direct-bob', }) ).resolves.toEqual({ delivered: false, reason: 'opencode_runtime_not_active', }); expect(adapter.reconcileInputs).toEqual([]); expect(adapter.messageInputs).toEqual([]); }); it('does not recover missing mixed OpenCode lanes from persisted runtime evidence when parent team is stopped', async () => { const teamName = 'mixed-opencode-stopped-missing-lane-recovery-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await fs.writeFile( path.join(getTeamsBasePath(), teamName, 'launch-state.json'), `${JSON.stringify( createPersistedLaunchSnapshot({ teamName, expectedMembers: ['alice', 'bob'], leadSessionId: 'lead-session', launchPhase: 'reconciled', members: { alice: { name: 'alice', providerId: 'codex', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'codex', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, lastEvaluatedAt: '2026-04-23T10:00:00.000Z', }, bob: { name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'failed_to_start', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'OpenCode bridge reported member launch failure', runtimePid: 7743, runtimeSessionId: 'ses_bob_materialized', livenessKind: 'runtime_process_candidate', pidSource: 'opencode_bridge', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', }, }, updatedAt: '2026-04-23T10:00:00.000Z', }), null, 2 )}\n`, 'utf8' ); const adapter = new FakeOpenCodeRuntimeAdapter('partial_pending', { bob: 'launching', }); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName) ).resolves.toMatchObject({ lanes: {} }); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'must not recover stopped parent team', messageId: 'msg-stopped-missing-lane-bob', }) ).resolves.toEqual({ delivered: false, reason: 'opencode_runtime_not_active', }); expect(adapter.reconcileInputs).toEqual([]); expect(adapter.messageInputs).toEqual([]); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject({ lanes: {}, }); }); it('recovers a missing mixed OpenCode lane index from materialized persisted runtime evidence but blocks direct delivery until bootstrap', async () => { const teamName = 'mixed-opencode-direct-message-recovers-missing-lane-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await fs.writeFile( path.join(getTeamsBasePath(), teamName, 'launch-state.json'), `${JSON.stringify( createPersistedLaunchSnapshot({ teamName, expectedMembers: ['alice', 'bob', 'tom'], leadSessionId: 'lead-session', launchPhase: 'reconciled', members: { alice: { name: 'alice', providerId: 'codex', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'codex', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, lastEvaluatedAt: '2026-04-23T10:00:00.000Z', }, bob: { name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'failed_to_start', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'OpenCode bridge reported member launch failure', runtimePid: 7743, runtimeSessionId: 'ses_bob_materialized', livenessKind: 'runtime_process_candidate', pidSource: 'opencode_bridge', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', }, tom: { name: 'tom', providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'OpenCode bridge reported member launch failure', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', }, }, updatedAt: '2026-04-23T10:00:00.000Z', }), null, 2 )}\n`, 'utf8' ); const adapter = new FakeOpenCodeRuntimeAdapter('partial_pending', { bob: 'launching', tom: 'failed', }); await writeAliveProcessRegistry(teamName); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName) ).resolves.toMatchObject({ lanes: {} }); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'recovered bob receives direct message', messageId: 'msg-recovered-missing-lane-bob', }) ).resolves.toEqual({ delivered: false, reason: 'opencode_runtime_not_active', diagnostics: [ 'OpenCode runtime bootstrap is not confirmed for bob. Message was saved and will be retried after runtime check-in.', ], }); expect(adapter.reconcileInputs).toHaveLength(1); expect(adapter.reconcileInputs[0]).toMatchObject({ teamName, laneId: 'secondary:opencode:bob', reason: 'startup_recovery', }); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { 'secondary:opencode:bob': { state: 'active', }, }, } ); expect(adapter.messageInputs).toEqual([]); }); it('recovers a missing mixed OpenCode lane index from confirmed-alive persisted runtime evidence before direct delivery', async () => { const teamName = 'mixed-opencode-direct-message-recovers-confirmed-missing-lane-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await fs.writeFile( path.join(getTeamsBasePath(), teamName, 'launch-state.json'), `${JSON.stringify( createPersistedLaunchSnapshot({ teamName, expectedMembers: ['alice', 'bob'], leadSessionId: 'lead-session', launchPhase: 'reconciled', members: { alice: { name: 'alice', providerId: 'codex', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'codex', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, lastEvaluatedAt: '2026-04-23T10:00:00.000Z', }, bob: { name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, runtimePid: 7743, runtimeSessionId: 'ses_bob_confirmed_materialized', livenessKind: 'runtime_process', pidSource: 'opencode_bridge', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', }, }, updatedAt: '2026-04-23T10:00:00.000Z', }), null, 2 )}\n`, 'utf8' ); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', }); await writeOpenCodeBootstrapSessionEvidenceForTest({ teamName, laneId: 'secondary:opencode:bob', memberName: 'bob', runId: null, sessionId: 'ses_bob_confirmed_materialized', }); await writeAliveProcessRegistry(teamName); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName) ).resolves.toMatchObject({ lanes: {} }); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'confirmed alive missing lane recovers', messageId: 'msg-recovered-confirmed-missing-lane-bob', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.reconcileInputs).toHaveLength(1); expect(adapter.reconcileInputs[0]).toMatchObject({ teamName, laneId: 'secondary:opencode:bob', reason: 'startup_recovery', }); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { 'secondary:opencode:bob': { state: 'active', }, }, } ); expect(adapter.messageInputs[0]).toMatchObject({ teamName, laneId: 'secondary:opencode:bob', memberName: 'bob', text: 'confirmed alive missing lane recovers', messageId: 'msg-recovered-confirmed-missing-lane-bob', }); }); it('recovers a missing mixed OpenCode lane index before watchdog scans unread OpenCode inbox', async () => { const teamName = 'mixed-opencode-watchdog-recovers-missing-lane-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await fs.writeFile( path.join(getTeamsBasePath(), teamName, 'launch-state.json'), `${JSON.stringify( createPersistedLaunchSnapshot({ teamName, expectedMembers: ['alice', 'bob', 'tom'], leadSessionId: 'lead-session', launchPhase: 'reconciled', members: { alice: { name: 'alice', providerId: 'codex', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'codex', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, lastEvaluatedAt: '2026-04-23T10:00:00.000Z', }, bob: { name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'failed_to_start', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'OpenCode bridge reported member launch failure', runtimePid: 7743, runtimeSessionId: 'ses_bob_materialized', livenessKind: 'runtime_process_candidate', pidSource: 'opencode_bridge', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', }, tom: { name: 'tom', providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'OpenCode bridge reported member launch failure', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', }, }, updatedAt: '2026-04-23T10:00:00.000Z', }), null, 2 )}\n`, 'utf8' ); const inboxDir = path.join(getTeamsBasePath(), teamName, 'inboxes'); await fs.mkdir(inboxDir, { recursive: true }); await fs.writeFile( path.join(inboxDir, 'bob.json'), `${JSON.stringify( [ { from: 'user', to: 'bob', text: 'recover this unread OpenCode message', timestamp: '2026-04-23T10:01:00.000Z', read: false, messageId: 'msg-watchdog-recovers-missing-lane-bob', }, ], null, 2 )}\n`, 'utf8' ); const adapter = new FakeOpenCodeRuntimeAdapter('partial_pending', { bob: 'launching', tom: 'failed', }); await writeAliveProcessRegistry(teamName); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const scheduledWatchdogJobs: unknown[] = []; (restartedService as any).scheduleOpenCodePromptDeliveryWatchdog = (input: unknown): void => { scheduledWatchdogJobs.push(input); }; await expect( readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName) ).resolves.toMatchObject({ lanes: {} }); await expect(restartedService.scanOpenCodePromptDeliveryWatchdog(teamName)).resolves.toBe(1); expect(adapter.reconcileInputs).toHaveLength(1); expect(adapter.reconcileInputs[0]).toMatchObject({ teamName, laneId: 'secondary:opencode:bob', reason: 'startup_recovery', }); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { 'secondary:opencode:bob': { state: 'active', }, }, } ); expect(scheduledWatchdogJobs).toEqual([ expect.objectContaining({ teamName, memberName: 'bob', messageId: 'msg-watchdog-recovers-missing-lane-bob', delayMs: 500, }), ]); }); it('recovers a missing mixed OpenCode lane index from committed session evidence before watchdog scans unread inbox', async () => { const teamName = 'mixed-opencode-watchdog-recovers-session-evidence-safe-e2e'; const laneId = 'secondary:opencode:bob'; const runId = 'session-evidence-opencode-run'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await writeOpenCodeBootstrapSessionEvidenceForTest({ teamName, laneId, runId, memberName: 'bob', sessionId: 'ses_bob_committed_session_only', }); const inboxDir = path.join(getTeamsBasePath(), teamName, 'inboxes'); await fs.mkdir(inboxDir, { recursive: true }); await fs.writeFile( path.join(inboxDir, 'bob.json'), `${JSON.stringify( [ { from: 'user', to: 'bob', text: 'recover this unread OpenCode message from committed session evidence', timestamp: '2026-04-23T10:01:00.000Z', read: false, messageId: 'msg-watchdog-recovers-session-evidence-bob', }, ], null, 2 )}\n`, 'utf8' ); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed' }); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const scheduledWatchdogJobs: unknown[] = []; (restartedService as any).scheduleOpenCodePromptDeliveryWatchdog = (input: unknown): void => { scheduledWatchdogJobs.push(input); }; await expect( readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName) ).resolves.toMatchObject({ lanes: {} }); await expect(restartedService.scanOpenCodePromptDeliveryWatchdog(teamName)).resolves.toBe(1); expect(adapter.reconcileInputs).toHaveLength(1); expect(adapter.reconcileInputs[0]).toMatchObject({ teamName, laneId, reason: 'startup_recovery', }); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { [laneId]: { state: 'active', diagnostics: expect.arrayContaining([ 'Recovered missing OpenCode runtime lane index from committed session evidence.', ]), }, }, } ); expect(scheduledWatchdogJobs).toEqual([ expect.objectContaining({ teamName, memberName: 'bob', messageId: 'msg-watchdog-recovers-session-evidence-bob', delayMs: 500, }), ]); }); it('does not recover committed OpenCode session evidence when the parent process registry is explicitly stopped', async () => { const teamName = 'mixed-opencode-watchdog-stopped-session-evidence-safe-e2e'; const laneId = 'secondary:opencode:bob'; const runId = 'stopped-session-evidence-opencode-run'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await writeOpenCodeBootstrapSessionEvidenceForTest({ teamName, laneId, runId, memberName: 'bob', sessionId: 'ses_bob_stopped_committed_session', }); await writeStoppedProcessRegistry(teamName); const inboxDir = path.join(getTeamsBasePath(), teamName, 'inboxes'); await fs.mkdir(inboxDir, { recursive: true }); await fs.writeFile( path.join(inboxDir, 'bob.json'), `${JSON.stringify( [ { from: 'user', to: 'bob', text: 'must not recover this stopped OpenCode message', timestamp: '2026-04-23T10:01:00.000Z', read: false, messageId: 'msg-watchdog-stopped-session-evidence-bob', }, ], null, 2 )}\n`, 'utf8' ); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed' }); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect(restartedService.scanOpenCodePromptDeliveryWatchdog(teamName)).resolves.toBe(0); expect(adapter.reconcileInputs).toEqual([]); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: {}, } ); }); it('recovers one missing mixed OpenCode lane before watchdog scans while sibling lane is active', async () => { const teamName = 'mixed-opencode-watchdog-recovers-one-missing-lane-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await fs.writeFile( path.join(getTeamsBasePath(), teamName, 'launch-state.json'), `${JSON.stringify( createPersistedLaunchSnapshot({ teamName, expectedMembers: ['alice', 'bob', 'tom'], leadSessionId: 'lead-session', launchPhase: 'reconciled', members: { alice: { name: 'alice', providerId: 'codex', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'codex', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, lastEvaluatedAt: '2026-04-23T10:00:00.000Z', }, bob: { name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'failed_to_start', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'OpenCode bridge reported member launch failure', runtimePid: 7743, runtimeSessionId: 'ses_bob_materialized', livenessKind: 'runtime_process_candidate', pidSource: 'opencode_bridge', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', }, tom: { name: 'tom', providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, runtimePid: 7750, runtimeSessionId: 'ses_tom_active', livenessKind: 'runtime_process', pidSource: 'opencode_bridge', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', }, }, updatedAt: '2026-04-23T10:00:00.000Z', }), null, 2 )}\n`, 'utf8' ); await upsertActiveOpenCodeRuntimeLaneForTest({ teamName, laneId: 'secondary:opencode:tom', }); const inboxDir = path.join(getTeamsBasePath(), teamName, 'inboxes'); await fs.mkdir(inboxDir, { recursive: true }); await fs.writeFile( path.join(inboxDir, 'bob.json'), `${JSON.stringify( [ { from: 'user', to: 'bob', text: 'recover only bob while tom stays active', timestamp: '2026-04-23T10:01:00.000Z', read: false, messageId: 'msg-watchdog-recovers-one-missing-lane-bob', }, ], null, 2 )}\n`, 'utf8' ); const adapter = new FakeOpenCodeRuntimeAdapter('partial_pending', { bob: 'launching', tom: 'confirmed', }); await writeAliveProcessRegistry(teamName); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const scheduledWatchdogJobs: unknown[] = []; (restartedService as any).scheduleOpenCodePromptDeliveryWatchdog = (input: unknown): void => { scheduledWatchdogJobs.push(input); }; await expect(restartedService.scanOpenCodePromptDeliveryWatchdog(teamName)).resolves.toBe(1); expect(adapter.reconcileInputs).toHaveLength(1); expect(adapter.reconcileInputs[0]).toMatchObject({ teamName, laneId: 'secondary:opencode:bob', reason: 'startup_recovery', }); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { 'secondary:opencode:bob': { state: 'active', }, 'secondary:opencode:tom': { state: 'active', }, }, } ); expect(scheduledWatchdogJobs).toEqual([ expect.objectContaining({ teamName, memberName: 'bob', messageId: 'msg-watchdog-recovers-one-missing-lane-bob', delayMs: 500, }), ]); }); it('does not recover a missing mixed OpenCode lane index from liveness-only persisted metadata', async () => { const teamName = 'mixed-opencode-direct-message-liveness-only-missing-lane-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await fs.writeFile( path.join(getTeamsBasePath(), teamName, 'launch-state.json'), `${JSON.stringify( createPersistedLaunchSnapshot({ teamName, expectedMembers: ['alice', 'bob'], leadSessionId: 'lead-session', launchPhase: 'reconciled', members: { alice: { name: 'alice', providerId: 'codex', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'codex', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, lastEvaluatedAt: '2026-04-23T10:00:00.000Z', }, bob: { name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'failed_to_start', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'OpenCode bridge reported member launch failure', livenessKind: 'runtime_process_candidate', pidSource: 'opencode_bridge', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', }, }, updatedAt: '2026-04-23T10:00:00.000Z', }), null, 2 )}\n`, 'utf8' ); const adapter = new FakeOpenCodeRuntimeAdapter('partial_pending', { bob: 'launching', }); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'must not recover from liveness-only stale metadata', messageId: 'msg-liveness-only-missing-lane-bob', }) ).resolves.toEqual({ delivered: false, reason: 'opencode_runtime_not_active', }); expect(adapter.reconcileInputs).toEqual([]); expect(adapter.messageInputs).toEqual([]); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: {} } ); }); it('does not deliver direct OpenCode member messages to one detached mixed lane while its sibling lane stays live', async () => { const teamName = 'mixed-opencode-direct-message-one-detached-lane-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); run.child = { kill: () => undefined }; trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await svc.detachOpenCodeOwnedMemberLane(teamName, 'bob'); expect(adapter.stopInputs).toHaveLength(1); expect(adapter.stopInputs[0]).toMatchObject({ runId: adapter.launchInputs.find((input) => input.laneId === 'secondary:opencode:bob') ?.runId, teamName, laneId: 'secondary:opencode:bob', reason: 'cleanup', }); expect( run.mixedSecondaryLanes.map((lane: { member: { name: string } }) => lane.member.name) ).toEqual(['tom']); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { 'secondary:opencode:tom': { state: 'active' }, }, } ); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'must not reach the detached mixed bob lane', messageId: 'msg-one-detached-mixed-bob', }) ).resolves.toEqual({ delivered: false, reason: 'opencode_runtime_not_active', }); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'tom', text: 'active mixed tom lane still receives direct message', messageId: 'msg-one-detached-mixed-tom', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ runId: latestOpenCodeLaunchRunId(adapter, teamName, 'secondary:opencode:tom'), teamName, laneId: 'secondary:opencode:tom', memberName: 'tom', cwd: projectPath, text: 'active mixed tom lane still receives direct message', messageId: 'msg-one-detached-mixed-tom', }); }); it('delivers direct OpenCode member messages to live mixed lanes with case-insensitive member input', async () => { const teamName = 'mixed-opencode-direct-message-live-case-insensitive-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); run.child = { kill: () => undefined }; trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'BOB', text: 'case-insensitive bob reaches live mixed lane', messageId: 'msg-live-case-insensitive-mixed-bob', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ runId: latestOpenCodeLaunchRunId(adapter, teamName, 'secondary:opencode:bob'), teamName, laneId: 'secondary:opencode:bob', memberName: 'bob', cwd: projectPath, text: 'case-insensitive bob reaches live mixed lane', messageId: 'msg-live-case-insensitive-mixed-bob', }); }); it('does not let stale active lane index resurrect direct OpenCode messages to a detached mixed lane', async () => { const teamName = 'mixed-opencode-direct-message-detached-stale-index-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); run.child = { kill: () => undefined }; trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await svc.detachOpenCodeOwnedMemberLane(teamName, 'bob'); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:bob', state: 'active', diagnostics: ['stale active lane index entry'], }); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { 'secondary:opencode:bob': { state: 'active' }, 'secondary:opencode:tom': { state: 'active' }, }, } ); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'stale active lane index must not revive detached bob', messageId: 'msg-detached-stale-index-bob', }) ).resolves.toEqual({ delivered: false, reason: 'opencode_runtime_not_active', }); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'tom', text: 'live tom still receives message despite stale bob index', messageId: 'msg-detached-stale-index-tom', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ runId: latestOpenCodeLaunchRunId(adapter, teamName, 'secondary:opencode:tom'), teamName, laneId: 'secondary:opencode:tom', memberName: 'tom', cwd: projectPath, text: 'live tom still receives message despite stale bob index', messageId: 'msg-detached-stale-index-tom', }); }); it('delivers direct OpenCode member messages to a reattached mixed lane after detach rejected stale delivery', async () => { const teamName = 'mixed-opencode-direct-message-reattached-lane-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); run.child = { kill: () => undefined }; trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await svc.detachOpenCodeOwnedMemberLane(teamName, 'bob'); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'detached bob must not receive message before reattach', messageId: 'msg-detached-before-reattach-bob', }) ).resolves.toEqual({ delivered: false, reason: 'opencode_runtime_not_active', }); expect(adapter.messageInputs).toEqual([]); await svc.reattachOpenCodeOwnedMemberLane(teamName, 'bob', { reason: 'member_updated' }); await waitForCondition(() => adapter.launchInputs.length === 3); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); expect( run.mixedSecondaryLanes .map((lane: { member: { name: string } }) => lane.member.name) .sort() ).toEqual(['bob', 'tom']); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { 'secondary:opencode:bob': { state: 'active' }, 'secondary:opencode:tom': { state: 'active' }, }, } ); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'reattached bob receives direct message', messageId: 'msg-reattached-mixed-bob', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ runId: latestOpenCodeLaunchRunId(adapter, teamName, 'secondary:opencode:bob'), teamName, laneId: 'secondary:opencode:bob', memberName: 'bob', cwd: projectPath, text: 'reattached bob receives direct message', messageId: 'msg-reattached-mixed-bob', }); }); it('keeps direct OpenCode member messages scoped when detaching one of two mixed teams with the same member name', async () => { const firstTeamName = 'mixed-opencode-direct-detach-cross-team-a-safe-e2e'; const secondTeamName = 'mixed-opencode-direct-detach-cross-team-b-safe-e2e'; await writeMixedTeamConfig({ teamName: firstTeamName, projectPath }); await writeTeamMeta(firstTeamName, projectPath); await writeMembersMeta(firstTeamName); await writeMixedTeamConfig({ teamName: secondTeamName, projectPath }); await writeTeamMeta(secondTeamName, projectPath); await writeMembersMeta(secondTeamName); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const firstRun = createMixedLiveRun({ teamName: firstTeamName, projectPath }); const secondRun = createMixedLiveRun({ teamName: secondTeamName, projectPath }); firstRun.child = { kill: () => undefined }; secondRun.child = { kill: () => undefined }; trackLiveRun(svc, firstRun); trackLiveRun(svc, secondRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(firstRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(secondRun); await waitForCondition(() => adapter.launchInputs.length === 4); await waitForCondition(() => firstRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await waitForCondition(() => secondRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await svc.detachOpenCodeOwnedMemberLane(secondTeamName, 'bob'); expect(adapter.stopInputs).toHaveLength(1); expect(adapter.stopInputs[0]).toMatchObject({ teamName: secondTeamName, laneId: 'secondary:opencode:bob', reason: 'cleanup', }); expect( firstRun.mixedSecondaryLanes.map((lane: { member: { name: string } }) => lane.member.name) ).toEqual(['bob', 'tom']); expect( secondRun.mixedSecondaryLanes.map((lane: { member: { name: string } }) => lane.member.name) ).toEqual(['tom']); await expect( svc.deliverOpenCodeMemberMessage(firstTeamName, { memberName: 'bob', text: 'first team bob still receives direct message', messageId: 'msg-cross-team-detach-first-bob', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); await expect( svc.deliverOpenCodeMemberMessage(secondTeamName, { memberName: 'bob', text: 'second team detached bob must not receive direct message', messageId: 'msg-cross-team-detach-second-bob', }) ).resolves.toEqual({ delivered: false, reason: 'opencode_runtime_not_active', }); await expect( svc.deliverOpenCodeMemberMessage(secondTeamName, { memberName: 'tom', text: 'second team tom still receives direct message', messageId: 'msg-cross-team-detach-second-tom', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(2); expect(adapter.messageInputs[0]).toMatchObject({ runId: latestOpenCodeLaunchRunId( adapter, firstTeamName, 'secondary:opencode:bob' ), teamName: firstTeamName, laneId: 'secondary:opencode:bob', memberName: 'bob', cwd: projectPath, text: 'first team bob still receives direct message', messageId: 'msg-cross-team-detach-first-bob', }); expect(adapter.messageInputs[1]).toMatchObject({ runId: latestOpenCodeLaunchRunId( adapter, secondTeamName, 'secondary:opencode:tom' ), teamName: secondTeamName, laneId: 'secondary:opencode:tom', memberName: 'tom', cwd: projectPath, text: 'second team tom still receives direct message', messageId: 'msg-cross-team-detach-second-tom', }); }); it('does not deliver direct OpenCode member messages while mixed OpenCode stop is in flight', async () => { const teamName = 'mixed-opencode-direct-message-stop-in-flight-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); run.child = { kill: () => undefined }; trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); let releaseStop: () => void = () => undefined; const stopRelease = new Promise((resolve) => { releaseStop = resolve; }); adapter.stop = (async (input) => { adapter.stopInputs.push(input); await stopRelease; return { runId: input.runId, teamName: input.teamName, stopped: true, members: {}, warnings: [], diagnostics: ['delayed fake stop'], }; }) as typeof adapter.stop; svc.stopTeam(teamName); await waitForCondition(() => adapter.stopInputs.length === 1); try { await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'must not reach mixed opencode while stop is in flight', messageId: 'msg-mixed-opencode-stop-in-flight', }) ).resolves.toEqual({ delivered: false, reason: 'opencode_runtime_not_active', }); expect(adapter.messageInputs).toEqual([]); } finally { releaseStop(); await waitForCondition(() => adapter.stopInputs.length === 2); await waitForCondition(() => !svc.isTeamAlive(teamName)); } }); it('delivers direct OpenCode member messages to recovered mixed OpenCode secondary lanes after service restart', async () => { const teamName = 'mixed-opencode-direct-message-recovered-lane-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await upsertActiveOpenCodeRuntimeLaneForTest({ teamName, laneId: 'secondary:opencode:bob', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'message recovered mixed opencode lane', messageId: 'msg-recovered-mixed-opencode', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ teamName, laneId: 'secondary:opencode:bob', memberName: 'bob', cwd: projectPath, text: 'message recovered mixed opencode lane', messageId: 'msg-recovered-mixed-opencode', }); expect(adapter.messageInputs[0]?.runId).toBeUndefined(); }); it('delivers direct OpenCode member messages after recovering a missing mixed lane from committed session evidence', async () => { const teamName = 'mixed-opencode-direct-message-committed-session-recovery-safe-e2e'; const laneId = 'secondary:opencode:bob'; const runId = 'committed-session-direct-opencode-run'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await writeOpenCodeBootstrapSessionEvidenceForTest({ teamName, laneId, runId, memberName: 'bob', sessionId: 'ses_bob_direct_committed_session', }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed' }); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'message recovered from committed session evidence', messageId: 'msg-recovered-committed-session-mixed-opencode', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.reconcileInputs).toHaveLength(1); expect(adapter.reconcileInputs[0]).toMatchObject({ teamName, laneId, reason: 'startup_recovery', }); expect(adapter.reconcileInputs[0]?.runId).toEqual(expect.any(String)); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ runId, teamName, laneId, memberName: 'bob', cwd: projectPath, text: 'message recovered from committed session evidence', messageId: 'msg-recovered-committed-session-mixed-opencode', }); }); it('does not deliver direct OpenCode member messages to a removed mixed teammate despite stale active lane index after service restart', async () => { const teamName = 'mixed-opencode-direct-message-removed-stale-index-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, removedMembers: ['bob'] }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName, { removedMembers: ['bob'] }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:bob', state: 'active', diagnostics: ['stale removed bob lane index entry'], }); await upsertActiveOpenCodeRuntimeLaneForTest({ teamName, laneId: 'secondary:opencode:tom', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'removed bob must not receive message despite stale active lane index', messageId: 'msg-removed-stale-index-mixed-bob', }) ).resolves.toEqual({ delivered: false, reason: 'recipient_removed', }); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'tom', text: 'active tom still receives message despite removed bob stale index', messageId: 'msg-removed-stale-index-mixed-tom', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ teamName, laneId: 'secondary:opencode:tom', memberName: 'tom', cwd: projectPath, text: 'active tom still receives message despite removed bob stale index', messageId: 'msg-removed-stale-index-mixed-tom', }); expect(adapter.messageInputs[0]?.runId).toBeUndefined(); }); it('does not deliver direct OpenCode member messages when members meta removed a mixed teammate but config and lane index are stale active after service restart', async () => { const teamName = 'mixed-opencode-direct-message-meta-removed-config-stale-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName, { removedMembers: ['bob'] }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:bob', state: 'active', diagnostics: ['stale active lane index while members meta removed bob'], }); await upsertActiveOpenCodeRuntimeLaneForTest({ teamName, laneId: 'secondary:opencode:tom', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'meta removed bob must not receive message despite stale active config', messageId: 'msg-meta-removed-config-stale-mixed-bob', }) ).resolves.toEqual({ delivered: false, reason: 'recipient_removed', }); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'tom', text: 'active tom still receives message despite stale removed sibling', messageId: 'msg-meta-removed-config-stale-mixed-tom', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ teamName, laneId: 'secondary:opencode:tom', memberName: 'tom', cwd: projectPath, text: 'active tom still receives message despite stale removed sibling', messageId: 'msg-meta-removed-config-stale-mixed-tom', }); expect(adapter.messageInputs[0]?.runId).toBeUndefined(); }); it('does not let an orphan active mixed OpenCode lane index entry create a direct message recipient after service restart', async () => { const teamName = 'mixed-opencode-direct-message-orphan-lane-index-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:ghost', state: 'active', diagnostics: ['orphan active lane index entry without roster member'], }); await upsertActiveOpenCodeRuntimeLaneForTest({ teamName, laneId: 'secondary:opencode:tom', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'ghost', text: 'orphan active lane index must not create ghost recipient', messageId: 'msg-orphan-lane-index-ghost', }) ).resolves.toEqual({ delivered: false, reason: 'recipient_is_not_opencode', }); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'tom', text: 'active tom still receives message despite orphan sibling lane', messageId: 'msg-orphan-lane-index-tom', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ teamName, laneId: 'secondary:opencode:tom', memberName: 'tom', cwd: projectPath, text: 'active tom still receives message despite orphan sibling lane', messageId: 'msg-orphan-lane-index-tom', }); expect(adapter.messageInputs[0]?.runId).toBeUndefined(); }); it('delivers direct OpenCode member messages to recovered mixed lanes with case-insensitive member input after service restart', async () => { const teamName = 'mixed-opencode-direct-message-case-insensitive-recovered-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await upsertActiveOpenCodeRuntimeLaneForTest({ teamName, laneId: 'secondary:opencode:bob', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'BOB', text: 'case-insensitive bob reaches recovered mixed lane', messageId: 'msg-case-insensitive-recovered-mixed-bob', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ teamName, laneId: 'secondary:opencode:bob', memberName: 'bob', cwd: projectPath, text: 'case-insensitive bob reaches recovered mixed lane', messageId: 'msg-case-insensitive-recovered-mixed-bob', }); expect(adapter.messageInputs[0]?.runId).toBeUndefined(); }); it('delivers direct OpenCode member messages after a removed mixed teammate is reattached with a stale active lane index', async () => { const teamName = 'mixed-opencode-direct-message-removed-reattached-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, removedMembers: ['bob'] }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName, { removedMembers: ['bob'] }); await upsertActiveOpenCodeRuntimeLaneForTest({ teamName, laneId: 'secondary:opencode:bob', diagnostics: ['stale active lane index while bob was removed'], }); const adapter = new FakeOpenCodeRuntimeAdapter(); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'removed bob must not receive message before reattach', messageId: 'msg-removed-before-reattach-mixed-bob', }) ).resolves.toEqual({ delivered: false, reason: 'recipient_removed', }); expect(adapter.messageInputs).toEqual([]); await writeMixedTeamConfig({ teamName, projectPath }); await writeMembersMeta(teamName); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'reattached bob receives message after removed state is cleared', messageId: 'msg-removed-reattached-mixed-bob', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ teamName, laneId: 'secondary:opencode:bob', memberName: 'bob', cwd: projectPath, text: 'reattached bob receives message after removed state is cleared', messageId: 'msg-removed-reattached-mixed-bob', }); expect(adapter.messageInputs[0]?.runId).toBeUndefined(); }); it('delivers direct OpenCode member messages to recovered mixed OpenCode lanes despite stale terminal provisioning state', async () => { const teamName = 'mixed-opencode-direct-message-recovered-stale-terminal-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await upsertActiveOpenCodeRuntimeLaneForTest({ teamName, laneId: 'secondary:opencode:bob', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); injectStaleTerminalProvisioningRun(svc, teamName, `run-${teamName}-stale`); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'message recovered mixed lane despite stale terminal state', messageId: 'msg-recovered-mixed-stale-terminal', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ teamName, laneId: 'secondary:opencode:bob', memberName: 'bob', cwd: projectPath, text: 'message recovered mixed lane despite stale terminal state', messageId: 'msg-recovered-mixed-stale-terminal', }); expect(adapter.messageInputs[0]?.runId).toBeUndefined(); }); it('delivers direct OpenCode member messages to recovered mixed OpenCode lanes when config is missing', async () => { const teamName = 'mixed-opencode-direct-message-meta-only-recovered-safe-e2e'; await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName, { memberCwd: projectPath }); await upsertActiveOpenCodeRuntimeLaneForTest({ teamName, laneId: 'secondary:opencode:bob', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'message mixed opencode recovered from meta only', messageId: 'msg-meta-only-mixed-opencode', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ teamName, laneId: 'secondary:opencode:bob', memberName: 'bob', cwd: projectPath, text: 'message mixed opencode recovered from meta only', messageId: 'msg-meta-only-mixed-opencode', }); expect(adapter.messageInputs[0]?.runId).toBeUndefined(); }); it('keeps recovered mixed OpenCode direct messages isolated across teams with the same member name', async () => { const activeTeamName = 'mixed-opencode-direct-message-cross-team-active-safe-e2e'; const degradedTeamName = 'mixed-opencode-direct-message-cross-team-degraded-safe-e2e'; const activeProjectPath = path.join(tempDir, 'project-active-mixed'); const degradedProjectPath = path.join(tempDir, 'project-degraded-mixed'); await fs.mkdir(activeProjectPath, { recursive: true }); await fs.mkdir(degradedProjectPath, { recursive: true }); await writeTeamMeta(activeTeamName, activeProjectPath); await writeMembersMeta(activeTeamName, { memberCwd: activeProjectPath }); await upsertActiveOpenCodeRuntimeLaneForTest({ teamName: activeTeamName, laneId: 'secondary:opencode:bob', }); await writeTeamMeta(degradedTeamName, degradedProjectPath); await writeMembersMeta(degradedTeamName, { memberCwd: degradedProjectPath }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName: degradedTeamName, laneId: 'secondary:opencode:bob', state: 'degraded', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( svc.deliverOpenCodeMemberMessage(activeTeamName, { memberName: 'bob', text: 'message only active mixed team', messageId: 'msg-cross-team-active-mixed', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); await expect( svc.deliverOpenCodeMemberMessage(degradedTeamName, { memberName: 'bob', text: 'must not reach degraded mixed team', messageId: 'msg-cross-team-degraded-mixed', }) ).resolves.toEqual({ delivered: false, reason: 'opencode_runtime_not_active', }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ teamName: activeTeamName, laneId: 'secondary:opencode:bob', memberName: 'bob', cwd: activeProjectPath, text: 'message only active mixed team', messageId: 'msg-cross-team-active-mixed', }); }); it('does not deliver direct OpenCode member messages to degraded recovered mixed OpenCode lanes', async () => { const teamName = 'mixed-opencode-direct-message-degraded-lane-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:bob', state: 'degraded', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const restartedService = new TeamProvisioningService(); restartedService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await expect( restartedService.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'must not reach degraded mixed opencode lane', messageId: 'msg-degraded-mixed-opencode', }) ).resolves.toEqual({ delivered: false, reason: 'opencode_runtime_not_active', }); expect(adapter.messageInputs).toEqual([]); }); it('does not deliver direct OpenCode member messages to degraded mixed OpenCode lanes despite stale terminal provisioning state', async () => { const teamName = 'mixed-opencode-direct-message-degraded-stale-terminal-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:bob', state: 'degraded', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); injectStaleTerminalProvisioningRun(svc, teamName, `run-${teamName}-stale`); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'must not reach degraded mixed opencode despite stale terminal state', messageId: 'msg-degraded-mixed-stale-terminal', }) ).resolves.toEqual({ delivered: false, reason: 'opencode_runtime_not_active', }); expect(adapter.messageInputs).toEqual([]); }); it('does not deliver direct OpenCode member messages to removed Anthropic and Gemini mixed teammates', async () => { const teamName = 'mixed-anthropic-gemini-direct-message-removed-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', removedMembers: ['bob'], }); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); addGeminiPrimaryToMixedRun(run); markMixedOpenCodeLaneConfirmedForTest(run, 'tom'); trackLiveRun(svc, run); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 'must not reach removed bob', messageId: 'msg-removed-bob', }) ).resolves.toEqual({ delivered: false, reason: 'recipient_removed', }); expect(adapter.messageInputs).toEqual([]); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'tom', text: 'active tom still receives direct opencode message', messageId: 'msg-active-tom', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ runId: run.runId, teamName, laneId: 'secondary:opencode:tom', memberName: 'tom', cwd: projectPath, text: 'active tom still receives direct opencode message', messageId: 'msg-active-tom', }); }); it('delivers direct OpenCode member messages to re-added Anthropic and Gemini mixed teammates when meta is active', async () => { const teamName = 'mixed-anthropic-gemini-direct-message-readded-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', removedMembers: ['bob'], }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); addGeminiPrimaryToMixedRun(run); markMixedOpenCodeLaneConfirmedForTest(run, 'bob'); trackLiveRun(svc, run); await expect( svc.deliverOpenCodeMemberMessage(teamName, { memberName: 'bob', text: 're-added bob should receive direct opencode message', messageId: 'msg-readded-bob', }) ).resolves.toEqual({ delivered: true, diagnostics: [], }); expect(adapter.messageInputs).toHaveLength(1); expect(adapter.messageInputs[0]).toMatchObject({ runId: run.runId, teamName, laneId: 'secondary:opencode:bob', memberName: 'bob', cwd: projectPath, text: 're-added bob should receive direct opencode message', messageId: 'msg-readded-bob', }); }); it('stops the current Anthropic and Gemini mixed run instead of a stale same-team run', async () => { const teamName = 'mixed-anthropic-gemini-stop-current-run-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const staleRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); const currentRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); addGeminiPrimaryToMixedRun(staleRun); addGeminiPrimaryToMixedRun(currentRun); staleRun.runId = `run-${teamName}-stale`; currentRun.runId = `run-${teamName}-current`; let staleKillCount = 0; let currentKillCount = 0; staleRun.child = { pid: 64901, kill: () => (staleKillCount += 1), stdin: { writable: true } }; currentRun.child = { pid: 64902, kill: () => (currentKillCount += 1), stdin: { writable: true }, }; trackLiveRun(svc, staleRun); trackLiveRun(svc, currentRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(currentRun); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); svc.stopTeam(teamName); await waitForCondition(() => adapter.stopInputs.length === 1); expectDirectChildKillCount(staleKillCount, 0); expectDirectChildKillCount(currentKillCount, 1); expect(staleRun.cancelRequested).toBe(false); expect(currentRun.cancelRequested).toBe(true); expect(adapter.stopInputs.map((input) => input.laneId).sort()).toEqual([ 'secondary:opencode:bob', ]); expect(await svc.getRuntimeState(teamName)).toMatchObject({ teamName, isAlive: false, runId: null, progress: null, }); }); it('cancels a stale Anthropic and Gemini mixed run without stopping current OpenCode lanes', async () => { const teamName = 'mixed-anthropic-gemini-cancel-stale-run-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const staleRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); const currentRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); addGeminiPrimaryToMixedRun(staleRun); addGeminiPrimaryToMixedRun(currentRun); staleRun.runId = `run-${teamName}-stale`; currentRun.runId = `run-${teamName}-current`; let staleKillCount = 0; let currentKillCount = 0; staleRun.child = { pid: 65001, kill: () => (staleKillCount += 1), stdin: { writable: true } }; currentRun.child = { pid: 65002, kill: () => (currentKillCount += 1), stdin: createWritableStdin([]), }; trackLiveRun(svc, staleRun); trackLiveRun(svc, currentRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(currentRun); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); await svc.cancelProvisioning(staleRun.runId); expectDirectChildKillCount(staleKillCount, 1); expectDirectChildKillCount(currentKillCount, 0); expect(staleRun.cancelRequested).toBe(true); expect(currentRun.cancelRequested).toBe(false); expect(adapter.stopInputs).toEqual([]); expect(svc.isTeamAlive(teamName)).toBe(true); expect(await svc.getRuntimeState(teamName)).toMatchObject({ teamName, isAlive: true, runId: currentRun.runId, }); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => currentRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.reviewer).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('stops the current pure Anthropic run instead of a stale same-team run', async () => { const teamName = 'pure-anthropic-stop-current-run-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const staleRun = createPureAnthropicLiveRun({ teamName, projectPath }); const currentRun = createPureAnthropicLiveRun({ teamName, projectPath }); staleRun.runId = `run-${teamName}-stale`; currentRun.runId = `run-${teamName}-current`; let staleKillCount = 0; let currentKillCount = 0; staleRun.child = { pid: 63101, kill: () => (staleKillCount += 1), stdin: { writable: true } }; currentRun.child = { pid: 63102, kill: () => (currentKillCount += 1), stdin: { writable: true }, }; trackLiveRun(svc, staleRun); trackLiveRun(svc, currentRun); expect(await svc.getRuntimeState(teamName)).toMatchObject({ teamName, isAlive: true, runId: currentRun.runId, }); svc.stopTeam(teamName); expectDirectChildKillCount(staleKillCount, 0); expectDirectChildKillCount(currentKillCount, 1); expect(staleRun.cancelRequested).toBe(false); expect(currentRun.cancelRequested).toBe(true); expect(await svc.getRuntimeState(teamName)).toMatchObject({ teamName, isAlive: false, runId: null, progress: null, }); }); it('cancels a stale pure Anthropic run without stopping the current same-team run', async () => { const teamName = 'pure-anthropic-cancel-stale-run-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const staleRun = createPureAnthropicLiveRun({ teamName, projectPath }); const currentRun = createPureAnthropicLiveRun({ teamName, projectPath }); staleRun.runId = `run-${teamName}-stale`; currentRun.runId = `run-${teamName}-current`; let staleKillCount = 0; let currentKillCount = 0; staleRun.child = { pid: 63301, kill: () => (staleKillCount += 1), stdin: { writable: true } }; currentRun.child = { pid: 63302, kill: () => (currentKillCount += 1), stdin: createWritableStdin([]), }; trackLiveRun(svc, staleRun); trackLiveRun(svc, currentRun); await svc.cancelProvisioning(staleRun.runId); expectDirectChildKillCount(staleKillCount, 1); expectDirectChildKillCount(currentKillCount, 0); expect(staleRun.cancelRequested).toBe(true); expect(currentRun.cancelRequested).toBe(false); expect(svc.isTeamAlive(teamName)).toBe(true); expect(await svc.getRuntimeState(teamName)).toMatchObject({ teamName, isAlive: true, runId: currentRun.runId, }); await svc.sendMessageToTeam(teamName, 'current run still receives messages'); }); it('cancels the current pure Anthropic run without resurrecting a stale same-team run', async () => { const teamName = 'pure-anthropic-cancel-current-no-stale-resurrect-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const staleRun = createPureAnthropicLiveRun({ teamName, projectPath }); const currentRun = createPureAnthropicLiveRun({ teamName, projectPath }); staleRun.runId = `run-${teamName}-stale`; currentRun.runId = `run-${teamName}-current`; let staleKillCount = 0; let currentKillCount = 0; staleRun.child = { pid: 63501, kill: () => (staleKillCount += 1), stdin: { writable: true } }; currentRun.child = { pid: 63502, kill: () => (currentKillCount += 1), stdin: createWritableStdin([]), }; trackLiveRun(svc, staleRun); trackLiveRun(svc, currentRun); await svc.cancelProvisioning(currentRun.runId); expectDirectChildKillCount(staleKillCount, 0); expectDirectChildKillCount(currentKillCount, 1); expect(staleRun.cancelRequested).toBe(false); expect(currentRun.cancelRequested).toBe(true); expect(svc.isTeamAlive(teamName)).toBe(false); expect(await svc.getRuntimeState(teamName)).toMatchObject({ teamName, isAlive: false, runId: null, progress: null, }); await expect(svc.sendMessageToTeam(teamName, 'must not hit stale run')).rejects.toThrow( `No active process for team "${teamName}"` ); }); it('refreshes runtime snapshot cache after same-team pure Anthropic relaunch', async () => { const teamName = 'pure-anthropic-runtime-cache-relaunch-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const staleRun = createPureAnthropicLiveRun({ teamName, projectPath }); staleRun.runId = `run-${teamName}-stale`; staleRun.child = { pid: 64101, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, staleRun); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ ['alice', { alive: true, pid: 64102, model: 'haiku-stale' }], ['bob', { alive: true, pid: 64103, model: 'sonnet-stale' }], ]); (svc as any).readProcessRssBytesByPid = async (pids: number[]) => new Map(pids.map((pid) => [pid, pid * 1_000])); const staleSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(staleSnapshot).toMatchObject({ runId: staleRun.runId, members: { 'team-lead': { pid: 64101, rssBytes: 64_101_000 }, alice: { pid: 64102, rssBytes: 64_102_000, runtimeModel: 'haiku-stale' }, bob: { pid: 64103, rssBytes: 64_103_000, runtimeModel: 'sonnet-stale' }, }, }); const currentRun = createPureAnthropicLiveRun({ teamName, projectPath }); currentRun.runId = `run-${teamName}-current`; currentRun.child = { pid: 64201, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, currentRun); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ ['alice', { alive: true, pid: 64202, model: 'haiku-current' }], ['bob', { alive: true, pid: 64203, model: 'sonnet-current' }], ]); const currentSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(currentSnapshot).toMatchObject({ runId: currentRun.runId, members: { 'team-lead': { pid: 64201, rssBytes: 64_201_000 }, alice: { pid: 64202, rssBytes: 64_202_000, runtimeModel: 'haiku-current' }, bob: { pid: 64203, rssBytes: 64_203_000, runtimeModel: 'sonnet-current' }, }, }); }); it('does not reuse stopped pure Anthropic runtime cache after relaunch', async () => { const teamName = 'pure-anthropic-runtime-cache-stop-relaunch-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const firstRun = createPureAnthropicLiveRun({ teamName, projectPath }); firstRun.runId = `run-${teamName}-first`; firstRun.child = { pid: 64501, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, firstRun); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ ['alice', { alive: true, pid: 64502, model: 'haiku-before-stop' }], ['bob', { alive: true, pid: 64503, model: 'sonnet-before-stop' }], ]); (svc as any).readProcessRssBytesByPid = async (pids: number[]) => new Map(pids.map((pid) => [pid, pid * 1_000])); const beforeStop = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(beforeStop).toMatchObject({ runId: firstRun.runId, members: { 'team-lead': { pid: 64501, rssBytes: 64_501_000 }, alice: { pid: 64502, rssBytes: 64_502_000, runtimeModel: 'haiku-before-stop' }, }, }); svc.stopTeam(teamName); const secondRun = createPureAnthropicLiveRun({ teamName, projectPath }); secondRun.runId = `run-${teamName}-second`; secondRun.child = { pid: 64601, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, secondRun); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ ['alice', { alive: true, pid: 64602, model: 'haiku-after-relaunch' }], ['bob', { alive: true, pid: 64603, model: 'sonnet-after-relaunch' }], ]); const afterRelaunch = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(afterRelaunch).toMatchObject({ runId: secondRun.runId, members: { 'team-lead': { pid: 64601, rssBytes: 64_601_000 }, alice: { pid: 64602, rssBytes: 64_602_000, runtimeModel: 'haiku-after-relaunch' }, bob: { pid: 64603, rssBytes: 64_603_000, runtimeModel: 'sonnet-after-relaunch' }, }, }); }); it('keeps runtime snapshot on current Anthropic provider while stale Codex relaunch metadata remains', async () => { const teamName = 'provider-switch-codex-anthropic-runtime-card-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); const svc = new TeamProvisioningService(); const firstRun = createMixedLiveRun({ teamName, projectPath }); firstRun.runId = `run-${teamName}-codex`; firstRun.child = { pid: 64611, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, firstRun); const beforeSwitch = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(beforeSwitch).toMatchObject({ runId: firstRun.runId, providerBackendId: 'codex-native', members: { 'team-lead': { runtimeModel: 'gpt-5.4' }, alice: { providerId: 'codex', runtimeModel: 'gpt-5.4-mini' }, bob: { providerId: 'opencode', runtimeModel: 'opencode/minimax-m2.5-free' }, }, }); await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); TeamConfigReader.invalidateTeam(teamName); const secondRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); secondRun.runId = `run-${teamName}-anthropic`; secondRun.request.model = 'haiku'; secondRun.request.effort = 'low'; secondRun.launchIdentity = { ...secondRun.launchIdentity, providerId: 'anthropic', providerBackendId: null, selectedModel: 'haiku', resolvedLaunchModel: 'haiku', catalogId: 'haiku', selectedEffort: 'low', resolvedEffort: 'low', }; secondRun.child = { pid: 64621, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, secondRun); const afterSwitch = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(afterSwitch).toMatchObject({ runId: secondRun.runId, members: { 'team-lead': { runtimeModel: 'haiku' }, alice: { providerId: 'anthropic', runtimeModel: 'haiku' }, bob: { providerId: 'opencode', runtimeModel: 'opencode/minimax-m2.5-free' }, tom: { providerId: 'opencode', runtimeModel: 'opencode/nemotron-3-super-free' }, }, }); expect(afterSwitch.providerBackendId).toBeUndefined(); expect(afterSwitch.members.alice.providerBackendId).toBeUndefined(); }); it('ignores stale Codex live metadata model for the current Anthropic provider snapshot', async () => { const teamName = 'provider-switch-anthropic-stale-live-codex-model-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); const svc = new TeamProvisioningService(); const firstRun = createMixedLiveRun({ teamName, projectPath }); firstRun.runId = `run-${teamName}-codex`; firstRun.child = { pid: 64651, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, firstRun); const beforeSwitch = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(beforeSwitch.members.alice).toMatchObject({ providerId: 'codex', runtimeModel: 'gpt-5.4-mini', }); await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); TeamConfigReader.invalidateTeam(teamName); const secondRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); secondRun.runId = `run-${teamName}-anthropic`; secondRun.child = { pid: 64661, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, secondRun); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'alice', { alive: true, pid: 64662, providerId: 'codex', model: 'gpt-5.4-mini', }, ], ]); (svc as any).readProcessRssBytesByPid = async (pids: number[]) => new Map(pids.map((pid) => [pid, pid * 1_000])); const afterSwitch = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(afterSwitch).toMatchObject({ runId: secondRun.runId, members: { 'team-lead': { runtimeModel: 'sonnet' }, alice: { providerId: 'anthropic', runtimeModel: 'haiku', pid: 64662, rssBytes: 64_662_000, }, }, }); expect(afterSwitch.providerBackendId).toBeUndefined(); expect(afterSwitch.members.alice.providerBackendId).toBeUndefined(); }); it('ignores stale Codex live provider evidence even when the live model cannot be inferred', async () => { const teamName = 'provider-switch-anthropic-stale-live-codex-unknown-model-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); const svc = new TeamProvisioningService(); const firstRun = createMixedLiveRun({ teamName, projectPath }); firstRun.runId = `run-${teamName}-codex`; firstRun.child = { pid: 64711, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, firstRun); await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); TeamConfigReader.invalidateTeam(teamName); const secondRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); secondRun.runId = `run-${teamName}-anthropic`; secondRun.child = { pid: 64721, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, secondRun); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'alice', { alive: true, pid: 64722, providerId: 'codex', model: 'legacy-enterprise-custom-model', }, ], ]); (svc as any).readProcessRssBytesByPid = async (pids: number[]) => new Map(pids.map((pid) => [pid, pid * 1_000])); const afterSwitch = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(afterSwitch).toMatchObject({ runId: secondRun.runId, members: { alice: { providerId: 'anthropic', runtimeModel: 'haiku', pid: 64722, rssBytes: 64_722_000, }, }, }); expect(afterSwitch.members.alice.providerBackendId).toBeUndefined(); }); it('keeps matching-provider custom live models that cannot be inferred', async () => { const teamName = 'provider-switch-anthropic-live-custom-model-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); const svc = new TeamProvisioningService(); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); run.runId = `run-${teamName}-anthropic`; run.child = { pid: 64731, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, run); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'alice', { alive: true, pid: 64732, providerId: 'anthropic', model: 'enterprise-custom-model', }, ], ]); (svc as any).readProcessRssBytesByPid = async (pids: number[]) => new Map(pids.map((pid) => [pid, pid * 1_000])); const snapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(snapshot.members.alice).toMatchObject({ providerId: 'anthropic', runtimeModel: 'enterprise-custom-model', pid: 64732, rssBytes: 64_732_000, }); expect(snapshot.members.alice.providerBackendId).toBeUndefined(); }); it('ignores a live Codex model even when stale metadata claims the current Anthropic provider', async () => { const teamName = 'provider-switch-anthropic-conflicting-live-model-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); const svc = new TeamProvisioningService(); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); run.runId = `run-${teamName}-anthropic`; run.child = { pid: 64801, kill: () => undefined, stdin: { writable: true } }; delete run.effectiveMembers[0].providerId; delete run.allEffectiveMembers[0].providerId; trackLiveRun(svc, run); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'alice', { alive: true, pid: 64802, providerId: 'anthropic', model: 'gpt-5.4-mini', }, ], ]); (svc as any).readProcessRssBytesByPid = async (pids: number[]) => new Map(pids.map((pid) => [pid, pid * 1_000])); const snapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(snapshot.members.alice).toMatchObject({ providerId: 'anthropic', runtimeModel: 'haiku', pid: 64802, rssBytes: 64_802_000, }); expect(snapshot.members.alice.providerBackendId).toBeUndefined(); }); it('ignores stale Codex live provider evidence for a current OpenCode side-lane unknown model', async () => { const teamName = 'provider-switch-opencode-stale-live-codex-unknown-model-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); const svc = new TeamProvisioningService(); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); run.runId = `run-${teamName}-anthropic`; run.child = { pid: 64761, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, run); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'bob', { alive: true, pid: 64762, providerId: 'codex', model: 'legacy-enterprise-custom-model', }, ], ]); (svc as any).readProcessRssBytesByPid = async (pids: number[]) => new Map(pids.map((pid) => [pid, pid * 1_000])); const snapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(snapshot.members.bob).toMatchObject({ providerId: 'opencode', runtimeModel: 'opencode/minimax-m2.5-free', pid: 64762, rssBytes: 64_762_000, }); expect(snapshot.members.bob.providerBackendId).toBeUndefined(); }); it('keeps matching OpenCode custom live models that cannot be inferred', async () => { const teamName = 'provider-switch-opencode-live-custom-model-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); const svc = new TeamProvisioningService(); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); run.runId = `run-${teamName}-anthropic`; run.child = { pid: 64771, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, run); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'bob', { alive: true, pid: 64772, providerId: 'opencode', model: 'local-custom-opencode-model', }, ], ]); (svc as any).readProcessRssBytesByPid = async (pids: number[]) => new Map(pids.map((pid) => [pid, pid * 1_000])); const snapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(snapshot.members.bob).toMatchObject({ providerId: 'opencode', runtimeModel: 'local-custom-opencode-model', pid: 64772, rssBytes: 64_772_000, }); expect(snapshot.members.bob.providerBackendId).toBeUndefined(); }); it('ignores stale Codex live provider evidence for a current Gemini teammate unknown model', async () => { const teamName = 'provider-switch-gemini-stale-live-codex-unknown-model-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const svc = new TeamProvisioningService(); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); addGeminiPrimaryToMixedRun(run); run.runId = `run-${teamName}-anthropic`; run.child = { pid: 64781, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, run); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'reviewer', { alive: true, pid: 64782, providerId: 'codex', model: 'legacy-enterprise-custom-model', }, ], ]); (svc as any).readProcessRssBytesByPid = async (pids: number[]) => new Map(pids.map((pid) => [pid, pid * 1_000])); const snapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(snapshot.members.reviewer).toMatchObject({ providerId: 'gemini', runtimeModel: 'gemini-2.5-flash', pid: 64782, rssBytes: 64_782_000, }); expect(snapshot.members.reviewer.providerBackendId).toBeUndefined(); }); it('keeps matching Gemini custom live models that cannot be inferred', async () => { const teamName = 'provider-switch-gemini-live-custom-model-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const svc = new TeamProvisioningService(); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); addGeminiPrimaryToMixedRun(run); run.runId = `run-${teamName}-anthropic`; run.child = { pid: 64791, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, run); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'reviewer', { alive: true, pid: 64792, providerId: 'gemini', model: 'enterprise-custom-gemini-model', }, ], ]); (svc as any).readProcessRssBytesByPid = async (pids: number[]) => new Map(pids.map((pid) => [pid, pid * 1_000])); const snapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(snapshot.members.reviewer).toMatchObject({ providerId: 'gemini', runtimeModel: 'enterprise-custom-gemini-model', pid: 64792, rssBytes: 64_792_000, }); expect(snapshot.members.reviewer.providerBackendId).toBeUndefined(); }); it('drops stale Codex launch-state backend when the current active run is Anthropic', async () => { const teamName = 'provider-switch-anthropic-stale-launch-state-backend-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ name: 'alice', providerId: 'codex', providerBackendId: 'codex-native', model: 'gpt-5.4-mini', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'codex', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), }, }); const svc = new TeamProvisioningService(); await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); TeamConfigReader.invalidateTeam(teamName); const currentRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); currentRun.runId = `run-${teamName}-anthropic`; currentRun.child = { pid: 64671, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, currentRun); const snapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(snapshot).toMatchObject({ runId: currentRun.runId, members: { alice: { providerId: 'anthropic', runtimeModel: 'haiku', }, }, }); expect(snapshot.providerBackendId).toBeUndefined(); expect(snapshot.members.alice.providerBackendId).toBeUndefined(); }); it('restores Codex backend on current Codex relaunch while stale Anthropic metadata remains', async () => { const teamName = 'provider-switch-anthropic-codex-runtime-card-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); const svc = new TeamProvisioningService(); const firstRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); firstRun.runId = `run-${teamName}-anthropic`; firstRun.child = { pid: 64631, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, firstRun); const beforeSwitch = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(beforeSwitch.runId).toBe(firstRun.runId); expect(beforeSwitch.providerBackendId).toBeUndefined(); expect(beforeSwitch.members.alice).toMatchObject({ providerId: 'anthropic', runtimeModel: 'haiku', }); await writeMixedTeamConfig({ teamName, projectPath }); TeamConfigReader.invalidateTeam(teamName); const secondRun = createMixedLiveRun({ teamName, projectPath }); secondRun.runId = `run-${teamName}-codex`; secondRun.child = { pid: 64641, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, secondRun); const afterSwitch = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(afterSwitch).toMatchObject({ runId: secondRun.runId, providerBackendId: 'codex-native', members: { 'team-lead': { runtimeModel: 'gpt-5.4' }, alice: { providerId: 'codex', runtimeModel: 'gpt-5.4-mini' }, bob: { providerId: 'opencode', runtimeModel: 'opencode/minimax-m2.5-free' }, tom: { providerId: 'opencode', runtimeModel: 'opencode/nemotron-3-super-free' }, }, }); }); it('ignores stale Anthropic live metadata model for the current Codex provider snapshot', async () => { const teamName = 'provider-switch-codex-stale-live-anthropic-model-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); const svc = new TeamProvisioningService(); const firstRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); firstRun.runId = `run-${teamName}-anthropic`; firstRun.child = { pid: 64681, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, firstRun); const beforeSwitch = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(beforeSwitch.members.alice).toMatchObject({ providerId: 'anthropic', runtimeModel: 'haiku', }); await writeMixedTeamConfig({ teamName, projectPath }); TeamConfigReader.invalidateTeam(teamName); const secondRun = createMixedLiveRun({ teamName, projectPath }); secondRun.runId = `run-${teamName}-codex`; secondRun.child = { pid: 64691, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, secondRun); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'alice', { alive: true, pid: 64692, providerId: 'anthropic', model: 'haiku', }, ], ]); (svc as any).readProcessRssBytesByPid = async (pids: number[]) => new Map(pids.map((pid) => [pid, pid * 1_000])); const afterSwitch = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(afterSwitch).toMatchObject({ runId: secondRun.runId, providerBackendId: 'codex-native', members: { 'team-lead': { runtimeModel: 'gpt-5.4' }, alice: { providerId: 'codex', providerBackendId: 'codex-native', runtimeModel: 'gpt-5.4-mini', pid: 64692, rssBytes: 64_692_000, }, }, }); }); it('ignores stale Anthropic live provider evidence with an unknown model for the current Codex snapshot', async () => { const teamName = 'provider-switch-codex-stale-live-anthropic-unknown-model-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { primaryProviderId: 'anthropic' }); const svc = new TeamProvisioningService(); const firstRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); firstRun.runId = `run-${teamName}-anthropic`; firstRun.child = { pid: 64741, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, firstRun); await writeMixedTeamConfig({ teamName, projectPath }); TeamConfigReader.invalidateTeam(teamName); const secondRun = createMixedLiveRun({ teamName, projectPath }); secondRun.runId = `run-${teamName}-codex`; secondRun.child = { pid: 64751, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, secondRun); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'alice', { alive: true, pid: 64752, providerId: 'anthropic', model: 'legacy-enterprise-custom-model', }, ], ]); (svc as any).readProcessRssBytesByPid = async (pids: number[]) => new Map(pids.map((pid) => [pid, pid * 1_000])); const afterSwitch = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(afterSwitch).toMatchObject({ runId: secondRun.runId, providerBackendId: 'codex-native', members: { alice: { providerId: 'codex', providerBackendId: 'codex-native', runtimeModel: 'gpt-5.4-mini', pid: 64752, rssBytes: 64_752_000, }, }, }); }); it('ignores a live Anthropic model even when stale metadata claims the current Codex provider', async () => { const teamName = 'provider-switch-codex-conflicting-live-model-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createMixedLiveRun({ teamName, projectPath }); run.runId = `run-${teamName}-codex`; run.child = { pid: 64811, kill: () => undefined, stdin: { writable: true } }; delete run.effectiveMembers[0].providerId; delete run.allEffectiveMembers[0].providerId; trackLiveRun(svc, run); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'alice', { alive: true, pid: 64812, providerId: 'codex', model: 'haiku', }, ], ]); (svc as any).readProcessRssBytesByPid = async (pids: number[]) => new Map(pids.map((pid) => [pid, pid * 1_000])); const snapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(snapshot).toMatchObject({ runId: run.runId, providerBackendId: 'codex-native', members: { alice: { providerId: 'codex', providerBackendId: 'codex-native', runtimeModel: 'gpt-5.4-mini', pid: 64812, rssBytes: 64_812_000, }, }, }); }); it('refreshes runtime snapshot cache after same-team Anthropic and Gemini mixed relaunch', async () => { const teamName = 'mixed-anthropic-gemini-runtime-cache-relaunch-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const svc = new TeamProvisioningService(); const staleRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); addGeminiPrimaryToMixedRun(staleRun); staleRun.runId = `run-${teamName}-stale`; staleRun.child = { pid: 64701, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, staleRun); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ ['alice', { alive: true, pid: 64702, model: 'haiku-stale' }], ['reviewer', { alive: true, pid: 64703, model: 'gemini-stale' }], ['bob', { alive: true, pid: 64704, model: 'opencode/minimax-stale' }], ['tom', { alive: true, pid: 64705, model: 'opencode/nemotron-stale' }], ]); (svc as any).readProcessRssBytesByPid = async (pids: number[]) => new Map(pids.map((pid) => [pid, pid * 1_000])); const staleSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(staleSnapshot).toMatchObject({ runId: staleRun.runId, members: { 'team-lead': { pid: 64701, rssBytes: 64_701_000 }, alice: { pid: 64702, rssBytes: 64_702_000, runtimeModel: 'haiku-stale' }, reviewer: { pid: 64703, rssBytes: 64_703_000, runtimeModel: 'gemini-stale' }, bob: { pid: 64704, rssBytes: 64_704_000, runtimeModel: 'opencode/minimax-stale' }, tom: { pid: 64705, rssBytes: 64_705_000, runtimeModel: 'opencode/nemotron-stale' }, }, }); const currentRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); addGeminiPrimaryToMixedRun(currentRun); currentRun.runId = `run-${teamName}-current`; currentRun.child = { pid: 64801, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, currentRun); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ ['alice', { alive: true, pid: 64802, model: 'haiku-current' }], ['reviewer', { alive: true, pid: 64803, model: 'gemini-current' }], ['bob', { alive: true, pid: 64804, model: 'opencode/minimax-current' }], ['tom', { alive: true, pid: 64805, model: 'opencode/nemotron-current' }], ]); const currentSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(currentSnapshot).toMatchObject({ runId: currentRun.runId, members: { 'team-lead': { pid: 64801, rssBytes: 64_801_000 }, alice: { pid: 64802, rssBytes: 64_802_000, runtimeModel: 'haiku-current' }, reviewer: { pid: 64803, rssBytes: 64_803_000, runtimeModel: 'gemini-current' }, bob: { pid: 64804, rssBytes: 64_804_000, runtimeModel: 'opencode/minimax-current' }, tom: { pid: 64805, rssBytes: 64_805_000, runtimeModel: 'opencode/nemotron-current' }, }, }); }); it('rejects messages to a stopped pure Anthropic team while another team remains sendable', async () => { const stoppedTeamName = 'pure-anthropic-message-stopped-safe-e2e'; const liveTeamName = 'pure-anthropic-message-live-safe-e2e'; await writePureAnthropicTeamConfig({ teamName: stoppedTeamName, projectPath }); await writePureAnthropicTeamMeta(stoppedTeamName, projectPath); await writePureAnthropicMembersMeta(stoppedTeamName); await writePureAnthropicTeamConfig({ teamName: liveTeamName, projectPath }); await writePureAnthropicTeamMeta(liveTeamName, projectPath); await writePureAnthropicMembersMeta(liveTeamName); const svc = new TeamProvisioningService(); const stoppedRun = createPureAnthropicLiveRun({ teamName: stoppedTeamName, projectPath }); const liveRun = createPureAnthropicLiveRun({ teamName: liveTeamName, projectPath }); stoppedRun.child = { kill: () => undefined, stdin: { writable: true } }; liveRun.child = { stdin: { writable: true } }; trackLiveRun(svc, stoppedRun); trackLiveRun(svc, liveRun); const delivered: Array<{ teamName: string; message: string }> = []; (svc as any).sendMessageToRun = async (run: { teamName: string }, message: string) => { delivered.push({ teamName: run.teamName, message }); }; svc.stopTeam(stoppedTeamName); await expect(svc.sendMessageToTeam(stoppedTeamName, 'should not send')).rejects.toThrow( `No active process for team "${stoppedTeamName}"` ); await svc.sendMessageToTeam(liveTeamName, 'still alive'); expect(delivered).toEqual([{ teamName: liveTeamName, message: 'still alive' }]); expect(svc.isTeamAlive(stoppedTeamName)).toBe(false); expect(svc.isTeamAlive(liveTeamName)).toBe(true); }); it('rejects messages when a pure Anthropic team stdin is not writable without marking it dead', async () => { const teamName = 'pure-anthropic-message-stdin-closed-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); run.child = { stdin: { writable: false } }; trackLiveRun(svc, run); await expect(svc.sendMessageToTeam(teamName, 'will fail')).rejects.toThrow( `Team "${teamName}" process stdin is not writable` ); expect(svc.isTeamAlive(teamName)).toBe(true); expect(run.cancelRequested).toBe(false); expect(run.memberSpawnStatuses.get('bob')).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('keeps runtime pid and memory snapshots isolated across two pure Anthropic teams', async () => { const firstTeamName = 'pure-anthropic-runtime-snapshot-a-safe-e2e'; const secondTeamName = 'pure-anthropic-runtime-snapshot-b-safe-e2e'; await writePureAnthropicTeamConfig({ teamName: firstTeamName, projectPath }); await writePureAnthropicTeamMeta(firstTeamName, projectPath); await writePureAnthropicMembersMeta(firstTeamName); await writePureAnthropicTeamConfig({ teamName: secondTeamName, projectPath }); await writePureAnthropicTeamMeta(secondTeamName, projectPath); await writePureAnthropicMembersMeta(secondTeamName); const svc = new TeamProvisioningService(); const firstRun = createPureAnthropicLiveRun({ teamName: firstTeamName, projectPath }); const secondRun = createPureAnthropicLiveRun({ teamName: secondTeamName, projectPath }); firstRun.child = { pid: 50101, kill: () => undefined, stdin: { writable: true } }; secondRun.child = { pid: 50201, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, firstRun); trackLiveRun(svc, secondRun); (svc as any).getLiveTeamAgentRuntimeMetadata = async (teamName: string) => new Map( teamName === firstTeamName ? [ ['alice', { alive: true, pid: 50102, model: 'haiku-runtime' }], ['bob', { alive: true, pid: 50103, model: 'sonnet-runtime' }], ] : [ ['alice', { alive: true, pid: 50202, model: 'haiku-runtime' }], ['bob', { alive: true, pid: 50203, model: 'sonnet-runtime' }], ] ); (svc as any).readProcessRssBytesByPid = async (pids: number[]) => new Map(pids.map((pid) => [pid, pid * 1_000])); const firstSnapshot = await svc.getTeamAgentRuntimeSnapshot(firstTeamName); const secondSnapshot = await svc.getTeamAgentRuntimeSnapshot(secondTeamName); expect(firstSnapshot.members['team-lead']).toMatchObject({ alive: true, pid: 50101, rssBytes: 50_101_000, runtimeModel: 'sonnet', }); expect(firstSnapshot.members.alice).toMatchObject({ alive: true, pid: 50102, rssBytes: 50_102_000, providerId: 'anthropic', runtimeModel: 'haiku-runtime', }); expect(firstSnapshot.members.bob).toMatchObject({ alive: true, pid: 50103, rssBytes: 50_103_000, providerId: 'anthropic', runtimeModel: 'sonnet-runtime', }); expect(secondSnapshot.members['team-lead']).toMatchObject({ alive: true, pid: 50201, rssBytes: 50_201_000, runtimeModel: 'sonnet', }); expect(secondSnapshot.members.alice).toMatchObject({ alive: true, pid: 50202, rssBytes: 50_202_000, providerId: 'anthropic', runtimeModel: 'haiku-runtime', }); expect(secondSnapshot.members.bob).toMatchObject({ alive: true, pid: 50203, rssBytes: 50_203_000, providerId: 'anthropic', runtimeModel: 'sonnet-runtime', }); }); it('clears cached runtime pid and memory after stopping one pure Anthropic team', async () => { const stoppedTeamName = 'pure-anthropic-runtime-cache-stopped-safe-e2e'; const liveTeamName = 'pure-anthropic-runtime-cache-live-safe-e2e'; await writePureAnthropicTeamConfig({ teamName: stoppedTeamName, projectPath }); await writePureAnthropicTeamMeta(stoppedTeamName, projectPath); await writePureAnthropicMembersMeta(stoppedTeamName); await writePureAnthropicTeamConfig({ teamName: liveTeamName, projectPath }); await writePureAnthropicTeamMeta(liveTeamName, projectPath); await writePureAnthropicMembersMeta(liveTeamName); const svc = new TeamProvisioningService(); const stoppedRun = createPureAnthropicLiveRun({ teamName: stoppedTeamName, projectPath }); const liveRun = createPureAnthropicLiveRun({ teamName: liveTeamName, projectPath }); stoppedRun.child = { pid: 60101, kill: () => undefined, stdin: { writable: true } }; liveRun.child = { pid: 60201, kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, stoppedRun); trackLiveRun(svc, liveRun); (svc as any).getLiveTeamAgentRuntimeMetadata = async (teamName: string) => { if (!svc.isTeamAlive(teamName)) { return new Map(); } return new Map( teamName === stoppedTeamName ? [ ['alice', { alive: true, pid: 60102, model: 'haiku-runtime' }], ['bob', { alive: true, pid: 60103, model: 'sonnet-runtime' }], ] : [ ['alice', { alive: true, pid: 60202, model: 'haiku-runtime' }], ['bob', { alive: true, pid: 60203, model: 'sonnet-runtime' }], ] ); }; (svc as any).readProcessRssBytesByPid = async (pids: number[]) => new Map(pids.map((pid) => [pid, pid * 1_000])); const beforeStop = await svc.getTeamAgentRuntimeSnapshot(stoppedTeamName); expect(beforeStop.members['team-lead']).toMatchObject({ alive: true, pid: 60101, rssBytes: 60_101_000, }); svc.stopTeam(stoppedTeamName); const stoppedSnapshot = await svc.getTeamAgentRuntimeSnapshot(stoppedTeamName); const liveSnapshot = await svc.getTeamAgentRuntimeSnapshot(liveTeamName); expect(stoppedSnapshot.members['team-lead']).toMatchObject({ alive: false }); expect(stoppedSnapshot.members['team-lead']?.pid).toBeUndefined(); expect(stoppedSnapshot.members.alice).toMatchObject({ alive: false }); expect(stoppedSnapshot.members.alice?.pid).toBeUndefined(); expect(liveSnapshot.members['team-lead']).toMatchObject({ alive: true, pid: 60201, rssBytes: 60_201_000, }); expect(liveSnapshot.members.alice).toMatchObject({ alive: true, pid: 60202, rssBytes: 60_202_000, }); }); it('reports lead activity as active for a live team and offline after stop', async () => { const teamName = 'pure-anthropic-lead-activity-stop-safe-e2e'; await writePureAnthropicTeamConfig({ teamName, projectPath }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); run.child = { kill: () => undefined, stdin: { writable: true } }; trackLiveRun(svc, run); expect(svc.getLeadActivityState(teamName)).toEqual({ state: 'active', runId: run.runId, }); svc.stopTeam(teamName); expect(svc.getLeadActivityState(teamName)).toEqual({ state: 'offline', runId: null, }); }); it('treats a suffixed registered live agent as the expected teammate during launch audit', async () => { const teamName = 'agent-audit-suffixed-registered-safe-e2e'; await writePureAnthropicTeamConfigWithMembers({ teamName, projectPath, members: ['alice', 'bob-2'], }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); run.expectedMembers = ['alice', 'bob']; run.memberSpawnStatuses.set('bob', { status: 'spawning', launchState: 'starting', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: new Date().toISOString(), lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); (svc as any).getLiveTeamAgentRuntimeMetadata = async () => new Map([ [ 'bob-2', { alive: true, model: 'sonnet', }, ], ]); await (svc as any).auditMemberSpawnStatuses(run); expect(run.memberSpawnStatuses.get('bob')).toMatchObject({ status: 'online', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: true, livenessSource: 'process', hardFailure: false, }); }); it('does not finalize a suffixed registered agent as missing during launch finalization', async () => { const teamName = 'agent-finalize-suffixed-registered-safe-e2e'; await writePureAnthropicTeamConfigWithMembers({ teamName, projectPath, members: ['alice', 'bob-2'], }); await writePureAnthropicTeamMeta(teamName, projectPath); await writePureAnthropicMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createPureAnthropicLiveRun({ teamName, projectPath }); run.expectedMembers = ['alice', 'bob']; run.memberSpawnStatuses.set('bob', { status: 'waiting', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); await (svc as any).finalizeMissingRegisteredMembersAsFailed(run); expect(run.memberSpawnStatuses.get('bob')).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', hardFailure: false, }); }); it('keeps OpenCode secondary lanes online when the primary Codex member failed to spawn', async () => { const teamName = 'mixed-primary-failure-opencode-ready-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); run.memberSpawnStatuses.set('alice', { status: 'error', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Codex native runtime unavailable', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await waitForCondition( () => run.memberSpawnStatuses.get('bob')?.launchState === 'confirmed_alive' ); await waitForCondition( () => run.memberSpawnStatuses.get('tom')?.launchState === 'confirmed_alive' ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, failedCount: 1, }); expect(statuses.statuses.alice).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Codex native runtime unavailable', }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:bob', laneKind: 'secondary', runtimeModel: 'opencode/minimax-m2.5-free', }); expect(runtimeSnapshot.members.tom).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:tom', laneKind: 'secondary', runtimeModel: 'opencode/nemotron-3-super-free', }); }); it('keeps OpenCode secondary lanes online when the primary Anthropic member failed to spawn', async () => { const teamName = 'mixed-anthropic-primary-failure-opencode-ready-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); trackLiveRun(svc, run); run.memberSpawnStatuses.set('alice', { status: 'error', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Anthropic pane exited before bootstrap', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await waitForCondition( () => run.memberSpawnStatuses.get('bob')?.launchState === 'confirmed_alive' ); await waitForCondition( () => run.memberSpawnStatuses.get('tom')?.launchState === 'confirmed_alive' ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, failedCount: 1, }); expect(statuses.statuses.alice).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Anthropic pane exited before bootstrap', }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.alice).toMatchObject({ providerId: 'anthropic', laneKind: 'primary', alive: false, runtimeModel: 'haiku', }); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:bob', laneKind: 'secondary', runtimeModel: 'opencode/minimax-m2.5-free', }); expect(runtimeSnapshot.members.tom).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:tom', laneKind: 'secondary', runtimeModel: 'opencode/nemotron-3-super-free', }); }); it('keeps OpenCode secondary lanes online when Anthropic and Gemini primary members both failed', async () => { const teamName = 'mixed-anthropic-gemini-primary-failure-opencode-ready-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); run.expectedMembers = ['alice', 'reviewer']; run.effectiveMembers = [ ...(run.effectiveMembers as Array>), { name: 'reviewer', role: 'Reviewer', providerId: 'gemini', model: 'gemini-2.5-flash', }, ]; run.allEffectiveMembers = [ ...(run.allEffectiveMembers as Array>), { name: 'reviewer', role: 'Reviewer', providerId: 'gemini', model: 'gemini-2.5-flash', }, ]; trackLiveRun(svc, run); run.memberSpawnStatuses.set('alice', { status: 'error', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Anthropic pane exited before bootstrap', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); run.memberSpawnStatuses.set('reviewer', { status: 'error', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Gemini pane failed to start', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await waitForCondition( () => run.memberSpawnStatuses.get('bob')?.launchState === 'confirmed_alive' ); await waitForCondition( () => run.memberSpawnStatuses.get('tom')?.launchState === 'confirmed_alive' ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.summary).toMatchObject({ confirmedCount: 2, failedCount: 2, }); expect(statuses.statuses.alice).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Anthropic pane exited before bootstrap', }); expect(statuses.statuses.reviewer).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Gemini pane failed to start', }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:bob', laneKind: 'secondary', runtimeModel: 'opencode/minimax-m2.5-free', }); expect(runtimeSnapshot.members.tom).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:tom', laneKind: 'secondary', runtimeModel: 'opencode/nemotron-3-super-free', }); }); it('detaches one OpenCode secondary lane after Anthropic and Gemini primary members both failed', async () => { const teamName = 'mixed-anthropic-gemini-primary-failure-opencode-detach-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); run.expectedMembers = ['alice', 'reviewer']; run.effectiveMembers = [ ...(run.effectiveMembers as Array>), { name: 'reviewer', role: 'Reviewer', providerId: 'gemini', model: 'gemini-2.5-flash', }, ]; run.allEffectiveMembers = [ ...(run.allEffectiveMembers as Array>), { name: 'reviewer', role: 'Reviewer', providerId: 'gemini', model: 'gemini-2.5-flash', }, ]; trackLiveRun(svc, run); run.memberSpawnStatuses.set('alice', { status: 'error', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Anthropic pane exited before bootstrap', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); run.memberSpawnStatuses.set('reviewer', { status: 'error', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Gemini pane failed to start', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await svc.detachOpenCodeOwnedMemberLane(teamName, 'bob'); expect(adapter.stopInputs).toHaveLength(1); expect(adapter.stopInputs[0]).toMatchObject({ laneId: 'secondary:opencode:bob', reason: 'cleanup', }); expect( run.mixedSecondaryLanes.map((lane: { member: { name: string } }) => lane.member.name) ).toEqual(['tom']); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'reviewer', 'tom']); expect(statuses.statuses.bob).toBeUndefined(); expect(statuses.statuses.alice).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Anthropic pane exited before bootstrap', }); expect(statuses.statuses.reviewer).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Gemini pane failed to start', }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { 'secondary:opencode:tom': { state: 'active' }, }, } ); }); it('restarts one OpenCode secondary lane after Anthropic and Gemini primary members both failed', async () => { const teamName = 'mixed-anthropic-gemini-primary-failure-opencode-restart-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); run.expectedMembers = ['alice', 'reviewer']; run.effectiveMembers = [ ...(run.effectiveMembers as Array>), { name: 'reviewer', role: 'Reviewer', providerId: 'gemini', model: 'gemini-2.5-flash', }, ]; run.allEffectiveMembers = [ ...(run.allEffectiveMembers as Array>), { name: 'reviewer', role: 'Reviewer', providerId: 'gemini', model: 'gemini-2.5-flash', }, ]; trackLiveRun(svc, run); run.memberSpawnStatuses.set('alice', { status: 'error', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Anthropic pane exited before bootstrap', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); run.memberSpawnStatuses.set('reviewer', { status: 'error', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'Gemini pane failed to start', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', }); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); adapter.setLaunchResult('partial_pending', { bob: 'permission' }); await svc.restartMember(teamName, 'bob'); await waitForCondition(() => adapter.launchInputs.length === 3); expect(adapter.stopInputs).toHaveLength(1); expect(adapter.stopInputs[0]).toMatchObject({ laneId: 'secondary:opencode:bob', reason: 'relaunch', }); expect(adapter.launchInputs.at(-1)).toMatchObject({ laneId: 'secondary:opencode:bob', expectedMembers: [expect.objectContaining({ name: 'bob', providerId: 'opencode' })], }); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.alice).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Anthropic pane exited before bootstrap', }); expect(statuses.statuses.reviewer).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'Gemini pane failed to start', }); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_permission', pendingPermissionRequestIds: ['perm-bob'], hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('fails mixed OpenCode secondary lanes clearly when the runtime adapter is not registered', async () => { const teamName = 'mixed-missing-opencode-adapter-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); const svc = new TeamProvisioningService(); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); const snapshot = await (svc as any).launchMixedSecondaryLaneIfNeeded(run); expect(snapshot).toMatchObject({ teamName, teamLaunchState: 'partial_failure', }); expect(run.mixedSecondaryLanes.map((lane: { state: string }) => lane.state)).toEqual([ 'finished', 'finished', ]); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'opencode_runtime_adapter_missing', }); expect(statuses.statuses.tom).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'opencode_runtime_adapter_missing', }); }); it('keeps Anthropic primary online when OpenCode secondary adapter is not registered', async () => { const teamName = 'mixed-anthropic-missing-opencode-adapter-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); const svc = new TeamProvisioningService(); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); trackLiveRun(svc, run); const snapshot = await (svc as any).launchMixedSecondaryLaneIfNeeded(run); expect(snapshot).toMatchObject({ teamName, teamLaunchState: 'partial_failure', }); expect(run.mixedSecondaryLanes.map((lane: { state: string }) => lane.state)).toEqual([ 'finished', 'finished', ]); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'opencode_runtime_adapter_missing', }); expect(statuses.statuses.tom).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'opencode_runtime_adapter_missing', }); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.alice).toMatchObject({ providerId: 'anthropic', laneKind: 'primary', alive: true, runtimeModel: 'haiku', }); }); it('restarts one mixed OpenCode secondary lane without touching other live teammates', async () => { const teamName = 'mixed-opencode-manual-restart-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); adapter.setLaunchResult('partial_pending', { bob: 'permission' }); await svc.restartMember(teamName, 'bob'); await waitForCondition(() => adapter.launchInputs.length === 3); expect(adapter.stopInputs).toHaveLength(1); expect(adapter.stopInputs[0]).toMatchObject({ laneId: 'secondary:opencode:bob', reason: 'relaunch', }); expect(adapter.launchInputs.at(-1)).toMatchObject({ laneId: 'secondary:opencode:bob', expectedMembers: [expect.objectContaining({ name: 'bob', providerId: 'opencode' })], }); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_permission', pendingPermissionRequestIds: ['perm-bob'], hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('restarts one Anthropic mixed OpenCode secondary lane without touching other live teammates', async () => { const teamName = 'mixed-anthropic-opencode-manual-restart-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); adapter.setLaunchResult('partial_pending', { bob: 'permission' }); await svc.restartMember(teamName, 'bob'); await waitForCondition(() => adapter.launchInputs.length === 3); expect(adapter.stopInputs).toHaveLength(1); expect(adapter.stopInputs[0]).toMatchObject({ laneId: 'secondary:opencode:bob', reason: 'relaunch', }); expect(adapter.launchInputs.at(-1)).toMatchObject({ laneId: 'secondary:opencode:bob', expectedMembers: [expect.objectContaining({ name: 'bob', providerId: 'opencode' })], }); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_permission', pendingPermissionRequestIds: ['perm-bob'], hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.alice).toMatchObject({ providerId: 'anthropic', laneKind: 'primary', runtimeModel: 'haiku', }); expect(runtimeSnapshot.members.bob).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:bob', laneKind: 'secondary', runtimeModel: 'opencode/minimax-m2.5-free', }); expect(runtimeSnapshot.members.tom).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:tom', laneKind: 'secondary', runtimeModel: 'opencode/nemotron-3-super-free', }); }); it('restarts only the targeted mixed OpenCode secondary lane when two teams share member names', async () => { const firstTeamName = 'mixed-opencode-restart-cross-team-a-safe-e2e'; const secondTeamName = 'mixed-opencode-restart-cross-team-b-safe-e2e'; await writeMixedTeamConfig({ teamName: firstTeamName, projectPath }); await writeMixedTeamConfig({ teamName: secondTeamName, projectPath }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const firstRun = createMixedLiveRun({ teamName: firstTeamName, projectPath }); const secondRun = createMixedLiveRun({ teamName: secondTeamName, projectPath }); trackLiveRun(svc, firstRun); trackLiveRun(svc, secondRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(firstRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(secondRun); await waitForCondition(() => adapter.launchInputs.length === 4); await waitForCondition(() => firstRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await waitForCondition(() => secondRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); adapter.setLaunchResult('partial_pending', { bob: 'permission' }); await svc.restartMember(secondTeamName, 'bob'); await waitForCondition(() => adapter.launchInputs.length === 5); expect(adapter.stopInputs).toHaveLength(1); expect(adapter.stopInputs[0]).toMatchObject({ teamName: secondTeamName, laneId: 'secondary:opencode:bob', reason: 'relaunch', }); expect(adapter.launchInputs.at(-1)).toMatchObject({ teamName: secondTeamName, laneId: 'secondary:opencode:bob', expectedMembers: [expect.objectContaining({ name: 'bob', providerId: 'opencode' })], }); const firstStatuses = await svc.getMemberSpawnStatuses(firstTeamName); const secondStatuses = await svc.getMemberSpawnStatuses(secondTeamName); expect(firstStatuses.teamLaunchState).toBe('clean_success'); expect(firstStatuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(secondStatuses.teamLaunchState).toBe('partial_pending'); expect(secondStatuses.statuses.bob).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_permission', pendingPermissionRequestIds: ['perm-bob'], hardFailure: false, }); expect(secondStatuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('detaches one mixed OpenCode secondary lane and keeps remaining teammates launchable', async () => { const teamName = 'mixed-opencode-detach-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await svc.detachOpenCodeOwnedMemberLane(teamName, 'bob'); expect(adapter.stopInputs).toHaveLength(1); expect(adapter.stopInputs[0]).toMatchObject({ laneId: 'secondary:opencode:bob', reason: 'cleanup', }); expect( run.mixedSecondaryLanes.map((lane: { member: { name: string } }) => lane.member.name) ).toEqual(['tom']); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'tom']); expect(statuses.statuses.bob).toBeUndefined(); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { 'secondary:opencode:tom': { state: 'active' }, }, } ); }); it('detaches only the targeted mixed OpenCode secondary lane when two teams share member names', async () => { const firstTeamName = 'mixed-opencode-detach-cross-team-a-safe-e2e'; const secondTeamName = 'mixed-opencode-detach-cross-team-b-safe-e2e'; await writeMixedTeamConfig({ teamName: firstTeamName, projectPath }); await writeMixedTeamConfig({ teamName: secondTeamName, projectPath }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const firstRun = createMixedLiveRun({ teamName: firstTeamName, projectPath }); const secondRun = createMixedLiveRun({ teamName: secondTeamName, projectPath }); trackLiveRun(svc, firstRun); trackLiveRun(svc, secondRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(firstRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(secondRun); await waitForCondition(() => adapter.launchInputs.length === 4); await waitForCondition(() => firstRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await waitForCondition(() => secondRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await svc.detachOpenCodeOwnedMemberLane(secondTeamName, 'bob'); expect(adapter.stopInputs).toHaveLength(1); expect(adapter.stopInputs[0]).toMatchObject({ teamName: secondTeamName, laneId: 'secondary:opencode:bob', reason: 'cleanup', }); expect( firstRun.mixedSecondaryLanes.map((lane: { member: { name: string } }) => lane.member.name) ).toEqual(['bob', 'tom']); expect( secondRun.mixedSecondaryLanes.map((lane: { member: { name: string } }) => lane.member.name) ).toEqual(['tom']); const firstStatuses = await svc.getMemberSpawnStatuses(firstTeamName); const secondStatuses = await svc.getMemberSpawnStatuses(secondTeamName); expect(firstStatuses.expectedMembers).toEqual(['alice', 'bob', 'tom']); expect(firstStatuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(secondStatuses.expectedMembers).toEqual(['alice', 'tom']); expect(secondStatuses.statuses.bob).toBeUndefined(); expect(secondStatuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('detaches one Anthropic mixed OpenCode secondary lane and keeps remaining teammates launchable', async () => { const teamName = 'mixed-anthropic-opencode-detach-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await svc.detachOpenCodeOwnedMemberLane(teamName, 'bob'); expect(adapter.stopInputs).toHaveLength(1); expect(adapter.stopInputs[0]).toMatchObject({ laneId: 'secondary:opencode:bob', reason: 'cleanup', }); expect( run.mixedSecondaryLanes.map((lane: { member: { name: string } }) => lane.member.name) ).toEqual(['tom']); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual(['alice', 'tom']); expect(statuses.statuses.bob).toBeUndefined(); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { 'secondary:opencode:tom': { state: 'active' }, }, } ); }); it('reattaches a newly added mixed OpenCode teammate without relaunching existing lanes', async () => { const teamName = 'mixed-opencode-add-member-reattach-safe-e2e'; const eve = { name: 'eve', providerId: 'opencode' as const, model: 'opencode/big-pickle', }; await writeMixedTeamConfig({ teamName, projectPath, extraMembers: [eve] }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName, { extraMembers: [eve] }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', eve: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await svc.reattachOpenCodeOwnedMemberLane(teamName, 'eve', { reason: 'member_added' }); await waitForCondition(() => adapter.launchInputs.length === 3); expect(adapter.stopInputs).toHaveLength(0); expect(adapter.launchInputs.at(-1)).toMatchObject({ laneId: 'secondary:opencode:eve', expectedMembers: [expect.objectContaining({ name: 'eve', providerId: 'opencode' })], }); expect( run.mixedSecondaryLanes.map((lane: { member: { name: string } }) => lane.member.name).sort() ).toEqual(['bob', 'eve', 'tom']); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).toEqual( expect.arrayContaining(['alice', 'bob', 'tom', 'eve']) ); expect(statuses.statuses.eve).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); expect(runtimeSnapshot.members.eve).toMatchObject({ providerId: 'opencode', laneId: 'secondary:opencode:eve', laneKind: 'secondary', runtimeModel: 'opencode/big-pickle', }); }); it('fresh relaunches a failed mixed OpenCode teammate without runtime evidence', async () => { const teamName = 'mixed-opencode-fresh-relaunch-no-runtime-evidence-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); run.mixedSecondaryLanes = run.mixedSecondaryLanes.filter( (lane: { member: { name: string } }) => lane.member.name !== 'bob' ); run.memberSpawnStatuses.set('bob', { status: 'error', launchState: 'failed_to_start', runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'File lock timeout: lanes.json', error: 'File lock timeout: lanes.json', agentToolAccepted: false, updatedAt: '2026-04-24T12:00:00.000Z', } as never); trackLiveRun(svc, run); await svc.restartMember(teamName, 'bob'); await waitForCondition(() => adapter.launchInputs.length === 1); expect(adapter.stopInputs).toHaveLength(0); expect(adapter.launchInputs[0]).toMatchObject({ teamName, laneId: 'secondary:opencode:bob', expectedMembers: [expect.objectContaining({ name: 'bob', providerId: 'opencode' })], }); const bobLane = run.mixedSecondaryLanes.find( (lane: { member: { name: string } }) => lane.member.name === 'bob' ); expect(bobLane).toMatchObject({ laneId: 'secondary:opencode:bob', diagnostics: expect.arrayContaining([ 'controlled_reattach:manual_restart', 'fresh_relaunch:no_runtime_evidence', ]), }); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('rejects duplicate fresh relaunch while a mixed OpenCode lane is queued', async () => { const teamName = 'mixed-opencode-fresh-relaunch-queued-reject-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); await expect(svc.restartMember(teamName, 'bob')).rejects.toThrow( 'Restart for teammate "bob" is already in progress' ); expect(adapter.launchInputs).toHaveLength(0); expect(adapter.stopInputs).toHaveLength(0); expect( run.mixedSecondaryLanes.find( (lane: { member: { name: string } }) => lane.member.name === 'bob' ) ).toMatchObject({ state: 'queued', }); }); it('reattaches an existing mixed OpenCode teammate after member update without changing siblings', async () => { const teamName = 'mixed-opencode-update-member-reattach-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await svc.reattachOpenCodeOwnedMemberLane(teamName, 'bob', { reason: 'member_updated' }); await waitForCondition(() => adapter.launchInputs.length === 3); expect(adapter.stopInputs).toHaveLength(1); expect(adapter.stopInputs[0]).toMatchObject({ laneId: 'secondary:opencode:bob', reason: 'relaunch', }); expect(adapter.launchInputs.at(-1)).toMatchObject({ laneId: 'secondary:opencode:bob', expectedMembers: [expect.objectContaining({ name: 'bob', providerId: 'opencode' })], }); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('rejects controlled OpenCode reattach for a primary-runtime teammate without dispatching lanes', async () => { const teamName = 'mixed-opencode-reattach-primary-reject-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); await expect(svc.reattachOpenCodeOwnedMemberLane(teamName, 'alice')).rejects.toThrow( 'Controlled reattach is only supported for OpenCode-owned members' ); expect(adapter.launchInputs).toHaveLength(0); expect(adapter.stopInputs).toHaveLength(0); expect(run.memberSpawnStatuses.get('alice')).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('rejects controlled OpenCode reattach for a removed teammate without launching a stale lane', async () => { const teamName = 'mixed-opencode-reattach-removed-reject-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName, { removedMembers: ['bob'] }); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); await expect(svc.reattachOpenCodeOwnedMemberLane(teamName, 'bob')).rejects.toThrow( 'Member "bob" has been removed' ); expect(adapter.launchInputs).toHaveLength(0); expect(adapter.stopInputs).toHaveLength(0); expect( run.mixedSecondaryLanes.map((lane: { member: { name: string } }) => lane.member.name) ).toEqual(['bob', 'tom']); }); it('rejects mixed OpenCode secondary restart when the runtime adapter is missing', async () => { const teamName = 'mixed-opencode-restart-missing-adapter-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); const svc = new TeamProvisioningService(); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); await expect(svc.restartMember(teamName, 'bob')).rejects.toThrow( 'OpenCode runtime adapter is not available for controlled lane reattach.' ); expect(run.mixedSecondaryLanes.map((lane: { state: string }) => lane.state)).toEqual([ 'queued', 'queued', ]); expect(run.pendingMemberRestarts.has('bob')).toBe(false); expect(run.memberSpawnStatuses.get('alice')).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(run.memberSpawnStatuses.get('bob')).toBeUndefined(); }); it('detaches a stale mixed OpenCode teammate that no longer has a runtime lane', async () => { const teamName = 'mixed-opencode-detach-stale-member-safe-e2e'; const eve = { name: 'eve', providerId: 'opencode' as const, model: 'opencode/big-pickle', }; await writeMixedTeamConfig({ teamName, projectPath, extraMembers: [eve] }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName, { extraMembers: [eve] }); const adapter = new FakeOpenCodeRuntimeAdapter(); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); run.allEffectiveMembers.push({ name: 'eve', role: 'Developer', providerId: 'opencode', model: 'opencode/big-pickle', }); run.request.members = run.allEffectiveMembers; trackLiveRun(svc, run); await svc.detachOpenCodeOwnedMemberLane(teamName, 'eve'); expect(adapter.stopInputs).toHaveLength(0); expect(run.allEffectiveMembers.map((member: { name: string }) => member.name)).not.toContain( 'eve' ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.expectedMembers).not.toContain('eve'); expect(statuses.statuses.eve).toBeUndefined(); expect(statuses.statuses.bob).toBeDefined(); expect(statuses.statuses.tom).toBeDefined(); }); it('shows mixed OpenCode secondary lanes as spawning while runtime adapter launch is in flight', async () => { const teamName = 'mixed-live-inflight-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); const initialSnapshot = await (svc as any).launchMixedSecondaryLaneIfNeeded(run); expect(initialSnapshot.teamLaunchState).toBe('partial_pending'); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); const inFlightStatuses = await svc.getMemberSpawnStatuses(teamName); expect(inFlightStatuses.teamLaunchState).toBe('partial_pending'); expect(inFlightStatuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 2, failedCount: 0, }); expect(inFlightStatuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); expect(inFlightStatuses.statuses.bob).toMatchObject({ status: 'spawning', launchState: 'starting', hardFailure: false, }); expect(inFlightStatuses.statuses.tom).toMatchObject({ status: 'spawning', launchState: 'starting', hardFailure: false, }); adapter.releaseLaunches(); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); const finalStatuses = await svc.getMemberSpawnStatuses(teamName); expect(finalStatuses.teamLaunchState).toBe('clean_success'); expect(finalStatuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); expect(finalStatuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); }); it('shows Anthropic mixed OpenCode secondary lanes as spawning while runtime adapter launch is in flight', async () => { const teamName = 'mixed-anthropic-live-inflight-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); trackLiveRun(svc, run); const initialSnapshot = await (svc as any).launchMixedSecondaryLaneIfNeeded(run); expect(initialSnapshot.teamLaunchState).toBe('partial_pending'); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); const inFlightStatuses = await svc.getMemberSpawnStatuses(teamName); expect(inFlightStatuses.teamLaunchState).toBe('partial_pending'); expect(inFlightStatuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 2, failedCount: 0, }); expect(inFlightStatuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); expect(inFlightStatuses.statuses.bob).toMatchObject({ status: 'spawning', launchState: 'starting', hardFailure: false, }); expect(inFlightStatuses.statuses.tom).toMatchObject({ status: 'spawning', launchState: 'starting', hardFailure: false, }); adapter.releaseLaunches(); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); const finalStatuses = await svc.getMemberSpawnStatuses(teamName); expect(finalStatuses.teamLaunchState).toBe('clean_success'); expect(finalStatuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(finalStatuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); expect(finalStatuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); }); it('does not double-dispatch mixed OpenCode secondary lanes when launch handoff is retried in flight', async () => { const teamName = 'mixed-retry-inflight-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); const firstLaneRunIds = run.mixedSecondaryLanes.map( (lane: { runId: string | null }) => lane.runId ); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); expect(adapter.pendingLaunchInputs).toHaveLength(1); expect(adapter.launchInputs).toHaveLength(0); expect(run.mixedSecondaryLanes.map((lane: { state: string }) => lane.state)).toEqual([ 'launching', 'queued', ]); expect(run.mixedSecondaryLanes.map((lane: { runId: string | null }) => lane.runId)).toEqual( firstLaneRunIds ); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); expect(adapter.launchInputs).toHaveLength(2); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); }); it('does not double-dispatch Anthropic mixed OpenCode secondary lanes when launch handoff is retried in flight', async () => { const teamName = 'mixed-anthropic-retry-inflight-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); const firstLaneRunIds = run.mixedSecondaryLanes.map( (lane: { runId: string | null }) => lane.runId ); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); expect(adapter.pendingLaunchInputs).toHaveLength(1); expect(adapter.launchInputs).toHaveLength(0); expect(run.mixedSecondaryLanes.map((lane: { state: string }) => lane.state)).toEqual([ 'launching', 'queued', ]); expect(run.mixedSecondaryLanes.map((lane: { runId: string | null }) => lane.runId)).toEqual( firstLaneRunIds ); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); expect(adapter.launchInputs).toHaveLength(2); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); }); it('does not dispatch mixed OpenCode secondary lanes after the primary launch run is cancelled', async () => { const teamName = 'mixed-cancel-before-handoff-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); run.cancelRequested = true; run.processKilled = true; const snapshot = await (svc as any).launchMixedSecondaryLaneIfNeeded(run); expect(snapshot).toBeNull(); expect(adapter.pendingLaunchInputs).toHaveLength(0); expect(adapter.launchInputs).toHaveLength(0); expect(run.mixedSecondaryLanes.map((lane: { state: string }) => lane.state)).toEqual([ 'queued', 'queued', ]); }); it('does not dispatch Anthropic mixed OpenCode secondary lanes after the primary launch run is cancelled', async () => { const teamName = 'mixed-anthropic-cancel-before-handoff-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); trackLiveRun(svc, run); run.cancelRequested = true; run.processKilled = true; const snapshot = await (svc as any).launchMixedSecondaryLaneIfNeeded(run); expect(snapshot).toBeNull(); expect(adapter.pendingLaunchInputs).toHaveLength(0); expect(adapter.launchInputs).toHaveLength(0); expect(run.mixedSecondaryLanes.map((lane: { state: string }) => lane.state)).toEqual([ 'queued', 'queued', ]); expect(run.memberSpawnStatuses.get('alice')).toMatchObject({ launchState: 'confirmed_alive', hardFailure: false, }); }); it('does not dispatch Anthropic and Gemini mixed OpenCode secondary lanes after the primary launch run is cancelled', async () => { const teamName = 'mixed-anthropic-gemini-cancel-before-handoff-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); addGeminiPrimaryToMixedRun(run); trackLiveRun(svc, run); run.cancelRequested = true; run.processKilled = true; const snapshot = await (svc as any).launchMixedSecondaryLaneIfNeeded(run); expect(snapshot).toBeNull(); expect(adapter.pendingLaunchInputs).toHaveLength(0); expect(adapter.launchInputs).toHaveLength(0); expect(run.mixedSecondaryLanes.map((lane: { state: string }) => lane.state)).toEqual([ 'queued', 'queued', ]); expect(run.memberSpawnStatuses.get('alice')).toMatchObject({ launchState: 'confirmed_alive', hardFailure: false, }); expect(run.memberSpawnStatuses.get('reviewer')).toMatchObject({ launchState: 'confirmed_alive', hardFailure: false, }); }); it('does not resurrect a stopped mixed launch when in-flight OpenCode lanes finish late', async () => { const teamName = 'mixed-stop-inflight-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); svc.stopTeam(teamName); await waitForCondition(() => !svc.isTeamAlive(teamName)); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs.map((input) => input.laneId).sort()).toEqual([ 'secondary:opencode:bob', ]); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 1); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(svc.isTeamAlive(teamName)).toBe(false); expect(statuses.teamLaunchState).not.toBe('clean_success'); expect(statuses.statuses.bob?.launchState).not.toBe('confirmed_alive'); expect(statuses.statuses.tom?.launchState).not.toBe('confirmed_alive'); }); it('does not resurrect a stopped Anthropic mixed launch when in-flight OpenCode lanes finish late', async () => { const teamName = 'mixed-anthropic-stop-inflight-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); svc.stopTeam(teamName); await waitForCondition(() => !svc.isTeamAlive(teamName)); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs.map((input) => input.laneId).sort()).toEqual([ 'secondary:opencode:bob', ]); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 1); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(svc.isTeamAlive(teamName)).toBe(false); expect(statuses.teamLaunchState).not.toBe('clean_success'); expect(statuses.statuses.alice).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.bob?.launchState).not.toBe('confirmed_alive'); expect(statuses.statuses.tom?.launchState).not.toBe('confirmed_alive'); }); it('does not resurrect a stopped Anthropic and Gemini mixed launch when in-flight OpenCode lanes finish late', async () => { const teamName = 'mixed-anthropic-gemini-stop-inflight-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); run.expectedMembers = ['alice', 'reviewer']; run.effectiveMembers = [ ...(run.effectiveMembers as Array>), { name: 'reviewer', role: 'Reviewer', providerId: 'gemini', model: 'gemini-2.5-flash', }, ]; run.allEffectiveMembers = [ ...(run.allEffectiveMembers as Array>), { name: 'reviewer', role: 'Reviewer', providerId: 'gemini', model: 'gemini-2.5-flash', }, ]; run.memberSpawnStatuses.set('reviewer', { status: 'online', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, lastHeartbeatAt: '2026-04-23T10:00:00.000Z', lastRuntimeAliveAt: '2026-04-23T10:00:00.000Z', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', livenessSource: 'heartbeat', }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); svc.stopTeam(teamName); await waitForCondition(() => !svc.isTeamAlive(teamName)); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs.map((input) => input.laneId).sort()).toEqual([ 'secondary:opencode:bob', ]); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 1); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(svc.isTeamAlive(teamName)).toBe(false); expect(statuses.teamLaunchState).not.toBe('clean_success'); expect(statuses.statuses.alice).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.reviewer).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.bob?.launchState).not.toBe('confirmed_alive'); expect(statuses.statuses.tom?.launchState).not.toBe('confirmed_alive'); }); it('stops one mixed in-flight launch without stopping another mixed team', async () => { const stoppedTeamName = 'mixed-stop-one-of-two-inflight-safe-e2e'; const survivingTeamName = 'mixed-survives-other-stop-inflight-safe-e2e'; await writeMixedTeamConfig({ teamName: stoppedTeamName, projectPath }); await writeTeamMeta(stoppedTeamName, projectPath); await writeMembersMeta(stoppedTeamName); await writeMixedTeamConfig({ teamName: survivingTeamName, projectPath }); await writeTeamMeta(survivingTeamName, projectPath); await writeMembersMeta(survivingTeamName); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const stoppedRun = createMixedLiveRun({ teamName: stoppedTeamName, projectPath }); const survivingRun = createMixedLiveRun({ teamName: survivingTeamName, projectPath }); stoppedRun.child = { kill: () => undefined }; survivingRun.child = { kill: () => undefined }; trackLiveRun(svc, stoppedRun); trackLiveRun(svc, survivingRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(stoppedRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(survivingRun); await waitForCondition(() => adapter.pendingLaunchInputs.some((input) => input.teamName === stoppedTeamName) ); svc.stopTeam(stoppedTeamName); await waitForCondition( () => adapter.stopInputs.filter((input) => input.teamName === stoppedTeamName).length === 1 ); expect(adapter.stopInputs.some((input) => input.teamName === survivingTeamName)).toBe(false); expect(svc.isTeamAlive(stoppedTeamName)).toBe(false); expect(svc.isTeamAlive(survivingTeamName)).toBe(true); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 3); await waitForCondition(() => survivingRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); const stoppedStatuses = await svc.getMemberSpawnStatuses(stoppedTeamName); const survivingStatuses = await svc.getMemberSpawnStatuses(survivingTeamName); expect(stoppedStatuses.teamLaunchState).not.toBe('clean_success'); expect(stoppedStatuses.statuses.bob?.launchState).not.toBe('confirmed_alive'); expect(stoppedStatuses.statuses.tom?.launchState).not.toBe('confirmed_alive'); expect(survivingStatuses.teamLaunchState).toBe('clean_success'); expect(survivingStatuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(survivingStatuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('does not let a stopped run late result overwrite newer mixed launch truth', async () => { const teamName = 'mixed-late-old-result-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const oldRun = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, oldRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(oldRun); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); svc.stopTeam(teamName); await waitForCondition(() => !svc.isTeamAlive(teamName)); await waitForCondition(() => adapter.stopInputs.length === 1); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ providerId: 'codex', providerBackendId: 'codex-native', model: 'gpt-5.4-mini', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'codex', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'runtime_pending_permission', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: false, hardFailure: false, pendingPermissionRequestIds: ['new-perm-bob'], }), tom: mixedMemberState({ providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'new run explicit failure', }), }, }); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 1); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); expect(statuses.statuses.bob).toMatchObject({ launchState: 'runtime_pending_permission', pendingPermissionRequestIds: ['new-perm-bob'], }); expect(statuses.statuses.tom).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailureReason: 'new run explicit failure', }); }); it('does not let a stopped Anthropic run late result overwrite newer mixed launch truth', async () => { const teamName = 'mixed-anthropic-late-old-result-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const oldRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); trackLiveRun(svc, oldRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(oldRun); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); svc.stopTeam(teamName); await waitForCondition(() => !svc.isTeamAlive(teamName)); await waitForCondition(() => adapter.stopInputs.length === 1); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'runtime_pending_permission', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: false, hardFailure: false, pendingPermissionRequestIds: ['new-perm-bob'], }), tom: mixedMemberState({ providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'new Anthropic run explicit failure', }), }, }); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 1); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ launchState: 'runtime_pending_permission', pendingPermissionRequestIds: ['new-perm-bob'], }); expect(statuses.statuses.tom).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailureReason: 'new Anthropic run explicit failure', }); }); it('does not let a stopped Anthropic and Gemini run late result overwrite newer mixed launch truth', async () => { const teamName = 'mixed-anthropic-gemini-late-old-result-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const oldRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); const reviewer = { name: 'reviewer', role: 'Reviewer', providerId: 'gemini', model: 'gemini-2.5-flash', }; oldRun.expectedMembers = ['alice', 'reviewer']; oldRun.effectiveMembers = [...oldRun.effectiveMembers, reviewer]; oldRun.allEffectiveMembers = [ ...oldRun.effectiveMembers, ...oldRun.allEffectiveMembers.filter( (member: { providerId?: string }) => member.providerId === 'opencode' ), ]; oldRun.memberSpawnStatuses.set('reviewer', { status: 'online', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, lastHeartbeatAt: '2026-04-23T10:00:00.000Z', lastRuntimeAliveAt: '2026-04-23T10:00:00.000Z', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', livenessSource: 'heartbeat', }); trackLiveRun(svc, oldRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(oldRun); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); svc.stopTeam(teamName); await waitForCondition(() => !svc.isTeamAlive(teamName)); await waitForCondition(() => adapter.stopInputs.length === 1); await writeMixedTeamLaunchState({ teamName, members: { alice: mixedMemberState({ providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), reviewer: mixedMemberState({ providerId: 'gemini', model: 'gemini-2.5-flash', laneId: 'primary:gemini:reviewer', laneKind: 'primary', laneOwnerProviderId: 'gemini', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'runtime_pending_permission', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: false, hardFailure: false, pendingPermissionRequestIds: ['new-perm-bob'], }), tom: mixedMemberState({ providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'failed_to_start', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason: 'new Anthropic and Gemini run explicit failure', }), }, }); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 1); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.reviewer).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ launchState: 'runtime_pending_permission', pendingPermissionRequestIds: ['new-perm-bob'], }); expect(statuses.statuses.tom).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailureReason: 'new Anthropic and Gemini run explicit failure', }); }); it('does not degrade stopped mixed launch lanes when in-flight OpenCode launch errors late', async () => { const teamName = 'mixed-stop-late-error-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); const adapter = new RejectingBlockingOpenCodeRuntimeAdapter('late fake bridge failure'); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); svc.stopTeam(teamName); await waitForCondition(() => !svc.isTeamAlive(teamName)); await waitForCondition(() => adapter.stopInputs.length === 1); adapter.releaseLaunches(); await waitForCondition(() => adapter.rejectedLaunchCount === 1); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: {}, } ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).not.toBe('partial_failure'); expect(statuses.statuses.bob).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.bob?.launchState).not.toBe('failed_to_start'); expect(statuses.statuses.tom).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.tom?.launchState).not.toBe('failed_to_start'); }); it('does not degrade stopped Anthropic mixed launch lanes when in-flight OpenCode launch errors late', async () => { const teamName = 'mixed-anthropic-stop-late-error-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); const adapter = new RejectingBlockingOpenCodeRuntimeAdapter( 'late fake Anthropic bridge failure' ); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); svc.stopTeam(teamName); await waitForCondition(() => !svc.isTeamAlive(teamName)); await waitForCondition(() => adapter.stopInputs.length === 1); adapter.releaseLaunches(); await waitForCondition(() => adapter.rejectedLaunchCount === 1); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: {}, } ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).not.toBe('partial_failure'); expect(statuses.statuses.alice).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.bob?.launchState).not.toBe('failed_to_start'); expect(statuses.statuses.tom).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.tom?.launchState).not.toBe('failed_to_start'); }); it('does not degrade stopped Anthropic and Gemini mixed launch lanes when in-flight OpenCode launch errors late', async () => { const teamName = 'mixed-anthropic-gemini-stop-late-error-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new RejectingBlockingOpenCodeRuntimeAdapter( 'late fake Anthropic and Gemini bridge failure' ); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); const reviewer = { name: 'reviewer', role: 'Reviewer', providerId: 'gemini', model: 'gemini-2.5-flash', }; run.expectedMembers = ['alice', 'reviewer']; run.effectiveMembers = [...run.effectiveMembers, reviewer]; run.allEffectiveMembers = [ ...run.effectiveMembers, ...run.allEffectiveMembers.filter( (member: { providerId?: string }) => member.providerId === 'opencode' ), ]; run.memberSpawnStatuses.set('reviewer', { status: 'online', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, lastHeartbeatAt: '2026-04-23T10:00:00.000Z', lastRuntimeAliveAt: '2026-04-23T10:00:00.000Z', lastEvaluatedAt: '2026-04-23T10:00:00.000Z', updatedAt: '2026-04-23T10:00:00.000Z', livenessSource: 'heartbeat', }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); svc.stopTeam(teamName); await waitForCondition(() => !svc.isTeamAlive(teamName)); await waitForCondition(() => adapter.stopInputs.length === 1); adapter.releaseLaunches(); await waitForCondition(() => adapter.rejectedLaunchCount === 1); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: {}, } ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).not.toBe('partial_failure'); expect(statuses.statuses.alice).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.reviewer).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.bob?.launchState).not.toBe('failed_to_start'); expect(statuses.statuses.tom).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.tom?.launchState).not.toBe('failed_to_start'); }); it('stops mixed OpenCode secondary lanes when provisioning is cancelled mid-launch', async () => { const teamName = 'mixed-cancel-inflight-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); await svc.cancelProvisioning(run.runId); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs.map((input) => input.laneId).sort()).toEqual([ 'secondary:opencode:bob', ]); expect(svc.isTeamAlive(teamName)).toBe(false); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 1); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).not.toBe('clean_success'); expect(statuses.statuses.bob?.launchState).not.toBe('confirmed_alive'); expect(statuses.statuses.tom?.launchState).not.toBe('confirmed_alive'); }); it('stops Anthropic mixed OpenCode secondary lanes when provisioning is cancelled mid-launch', async () => { const teamName = 'mixed-anthropic-cancel-inflight-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); await svc.cancelProvisioning(run.runId); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs.map((input) => input.laneId).sort()).toEqual([ 'secondary:opencode:bob', ]); expect(svc.isTeamAlive(teamName)).toBe(false); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 1); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).not.toBe('clean_success'); expect(statuses.statuses.alice).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.bob?.launchState).not.toBe('confirmed_alive'); expect(statuses.statuses.tom?.launchState).not.toBe('confirmed_alive'); }); it('stops Anthropic and Gemini mixed OpenCode secondary lanes when provisioning is cancelled mid-launch', async () => { const teamName = 'mixed-anthropic-gemini-cancel-inflight-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); addGeminiPrimaryToMixedRun(run); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); await svc.cancelProvisioning(run.runId); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs.map((input) => input.laneId).sort()).toEqual([ 'secondary:opencode:bob', ]); expect(svc.isTeamAlive(teamName)).toBe(false); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 1); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).not.toBe('clean_success'); expect(statuses.statuses.alice).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.reviewer).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.bob?.launchState).not.toBe('confirmed_alive'); expect(statuses.statuses.tom?.launchState).not.toBe('confirmed_alive'); }); it('allows fresh Anthropic and Gemini mixed OpenCode lanes after cancel cancelled in-flight handoff', async () => { const teamName = 'mixed-anthropic-gemini-fresh-after-cancelled-handoff-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const cancelledRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic', }); addGeminiPrimaryToMixedRun(cancelledRun); trackLiveRun(svc, cancelledRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(cancelledRun); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); await svc.cancelProvisioning(cancelledRun.runId); await waitForCondition(() => adapter.stopInputs.length === 1); expect(adapter.stopInputs.map((input) => input.laneId).sort()).toEqual([ 'secondary:opencode:bob', ]); expect(svc.isTeamAlive(teamName)).toBe(false); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 1); const cancelledStatuses = await svc.getMemberSpawnStatuses(teamName); expect(cancelledStatuses.teamLaunchState).not.toBe('clean_success'); expect(cancelledStatuses.statuses.alice).toMatchObject({ hardFailure: false }); expect(cancelledStatuses.statuses.reviewer).toMatchObject({ hardFailure: false }); expect(cancelledStatuses.statuses.bob?.launchState).not.toBe('confirmed_alive'); expect(cancelledStatuses.statuses.tom?.launchState).not.toBe('confirmed_alive'); const freshRun = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); freshRun.runId = `${cancelledRun.runId}-fresh`; freshRun.detectedSessionId = 'lead-session-fresh'; addGeminiPrimaryToMixedRun(freshRun); trackLiveRun(svc, freshRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(freshRun); await waitForCondition(() => adapter.launchInputs.length === 3); await waitForCondition(() => freshRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); const freshStatuses = await svc.getMemberSpawnStatuses(teamName); expect(freshStatuses.teamLaunchState).toBe('clean_success'); expect(freshStatuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(freshStatuses.statuses.reviewer).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(freshStatuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(freshStatuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('cancels one mixed in-flight launch without cancelling another mixed team', async () => { const cancelledTeamName = 'mixed-cancel-one-of-two-inflight-safe-e2e'; const survivingTeamName = 'mixed-survives-other-cancel-inflight-safe-e2e'; await writeMixedTeamConfig({ teamName: cancelledTeamName, projectPath }); await writeTeamMeta(cancelledTeamName, projectPath); await writeMembersMeta(cancelledTeamName); await writeMixedTeamConfig({ teamName: survivingTeamName, projectPath }); await writeTeamMeta(survivingTeamName, projectPath); await writeMembersMeta(survivingTeamName); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const cancelledRun = createMixedLiveRun({ teamName: cancelledTeamName, projectPath }); const survivingRun = createMixedLiveRun({ teamName: survivingTeamName, projectPath }); cancelledRun.child = { kill: () => undefined }; survivingRun.child = { kill: () => undefined }; trackLiveRun(svc, cancelledRun); trackLiveRun(svc, survivingRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(cancelledRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(survivingRun); await waitForCondition(() => adapter.pendingLaunchInputs.some((input) => input.teamName === cancelledTeamName) ); await svc.cancelProvisioning(cancelledRun.runId); await waitForCondition( () => adapter.stopInputs.filter((input) => input.teamName === cancelledTeamName).length === 1 ); expect(adapter.stopInputs.some((input) => input.teamName === survivingTeamName)).toBe(false); expect(svc.isTeamAlive(cancelledTeamName)).toBe(false); expect(svc.isTeamAlive(survivingTeamName)).toBe(true); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 3); await waitForCondition(() => survivingRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); const cancelledStatuses = await svc.getMemberSpawnStatuses(cancelledTeamName); const survivingStatuses = await svc.getMemberSpawnStatuses(survivingTeamName); expect(cancelledStatuses.teamLaunchState).not.toBe('clean_success'); expect(cancelledStatuses.statuses.bob?.launchState).not.toBe('confirmed_alive'); expect(cancelledStatuses.statuses.tom?.launchState).not.toBe('confirmed_alive'); expect(survivingStatuses.teamLaunchState).toBe('clean_success'); expect(survivingStatuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(survivingStatuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('cancels one Anthropic and Gemini mixed in-flight launch without cancelling another mixed team', async () => { const cancelledTeamName = 'mixed-anthropic-gemini-cancel-one-inflight-safe-e2e'; const survivingTeamName = 'mixed-anthropic-gemini-survives-cancel-safe-e2e'; await writeMixedTeamConfig({ teamName: cancelledTeamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(cancelledTeamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(cancelledTeamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeMixedTeamConfig({ teamName: survivingTeamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(survivingTeamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(survivingTeamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const cancelledRun = createMixedLiveRun({ teamName: cancelledTeamName, projectPath, primaryProviderId: 'anthropic', }); const survivingRun = createMixedLiveRun({ teamName: survivingTeamName, projectPath, primaryProviderId: 'anthropic', }); addGeminiPrimaryToMixedRun(cancelledRun); addGeminiPrimaryToMixedRun(survivingRun); cancelledRun.child = { kill: () => undefined }; survivingRun.child = { kill: () => undefined }; trackLiveRun(svc, cancelledRun); trackLiveRun(svc, survivingRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(cancelledRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(survivingRun); await waitForCondition(() => adapter.pendingLaunchInputs.length === 2); await svc.cancelProvisioning(cancelledRun.runId); await waitForCondition( () => adapter.stopInputs.filter((input) => input.teamName === cancelledTeamName).length === 1 ); expect(adapter.stopInputs.some((input) => input.teamName === survivingTeamName)).toBe(false); expect(svc.isTeamAlive(cancelledTeamName)).toBe(false); expect(svc.isTeamAlive(survivingTeamName)).toBe(true); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 3); await waitForCondition(() => survivingRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); const cancelledStatuses = await svc.getMemberSpawnStatuses(cancelledTeamName); const survivingStatuses = await svc.getMemberSpawnStatuses(survivingTeamName); expect(cancelledStatuses.teamLaunchState).not.toBe('clean_success'); expect(cancelledStatuses.statuses.bob?.launchState).not.toBe('confirmed_alive'); expect(cancelledStatuses.statuses.tom?.launchState).not.toBe('confirmed_alive'); expect(survivingStatuses.teamLaunchState).toBe('clean_success'); expect(survivingStatuses.statuses.reviewer).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(survivingStatuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(survivingStatuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('allows a cancelled mixed team to relaunch OpenCode secondary lanes without disturbing its surviving sibling', async () => { const cancelledTeamName = 'mixed-cancel-one-then-relaunch-safe-e2e'; const survivingTeamName = 'mixed-survives-cancel-while-sibling-relaunches-safe-e2e'; await writeMixedTeamConfig({ teamName: cancelledTeamName, projectPath }); await writeTeamMeta(cancelledTeamName, projectPath); await writeMembersMeta(cancelledTeamName); await writeMixedTeamConfig({ teamName: survivingTeamName, projectPath }); await writeTeamMeta(survivingTeamName, projectPath); await writeMembersMeta(survivingTeamName); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const cancelledRun = createMixedLiveRun({ teamName: cancelledTeamName, projectPath }); const survivingRun = createMixedLiveRun({ teamName: survivingTeamName, projectPath }); cancelledRun.child = { kill: () => undefined }; survivingRun.child = { kill: () => undefined }; trackLiveRun(svc, cancelledRun); trackLiveRun(svc, survivingRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(cancelledRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(survivingRun); await waitForCondition(() => adapter.pendingLaunchInputs.length === 2); await svc.cancelProvisioning(cancelledRun.runId); await waitForCondition( () => adapter.stopInputs.filter((input) => input.teamName === cancelledTeamName).length === 1 ); expect(adapter.stopInputs.some((input) => input.teamName === survivingTeamName)).toBe(false); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 3); await waitForCondition(() => survivingRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); const freshRun = createMixedLiveRun({ teamName: cancelledTeamName, projectPath }); freshRun.runId = `${cancelledRun.runId}-fresh`; freshRun.detectedSessionId = 'lead-session-fresh'; freshRun.child = { kill: () => undefined }; trackLiveRun(svc, freshRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(freshRun); await waitForCondition(() => adapter.launchInputs.length === 5); await waitForCondition(() => freshRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); const relaunchedStatuses = await svc.getMemberSpawnStatuses(cancelledTeamName); const survivingStatuses = await svc.getMemberSpawnStatuses(survivingTeamName); expect(relaunchedStatuses.teamLaunchState).toBe('clean_success'); expect(relaunchedStatuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(relaunchedStatuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(survivingStatuses.teamLaunchState).toBe('clean_success'); expect(survivingStatuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(survivingStatuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('allows a cancelled Anthropic and Gemini mixed team to relaunch while its sibling stays online', async () => { const cancelledTeamName = 'mixed-anthropic-gemini-cancel-one-then-relaunch-safe-e2e'; const survivingTeamName = 'mixed-anthropic-gemini-survives-sibling-relaunch-safe-e2e'; await writeMixedTeamConfig({ teamName: cancelledTeamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(cancelledTeamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(cancelledTeamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeMixedTeamConfig({ teamName: survivingTeamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(survivingTeamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(survivingTeamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new BlockingOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const cancelledRun = createMixedLiveRun({ teamName: cancelledTeamName, projectPath, primaryProviderId: 'anthropic', }); const survivingRun = createMixedLiveRun({ teamName: survivingTeamName, projectPath, primaryProviderId: 'anthropic', }); addGeminiPrimaryToMixedRun(cancelledRun); addGeminiPrimaryToMixedRun(survivingRun); cancelledRun.child = { kill: () => undefined }; survivingRun.child = { kill: () => undefined }; trackLiveRun(svc, cancelledRun); trackLiveRun(svc, survivingRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(cancelledRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(survivingRun); await waitForCondition(() => adapter.pendingLaunchInputs.length === 2); await svc.cancelProvisioning(cancelledRun.runId); await waitForCondition( () => adapter.stopInputs.filter((input) => input.teamName === cancelledTeamName).length === 1 ); expect(adapter.stopInputs.some((input) => input.teamName === survivingTeamName)).toBe(false); adapter.releaseLaunches(); await waitForCondition(() => adapter.launchInputs.length === 3); await waitForCondition(() => survivingRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); const freshRun = createMixedLiveRun({ teamName: cancelledTeamName, projectPath, primaryProviderId: 'anthropic', }); freshRun.runId = `${cancelledRun.runId}-fresh`; freshRun.detectedSessionId = 'lead-session-fresh'; freshRun.child = { kill: () => undefined }; addGeminiPrimaryToMixedRun(freshRun); trackLiveRun(svc, freshRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(freshRun); await waitForCondition(() => adapter.launchInputs.length === 5); await waitForCondition(() => freshRun.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished') ); const relaunchedStatuses = await svc.getMemberSpawnStatuses(cancelledTeamName); const survivingStatuses = await svc.getMemberSpawnStatuses(survivingTeamName); expect(relaunchedStatuses.teamLaunchState).toBe('clean_success'); expect(relaunchedStatuses.statuses.reviewer).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(relaunchedStatuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(relaunchedStatuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(survivingStatuses.teamLaunchState).toBe('clean_success'); expect(survivingStatuses.statuses.reviewer).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(survivingStatuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(survivingStatuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); }); it('does not degrade mixed OpenCode lanes when in-flight launch errors after cancel', async () => { const teamName = 'mixed-cancel-late-error-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); const adapter = new RejectingBlockingOpenCodeRuntimeAdapter('late fake cancel bridge failure'); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); await svc.cancelProvisioning(run.runId); await waitForCondition(() => adapter.stopInputs.length === 1); adapter.releaseLaunches(); await waitForCondition(() => adapter.rejectedLaunchCount === 1); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: {}, } ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).not.toBe('partial_failure'); expect(statuses.statuses.bob).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.bob?.launchState).not.toBe('failed_to_start'); expect(statuses.statuses.tom).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.tom?.launchState).not.toBe('failed_to_start'); }); it('does not degrade Anthropic mixed OpenCode lanes when in-flight launch errors after cancel', async () => { const teamName = 'mixed-anthropic-cancel-late-error-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, primaryProviderId: 'anthropic' }); const adapter = new RejectingBlockingOpenCodeRuntimeAdapter( 'late fake Anthropic cancel bridge failure' ); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); await svc.cancelProvisioning(run.runId); await waitForCondition(() => adapter.stopInputs.length === 1); adapter.releaseLaunches(); await waitForCondition(() => adapter.rejectedLaunchCount === 1); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: {}, } ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).not.toBe('partial_failure'); expect(statuses.statuses.alice).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.bob?.launchState).not.toBe('failed_to_start'); expect(statuses.statuses.tom).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.tom?.launchState).not.toBe('failed_to_start'); }); it('does not degrade Anthropic and Gemini mixed OpenCode lanes when in-flight launch errors after cancel', async () => { const teamName = 'mixed-anthropic-gemini-cancel-late-error-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); const adapter = new RejectingBlockingOpenCodeRuntimeAdapter( 'late fake Anthropic and Gemini cancel bridge failure' ); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); addGeminiPrimaryToMixedRun(run); trackLiveRun(svc, run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); await svc.cancelProvisioning(run.runId); await waitForCondition(() => adapter.stopInputs.length === 1); adapter.releaseLaunches(); await waitForCondition(() => adapter.rejectedLaunchCount === 1); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: {}, } ); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).not.toBe('partial_failure'); expect(statuses.statuses.alice).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.reviewer).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.bob).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.bob?.launchState).not.toBe('failed_to_start'); expect(statuses.statuses.tom).toMatchObject({ hardFailure: false, }); expect(statuses.statuses.tom?.launchState).not.toBe('failed_to_start'); }); it('degrades stale active mixed OpenCode lanes when lane state is missing on disk', async () => { const teamName = 'mixed-stale-lanes-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:bob', state: 'active', }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:tom', state: 'active', }); const svc = new TeamProvisioningService(); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.expectedMembers).toEqual(expect.arrayContaining(['alice', 'bob', 'tom'])); expect(statuses.statuses.bob).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, error: expect.stringContaining('no lane state exists on disk'), }); expect(statuses.statuses.tom).toMatchObject({ status: 'error', launchState: 'failed_to_start', hardFailure: true, error: expect.stringContaining('no lane state exists on disk'), }); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { 'secondary:opencode:bob': { state: 'degraded' }, 'secondary:opencode:tom': { state: 'degraded' }, }, } ); }); it('recovers stale active mixed OpenCode lanes from runtime reconcile before degrading them', async () => { const teamName = 'mixed-runtime-recover-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:bob', state: 'active', }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:tom', state: 'active', }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'confirmed', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(adapter.reconcileInputs.map((input) => input.laneId).sort()).toEqual([ 'secondary:opencode:bob', 'secondary:opencode:tom', ]); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); expect(statuses.statuses.tom).toMatchObject({ status: 'online', launchState: 'confirmed_alive', }); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { 'secondary:opencode:bob': { state: 'active' }, 'secondary:opencode:tom': { state: 'active' }, }, } ); }); it('recovers stale active mixed OpenCode lanes into ready and permission-pending states before degrading them', async () => { const teamName = 'mixed-runtime-recover-split-permission-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:bob', state: 'active', }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:tom', state: 'active', }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'permission', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(adapter.reconcileInputs.map((input) => input.laneId).sort()).toEqual([ 'secondary:opencode:bob', 'secondary:opencode:tom', ]); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_permission', hardFailure: false, pendingPermissionRequestIds: ['perm-tom'], }); expect(statuses.statuses.bob?.launchState).not.toBe('failed_to_start'); expect(statuses.statuses.tom?.launchState).not.toBe('failed_to_start'); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { 'secondary:opencode:bob': { state: 'active' }, 'secondary:opencode:tom': { state: 'active' }, }, } ); }); it('recovers stale active mixed OpenCode lanes into ready and bootstrap-pending states before degrading them', async () => { const teamName = 'mixed-runtime-recover-split-bootstrap-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath }); await writeTeamMeta(teamName, projectPath); await writeMembersMeta(teamName); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:bob', state: 'active', }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:tom', state: 'active', }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'launching', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(adapter.reconcileInputs.map((input) => input.laneId).sort()).toEqual([ 'secondary:opencode:bob', 'secondary:opencode:tom', ]); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 2, failedCount: 0, runtimeAlivePendingCount: 0, }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }); expect(statuses.statuses.bob?.launchState).not.toBe('failed_to_start'); expect(statuses.statuses.tom?.launchState).not.toBe('failed_to_start'); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { 'secondary:opencode:bob': { state: 'active' }, 'secondary:opencode:tom': { state: 'active' }, }, } ); }); it('recovers stale active Anthropic and Gemini configured OpenCode lanes into ready and permission-pending states before degrading them', async () => { const teamName = 'mixed-anthropic-gemini-configured-runtime-recover-split-permission-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:bob', state: 'active', }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:tom', state: 'active', }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'permission', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(adapter.reconcileInputs.map((input) => input.laneId).sort()).toEqual([ 'secondary:opencode:bob', 'secondary:opencode:tom', ]); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 3, failedCount: 0, }); expect(statuses.expectedMembers).toEqual( expect.arrayContaining(['alice', 'reviewer', 'bob', 'tom']) ); expect(statuses.statuses.alice?.launchState).not.toBe('failed_to_start'); expect(statuses.statuses.reviewer?.launchState).not.toBe('failed_to_start'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_permission', hardFailure: false, pendingPermissionRequestIds: ['perm-tom'], }); expect(statuses.statuses.bob?.launchState).not.toBe('failed_to_start'); expect(statuses.statuses.tom?.launchState).not.toBe('failed_to_start'); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { 'secondary:opencode:bob': { state: 'active' }, 'secondary:opencode:tom': { state: 'active' }, }, } ); }); it('recovers stale active Anthropic and Gemini configured OpenCode lanes into ready and bootstrap-pending states before degrading them', async () => { const teamName = 'mixed-anthropic-gemini-configured-runtime-recover-split-bootstrap-safe-e2e'; await writeMixedTeamConfig({ teamName, projectPath, includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(teamName, { includeGeminiPrimary: true, primaryProviderId: 'anthropic', }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:bob', state: 'active', }); await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName, laneId: 'secondary:opencode:tom', state: 'active', }); const adapter = new FakeOpenCodeRuntimeAdapter('clean_success', { bob: 'confirmed', tom: 'launching', }); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); const statuses = await svc.getMemberSpawnStatuses(teamName); expect(adapter.reconcileInputs.map((input) => input.laneId).sort()).toEqual([ 'secondary:opencode:bob', 'secondary:opencode:tom', ]); expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.summary).toMatchObject({ confirmedCount: 1, pendingCount: 3, failedCount: 0, runtimeAlivePendingCount: 0, }); expect(statuses.expectedMembers).toEqual( expect.arrayContaining(['alice', 'reviewer', 'bob', 'tom']) ); expect(statuses.statuses.alice?.launchState).not.toBe('failed_to_start'); expect(statuses.statuses.reviewer?.launchState).not.toBe('failed_to_start'); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(statuses.statuses.tom).toMatchObject({ status: 'waiting', launchState: 'runtime_pending_bootstrap', runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, }); expect(statuses.statuses.bob?.launchState).not.toBe('failed_to_start'); expect(statuses.statuses.tom?.launchState).not.toBe('failed_to_start'); await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( { lanes: { 'secondary:opencode:bob': { state: 'active' }, 'secondary:opencode:tom': { state: 'active' }, }, } ); }); it('recovers pure OpenCode launch statuses from disk after service restart', async () => { const adapter = new FakeOpenCodeRuntimeAdapter(); const firstService = new TeamProvisioningService(); firstService.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await firstService.createTeam( { teamName: 'restart-opencode-safe-e2e', cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [ { name: 'alice', role: 'Developer', providerId: 'opencode' }, { name: 'bob', role: 'Reviewer', providerId: 'opencode' }, ], }, () => undefined ); const restartedService = new TeamProvisioningService(); const statuses = await restartedService.getMemberSpawnStatuses('restart-opencode-safe-e2e'); expect(statuses).toMatchObject({ source: 'persisted', teamLaunchState: 'clean_success', }); expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', runtimeAlive: true, }); expect(statuses.statuses.bob).toMatchObject({ status: 'online', launchState: 'confirmed_alive', runtimeAlive: true, }); }); it('relaunches an OpenCode team after a failed runtime adapter launch and replaces stale failures', async () => { const adapter = new FakeOpenCodeRuntimeAdapter('partial_failure'); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await svc.createTeam( { teamName: 'failed-then-relaunch-opencode-safe-e2e', cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); const failedStatuses = await svc.getMemberSpawnStatuses( 'failed-then-relaunch-opencode-safe-e2e' ); expect(failedStatuses.teamLaunchState).toBe('partial_failure'); expect(failedStatuses.statuses.alice).toMatchObject({ status: 'error', hardFailure: true, }); adapter.setLaunchResult('clean_success'); await svc.launchTeam( { teamName: 'failed-then-relaunch-opencode-safe-e2e', cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, }, () => undefined ); const relaunchedStatuses = await svc.getMemberSpawnStatuses( 'failed-then-relaunch-opencode-safe-e2e' ); expect(relaunchedStatuses.teamLaunchState).toBe('clean_success'); expect(relaunchedStatuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', hardFailure: false, }); expect(relaunchedStatuses.statuses.alice?.hardFailureReason).toBeUndefined(); }); it('relaunches an OpenCode team after permission-pending stop and clears pending permissions', async () => { const adapter = new FakeOpenCodeRuntimeAdapter('partial_pending'); const svc = new TeamProvisioningService(); svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); await svc.createTeam( { teamName: 'pending-then-relaunch-opencode-safe-e2e', cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: false, members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], }, () => undefined ); const pendingStatuses = await svc.getMemberSpawnStatuses( 'pending-then-relaunch-opencode-safe-e2e' ); expect(pendingStatuses.statuses.alice).toMatchObject({ launchState: 'runtime_pending_permission', pendingPermissionRequestIds: ['perm-alice'], }); svc.stopTeam('pending-then-relaunch-opencode-safe-e2e'); await waitForCondition(() => adapter.stopInputs.length === 1); adapter.setLaunchResult('clean_success'); await svc.launchTeam( { teamName: 'pending-then-relaunch-opencode-safe-e2e', cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', skipPermissions: true, }, () => undefined ); const relaunchedStatuses = await svc.getMemberSpawnStatuses( 'pending-then-relaunch-opencode-safe-e2e' ); expect(relaunchedStatuses.teamLaunchState).toBe('clean_success'); expect(relaunchedStatuses.statuses.alice).toMatchObject({ status: 'online', launchState: 'confirmed_alive', bootstrapConfirmed: true, }); expect(relaunchedStatuses.statuses.alice?.pendingPermissionRequestIds).toBeUndefined(); }); }, LAUNCH_MATRIX_SAFE_E2E_TIMEOUT_MS); type FakeMemberOutcome = 'confirmed' | 'permission' | 'launching' | 'failed'; type MixedPrimaryProviderId = 'anthropic' | 'codex'; class FakeOpenCodeRuntimeAdapter implements TeamLaunchRuntimeAdapter { readonly providerId = 'opencode' as const; readonly launchInputs: TeamRuntimeLaunchInput[] = []; readonly messageInputs: OpenCodeTeamRuntimeMessageInput[] = []; readonly reconcileInputs: TeamRuntimeReconcileInput[] = []; readonly stopInputs: TeamRuntimeStopInput[] = []; constructor( private launchState: TeamRuntimeLaunchResult['teamLaunchState'] = 'clean_success', private memberOutcomes: Record = {} ) {} setLaunchResult( launchState: TeamRuntimeLaunchResult['teamLaunchState'], memberOutcomes: Record = {} ): void { this.launchState = launchState; this.memberOutcomes = memberOutcomes; } async prepare(input: TeamRuntimeLaunchInput): Promise { return { ok: true, providerId: 'opencode', modelId: input.model ?? null, diagnostics: [], warnings: [], }; } async launch(input: TeamRuntimeLaunchInput): Promise { this.launchInputs.push(input); return { runId: input.runId, teamName: input.teamName, launchPhase: 'finished', teamLaunchState: this.aggregateLaunchState(input.expectedMembers), members: Object.fromEntries( input.expectedMembers.map((member, index) => [ member.name, this.buildMemberEvidence(member, index), ]) ), warnings: [], diagnostics: this.launchState === 'partial_failure' ? ['fake OpenCode launch failed'] : this.launchState === 'partial_pending' ? ['fake OpenCode launch awaiting permission'] : ['fake OpenCode launch ready'], }; } async sendMessageToMember( input: OpenCodeTeamRuntimeMessageInput ): Promise { this.messageInputs.push(input); return { ok: true, providerId: 'opencode', memberName: input.memberName, sessionId: `session-${input.memberName}`, runtimePromptMessageId: input.messageId ? `prompt-${input.messageId}` : `prompt-${input.memberName}-${this.messageInputs.length}`, runtimePid: 12_000 + this.messageInputs.length, diagnostics: [], }; } async reconcile(input: TeamRuntimeReconcileInput): Promise { this.reconcileInputs.push(input); const members = Object.fromEntries( input.expectedMembers.map((member, index) => [ member.name, this.buildMemberEvidence(member, index), ]) ); return { runId: input.runId, teamName: input.teamName, launchPhase: 'reconciled', teamLaunchState: this.aggregateLaunchState(input.expectedMembers), members, snapshot: null, warnings: [], diagnostics: ['fake reconcile'], }; } async stop(input: TeamRuntimeStopInput): Promise { this.stopInputs.push(input); return { runId: input.runId, teamName: input.teamName, stopped: true, members: {}, warnings: [], diagnostics: ['fake stop'], }; } private defaultOutcome(): FakeMemberOutcome { if (this.launchState === 'partial_failure') { return 'failed'; } if (this.launchState === 'partial_pending') { return 'permission'; } return 'confirmed'; } private buildMemberEvidence( member: Pick, index: number ): TeamRuntimeMemberLaunchEvidence { const outcome = this.memberOutcomes[member.name] ?? this.defaultOutcome(); const failed = outcome === 'failed'; const permissionPending = outcome === 'permission'; const bootstrapPending = outcome === 'launching'; const livenessKind = failed ? 'not_found' : permissionPending ? 'permission_blocked' : bootstrapPending ? 'runtime_process_candidate' : 'confirmed_bootstrap'; const runtimeDiagnostic = permissionPending ? 'OpenCode runtime is waiting for permission approval' : bootstrapPending ? 'OpenCode runtime pid reported by bridge without local process verification' : undefined; return { memberName: member.name, providerId: 'opencode', launchState: failed ? 'failed_to_start' : permissionPending ? 'runtime_pending_permission' : bootstrapPending ? 'runtime_pending_bootstrap' : 'confirmed_alive', agentToolAccepted: !failed, runtimeAlive: !failed && !permissionPending && !bootstrapPending, bootstrapConfirmed: !failed && !permissionPending && !bootstrapPending, hardFailure: failed, hardFailureReason: failed ? 'fake_open_code_launch_failure' : undefined, pendingPermissionRequestIds: permissionPending ? [`perm-${member.name}`] : undefined, sessionId: failed ? undefined : `session-${member.name}`, runtimePid: failed ? undefined : 10_000 + index, livenessKind, pidSource: failed ? undefined : 'opencode_bridge', runtimeDiagnostic, diagnostics: failed ? ['fake OpenCode launch failure'] : permissionPending ? ['fake OpenCode launch awaiting permission'] : bootstrapPending ? ['fake OpenCode launch awaiting bootstrap'] : ['fake OpenCode launch ready'], }; } private aggregateLaunchState( members: readonly Pick[] ): TeamRuntimeLaunchResult['teamLaunchState'] { const outcomes = members.map( (member) => this.memberOutcomes[member.name] ?? this.defaultOutcome() ); if (outcomes.some((outcome) => outcome === 'failed')) { return 'partial_failure'; } if (outcomes.some((outcome) => outcome === 'permission' || outcome === 'launching')) { return 'partial_pending'; } return 'clean_success'; } } class VisibleReplyOpenCodeRuntimeAdapter extends FakeOpenCodeRuntimeAdapter { constructor(private readonly options: { replySource: InboxMessage['source'] }) { super(); } override async sendMessageToMember( input: OpenCodeTeamRuntimeMessageInput ): Promise { const result = await super.sendMessageToMember(input); const relayOfMessageId = input.messageId?.trim() || `message-${this.messageInputs.length}`; const replyRecipient = input.replyRecipient?.trim() || 'user'; const replyMessageId = `reply-${relayOfMessageId}`; const inboxPath = path.join( getTeamsBasePath(), input.teamName, 'inboxes', `${replyRecipient}.json` ); const rows: InboxMessage[] = await readInboxRows(input.teamName, replyRecipient).catch( () => [] ); rows.push({ from: input.memberName, to: replyRecipient, text: `Visible reply for ${relayOfMessageId}`, summary: 'visible reply', timestamp: '2026-05-08T10:00:00.000Z', read: false, messageId: replyMessageId, relayOfMessageId, source: this.options.replySource, }); await fs.mkdir(path.dirname(inboxPath), { recursive: true }); await fs.writeFile(inboxPath, `${JSON.stringify(rows, null, 2)}\n`, 'utf8'); return { ...result, responseObservation: { state: 'responded_visible_message', deliveredUserMessageId: `delivered-${relayOfMessageId}`, assistantMessageId: `assistant-${relayOfMessageId}`, toolCallNames: ['message_send'], visibleMessageToolCallId: `call-${relayOfMessageId}`, visibleReplyMessageId: replyMessageId, visibleReplyCorrelation: 'relayOfMessageId', latestAssistantPreview: null, reason: 'visible_message_sent', }, }; } } class BootstrapCheckingOpenCodeRuntimeAdapter extends FakeOpenCodeRuntimeAdapter { readonly bootstrapCheckins: { memberName: string; runId: string; state: string }[] = []; constructor(private readonly svc: TeamProvisioningService) { super(); } override async launch(input: TeamRuntimeLaunchInput): Promise { const firstMember = input.expectedMembers[0]; if (!firstMember) { return super.launch(input); } const ack = await this.svc.recordOpenCodeRuntimeBootstrapCheckin({ teamName: input.teamName, runId: input.runId, memberName: firstMember.name, runtimeSessionId: `session-${firstMember.name}`, observedAt: new Date().toISOString(), }); this.bootstrapCheckins.push({ memberName: firstMember.name, runId: input.runId, state: ack.state, }); return super.launch(input); } } class BlockingOpenCodeRuntimeAdapter extends FakeOpenCodeRuntimeAdapter { readonly pendingLaunchInputs: TeamRuntimeLaunchInput[] = []; private releaseGate: (() => void) | null = null; private readonly gate = new Promise((resolve) => { this.releaseGate = resolve; }); override async launch(input: TeamRuntimeLaunchInput): Promise { this.pendingLaunchInputs.push(input); await this.gate; return super.launch(input); } releaseLaunches(): void { this.releaseGate?.(); } } function latestOpenCodeLaunchRunId( adapter: { readonly launchInputs: readonly TeamRuntimeLaunchInput[] }, teamName: string, laneId: string ): string { const launchInput = [...adapter.launchInputs] .reverse() .find((input) => input.teamName === teamName && input.laneId === laneId); expect(launchInput?.runId).toBeTruthy(); return launchInput!.runId; } class BlockingStopOpenCodeRuntimeAdapter extends BlockingOpenCodeRuntimeAdapter { private releaseStopGate: (() => void) | null = null; private readonly stopGate = new Promise((resolve) => { this.releaseStopGate = resolve; }); override async stop(input: TeamRuntimeStopInput): Promise { this.stopInputs.push(input); await this.stopGate; return { runId: input.runId, teamName: input.teamName, stopped: true, members: {}, warnings: [], diagnostics: ['fake delayed stop'], }; } releaseStops(): void { this.releaseStopGate?.(); } } class RejectingBlockingOpenCodeRuntimeAdapter extends FakeOpenCodeRuntimeAdapter { readonly pendingLaunchInputs: TeamRuntimeLaunchInput[] = []; rejectedLaunchCount = 0; private releaseGate: (() => void) | null = null; private readonly gate = new Promise((resolve) => { this.releaseGate = resolve; }); constructor(private readonly errorMessage: string) { super(); } override async launch(input: TeamRuntimeLaunchInput): Promise { this.pendingLaunchInputs.push(input); await this.gate; this.rejectedLaunchCount += 1; throw new Error(this.errorMessage); } releaseLaunches(): void { this.releaseGate?.(); } } async function upsertActiveOpenCodeRuntimeLaneForTest(input: { teamName: string; laneId: string; runId?: string | null; diagnostics?: string[]; }): Promise { const runId = input.runId ?? null; await upsertOpenCodeRuntimeLaneIndexEntry({ teamsBasePath: getTeamsBasePath(), teamName: input.teamName, laneId: input.laneId, state: 'active', diagnostics: input.diagnostics, }); await setOpenCodeRuntimeActiveRunManifest({ teamsBasePath: getTeamsBasePath(), teamName: input.teamName, laneId: input.laneId, runId, }); await writeOpenCodeBootstrapSessionEvidenceForTest({ teamName: input.teamName, laneId: input.laneId, runId, }); } async function writeOpenCodeBootstrapSessionEvidenceForTest(input: { teamName: string; laneId: string; runId?: string | null; memberName?: string; sessionId?: string; }): Promise { const runId = input.runId ?? null; const descriptor = OPENCODE_RUNTIME_STORE_DESCRIPTORS.find( (candidate) => candidate.schemaName === 'opencode.sessionStore' ); if (!descriptor) { throw new Error('OpenCode session store descriptor missing'); } const manifestPath = getOpenCodeRuntimeManifestPath( getTeamsBasePath(), input.teamName, input.laneId ); const runtimeDirectory = path.dirname(manifestPath); await fs.mkdir(runtimeDirectory, { recursive: true }); const memberName = input.memberName ?? input.laneId.split(':').at(-1) ?? input.laneId; const writer = new RuntimeStoreBatchWriter( runtimeDirectory, createRuntimeStoreManifestStore({ filePath: manifestPath, teamName: input.teamName, }), createRuntimeStoreReceiptStore({ filePath: path.join(runtimeDirectory, 'opencode-runtime-receipts.json'), }), { clock: () => new Date('2026-04-23T10:00:00.000Z'), batchIdFactory: () => `batch-${input.teamName}-${input.laneId}`, receiptIdFactory: () => `receipt-${input.teamName}-${input.laneId}`, } ); await writer.writeBatch({ teamName: input.teamName, runId, capabilitySnapshotId: null, behaviorFingerprint: null, reason: 'launch_checkpoint', writes: [ { descriptor, data: { sessions: [ { id: input.sessionId ?? `ses-${input.teamName}-${input.laneId}`, teamName: input.teamName, memberName, laneId: input.laneId, runId, observedAt: '2026-04-23T10:00:00.000Z', source: 'runtime_bootstrap_checkin', }, ], }, }, ], }); } async function waitForCondition(assertion: () => boolean | Promise): Promise { const startedAt = Date.now(); while (Date.now() - startedAt < 20_000) { if (await assertion()) { return; } await new Promise((resolve) => setTimeout(resolve, 10)); } expect(await assertion()).toBe(true); } async function removeTempDirWithRetries(dir: string): Promise { let lastError: unknown; for (let attempt = 0; attempt < 5; attempt += 1) { try { await fs.rm(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 20 }); return; } catch (error) { lastError = error; await new Promise((resolve) => setTimeout(resolve, 25 * (attempt + 1))); } } throw lastError; } function createMixedLiveRun(input: { teamName: string; projectPath: string; primaryProviderId?: MixedPrimaryProviderId; }): any { const now = '2026-04-23T10:00:00.000Z'; const primary = getMixedPrimaryFixture(input.primaryProviderId); return { runId: `run-${input.teamName}`, teamName: input.teamName, startedAt: now, detectedSessionId: 'lead-session', isLaunch: true, provisioningComplete: false, processKilled: false, cancelRequested: false, leadActivityState: 'active', request: { teamName: input.teamName, cwd: input.projectPath, providerId: primary.providerId, providerBackendId: primary.providerBackendId, model: primary.leadModel, skipPermissions: false, members: [], }, progress: { state: 'finalizing', message: 'Finishing launch - waiting for secondary runtime lanes', updatedAt: now, assistantOutput: null, }, onProgress: () => undefined, launchIdentity: { providerId: primary.providerId, providerBackendId: primary.providerBackendId ?? null, selectedModel: primary.leadModel, selectedModelKind: 'explicit', resolvedLaunchModel: primary.leadModel, catalogId: primary.leadModel, catalogSource: 'bundled', catalogFetchedAt: now, selectedEffort: 'medium', resolvedEffort: 'medium', selectedFastMode: null, resolvedFastMode: null, fastResolutionReason: null, }, expectedMembers: ['alice'], effectiveMembers: [ { name: 'alice', role: 'Reviewer', providerId: primary.providerId, providerBackendId: primary.providerBackendId, model: primary.memberModel, }, ], allEffectiveMembers: [ { name: 'alice', role: 'Reviewer', providerId: primary.providerId, providerBackendId: primary.providerBackendId, model: primary.memberModel, }, { name: 'bob', role: 'Developer', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', }, { name: 'tom', role: 'Developer', providerId: 'opencode', model: 'opencode/nemotron-3-super-free', }, ], memberSpawnStatuses: new Map([ [ 'alice', { status: 'online', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, lastHeartbeatAt: now, lastRuntimeAliveAt: now, lastEvaluatedAt: now, updatedAt: now, livenessSource: 'heartbeat', }, ], ]), mixedSecondaryLanes: [ { laneId: 'secondary:opencode:bob', providerId: 'opencode', member: { name: 'bob', role: 'Developer', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', }, runId: null, state: 'queued', result: null, warnings: [], diagnostics: [], }, { laneId: 'secondary:opencode:tom', providerId: 'opencode', member: { name: 'tom', role: 'Developer', providerId: 'opencode', model: 'opencode/nemotron-3-super-free', }, runId: null, state: 'queued', result: null, warnings: [], diagnostics: [], }, ], memberSpawnToolUseIds: new Map(), pendingMemberRestarts: new Map(), pendingApprovals: new Map(), memberSpawnLeadInboxCursorByMember: new Map(), provisioningOutputParts: [], stdoutBuffer: '', stderrBuffer: '', claudeLogLines: [], activeToolCalls: new Map(), activeCrossTeamReplyHints: [], pendingInboxRelayCandidates: [], mcpConfigPath: null, bootstrapSpecPath: null, bootstrapUserPromptPath: null, }; } function markMixedOpenCodeLaneConfirmedForTest(run: any, memberName: string): void { const now = '2026-04-23T10:00:00.000Z'; const laneId = `secondary:opencode:${memberName}`; const lane = run.mixedSecondaryLanes?.find((candidate: any) => candidate.laneId === laneId); if (!lane) { throw new Error(`Missing mixed OpenCode lane fixture for ${memberName}`); } lane.runId = run.runId; lane.state = 'active'; lane.result = { runId: run.runId, teamName: run.teamName, launchPhase: 'reconciled', teamLaunchState: 'clean_success', members: { [memberName]: { memberName, providerId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, sessionId: `session-${memberName}`, runtimePid: 10_000, livenessKind: 'confirmed_bootstrap', pidSource: 'opencode_bridge', diagnostics: ['fake OpenCode launch ready'], lastEvaluatedAt: now, }, }, warnings: [], diagnostics: ['fake OpenCode launch ready'], }; } function removeMixedOpenCodeLaneForTest(run: any, memberName: string): void { run.allEffectiveMembers = (run.allEffectiveMembers ?? []).filter( (member: { name?: string }) => member.name !== memberName ); run.mixedSecondaryLanes = (run.mixedSecondaryLanes ?? []).filter( (lane: { member?: { name?: string } }) => lane.member?.name !== memberName ); } function addGeminiPrimaryToMixedRun(run: any): void { const now = '2026-04-23T10:00:00.000Z'; const reviewer = { name: 'reviewer', role: 'Reviewer', providerId: 'gemini', model: 'gemini-2.5-flash', }; run.expectedMembers = Array.from(new Set([...(run.expectedMembers ?? []), 'reviewer'])); run.effectiveMembers = [...(run.effectiveMembers ?? []), reviewer]; run.allEffectiveMembers = [ ...run.effectiveMembers, ...((run.allEffectiveMembers ?? []) as Array>).filter( (member) => member.providerId === 'opencode' ), ]; run.memberSpawnStatuses.set('reviewer', { status: 'online', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, lastHeartbeatAt: now, lastRuntimeAliveAt: now, lastEvaluatedAt: now, updatedAt: now, livenessSource: 'heartbeat', }); } function createPureAnthropicLiveRun(input: { teamName: string; projectPath: string }): any { const now = '2026-04-23T10:00:00.000Z'; const memberStatus = { status: 'online', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, lastHeartbeatAt: now, lastRuntimeAliveAt: now, lastEvaluatedAt: now, updatedAt: now, livenessSource: 'heartbeat', }; return { ...createMixedLiveRun({ teamName: input.teamName, projectPath: input.projectPath, primaryProviderId: 'anthropic', }), request: { teamName: input.teamName, cwd: input.projectPath, providerId: 'anthropic', model: 'sonnet', skipPermissions: false, members: [], }, expectedMembers: ['alice', 'bob'], effectiveMembers: [ { name: 'alice', role: 'Reviewer', providerId: 'anthropic', model: 'haiku', }, { name: 'bob', role: 'Developer', providerId: 'anthropic', model: 'sonnet', }, ], allEffectiveMembers: [ { name: 'alice', role: 'Reviewer', providerId: 'anthropic', model: 'haiku', }, { name: 'bob', role: 'Developer', providerId: 'anthropic', model: 'sonnet', }, ], memberSpawnStatuses: new Map([ ['alice', { ...memberStatus }], ['bob', { ...memberStatus }], ]), mixedSecondaryLanes: [], }; } function trackLiveRun(svc: TeamProvisioningService, run: any): void { (svc as any).runs.set(run.runId, run); (svc as any).provisioningRunByTeam.set(run.teamName, run.runId); (svc as any).aliveRunByTeam.set(run.teamName, run.runId); } async function writeAliveProcessRegistry(teamName: string): Promise { const teamDir = path.join(getTeamsBasePath(), teamName); await fs.mkdir(teamDir, { recursive: true }); await fs.writeFile( path.join(teamDir, 'processes.json'), `${JSON.stringify( [ { id: 'lead-process', label: 'Team Lead', pid: process.pid, registeredAt: '2026-04-23T10:00:00.000Z', }, ], null, 2 )}\n`, 'utf8' ); } async function writeStoppedProcessRegistry(teamName: string): Promise { const teamDir = path.join(getTeamsBasePath(), teamName); await fs.mkdir(teamDir, { recursive: true }); await fs.writeFile( path.join(teamDir, 'processes.json'), `${JSON.stringify( [ { id: 'lead-process', label: 'Team Lead', pid: 987_654, registeredAt: '2026-04-23T10:00:00.000Z', stoppedAt: '2026-04-23T10:05:00.000Z', }, ], null, 2 )}\n`, 'utf8' ); } function expectDirectChildKillCount(actual: number, expected: number): void { // Windows uses taskkill.exe for process-tree termination, so fake child.kill is not called. expect(actual).toBe(process.platform === 'win32' ? 0 : expected); } function injectStaleTerminalProvisioningRun( svc: TeamProvisioningService, teamName: string, runId: string ): void { const timestamp = '2026-04-23T10:00:00.000Z'; (svc as any).provisioningRunByTeam.set(teamName, runId); (svc as any).runtimeAdapterProgressByRunId.set(runId, { runId, teamName, state: 'failed', message: 'stale provisioning failure', startedAt: timestamp, updatedAt: timestamp, } satisfies TeamProvisioningProgress); } function createWritableStdin(writes: string[]): { writable: true; write: (chunk: string, callback?: (error?: Error | null) => void) => boolean; } { return { writable: true, write: (chunk, callback) => { writes.push(chunk); callback?.(); return true; }, }; } async function writeOpenCodeTeamConfig(input: { teamName: string; projectPath: string; members: string[]; removedMembers?: string[]; }): Promise { const teamDir = path.join(getTeamsBasePath(), input.teamName); const removedMembers = new Set(input.removedMembers ?? []); await fs.mkdir(teamDir, { recursive: true }); await fs.writeFile( path.join(teamDir, 'config.json'), `${JSON.stringify( { name: input.teamName, projectPath: input.projectPath, members: [ { name: 'team-lead', agentType: 'team-lead', providerId: 'opencode', model: 'opencode/big-pickle', }, ...input.members.map((name) => ({ name, role: 'Developer', providerId: 'opencode', model: 'opencode/big-pickle', ...(removedMembers.has(name) ? { removedAt: 1_777_000_000_000 } : {}), })), ], }, null, 2 )}\n`, 'utf8' ); } async function writeAnthropicTeamConfig(input: { teamName: string; projectPath: string; members: string[]; }): Promise { const teamDir = path.join(getTeamsBasePath(), input.teamName); const config = { name: input.teamName, projectPath: input.projectPath, color: 'blue', members: [ { name: 'team-lead', agentType: 'team-lead', providerId: 'anthropic', model: 'sonnet', }, ...input.members.map((name) => ({ name, role: 'Developer', providerId: 'anthropic', model: name === 'alice' ? 'haiku' : 'sonnet', })), ], }; const raw = `${JSON.stringify(config, null, 2)}\n`; await fs.mkdir(teamDir, { recursive: true }); await fs.writeFile(path.join(teamDir, 'config.json'), raw, 'utf8'); return raw; } async function writeJsonFile(filePath: string, value: unknown): Promise { await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8'); } async function writeFakeClaudeCli(rootDir: string): Promise { const binDir = path.join(rootDir, 'fake-bin'); const cliPath = path.join(binDir, process.platform === 'win32' ? 'claude.cmd' : 'claude'); const script = `#!/usr/bin/env node const args = process.argv.slice(2); const providerIndex = args.lastIndexOf('--provider'); const provider = providerIndex >= 0 ? args[providerIndex + 1] : 'anthropic'; function hasCommand(...parts) { return parts.every((part) => args.includes(part)); } function modelCatalog(providerId) { const base = { schemaVersion: 1, providerId, source: 'runtime', status: 'ready', fetchedAt: '2026-05-13T00:00:00.000Z', staleAt: '2026-05-13T01:00:00.000Z', diagnostics: { configReadState: 'ready', appServerState: 'healthy', }, }; if (providerId === 'anthropic') { return { ...base, defaultModelId: 'sonnet', defaultLaunchModel: 'sonnet', models: [ { id: 'sonnet', launchModel: 'sonnet', displayName: 'Sonnet', supportedReasoningEfforts: ['low', 'medium', 'high'], defaultReasoningEffort: 'medium', supportsFastMode: false, }, { id: 'haiku', launchModel: 'haiku', displayName: 'Haiku', supportedReasoningEfforts: ['low', 'medium'], defaultReasoningEffort: 'medium', supportsFastMode: false, }, ], }; } return { ...base, defaultModelId: 'gpt-5.5', defaultLaunchModel: 'gpt-5.5', models: [ { id: 'gpt-5.5', launchModel: 'gpt-5.5', displayName: 'GPT-5.5', supportedReasoningEfforts: ['low', 'medium', 'high'], defaultReasoningEffort: 'medium', }, ], }; } if (hasCommand('model', 'list')) { const catalog = modelCatalog(provider); console.log(JSON.stringify({ schemaVersion: 1, providers: { [provider]: { defaultModel: catalog.defaultLaunchModel, models: catalog.models.map((model) => ({ id: model.launchModel, label: model.displayName })), }, }, })); process.exit(0); } if (hasCommand('runtime', 'status')) { const catalog = modelCatalog(provider); console.log(JSON.stringify({ providers: { [provider]: { providerId: provider, displayName: provider, supported: true, authenticated: true, authMethod: 'test', verificationState: 'verified', models: catalog.models.map((model) => model.launchModel), modelCatalog: catalog, runtimeCapabilities: { modelCatalog: { dynamic: false, source: 'runtime' }, reasoningEffort: { supported: true, values: ['low', 'medium', 'high'], configPassthrough: true, }, fastMode: { supported: true, available: false, reason: 'test runtime', source: 'runtime', }, }, canLoginFromUi: false, capabilities: { teamLaunch: true, oneShot: true, extensions: {}, }, }, }, })); process.exit(0); } console.log(JSON.stringify({ ok: true })); `; await fs.mkdir(binDir, { recursive: true }); await fs.writeFile(cliPath, script, 'utf8'); if (process.platform !== 'win32') { await fs.chmod(cliPath, 0o755); } return cliPath; } function createBlockedWorkspaceTrustCoordinator(input: { errorMessage: string; evidence?: string[]; rawTail?: string; }) { const planArgsOnly = vi.fn( async (_request: Parameters[0]) => ({ launchArgPatches: [], }) ); const planFull = vi.fn(async (request: Parameters[0]) => ({ workspaces: request.workspaces, launchArgPatches: [], })); const execute = vi.fn(async (_plan: WorkspaceTrustExecutionPlan) => ({ id: 'claude-pty-workspace-trust', provider: 'claude' as const, status: 'blocked' as const, workspaceIds: ['workspace-trust-1'], errorCode: 'workspace_trust_preflight_not_confirmed', errorMessage: input.errorMessage, evidence: input.evidence ?? ['workspace trust was not confirmed'], ...(input.rawTail ? { rawTail: input.rawTail } : {}), })); const coordinator: WorkspaceTrustCoordinator = { planArgsOnly, planFull, execute }; return { coordinator, planArgsOnly, planFull, execute, }; } async function readLatestLaunchFailureManifest(teamName: string): Promise> { const latestPath = path.join( getTeamsBasePath(), teamName, 'launch-failure-artifacts', 'latest.json' ); await waitForCondition(async () => { try { await fs.access(latestPath); return true; } catch { return false; } }); const latest = JSON.parse(await fs.readFile(latestPath, 'utf8')) as { manifestPath?: string }; expect(latest.manifestPath).toEqual(expect.any(String)); return JSON.parse(await fs.readFile(latest.manifestPath!, 'utf8')) as Record; } function snapshotWorkspaceTrustTestEnv(): Partial< Record > { return Object.fromEntries( WORKSPACE_TRUST_TEST_ENV_NAMES.map((name) => [name, process.env[name]]) ) as Partial>; } function restoreOptionalEnvValue(name: string, value: string | undefined): void { if (value === undefined) { delete process.env[name]; return; } process.env[name] = value; } function restoreWorkspaceTrustTestEnv( snapshot: Partial> ): void { for (const name of WORKSPACE_TRUST_TEST_ENV_NAMES) { restoreOptionalEnvValue(name, snapshot[name]); } } function forceWorkspaceTrustPreflightEnv(): void { process.env.AGENT_TEAMS_WORKSPACE_TRUST_PREFLIGHT = '1'; process.env.AGENT_TEAMS_WORKSPACE_TRUST_CLAUDE_PTY = '1'; process.env.AGENT_TEAMS_WORKSPACE_TRUST_CODEX_SETTINGS = '1'; process.env.AGENT_TEAMS_WORKSPACE_TRUST_RETRY = '0'; } async function writeOpenCodeMembersMeta( teamName: string, options: { members: string[]; removedMembers?: string[]; memberCwd?: string; } ): Promise { const teamDir = path.join(getTeamsBasePath(), teamName); const removedMembers = new Set(options.removedMembers ?? []); await fs.mkdir(teamDir, { recursive: true }); await fs.writeFile( path.join(teamDir, 'members.meta.json'), `${JSON.stringify( { version: 1, members: options.members.map((name) => ({ name, providerId: 'opencode', model: 'opencode/big-pickle', ...(options.memberCwd ? { cwd: options.memberCwd } : {}), ...(removedMembers.has(name) ? { removedAt: 1_777_000_000_000 } : {}), })), }, null, 2 )}\n`, 'utf8' ); } async function writeOpenCodeTeamMeta(teamName: string, projectPath: string): Promise { const teamDir = path.join(getTeamsBasePath(), teamName); await fs.mkdir(teamDir, { recursive: true }); await fs.writeFile( path.join(teamDir, 'team.meta.json'), `${JSON.stringify( { version: 1, cwd: projectPath, providerId: 'opencode', model: 'opencode/big-pickle', effort: 'medium', createdAt: Date.now(), }, null, 2 )}\n`, 'utf8' ); } async function writePureAnthropicTeamConfig(input: { teamName: string; projectPath: string; }): Promise { await writePureAnthropicTeamConfigWithMembers({ ...input, members: ['alice', 'bob'], }); } async function writePureAnthropicTeamConfigWithMembers(input: { teamName: string; projectPath: string; members: string[]; }): Promise { const teamDir = path.join(getTeamsBasePath(), input.teamName); await fs.mkdir(teamDir, { recursive: true }); await fs.writeFile( path.join(teamDir, 'config.json'), `${JSON.stringify( { name: input.teamName, projectPath: input.projectPath, providerId: 'anthropic', model: 'sonnet', members: [ { name: 'team-lead', agentType: 'team-lead', providerId: 'anthropic', model: 'sonnet', }, ...input.members.map((name, index) => ({ name, role: index === 0 ? 'Reviewer' : 'Developer', providerId: 'anthropic', model: index === 0 ? 'haiku' : 'sonnet', })), ], }, null, 2 )}\n`, 'utf8' ); } async function writeMixedTeamConfig(input: { teamName: string; projectPath: string; includeGeminiPrimary?: boolean; primaryProviderId?: MixedPrimaryProviderId; removedMembers?: string[]; extraMembers?: Array<{ name: string; providerId: 'opencode'; model: string }>; }): Promise { const teamDir = path.join(getTeamsBasePath(), input.teamName); const primary = getMixedPrimaryFixture(input.primaryProviderId); const removedMembers = new Set(input.removedMembers ?? []); const extraMembers = input.extraMembers ?? []; await fs.mkdir(teamDir, { recursive: true }); await fs.writeFile( path.join(teamDir, 'config.json'), `${JSON.stringify( { name: input.teamName, projectPath: input.projectPath, providerId: primary.providerId, ...(primary.providerBackendId ? { providerBackendId: primary.providerBackendId } : {}), model: primary.leadModel, members: [ { name: 'team-lead', agentType: 'team-lead', providerId: primary.providerId, ...(primary.providerBackendId ? { providerBackendId: primary.providerBackendId } : {}), model: primary.leadModel, }, { name: 'alice', role: 'Reviewer', providerId: primary.providerId, ...(primary.providerBackendId ? { providerBackendId: primary.providerBackendId } : {}), model: primary.memberModel, ...(removedMembers.has('alice') ? { removedAt: 1_777_000_000_000 } : {}), }, ...(input.includeGeminiPrimary ? [ { name: 'reviewer', role: 'Reviewer', providerId: 'gemini', model: 'gemini-2.5-flash', ...(removedMembers.has('reviewer') ? { removedAt: 1_777_000_000_000 } : {}), }, ] : []), { name: 'bob', role: 'Developer', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', ...(removedMembers.has('bob') ? { removedAt: 1_777_000_000_000 } : {}), }, { name: 'tom', role: 'Developer', providerId: 'opencode', model: 'opencode/nemotron-3-super-free', ...(removedMembers.has('tom') ? { removedAt: 1_777_000_000_000 } : {}), }, ...extraMembers.map((member) => ({ name: member.name, role: 'Developer', providerId: member.providerId, model: member.model, ...(removedMembers.has(member.name) ? { removedAt: 1_777_000_000_000 } : {}), })), ], }, null, 2 )}\n`, 'utf8' ); } async function writeMixedTeamConfigWithoutOpenCodeProviderMetadata(input: { teamName: string; projectPath: string; }): Promise { const teamDir = path.join(getTeamsBasePath(), input.teamName); await fs.mkdir(teamDir, { recursive: true }); await fs.writeFile( path.join(teamDir, 'config.json'), `${JSON.stringify( { name: input.teamName, projectPath: input.projectPath, providerId: 'codex', providerBackendId: 'codex-native', model: 'gpt-5.4', members: [ { name: 'team-lead', agentType: 'team-lead', providerId: 'codex', providerBackendId: 'codex-native', model: 'gpt-5.4', }, { name: 'alice', role: 'Reviewer', providerId: 'codex', providerBackendId: 'codex-native', model: 'gpt-5.4-mini', }, { name: 'bob', role: 'Developer', model: 'opencode/minimax-m2.5-free', }, ], }, null, 2 )}\n`, 'utf8' ); } async function writeMixedTeamLaunchState(input: { teamName: string; updatedAt?: string; members: Record>; }): Promise { const teamDir = path.join(getTeamsBasePath(), input.teamName); await fs.mkdir(teamDir, { recursive: true }); const snapshot = createPersistedLaunchSnapshot({ teamName: input.teamName, leadSessionId: 'lead-session', launchPhase: 'active', expectedMembers: Object.keys(input.members), bootstrapExpectedMembers: ['alice'], members: input.members as any, updatedAt: input.updatedAt, }); await fs.writeFile( path.join(teamDir, 'launch-state.json'), `${JSON.stringify(snapshot, null, 2)}\n`, 'utf8' ); } async function writePureAnthropicTeamLaunchState(input: { teamName: string; launchPhase?: 'active' | 'finished' | 'reconciled'; expectedMembers?: string[]; members: Record>; }): Promise { const teamDir = path.join(getTeamsBasePath(), input.teamName); await fs.mkdir(teamDir, { recursive: true }); const expectedMembers = input.expectedMembers ?? Object.keys(input.members); const snapshot = createPersistedLaunchSnapshot({ teamName: input.teamName, leadSessionId: 'lead-session', launchPhase: input.launchPhase ?? 'active', expectedMembers, bootstrapExpectedMembers: expectedMembers, members: input.members as any, }); await fs.writeFile( path.join(teamDir, 'launch-state.json'), `${JSON.stringify(snapshot, null, 2)}\n`, 'utf8' ); } async function writePureAnthropicPendingBobFixture(input: { teamName: string; projectPath: string; acceptedAt: string; }): Promise { await writePureAnthropicTeamConfig({ teamName: input.teamName, projectPath: input.projectPath }); await writePureAnthropicTeamMeta(input.teamName, input.projectPath); await writePureAnthropicMembersMeta(input.teamName); await writePureAnthropicTeamLaunchState({ teamName: input.teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: input.acceptedAt, }), }, }); } async function writePureAnthropicPendingMembersFixture(input: { teamName: string; projectPath: string; acceptedAt: string; }): Promise { await writePureAnthropicTeamConfig({ teamName: input.teamName, projectPath: input.projectPath }); await writePureAnthropicTeamMeta(input.teamName, input.projectPath); await writePureAnthropicMembersMeta(input.teamName); await writePureAnthropicTeamLaunchState({ teamName: input.teamName, launchPhase: 'active', members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: input.acceptedAt, }), bob: mixedMemberState({ name: 'bob', providerId: 'anthropic', model: 'sonnet', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: input.acceptedAt, }), }, }); } async function writeMixedAnthropicPendingAliceFixture(input: { teamName: string; projectPath: string; acceptedAt: string; }): Promise { await writeMixedTeamConfig({ teamName: input.teamName, projectPath: input.projectPath, primaryProviderId: 'anthropic', }); await writeTeamMeta(input.teamName, input.projectPath, { primaryProviderId: 'anthropic' }); await writeMembersMeta(input.teamName, { primaryProviderId: 'anthropic' }); await writeMixedTeamLaunchState({ teamName: input.teamName, members: { alice: mixedMemberState({ name: 'alice', providerId: 'anthropic', model: 'haiku', laneId: 'primary', laneKind: 'primary', laneOwnerProviderId: 'anthropic', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, firstSpawnAcceptedAt: input.acceptedAt, }), bob: mixedMemberState({ name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', laneId: 'secondary:opencode:bob', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), tom: mixedMemberState({ name: 'tom', providerId: 'opencode', model: 'opencode/nemotron-3-super-free', laneId: 'secondary:opencode:tom', laneKind: 'secondary', laneOwnerProviderId: 'opencode', launchState: 'confirmed_alive', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: true, hardFailure: false, }), }, }); } async function writeLegacyPartialLaunchState(input: { teamName: string; expectedMembers: string[]; confirmedMembers: string[]; missingMembers: string[]; }): Promise { const teamDir = path.join(getTeamsBasePath(), input.teamName); await fs.mkdir(teamDir, { recursive: true }); await fs.writeFile( path.join(teamDir, 'launch-state.json'), `${JSON.stringify( { state: 'partial_launch_failure', expectedMembers: input.expectedMembers, confirmedMembers: input.confirmedMembers, missingMembers: input.missingMembers, leadSessionId: 'lead-session', updatedAt: '2026-04-23T10:00:00.000Z', }, null, 2 )}\n`, 'utf8' ); } async function writeBootstrapState( teamName: string, members: Array<{ name: string; status: string; lastAttemptAt?: number; lastObservedAt?: number; failureReason?: string; }> ): Promise { const teamDir = path.join(getTeamsBasePath(), teamName); await fs.mkdir(teamDir, { recursive: true }); await fs.writeFile( path.join(teamDir, 'bootstrap-state.json'), `${JSON.stringify( { version: 1, teamName, updatedAt: '2026-04-23T10:00:06.000Z', phase: 'completed', members, }, null, 2 )}\n`, 'utf8' ); } async function writeLeadInboxMessages( teamName: string, messages: Array<{ from: string; text: string; timestamp: string; messageId: string; read?: boolean; }> ): Promise { const inboxDir = path.join(getTeamsBasePath(), teamName, 'inboxes'); await fs.mkdir(inboxDir, { recursive: true }); await fs.writeFile( path.join(inboxDir, 'team-lead.json'), `${JSON.stringify( messages.map((message) => ({ ...message, to: 'team-lead', read: message.read ?? false, })), null, 2 )}\n`, 'utf8' ); } async function writeMemberTranscript(input: { projectPath: string; sessionId: string; records: Record[]; }): Promise { const projectDir = getProjectTranscriptDir(input.projectPath); await fs.mkdir(projectDir, { recursive: true }); await fs.writeFile( path.join(projectDir, `${input.sessionId}.jsonl`), `${input.records.map((record) => JSON.stringify(record)).join('\n')}\n`, 'utf8' ); } async function writeRawMemberTranscript(input: { projectPath: string; sessionId: string; lines: string[]; }): Promise { const projectDir = getProjectTranscriptDir(input.projectPath); await fs.mkdir(projectDir, { recursive: true }); await fs.writeFile( path.join(projectDir, `${input.sessionId}.jsonl`), `${input.lines.join('\n')}\n`, 'utf8' ); } function getProjectTranscriptDir(projectPath: string): string { return path.join(getProjectsBasePath(), extractBaseDir(encodePath(projectPath))); } function getMemberTranscriptPath(projectPath: string, sessionId: string): string { return path.join(getProjectTranscriptDir(projectPath), `${sessionId}.jsonl`); } function bootstrapTranscriptRecord(input: { timestamp: string; teamName: string; memberName: string; agentName?: string; }): Record { return { timestamp: input.timestamp, teamName: input.teamName, agentName: input.agentName ?? input.memberName, type: 'user', message: { role: 'user', content: `You are bootstrapping into team "${input.teamName}" as member "${input.memberName}".`, }, }; } function bootstrapSuccessTranscriptRecord(input: { timestamp: string; teamName: string; memberName: string; agentName?: string; }): Record { return { timestamp: input.timestamp, teamName: input.teamName, agentName: input.agentName ?? input.memberName, type: 'assistant', message: { role: 'assistant', content: [ { type: 'text', text: `Member briefing for ${input.memberName} on team "${input.teamName}" (${input.teamName}).\nTask briefing for ${input.memberName}:\nNo actionable tasks.`, }, ], }, }; } function bootstrapFailureTranscriptRecord(input: { timestamp: string; teamName: string; memberName: string; agentName?: string; }): Record { return { timestamp: input.timestamp, teamName: input.teamName, agentName: input.agentName ?? input.memberName, type: 'assistant', isApiErrorMessage: true, message: { role: 'assistant', content: [ { type: 'text', text: 'API Error: 400 {"detail":"The requested Anthropic model is not available for your account."}', }, ], }, }; } function genericTranscriptApiErrorRecord(input: { timestamp: string }): Record { return { timestamp: input.timestamp, type: 'assistant', isApiErrorMessage: true, message: { role: 'assistant', content: [ { type: 'text', text: 'API Error: 400 {"detail":"The requested Anthropic model is not available for your account."}', }, ], }, }; } function withoutAgentName(record: Record): Record { const next = { ...record }; delete next.agentName; return next; } function mixedMemberState(overrides: Record): Record { return { name: overrides.name, launchState: 'starting', agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: false, lastEvaluatedAt: '2026-04-23T10:00:00.000Z', ...overrides, }; } function getMixedPrimaryFixture(providerId: MixedPrimaryProviderId = 'codex'): { providerId: MixedPrimaryProviderId; providerBackendId?: string; leadModel: string; memberModel: string; } { if (providerId === 'anthropic') { return { providerId, leadModel: 'sonnet', memberModel: 'haiku', }; } return { providerId, providerBackendId: 'codex-native', leadModel: 'gpt-5.4', memberModel: 'gpt-5.4-mini', }; } async function readInboxRows(teamName: string, inboxName: string): Promise { const inboxPath = path.join(getTeamsBasePath(), teamName, 'inboxes', `${inboxName}.json`); const raw = await fs.readFile(inboxPath, 'utf8'); const parsed = JSON.parse(raw) as unknown; return Array.isArray(parsed) ? (parsed as InboxMessage[]) : []; } async function writeTeamMeta( teamName: string, projectPath: string, options: { primaryProviderId?: MixedPrimaryProviderId } = {} ): Promise { const teamDir = path.join(getTeamsBasePath(), teamName); const primary = getMixedPrimaryFixture(options.primaryProviderId); await fs.mkdir(teamDir, { recursive: true }); await fs.writeFile( path.join(teamDir, 'team.meta.json'), `${JSON.stringify( { version: 1, cwd: projectPath, providerId: primary.providerId, ...(primary.providerBackendId ? { providerBackendId: primary.providerBackendId } : {}), model: primary.leadModel, effort: 'medium', createdAt: Date.now(), }, null, 2 )}\n`, 'utf8' ); } async function writePureAnthropicTeamMeta(teamName: string, projectPath: string): Promise { const teamDir = path.join(getTeamsBasePath(), teamName); await fs.mkdir(teamDir, { recursive: true }); await fs.writeFile( path.join(teamDir, 'team.meta.json'), `${JSON.stringify( { version: 1, cwd: projectPath, providerId: 'anthropic', model: 'sonnet', effort: 'medium', createdAt: Date.now(), }, null, 2 )}\n`, 'utf8' ); } async function writePureAnthropicMembersMeta( teamName: string, options: { removedMembers?: string[]; extraMembers?: Array<{ name: string; providerId: 'anthropic'; model: string }>; } = {} ): Promise { const teamDir = path.join(getTeamsBasePath(), teamName); const removedMembers = new Set(options.removedMembers ?? []); const extraMembers = options.extraMembers ?? []; await fs.mkdir(teamDir, { recursive: true }); await fs.writeFile( path.join(teamDir, 'members.meta.json'), `${JSON.stringify( { version: 1, members: [ { name: 'alice', providerId: 'anthropic', model: 'haiku', ...(removedMembers.has('alice') ? { removedAt: 1_777_000_000_000 } : {}), }, { name: 'bob', providerId: 'anthropic', model: 'sonnet', ...(removedMembers.has('bob') ? { removedAt: 1_777_000_000_000 } : {}), }, ...extraMembers.map((member) => ({ name: member.name, providerId: member.providerId, model: member.model, ...(removedMembers.has(member.name) ? { removedAt: 1_777_000_000_000 } : {}), })), ], }, null, 2 )}\n`, 'utf8' ); } async function writeMembersMeta( teamName: string, options: { includeGeminiPrimary?: boolean; primaryProviderId?: MixedPrimaryProviderId; removedMembers?: string[]; extraMembers?: Array<{ name: string; providerId: 'opencode'; model: string }>; memberCwd?: string; } = {} ): Promise { const teamDir = path.join(getTeamsBasePath(), teamName); const primary = getMixedPrimaryFixture(options.primaryProviderId); const removedMembers = new Set(options.removedMembers ?? []); const extraMembers = options.extraMembers ?? []; await fs.mkdir(teamDir, { recursive: true }); await fs.writeFile( path.join(teamDir, 'members.meta.json'), `${JSON.stringify( { version: 1, ...(primary.providerBackendId ? { providerBackendId: primary.providerBackendId } : {}), members: [ { name: 'alice', providerId: primary.providerId, ...(primary.providerBackendId ? { providerBackendId: primary.providerBackendId } : {}), model: primary.memberModel, ...(options.memberCwd ? { cwd: options.memberCwd } : {}), ...(removedMembers.has('alice') ? { removedAt: 1_777_000_000_000 } : {}), }, ...(options.includeGeminiPrimary ? [ { name: 'reviewer', providerId: 'gemini', model: 'gemini-2.5-flash', ...(options.memberCwd ? { cwd: options.memberCwd } : {}), ...(removedMembers.has('reviewer') ? { removedAt: 1_777_000_000_000 } : {}), }, ] : []), { name: 'bob', providerId: 'opencode', model: 'opencode/minimax-m2.5-free', ...(options.memberCwd ? { cwd: options.memberCwd } : {}), ...(removedMembers.has('bob') ? { removedAt: 1_777_000_000_000 } : {}), }, { name: 'tom', providerId: 'opencode', model: 'opencode/nemotron-3-super-free', ...(options.memberCwd ? { cwd: options.memberCwd } : {}), ...(removedMembers.has('tom') ? { removedAt: 1_777_000_000_000 } : {}), }, ...extraMembers.map((member) => ({ name: member.name, providerId: member.providerId, model: member.model, ...(options.memberCwd ? { cwd: options.memberCwd } : {}), ...(removedMembers.has(member.name) ? { removedAt: 1_777_000_000_000 } : {}), })), ], }, null, 2 )}\n`, 'utf8' ); }