import { randomUUID } from 'crypto'; import type { OpenCodeBridgeRuntimeSnapshot, OpenCodeLaunchTeamCommandBody, OpenCodeLaunchTeamCommandData, OpenCodeReconcileTeamCommandBody, OpenCodeSendMessageCommandBody, OpenCodeSendMessageCommandData, OpenCodeStopTeamCommandBody, OpenCodeStopTeamCommandData, OpenCodeTeamLaunchMode, OpenCodeTeamMemberLaunchBridgeState, } from '../opencode/bridge/OpenCodeBridgeCommandContract'; import type { OpenCodeTeamLaunchReadiness } from '../opencode/readiness/OpenCodeTeamLaunchReadiness'; import type { TeamLaunchRuntimeAdapter, TeamRuntimeLaunchInput, TeamRuntimeLaunchResult, TeamRuntimeMemberLaunchEvidence, TeamRuntimeMemberStopEvidence, TeamRuntimePrepareResult, TeamRuntimeReconcileInput, TeamRuntimeReconcileResult, TeamRuntimeStopInput, TeamRuntimeStopResult, } from './TeamRuntimeAdapter'; export interface OpenCodeTeamRuntimeBridgePort { checkOpenCodeTeamLaunchReadiness(input: { projectPath: string; selectedModel: string | null; requireExecutionProbe: boolean; launchMode?: OpenCodeTeamLaunchMode; }): Promise; getLastOpenCodeRuntimeSnapshot?(projectPath: string): OpenCodeBridgeRuntimeSnapshot | null; launchOpenCodeTeam?(input: OpenCodeLaunchTeamCommandBody): Promise; reconcileOpenCodeTeam?( input: OpenCodeReconcileTeamCommandBody ): Promise; stopOpenCodeTeam?(input: OpenCodeStopTeamCommandBody): Promise; sendOpenCodeTeamMessage?( input: OpenCodeSendMessageCommandBody ): Promise; } export interface OpenCodeTeamRuntimeAdapterOptions { launchMode?: OpenCodeTeamLaunchMode; /** * @deprecated Use launchMode. Kept for older tests/callers until the production gate is fully wired. */ launchEnabled?: boolean; } export interface OpenCodeTeamRuntimeMessageInput { runId?: string; teamName: string; laneId: string; memberName: string; cwd: string; text: string; messageId?: string; } export interface OpenCodeTeamRuntimeMessageResult { ok: boolean; providerId: 'opencode'; memberName: string; sessionId?: string; runtimePid?: number; diagnostics: string[]; } export { type OpenCodeTeamLaunchMode } from '../opencode/bridge/OpenCodeBridgeCommandContract'; const REQUIRED_READY_CHECKPOINTS = new Set([ 'required_tools_proven', 'delivery_ready', 'member_ready', 'run_ready', ]); export class OpenCodeTeamRuntimeAdapter implements TeamLaunchRuntimeAdapter { readonly providerId = 'opencode' as const; private readonly lastProjectPathByTeamName = new Map(); private readonly lastReadinessByProjectPath = new Map(); constructor( private readonly bridge: OpenCodeTeamRuntimeBridgePort, private readonly options: OpenCodeTeamRuntimeAdapterOptions = {} ) {} async prepare(input: TeamRuntimeLaunchInput): Promise { const configuredLaunchMode = resolveOpenCodeTeamLaunchMode(this.options); if (configuredLaunchMode === 'disabled') { return { ok: false, providerId: this.providerId, reason: 'opencode_team_launch_disabled', retryable: false, diagnostics: [ 'OpenCode team launch mode is disabled. Set CLAUDE_TEAM_OPENCODE_LAUNCH_MODE=dogfood for local dogfood testing or production after strict readiness evidence exists.', ], warnings: [], }; } const runtimeOnly = input.runtimeOnly === true; const readiness = await this.bridge.checkOpenCodeTeamLaunchReadiness({ projectPath: input.cwd, selectedModel: input.model ?? null, requireExecutionProbe: !runtimeOnly, launchMode: runtimeOnly ? undefined : configuredLaunchMode, }); this.lastReadinessByProjectPath.set(input.cwd, readiness); if (!readiness.launchAllowed) { return { ok: false, providerId: this.providerId, reason: readiness.state, retryable: isRetryableReadinessState(readiness.state), diagnostics: mergeDiagnostics(readiness.diagnostics, readiness.missing), warnings: [], }; } const warnings = configuredLaunchMode === 'dogfood' && !runtimeOnly ? [ 'OpenCode dogfood launch mode is active. This is local test mode and may run without production E2E evidence.', ] : []; if ( !runtimeOnly && configuredLaunchMode === 'production' && readiness.supportLevel !== 'production_supported' ) { return { ok: false, providerId: this.providerId, reason: 'opencode_production_e2e_evidence_missing', retryable: false, diagnostics: [ 'OpenCode production launch requires strict production E2E evidence before enabling team launch.', ], warnings, }; } return { ok: true, providerId: this.providerId, modelId: readiness.modelId, diagnostics: readiness.diagnostics, warnings, }; } getLastOpenCodeTeamLaunchReadiness(projectPath: string): OpenCodeTeamLaunchReadiness | null { return this.lastReadinessByProjectPath.get(projectPath) ?? null; } async launch(input: TeamRuntimeLaunchInput): Promise { const memberValidationDiagnostics = validateOpenCodeRuntimeMembers(input.expectedMembers); if (memberValidationDiagnostics.length > 0) { return blockedLaunchResult( input, 'opencode_invalid_expected_members', memberValidationDiagnostics ); } const configuredLaunchMode = resolveOpenCodeTeamLaunchMode(this.options); const prepared = await this.prepare(input); if (!prepared.ok) { return blockedLaunchResult(input, prepared.reason, prepared.diagnostics, prepared.warnings); } if (!this.bridge.launchOpenCodeTeam) { return blockedLaunchResult(input, 'opencode_launch_bridge_missing', [ 'OpenCode readiness passed, but the state-changing launch bridge is not registered.', ]); } const selectedModel = prepared.modelId ?? input.model?.trim() ?? ''; if (!selectedModel) { return blockedLaunchResult(input, 'opencode_model_unavailable', [ 'OpenCode launch requires a selected raw model id.', ]); } const runtimeSnapshot = this.bridge.getLastOpenCodeRuntimeSnapshot?.(input.cwd) ?? null; this.lastProjectPathByTeamName.set(input.teamName, input.cwd); const data = await this.bridge.launchOpenCodeTeam({ mode: configuredLaunchMode, runId: input.runId, laneId: input.laneId?.trim() || 'primary', teamId: input.teamName, teamName: input.teamName, projectPath: input.cwd, selectedModel, members: input.expectedMembers.map((member) => ({ name: member.name, role: member.role?.trim() || member.workflow?.trim() || 'teammate', prompt: buildMemberBootstrapPrompt(input, member), })), leadPrompt: input.prompt?.trim() ?? '', expectedCapabilitySnapshotId: runtimeSnapshot?.capabilitySnapshotId ?? null, manifestHighWatermark: null, }); return mapOpenCodeLaunchDataToRuntimeResult(input, data, prepared.warnings); } async reconcile(input: TeamRuntimeReconcileInput): Promise { const memberValidationDiagnostics = validateOpenCodeRuntimeMembers(input.expectedMembers); if (memberValidationDiagnostics.length > 0) { return { ...blockedLaunchResult( { runId: input.runId, teamName: input.teamName, cwd: input.expectedMembers[0]?.cwd ?? '', providerId: this.providerId, skipPermissions: false, expectedMembers: input.expectedMembers, previousLaunchState: input.previousLaunchState, }, 'opencode_invalid_expected_members', memberValidationDiagnostics ), snapshot: input.previousLaunchState, }; } if (this.bridge.reconcileOpenCodeTeam) { const projectPath = input.expectedMembers[0]?.cwd ?? this.lastProjectPathByTeamName.get(input.teamName); const runtimeSnapshot = projectPath ? (this.bridge.getLastOpenCodeRuntimeSnapshot?.(projectPath) ?? null) : null; const data = await this.bridge.reconcileOpenCodeTeam({ runId: input.runId, laneId: input.laneId?.trim() || 'primary', teamId: input.teamName, teamName: input.teamName, projectPath, expectedCapabilitySnapshotId: runtimeSnapshot?.capabilitySnapshotId ?? null, manifestHighWatermark: null, reconcileAttemptId: `opencode-reconcile-${randomUUID()}`, expectedMembers: input.expectedMembers.map((member) => ({ name: member.name, model: member.model ?? null, })), reason: input.reason, }); const mapped = mapOpenCodeLaunchDataToRuntimeResult( { runId: input.runId, teamName: input.teamName, cwd: input.expectedMembers[0]?.cwd ?? '', providerId: this.providerId, skipPermissions: false, expectedMembers: input.expectedMembers, previousLaunchState: input.previousLaunchState, }, data, [] ); return { ...mapped, snapshot: input.previousLaunchState, }; } const snapshot = input.previousLaunchState; if (!snapshot) { return { runId: input.runId, teamName: input.teamName, launchPhase: 'reconciled', teamLaunchState: 'partial_pending', members: {}, snapshot: null, warnings: [], diagnostics: ['No previous OpenCode launch snapshot was available for reconciliation.'], }; } return { runId: input.runId, teamName: input.teamName, launchPhase: snapshot.launchPhase, teamLaunchState: snapshot.teamLaunchState, members: Object.fromEntries( Object.entries(snapshot.members).map(([memberName, member]) => [ memberName, { memberName, providerId: this.providerId, launchState: member.launchState, agentToolAccepted: member.agentToolAccepted, runtimeAlive: member.runtimeAlive, bootstrapConfirmed: member.bootstrapConfirmed, hardFailure: member.hardFailure, hardFailureReason: member.hardFailureReason, diagnostics: member.diagnostics ?? [], } satisfies TeamRuntimeMemberLaunchEvidence, ]) ), snapshot, warnings: [], diagnostics: [`OpenCode launch snapshot reconciled from ${input.reason}.`], }; } async sendMessageToMember( input: OpenCodeTeamRuntimeMessageInput ): Promise { if (!this.bridge.sendOpenCodeTeamMessage) { return { ok: false, providerId: this.providerId, memberName: input.memberName, diagnostics: ['OpenCode message bridge is not registered.'], }; } const data = await this.bridge.sendOpenCodeTeamMessage({ runId: input.runId, laneId: input.laneId, teamId: input.teamName, teamName: input.teamName, projectPath: input.cwd, memberName: input.memberName, text: buildOpenCodeRuntimeMessageText(input), messageId: input.messageId, agent: 'teammate', }); return { ok: data.accepted, providerId: this.providerId, memberName: input.memberName, sessionId: data.sessionId, runtimePid: data.runtimePid, diagnostics: data.diagnostics.map((diagnostic) => diagnostic.message), }; } async stop(input: TeamRuntimeStopInput): Promise { if (this.bridge.stopOpenCodeTeam) { const projectPath = input.cwd ?? this.lastProjectPathByTeamName.get(input.teamName); const runtimeSnapshot = projectPath ? (this.bridge.getLastOpenCodeRuntimeSnapshot?.(projectPath) ?? null) : null; const data = await this.bridge.stopOpenCodeTeam({ runId: input.runId, laneId: input.laneId?.trim() || 'primary', teamId: input.teamName, teamName: input.teamName, projectPath, expectedCapabilitySnapshotId: runtimeSnapshot?.capabilitySnapshotId ?? null, manifestHighWatermark: null, reason: input.reason, force: input.force, }); if (data.stopped) { this.lastProjectPathByTeamName.delete(input.teamName); } return { runId: input.runId, teamName: input.teamName, stopped: data.stopped, members: Object.fromEntries( Object.entries(data.members).map(([memberName, member]) => [ memberName, { memberName, providerId: this.providerId, stopped: member.stopped, sessionId: member.sessionId, diagnostics: member.diagnostics, } satisfies TeamRuntimeMemberStopEvidence, ]) ), warnings: data.warnings.map((warning) => warning.message), diagnostics: data.diagnostics.map(formatOpenCodeBridgeDiagnostic), }; } const members = input.previousLaunchState ? Object.fromEntries( Object.keys(input.previousLaunchState.members).map((memberName) => [ memberName, { memberName, providerId: this.providerId, stopped: true, diagnostics: [ 'No live OpenCode session stop command is wired in this adapter shell.', ], } satisfies TeamRuntimeMemberStopEvidence, ]) ) : {}; return { runId: input.runId, teamName: input.teamName, stopped: true, members, warnings: [], diagnostics: input.previousLaunchState ? ['OpenCode stop was acknowledged without live session ownership changes.'] : ['No previous OpenCode launch snapshot was available to stop.'], }; } } export function resolveOpenCodeTeamLaunchMode( options: OpenCodeTeamRuntimeAdapterOptions = {} ): OpenCodeTeamLaunchMode { if (options.launchMode) { return options.launchMode; } if (options.launchEnabled === true) { return 'production'; } return 'disabled'; } function mapOpenCodeLaunchDataToRuntimeResult( input: TeamRuntimeLaunchInput, data: OpenCodeLaunchTeamCommandData, prepareWarnings: string[] ): TeamRuntimeLaunchResult { const checkpointNames = extractCheckpointNames(data); const readyCheckpointsPresent = [...REQUIRED_READY_CHECKPOINTS].every((name) => checkpointNames.has(name) ); const bridgeReady = data.teamLaunchState === 'ready'; const missingExpectedMembers = input.expectedMembers .map((member) => member.name) .filter((memberName) => data.members[memberName] == null); const unconfirmedExpectedMembers = input.expectedMembers .map((member) => member.name) .filter((memberName) => data.members[memberName]?.launchState !== 'confirmed_alive'); const anyExpectedMemberFailed = input.expectedMembers.some( (member) => data.members[member.name]?.launchState === 'failed' ); const allExpectedMembersConfirmed = input.expectedMembers.length > 0 && unconfirmedExpectedMembers.length === 0; const success = bridgeReady && readyCheckpointsPresent && allExpectedMembersConfirmed; const checkpointDiagnostic = success ? [] : bridgeReady && !readyCheckpointsPresent ? [ `OpenCode bridge reported ready without all required durable checkpoints: missing ${[ ...REQUIRED_READY_CHECKPOINTS, ] .filter((name) => !checkpointNames.has(name)) .join(', ')}`, ] : []; const incompleteReadyDiagnostic = bridgeReady && readyCheckpointsPresent && !allExpectedMembersConfirmed ? [ `OpenCode bridge reported ready before all expected members were confirmed: pending ${unconfirmedExpectedMembers.join(', ')}`, ] : []; const members = Object.fromEntries( input.expectedMembers.map((member) => { const bridgeMember = data.members[member.name]; const fallbackLaunchState = bridgeMember ? bridgeMember.launchState : data.teamLaunchState === 'failed' ? 'failed' : 'created'; return [ member.name, mapBridgeMemberToRuntimeEvidence( member.name, fallbackLaunchState, bridgeMember?.sessionId, bridgeMember?.runtimePid, bridgeMember?.pendingPermissionRequestIds, bridgeMember != null, [ ...(bridgeMember ? [] : [ `OpenCode bridge response did not include ${member.name}; keeping the member pending until lane state materializes.`, ]), ...(bridgeMember?.diagnostics ?? []), ...(bridgeMember?.evidence ?? []).map( (evidence) => `${evidence.kind} at ${evidence.observedAt}` ), ...checkpointDiagnostic, ...(missingExpectedMembers.includes(member.name) ? incompleteReadyDiagnostic : []), ] ), ]; }) ); return { runId: input.runId, teamName: input.teamName, launchPhase: success ? 'finished' : data.teamLaunchState === 'launching' || (bridgeReady && !anyExpectedMemberFailed) ? 'active' : 'finished', teamLaunchState: success ? 'clean_success' : anyExpectedMemberFailed || data.teamLaunchState === 'failed' ? 'partial_failure' : data.teamLaunchState === 'launching' || data.teamLaunchState === 'permission_blocked' || bridgeReady ? 'partial_pending' : 'partial_failure', members, warnings: [...prepareWarnings, ...data.warnings.map((warning) => warning.message)], diagnostics: [ ...data.diagnostics.map(formatOpenCodeBridgeDiagnostic), ...checkpointDiagnostic, ...incompleteReadyDiagnostic, ], }; } function mapBridgeMemberToRuntimeEvidence( memberName: string, launchState: OpenCodeTeamMemberLaunchBridgeState, sessionId: string | undefined, runtimePid: number | undefined, pendingPermissionRequestIds: string[] | undefined, runtimeMaterialized: boolean, diagnostics: string[] ): TeamRuntimeMemberLaunchEvidence { const confirmed = launchState === 'confirmed_alive'; const createdOrBlocked = launchState === 'created' || launchState === 'permission_blocked'; const failed = launchState === 'failed'; const hasRuntimePid = typeof runtimePid === 'number' && Number.isFinite(runtimePid) && runtimePid > 0; const pendingRuntimeObserved = launchState === 'created' && hasRuntimePid; const livenessKind = confirmed ? 'confirmed_bootstrap' : pendingRuntimeObserved ? 'runtime_process' : launchState === 'permission_blocked' ? 'permission_blocked' : runtimeMaterialized || sessionId ? 'runtime_process_candidate' : 'registered_only'; const runtimeDiagnostic = pendingRuntimeObserved ? 'OpenCode runtime process reported by bridge' : launchState === 'permission_blocked' ? 'OpenCode runtime is waiting for permission approval' : runtimeMaterialized || sessionId ? 'OpenCode session exists without verified runtime pid' : undefined; return { memberName, providerId: 'opencode', launchState: failed ? 'failed_to_start' : confirmed ? 'confirmed_alive' : launchState === 'permission_blocked' ? 'runtime_pending_permission' : 'runtime_pending_bootstrap', agentToolAccepted: confirmed || createdOrBlocked || runtimeMaterialized, runtimeAlive: confirmed || pendingRuntimeObserved, bootstrapConfirmed: confirmed, hardFailure: failed, hardFailureReason: failed ? 'OpenCode bridge reported member launch failure' : undefined, pendingPermissionRequestIds: pendingPermissionRequestIds && pendingPermissionRequestIds.length > 0 ? [...new Set(pendingPermissionRequestIds)] : undefined, sessionId, ...(hasRuntimePid ? { runtimePid } : {}), livenessKind, ...(hasRuntimePid ? { pidSource: 'opencode_bridge' as const } : {}), ...(runtimeDiagnostic ? { runtimeDiagnostic } : {}), diagnostics, }; } function extractCheckpointNames(data: OpenCodeLaunchTeamCommandData): Set { const names = new Set(); for (const checkpoint of data.durableCheckpoints ?? []) { if (checkpoint.name.trim()) names.add(checkpoint.name); } for (const member of Object.values(data.members)) { for (const evidence of member.evidence) { if (evidence.kind.trim()) names.add(evidence.kind); } } return names; } function buildMemberBootstrapPrompt( input: TeamRuntimeLaunchInput, member: TeamRuntimeLaunchInput['expectedMembers'][number] ): string { const teamPrompt = input.prompt?.trim(); const role = member.role?.trim() || member.workflow?.trim() || 'teammate'; const workflow = member.workflow?.trim(); return [ `You are ${member.name}, a ${role} on team "${input.teamName}".`, teamPrompt ? `Team launch context:\n${teamPrompt}` : null, workflow ? `Workflow:\n${workflow}` : null, '', 'This OpenCode session is already attached by the desktop app. Do NOT create local team files, run join scripts, or search the project for a fake team registry.', 'Use the app MCP tools exposed by the "agent-teams" server for team communication and task state.', 'If available, your first app-team action is to call MCP tool agent-teams_member_briefing (or mcp__agent-teams__member_briefing if that is the exposed name) with:', `{ "teamName": "${input.teamName}", "memberName": "${member.name}" }`, 'If that tool is not available, stay idle and wait for app-delivered instructions. Do not improvise a replacement workflow.', '', 'When you need to message the human user, team lead, or another teammate, call MCP tool agent-teams_message_send (or mcp__agent-teams__message_send) with teamName, to, from, text, and optional summary.', `Always set from="${member.name}" when sending a team message from this OpenCode teammate.`, 'Do not answer team/app messages only as plain assistant text when agent-teams_message_send is available.', ] .filter((line): line is string => line !== null) .join('\n'); } function buildOpenCodeRuntimeMessageText(input: OpenCodeTeamRuntimeMessageInput): string { const replyRecipient = extractRequestedReplyRecipient(input.text); const replyLine = replyRecipient ? `For this message, if you reply, call agent-teams_message_send with to="${replyRecipient}" and from="${input.memberName}".` : `If you reply, call agent-teams_message_send with the requested recipient and from="${input.memberName}".`; return [ '', 'You are running in OpenCode, not Claude Code or Codex native.', 'If the incoming message below mentions SendMessage, treat that as a UI abstraction for other runtimes. Do not import, require, create, or run a SendMessage script.', 'To make your reply visible in the app Messages UI, call MCP tool agent-teams_message_send (or mcp__agent-teams__message_send if that is the exposed name).', `Use teamName="${input.teamName}". ${replyLine}`, 'Pass your human-readable reply as text and a short summary as summary. Do not answer only with plain assistant text when the tool is available.', input.messageId ? `The inbound app messageId is "${input.messageId}"; keep it only as context unless a tool explicitly asks for provenance.` : null, '', '', input.text, ] .filter((line): line is string => line !== null) .join('\n'); } function extractRequestedReplyRecipient(text: string): string | null { const replyRecipientMatch = /reply back to recipient "([^"]+)"/i.exec(text); if (replyRecipientMatch?.[1]?.trim()) { return replyRecipientMatch[1].trim(); } const destinationMatch = /destination must be exactly to="([^"]+)"/i.exec(text); if (destinationMatch?.[1]?.trim()) { return destinationMatch[1].trim(); } return null; } function validateOpenCodeRuntimeMembers( members: TeamRuntimeLaunchInput['expectedMembers'] ): string[] { if (members.length === 0) { return ['OpenCode runtime adapter requires at least one expected OpenCode member.']; } return members.flatMap((member, index) => { const name = member.name.trim() || ``; if (member.providerId === 'opencode') { return []; } return [ `OpenCode runtime adapter received non-OpenCode member "${name}" with provider "${member.providerId}".`, ]; }); } function formatOpenCodeBridgeDiagnostic(diagnostic: { code: string; severity: 'info' | 'warning' | 'error'; message: string; }): string { return `${diagnostic.severity}:${diagnostic.code}: ${diagnostic.message}`; } function blockedLaunchResult( input: TeamRuntimeLaunchInput, reason: string, diagnostics: string[], warnings: string[] = [] ): TeamRuntimeLaunchResult { const hardFailureReason = reason === 'unknown_error' && diagnostics[0]?.trim() ? diagnostics[0].trim() : reason; const members = Object.fromEntries( input.expectedMembers.map((member) => [ member.name, { memberName: member.name, providerId: 'opencode' as const, launchState: 'failed_to_start' as const, agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason, diagnostics, }, ]) ); return { runId: input.runId, teamName: input.teamName, launchPhase: 'finished', teamLaunchState: 'partial_failure', members, warnings, diagnostics, }; } function isRetryableReadinessState(state: OpenCodeTeamLaunchReadiness['state']): boolean { return ( state === 'not_installed' || state === 'not_authenticated' || state === 'e2e_missing' || state === 'runtime_store_blocked' || state === 'mcp_unavailable' || state === 'model_unavailable' || state === 'unknown_error' ); } function mergeDiagnostics(left: string[], right: string[]): string[] { return [...new Set([...left, ...right].filter((value) => value.trim().length > 0))]; }